todo.sr.ht/todosrht/search.py

131 lines
3.9 KiB
Python

from sqlalchemy import or_
from srht import search
from todosrht.types import Label, Ticket, TicketStatus, TicketComment
from todosrht.types import Participant, User
STATUS_ALIASES = {
"open": [
TicketStatus.reported,
TicketStatus.confirmed,
TicketStatus.in_progress,
TicketStatus.pending,
],
"closed": [TicketStatus.resolved]
}
def status_filter(value):
if value == "any":
return True
if value in STATUS_ALIASES:
return Ticket.status.in_(STATUS_ALIASES[value])
status = getattr(TicketStatus, value, None)
if status is None:
raise ValueError(f"Invalid status: '{value}'")
return Ticket.status == status
def submitter_filter(value, current_user):
if value == "me" and current_user:
return Ticket.submitter == current_user
else:
return Ticket.submitter.has(
Participant.user.has(User.username.ilike(value.lstrip("~")))
)
def asignee_filter(value, current_user):
if value == "me" and current_user:
return Ticket.assigned_users.contains(current_user)
else:
return Ticket.assigned_users.any(
User.username.ilike(value.lstrip("~"))
)
def label_filter(value):
return Ticket.labels.any(Label.name == value)
def no_filter(value):
if value == "assignee":
return Ticket.assigned_users == None
if value == "label":
return Ticket.labels == None
raise ValueError(f"Invalid search term: 'no:{value}'")
def default_filter(value):
return or_(
Ticket.description.ilike(f"%{value}%"),
Ticket.title.ilike(f"%{value}%"),
Ticket.comments.any(TicketComment.text.ilike(f"%{value}%"))
)
def apply_sort(query, terms, column_map):
for term in terms:
column_name = term.value
if column_name.startswith("-"):
column_name = column_name[1:]
if column_name not in column_map:
valid = ", ".join(f"'{c}'" for c in column_map.keys())
raise ValueError(
f"Invalid {term.key} value: '{column_name}'. "
f"Supported values are: {valid}."
)
column = column_map[column_name]
reverse = term.key == "rsort"
ordering = column.asc() if reverse else column.desc()
query = query.order_by(ordering)
return query
def apply_search(query, search_string, current_user):
terms = list(search.parse_terms(search_string))
sort_terms = [t for t in terms if t.key in ["sort", "rsort"]]
search_terms = [t for t in terms if t.key not in ["sort", "rsort"]]
# If search does not include a status filter, show open tickets
if not any([term.key == "status" for term in search_terms]):
search_terms.append(search.Term("status", "open", False))
# Set default sort to 'updated desc' if not specified
if not sort_terms:
sort_terms = [search.Term("sort", "updated", True)]
query = search.apply_terms(query, search_terms, default_filter, key_fns={
"status": status_filter,
"submitter": lambda v: submitter_filter(v, current_user),
"assigned": lambda v: asignee_filter(v, current_user),
"label": label_filter,
"no": no_filter,
})
return apply_sort(query, sort_terms, {
"created": Ticket.created,
"updated": Ticket.updated,
"comments": Ticket.comment_count,
})
def find_usernames(query, limit=20):
"""Given a partial username string, returns matching usernames."""
if not query or query == '~':
return []
if query.startswith("~"):
where = User.username.startswith(query[1:], autoescape=True)
else:
where = User.username.contains(query, autoescape=True)
from todosrht.app import db
rows = (db.session
.query(User.username)
.filter(where)
.order_by(User.username)
.limit(limit))
return [f"~{r[0]}" for r in rows]