Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2202 multicolumn sort interface #130

Open
wants to merge 19 commits into
base: 7.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
abbc66b
2202 add deselect to next_sort helper text
leckronz Sep 23, 2020
24c9c79
2202 Allow the sorting column to be deselected when it is currently D…
leckronz Sep 24, 2020
76a4536
2202 Enable AdvancedColumn header to pass sort value as a list, and e…
leckronz Oct 7, 2020
a4197ed
2202 reorganized AdvancedColumn header to remove repeated & commented…
leckronz Oct 7, 2020
cca0164
2202 loop through search_object['sort'] when it is a list to create s…
leckronz Mar 23, 2021
f7a3a83
resolve merge conflict
leckronz Mar 23, 2021
1edd4d0
2202 remove unnecessary changes
leckronz Mar 23, 2021
43f4bc7
2202 Add numbers to column headers to show their precedence in the so…
leckronz Mar 24, 2021
0675fed
2202 indicate default sorted column, simplify changes and allow norma…
leckronz Mar 29, 2021
9c142f1
#2202 - resolve merge conflict (accept incoming changes of using form…
leckronz Jan 14, 2022
0c4ede7
2202 Revert changes to apply_sort_descriptor, split one-line if/else/…
leckronz Jan 18, 2022
f9f8b6c
2202 Correct sort order toggling for default sort field, remove indic…
leckronz Jan 21, 2022
639c26c
WIP: 2202 blank sort field indicator in searchObject['sort']
leckronz Feb 3, 2022
61ee487
#2202 - use sort object flag isInitialSort/is_initial_sort to determi…
leckronz Feb 3, 2022
885103b
#2202 - prevent situation where initial load would store an empty str…
leckronz Feb 3, 2022
4e01f24
2202 Use presence of isInitialSort in the search object to correctly …
leckronz Feb 9, 2022
58c3159
WIP: 2202 - Revert sort_descriptor to its original return type (dicti…
leckronz Feb 10, 2022
c1adfbe
2202 Add multisort as the default functionality to SeekerView Column.…
leckronz Feb 11, 2022
529f858
2202 consolidate code related to sort_rank class
leckronz Feb 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions seeker/templates/advanced_seeker/results.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
{% endif %}
{% for col in display_columns %}
{% if use_wordwrap_header %}
{% seeker_column_header col results %}
{% seeker_column_header col results sort_descriptor_list %}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sort_descriptor_list is passed to column header() to determine if there's a default column being sorted upon (so that it can be indicated)

{% else %}
{{ col.header }}
{% column_header col sort_descriptor_list %}
{% endif %}
{% endfor %}
{% endblock table-headers %}
Expand Down
1 change: 1 addition & 0 deletions seeker/templates/advanced_seeker/seeker.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
th.sort.asc, th.sort.desc { background-color: #ffe; }
th.sort.asc:before { padding-right: 5px; content: '\25B3'; }
th.sort.desc:before { padding-right: 5px; content: '\25BD'; }
.sort_rank { color: #6a6a6a; font-size: 12px; padding-right: 5px; }
.table-seeker em { background-color: #ffd; padding: 1px 3px; border-radius: 4px; border: 1px solid #eee; }
#criteria { padding-top: 10px; }
</style>
Expand Down
1 change: 1 addition & 0 deletions seeker/templates/seeker/seeker.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
th.sort.asc, th.sort.desc { background-color: #ffe; }
th.sort.asc:before { padding-right: 5px; content: '\25B3'; }
th.sort.desc:before { padding-right: 5px; content: '\25BD'; }
.sort_rank { color: #6a6a6a; font-size: 12px; padding-right: 5px; }
.table-seeker em { background-color: #ffd; padding: 1px 3px; border-radius: 4px; border: 1px solid #eee; }
#criteria { padding-top: 10px; }
</style>
Expand Down
8 changes: 6 additions & 2 deletions seeker/templatetags/seeker.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,12 @@ def seeker_column(column, result, **kwargs):


@register.simple_tag
def seeker_column_header(column, results=None):
return column.header(results)
def seeker_column_header(column, results=None, sort_descriptor_list=None):
return column.header(results, sort_descriptor_list)

@register.simple_tag
def column_header(column, sort_descriptor_list=None):
return column.header(sort_descriptor_list=sort_descriptor_list)


@register.simple_tag
Expand Down
175 changes: 121 additions & 54 deletions seeker/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,33 +88,69 @@ def header(self):
if not self.sort:
return format_html('<th class="{}">{}</th>', cls, self.header_html)
q = self.view.request.GET.copy()
field = q.get('s', '')
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using the existing behavior of SeekerView -- passing the sort information through the querystring, without modifying the context.

sort_fields = q.getlist('s', [])
sort = None
sort_order = 0
sort_rank = ''
cls += ' sort'
if field.lstrip('-') == self.field:
# If the current sort field is this field, give it a class a change direction.
sort = 'Descending' if field.startswith('-') else 'Ascending'
cls += ' desc' if field.startswith('-') else ' asc'
d = '' if field.startswith('-') else '-'
q['s'] = '%s%s' % (d, self.field)
sr_label = ''
data_sort = ''
if sort_fields:
if (self.field in sort_fields or '-{}'.format(self.field) in sort_fields):
field = self.field
d = '-'
# If the current sort field is this field, give it a class a change direction.
if not self.field in sort_fields:
field = '-{}'.format(self.field)
d = ''
sort_order = q.getlist('s', []).index(field) + 1
sort = 'Ascending' if d else 'Descending'
cls += ' asc' if d else ' desc'
data_sort = '%s%s' % (d, self.field)
if d:
# if this column is selected, its sort rank will be maintained when its direction is changed
current_sort = q.getlist('s', [])
field_index = current_sort.index(field)
current_sort[field_index] = data_sort
q.setlist('s', current_sort)
else:
# remove from the sort
current_sort = q.getlist('s', [])
current_sort.remove(field)
q.setlist('s', current_sort)
else:
data_sort = self.field
if self.field not in q.getlist('s', []):
q.appendlist('s', data_sort)
else:
q['s'] = self.field
next_sort = 'descending' if sort == 'Ascending' else 'ascending'
sr_label = format_html(' <span class="sr-only">({})</span>', sort) if sort else ''

if sort:
if len(sort_fields) == 1:
sr_label = format_html('<span class="sr-only">({})</span>', sort)
else:
sr_label = format_html('<span class="sr-only">Number {} in sort order ({})</span>', sort_order, sort)
next_sorting = {
'Ascending': 'sort descending',
'Descending': 'remove from sort'
}
#If this field isn't already being sorted upon, label it as being sorted ascending
next_sort = next_sorting.get(sort, 'sort ascending')
if self.field_definition:
span = format_html('<span title="{}" class ="fa fa-question-circle"></span>', self.field_definition)
else:
span = ''

if sort_order and len(sort_fields) > 1:
sort_rank = format_html('<span class="sort_rank">{} </span>', sort_order)
else:
# Don't indicate sort rank when only one field is being sorted upon
sort_rank = ''


html = format_html(
'<th class="{}"><a href="?{}" title="Click to sort {}" data-sort="{}">{}{} {}</a></th>',
cls,
q.urlencode(),
next_sort,
q['s'],
self.header_html,
sr_label,
span
)
'<th class="{}">{}<a href="?{}" title="Click to {}" data-sort="{}">{}{} {}</a></th>',
cls, sort_rank, q.urlencode(), next_sort, data_sort, self.header_html, sr_label, span)
return html

def context(self, result, **kwargs):
Expand Down Expand Up @@ -795,23 +831,18 @@ def apply_sort_descriptor(self, sort):
missing = '_last' if desc else '_first'
elif missing == '_high':
missing = '_first' if desc else '_last'
return {
field: {
'order': 'desc' if desc else 'asc',
'missing': missing,
sort_descriptor = {
field: {'order': 'desc' if desc else 'asc',}
}
}
if missing:
sort_descriptor[field]['missing'] = missing
return sort_descriptor

def sort_descriptor(self, sort):
if self.missing_sort is None or isinstance(sort, dict):
if isinstance(sort, dict):
return sort
if not isinstance(sort, list):
return self.apply_sort_descriptor(sort)
else:
sort_dict = {}
for s in sort:
sort_dict.update(self.apply_sort_descriptor(s))
return sort_dict
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During a meeting we changed sort_descriptor() so that it would return a list of dictionaries for the Elasticsearch DSL sort to use (unpacked). I changed it back to returning a single dictionary so that it wouldn't be breaking for certain sites using SeekerView that are calling sort_descriptor().

return self.apply_sort_descriptor(sort)

def is_initial(self):
"""
Expand Down Expand Up @@ -1118,28 +1149,51 @@ def dispatch(self, request, *args, **kwargs):

class AdvancedColumn(Column):

def header(self, results=None):
def header(self, results=None, sort_descriptor_list=None):
cls = '{}_{}'.format(self.view.document._doc_type.name, self.field.replace('.', '_'))
cls += ' {}_{}'.format(self.view.document.__name__.lower(), self.field.replace('.', '_'))
if self.model_lower:
cls += ' {}_{}'.format(self.model_lower, self.field.replace('.', '_'))
if not self.sort:
return format_html('<th class="{}">{}</th>', cls, self.header_html)
is_multicolumn = 'isInitialSort' in self.view.search_object
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_multicolumn checks for a flag in the search object to determine the jquery file passing the search object has been set up to build a search object list. This allows us to determine if "remove from sort" should be an option

current_sort = self.view.search_object['sort']
if not isinstance(current_sort, list):
current_sort = [current_sort]
if '' in current_sort:
current_sort.remove('')
sort = None
sort_order = 0
cls += ' sort'
if current_sort.lstrip('-') == self.field:
# If the current sort field is this field, give it a class a change direction.
sort = 'Descending' if current_sort.startswith('-') else 'Ascending'
cls += ' desc' if current_sort.startswith('-') else ' asc'
d = '' if current_sort.startswith('-') else '-'
data_sort = '{}{}'.format(d, self.field)
else:
data_sort = self.field

next_sort = 'descending' if sort == 'Ascending' else 'ascending'
sr_label = format_html(' <span class="sr-only">({})</span>', sort) if sort else ''

sr_label = ''
next_sort = ''
data_sort = self.field
if sort_descriptor_list:
potential_default = list(sort_descriptor_list[0].keys())[0]
if sort_descriptor_list[0][potential_default].get('order', None) == 'desc':
potential_default = '{}{}'.format('-', potential_default)
potential_default = potential_default.replace('.raw', '').replace('.label','')
if potential_default and potential_default not in current_sort:
current_sort.append(potential_default)
for sort_field in current_sort:
if sort_field.lstrip('-') == self.field:
# If the current sort field is this field, give it a class a change direction.
sort = 'Descending' if sort_field.startswith('-') else 'Ascending'
cls += ' desc' if sort_field.startswith('-') else ' asc'
d = '' if sort_field.startswith('-') else '-'
data_sort = '{}{}'.format(d, self.field)
sort_order = current_sort.index(sort_field) + 1
if len(current_sort) == 1:
sr_label = format_html('<span class="sr-only">({})</span>', sort)
else:
sr_label = format_html('<span class="sr-only">Number {} in sort order ({})</span>', sort_order, sort)
next_sorting = {
'Ascending': 'sort descending',
'Descending': 'remove from sort' if is_multicolumn else 'sort ascending'
}
# If this field isn't already being sorted upon, label its next sort direction as ascending
next_sort = next_sorting.get(sort, 'sort ascending')

# If results provided, we check to see if header has space to allow for wordwrapping. If it already wordwrapped
# (i.e. has <br> in header) we skip it.
if results and ' ' in self.header_html and not '<br' in self.header_html:
Expand All @@ -1149,10 +1203,16 @@ def header(self, results=None):
span = format_html('<span title="{}" class ="fa fa-question-circle"></span>', self.field_definition)
else:
span = ''

# Don't indicate sort rank when only one field is being sorted upon
if sort_order and len(current_sort) > 1:
sort_rank = format_html('<span class="sort_rank">{} </span>', sort_order)
else:
sort_rank = ''

html = format_html(
'<th class="{}"><a href="#" title="Click to sort {}" data-sort="{}">{}{} {}</a></th>',
cls, next_sort, data_sort, self.header_html, sr_label, span
)
'<th class="{}">{}<a href="#" title="Click to {}" data-sort={}>{}{} {}</a></th>',
cls, sort_rank, next_sort, data_sort, self.header_html, sr_label, span)
return html

def get_data_max_length(self, results):
Expand Down Expand Up @@ -1404,16 +1464,22 @@ def apply_sort_field(self, column_lookup, sort):
return('-{}'.format(c.sort) if sort.startswith('-') else c.sort)
return sort

def get_sort_field(self, columns, sort, display):
def get_sort_field(self, columns, sort, is_initial_sort, display):
"""
Returns the appropriate sort field for a given sort value.
"""

# Make sure we sanitize the sort fields.
sort_fields = []
column_lookup = { c.field: c for c in columns }

# Order of precedence for sort is: parameter, the default from the view, and then the first displayed column (if any are displayed)
sort = sort or self.sort or display[0] if len(display) else ''
if is_initial_sort:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this flag allows us to remove the default sort column (specified in the view) from the sort and to sort on nothing

sort = sort or self.sort or display[0] if len(display) else ''
else:
# If all columns have been intentionally deselected from the sort, do not default to any column
sort = sort if sort else []

# Get the column based on the field name, and use it's "sort" field, if applicable.
if not isinstance(sort, list):
return self.apply_sort_field(column_lookup, sort)
Expand Down Expand Up @@ -1468,7 +1534,7 @@ def post(self, request, *args, **kwargs):
except ValueError:
return HttpResponseBadRequest("Improperly formatted 'search_object', json.loads failed.")

# Sanity check that the search object has all of it's required components
# Sanity check that the search object has all of its required components
if not all(k in self.search_object for k in ('query', 'keywords', 'page', 'sort', 'display')):
return HttpResponseBadRequest("The 'search_object' is not in the proper format.")

Expand Down Expand Up @@ -1598,12 +1664,12 @@ def render_results(self, export):
search = self.apply_highlight(search, columns)

# Finally, grab the results.
sort = self.get_sort_field(columns, self.search_object['sort'], display)
sort = self.get_sort_field(columns, self.search_object['sort'], self.search_object.get('isInitialSort', True), display)
sort = [sort] if isinstance(sort, str) else sort
sort_descriptor_list = [self.sort_descriptor(s) for s in sort]

if sort:
if (self.missing_sort is None or isinstance(sort, dict)) and isinstance(sort, list):
results = search.sort(*self.sort_descriptor(sort))[offset:offset + page_size].execute()
else:
results = search.sort(self.sort_descriptor(sort))[offset:offset + page_size].execute()
results = search.sort(*sort_descriptor_list)[offset:offset + page_size].execute()
else:
results = search[offset:offset + page_size].execute()

Expand Down Expand Up @@ -1633,6 +1699,7 @@ def render_results(self, export):
'total_hits': results.hits.total.value,
'show_rank': self.show_rank,
'sort': sort,
'sort_descriptor_list': sort_descriptor_list,
'export_name': self.export_name,
'use_wordwrap_header': self.use_wordwrap_header,
'search': search
Expand Down