Implement sorting in ticket search
By default tickets are sorted by updated DESC, if not otherwise defined. Sort order is defined by entering `sort:<term>` in the search box where `<term>` can be one of `updated` or `created`. More terms can be added later. Sort order is descending by default, it can be reversed by specifying `rsort:<term>` instead.
This commit is contained in:
parent
6d1d4764dd
commit
8b056f3d30
|
@ -1,5 +1,6 @@
|
|||
import pytest
|
||||
|
||||
from datetime import datetime
|
||||
from tests import factories as f
|
||||
from todosrht.search import apply_search
|
||||
from todosrht.types import Ticket, TicketStatus
|
||||
|
@ -95,8 +96,8 @@ def test_ticket_search(client):
|
|||
assert search("description_4") == [ticket4]
|
||||
assert search("description_5") == [ticket5]
|
||||
|
||||
assert search("lightsabre") == [ticket1, ticket3, ticket5]
|
||||
assert search("blaster") == [ticket2, ticket4]
|
||||
assert search("lightsabre") == [ticket5, ticket3, ticket1]
|
||||
assert search("blaster") == [ticket4, ticket2]
|
||||
|
||||
# Search by comment
|
||||
assert search("parsecs") == [ticket1]
|
||||
|
@ -110,29 +111,29 @@ def test_ticket_search(client):
|
|||
assert search("'force may with you be'") == []
|
||||
|
||||
# Search either title, description, or comment
|
||||
assert search("used_to_test_or") == [ticket1, ticket2, ticket3]
|
||||
assert search("used_to_test_or") == [ticket3, ticket2, ticket1]
|
||||
|
||||
# Search by submitter
|
||||
assert search("submitter:luke") == [ticket3]
|
||||
assert search("submitter:leia") == [ticket4, ticket5]
|
||||
assert search("submitter:han") == [ticket1, ticket2]
|
||||
assert search("submitter:leia") == [ticket5, ticket4]
|
||||
assert search("submitter:han") == [ticket2, ticket1]
|
||||
|
||||
assert search("!submitter:luke") == [ticket1, ticket2, ticket4, ticket5]
|
||||
assert search("!submitter:leia") == [ticket1, ticket2, ticket3]
|
||||
assert search("!submitter:han") == [ticket3, ticket4, ticket5]
|
||||
assert search("!submitter:luke") == [ticket5, ticket4, ticket2, ticket1]
|
||||
assert search("!submitter:leia") == [ticket3, ticket2, ticket1]
|
||||
assert search("!submitter:han") == [ticket5, ticket4, ticket3]
|
||||
|
||||
# Search by asignee
|
||||
assert search("assigned:luke") == [ticket1, ticket2]
|
||||
assert search("assigned:leia") == [ticket2, ticket3]
|
||||
assert search("!assigned:luke") == [ticket3, ticket4, ticket5]
|
||||
assert search("!assigned:leia") == [ticket1, ticket4, ticket5]
|
||||
assert search("assigned:luke") == [ticket2, ticket1]
|
||||
assert search("assigned:leia") == [ticket3, ticket2]
|
||||
assert search("!assigned:luke") == [ticket5, ticket4, ticket3]
|
||||
assert search("!assigned:leia") == [ticket5, ticket4, ticket1]
|
||||
assert search("assigned:luke assigned:leia") == [ticket2]
|
||||
assert search("assigned:luke !assigned:leia") == [ticket1]
|
||||
assert search("!assigned:luke assigned:leia") == [ticket3]
|
||||
assert search("!assigned:luke !assigned:leia") == [ticket4, ticket5]
|
||||
assert search("!assigned:luke !assigned:leia") == [ticket5, ticket4]
|
||||
|
||||
assert search("no:assignee") == [ticket4, ticket5]
|
||||
assert search("!no:assignee") == [ticket1, ticket2, ticket3]
|
||||
assert search("no:assignee") == [ticket5, ticket4]
|
||||
assert search("!no:assignee") == [ticket3, ticket2, ticket1]
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
search("no:foo")
|
||||
|
@ -140,23 +141,23 @@ def test_ticket_search(client):
|
|||
|
||||
assert search("assigned:me") == []
|
||||
assert search("assigned:me", han.user) == []
|
||||
assert search("assigned:me", luke.user) == [ticket1, ticket2]
|
||||
assert search("assigned:me", leia.user) == [ticket2, ticket3]
|
||||
assert search("assigned:me", luke.user) == [ticket2, ticket1]
|
||||
assert search("assigned:me", leia.user) == [ticket3, ticket2]
|
||||
|
||||
assert search("!assigned:me") == [ticket1, ticket2, ticket3, ticket4, ticket5]
|
||||
assert search("!assigned:me", han.user) == [ticket1, ticket2, ticket3, ticket4, ticket5]
|
||||
assert search("!assigned:me", luke.user) == [ticket3, ticket4, ticket5]
|
||||
assert search("!assigned:me", leia.user) == [ticket1, ticket4, ticket5]
|
||||
assert search("!assigned:me") == [ticket5, ticket4, ticket3, ticket2, ticket1]
|
||||
assert search("!assigned:me", han.user) == [ticket5, ticket4, ticket3, ticket2, ticket1]
|
||||
assert search("!assigned:me", luke.user) == [ticket5, ticket4, ticket3]
|
||||
assert search("!assigned:me", leia.user) == [ticket5, ticket4, ticket1]
|
||||
|
||||
# Search by label
|
||||
assert search("label:jedi") == [ticket1, ticket5]
|
||||
assert search("label:sith") == [ticket2, ticket5]
|
||||
assert search("!label:jedi") == [ticket2, ticket3, ticket4]
|
||||
assert search("!label:sith") == [ticket1, ticket3, ticket4]
|
||||
assert search("label:jedi") == [ticket5, ticket1]
|
||||
assert search("label:sith") == [ticket5, ticket2]
|
||||
assert search("!label:jedi") == [ticket4, ticket3, ticket2]
|
||||
assert search("!label:sith") == [ticket4, ticket3, ticket1]
|
||||
assert search("label:jedi label:sith") == [ticket5]
|
||||
|
||||
assert search("no:label") == [ticket3, ticket4]
|
||||
assert search("!no:label") == [ticket1, ticket2, ticket5]
|
||||
assert search("no:label") == [ticket4, ticket3]
|
||||
assert search("!no:label") == [ticket5, ticket2, ticket1]
|
||||
|
||||
# Combinations
|
||||
assert search(
|
||||
|
@ -184,14 +185,14 @@ def test_ticket_search_by_status(client):
|
|||
def search(search_string, user=owner):
|
||||
return apply_search(query, search_string, user).all()
|
||||
|
||||
assert search("") == [ticket1, ticket2, ticket3, ticket4]
|
||||
assert search("status:any") == [ticket1, ticket2, ticket3, ticket4, ticket5]
|
||||
assert search("status:open") == [ticket1, ticket2, ticket3, ticket4]
|
||||
assert search("") == [ticket4, ticket3, ticket2, ticket1]
|
||||
assert search("status:any") == [ticket5, ticket4, ticket3, ticket2, ticket1]
|
||||
assert search("status:open") == [ticket4, ticket3, ticket2, ticket1]
|
||||
assert search("status:closed") == [ticket5]
|
||||
|
||||
assert search("!status:any") == []
|
||||
assert search("!status:open") == [ticket5]
|
||||
assert search("!status:closed") == [ticket1, ticket2, ticket3, ticket4]
|
||||
assert search("!status:closed") == [ticket4, ticket3, ticket2, ticket1]
|
||||
|
||||
assert search("status:reported") == [ticket1]
|
||||
assert search("status:confirmed") == [ticket2]
|
||||
|
@ -199,12 +200,51 @@ def test_ticket_search_by_status(client):
|
|||
assert search("status:pending") == [ticket4]
|
||||
assert search("status:resolved") == [ticket5]
|
||||
|
||||
assert search("!status:reported") == [ticket2, ticket3, ticket4, ticket5]
|
||||
assert search("!status:confirmed") == [ticket1, ticket3, ticket4, ticket5]
|
||||
assert search("!status:in_progress") == [ticket1, ticket2, ticket4, ticket5]
|
||||
assert search("!status:pending") == [ticket1, ticket2, ticket3, ticket5]
|
||||
assert search("!status:resolved") == [ticket1, ticket2, ticket3, ticket4]
|
||||
assert search("!status:reported") == [ticket5, ticket4, ticket3, ticket2]
|
||||
assert search("!status:confirmed") == [ticket5, ticket4, ticket3, ticket1]
|
||||
assert search("!status:in_progress") == [ticket5, ticket4, ticket2, ticket1]
|
||||
assert search("!status:pending") == [ticket5, ticket3, ticket2, ticket1]
|
||||
assert search("!status:resolved") == [ticket4, ticket3, ticket2, ticket1]
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
search("status:foo")
|
||||
assert str(excinfo.value) == "Invalid status: 'foo'"
|
||||
|
||||
def test_sorting(client):
|
||||
owner = f.UserFactory()
|
||||
tracker = f.TrackerFactory(owner=owner)
|
||||
|
||||
ticket1 = f.TicketFactory(tracker=tracker)
|
||||
ticket2 = f.TicketFactory(tracker=tracker)
|
||||
ticket3 = f.TicketFactory(tracker=tracker)
|
||||
ticket4 = f.TicketFactory(tracker=tracker)
|
||||
ticket5 = f.TicketFactory(tracker=tracker)
|
||||
db.session.commit()
|
||||
|
||||
query = Ticket.query.filter(Ticket.tracker_id == tracker.id)
|
||||
|
||||
def search(search_string, user=owner):
|
||||
return apply_search(query, search_string, user).all()
|
||||
|
||||
assert search("") == [ticket5, ticket4, ticket3, ticket2, ticket1]
|
||||
assert search("sort:updated") == [ticket5, ticket4, ticket3, ticket2, ticket1]
|
||||
assert search("rsort:updated") == [ticket1, ticket2, ticket3, ticket4, ticket5]
|
||||
assert search("sort:created") == [ticket5, ticket4, ticket3, ticket2, ticket1]
|
||||
assert search("rsort:created") == [ticket1, ticket2, ticket3, ticket4, ticket5]
|
||||
|
||||
# Changing updated timestamp changes sorting order
|
||||
ticket3.updated = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
assert search("") == [ticket3, ticket5, ticket4, ticket2, ticket1]
|
||||
assert search("sort:updated") == [ticket3, ticket5, ticket4, ticket2, ticket1]
|
||||
assert search("rsort:updated") == [ticket1, ticket2, ticket4, ticket5, ticket3]
|
||||
|
||||
# Sort by created remains the same
|
||||
assert search("sort:created") == [ticket5, ticket4, ticket3, ticket2, ticket1]
|
||||
assert search("rsort:created") == [ticket1, ticket2, ticket3, ticket4, ticket5]
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
search("sort:foo")
|
||||
|
||||
assert str(excinfo.value).startswith("Invalid sort value: 'foo'.")
|
||||
|
|
|
@ -90,8 +90,7 @@ def return_tracker(tracker, access, **kwargs):
|
|||
tickets = (Ticket.query
|
||||
.filter(Ticket.tracker_id == tracker.id)
|
||||
.options(subqueryload(Ticket.labels))
|
||||
.options(subqueryload(Ticket.submitter))
|
||||
.order_by(Ticket.updated.desc()))
|
||||
.options(subqueryload(Ticket.submitter)))
|
||||
|
||||
try:
|
||||
terms = request.args.get("search")
|
||||
|
|
|
@ -62,14 +62,41 @@ def default_filter(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 terms]):
|
||||
terms.append(search.Term("status", "open", False))
|
||||
if not any([term.key == "status" for term in search_terms]):
|
||||
search_terms.append(search.Term("status", "open", False))
|
||||
|
||||
return search.apply_terms(query, terms, default_filter, key_fns={
|
||||
# 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),
|
||||
|
@ -77,6 +104,11 @@ def apply_search(query, search_string, current_user):
|
|||
"no": no_filter,
|
||||
})
|
||||
|
||||
return apply_sort(query, sort_terms, {
|
||||
"created": Ticket.created,
|
||||
"updated": Ticket.updated,
|
||||
})
|
||||
|
||||
def find_usernames(query, limit=20):
|
||||
"""Given a partial username string, returns matching usernames."""
|
||||
if not query or query == '~':
|
||||
|
|
|
@ -177,7 +177,7 @@
|
|||
<input
|
||||
name="search"
|
||||
type="text"
|
||||
placeholder="Search tickets... status:closed label:label{{" submitter:me" if current_user else ""}}"
|
||||
placeholder="Search tickets... status:closed sort:created label:label{{" submitter:me" if current_user else ""}}"
|
||||
class="form-control{% if search_error %} is-invalid{% endif %}"
|
||||
{% if not another %}
|
||||
autofocus
|
||||
|
|
Loading…
Reference in New Issue