diff --git a/seeker/templates/advanced_seeker/results.html b/seeker/templates/advanced_seeker/results.html index ef8168b..ff7e4e5 100644 --- a/seeker/templates/advanced_seeker/results.html +++ b/seeker/templates/advanced_seeker/results.html @@ -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 %} {% else %} - {{ col.header }} + {% column_header col sort_descriptor_list %} {% endif %} {% endfor %} {% endblock table-headers %} diff --git a/seeker/templates/advanced_seeker/seeker.html b/seeker/templates/advanced_seeker/seeker.html index ea33699..61dcde3 100644 --- a/seeker/templates/advanced_seeker/seeker.html +++ b/seeker/templates/advanced_seeker/seeker.html @@ -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; } diff --git a/seeker/templates/seeker/seeker.html b/seeker/templates/seeker/seeker.html index a4d4b1e..91de5fb 100644 --- a/seeker/templates/seeker/seeker.html +++ b/seeker/templates/seeker/seeker.html @@ -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; } diff --git a/seeker/templatetags/seeker.py b/seeker/templatetags/seeker.py index 4a86de8..0d52a30 100644 --- a/seeker/templatetags/seeker.py +++ b/seeker/templatetags/seeker.py @@ -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 diff --git a/seeker/views.py b/seeker/views.py index 18de47f..0b32569 100644 --- a/seeker/views.py +++ b/seeker/views.py @@ -88,33 +88,69 @@ def header(self): if not self.sort: return format_html('{}', cls, self.header_html) q = self.view.request.GET.copy() - field = q.get('s', '') + 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(' ({})', sort) if sort else '' + + if sort: + if len(sort_fields) == 1: + sr_label = format_html('({})', sort) + else: + sr_label = format_html('Number {} in sort order ({})', 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('', self.field_definition) else: span = '' + + if sort_order and len(sort_fields) > 1: + sort_rank = format_html('{} ', sort_order) + else: + # Don't indicate sort rank when only one field is being sorted upon + sort_rank = '' + + html = format_html( - '{}{} {}', - cls, - q.urlencode(), - next_sort, - q['s'], - self.header_html, - sr_label, - span - ) + '{}{}{} {}', + cls, sort_rank, q.urlencode(), next_sort, data_sort, self.header_html, sr_label, span) return html def context(self, result, **kwargs): @@ -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 + return self.apply_sort_descriptor(sort) def is_initial(self): """ @@ -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('{}', cls, self.header_html) + is_multicolumn = 'isInitialSort' in self.view.search_object 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(' ({})', 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('({})', sort) + else: + sr_label = format_html('Number {} in sort order ({})', 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
in header) we skip it. if results and ' ' in self.header_html and not '', 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('{} ', sort_order) + else: + sort_rank = '' + html = format_html( - '{}{} {}', - cls, next_sort, data_sort, self.header_html, sr_label, span - ) + '{}{}{} {}', + cls, sort_rank, next_sort, data_sort, self.header_html, sr_label, span) return html def get_data_max_length(self, results): @@ -1404,7 +1464,7 @@ 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. """ @@ -1412,8 +1472,14 @@ def get_sort_field(self, columns, sort, display): # 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: + 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) @@ -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.") @@ -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() @@ -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