From cd00c6abf08bca7cec3cbe3736fadcaa47730ec9 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 23 Apr 2019 19:22:35 -0400 Subject: [PATCH] Support marking columns as indetifier and sortable Signed-off-by: Roberto Rosario --- HISTORY.rst | 4 +- docs/releases/3.2.rst | 3 + .../generic_list_items_subtemplate.html | 28 ++-- .../appearance/generic_list_subtemplate.html | 138 +++++++++++------- mayan/apps/common/generics.py | 54 ++++++- mayan/apps/common/icons.py | 2 + mayan/apps/common/literals.py | 8 + mayan/apps/navigation/classes.py | 121 +++++++++++++-- .../templatetags/navigation_tags.py | 24 ++- 9 files changed, 291 insertions(+), 91 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 670261ebf6..765d56f56f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -124,7 +124,9 @@ * Add document state action view test. * Remove sidebar menu instance. The secondary menu and the previour sidebar menu now perform the same function. - +* Backport source column identifiable and sortable + improvements. + 3.1.11 (2019-04-XX) =================== * Fix multiple tag selection wizard step. diff --git a/docs/releases/3.2.rst b/docs/releases/3.2.rst index 9109e6b45a..0fab951683 100644 --- a/docs/releases/3.2.rst +++ b/docs/releases/3.2.rst @@ -156,6 +156,9 @@ Other changes * Add document state action view test. * Remove sidebar menu instance. The secondary menu and the previour sidebar menu now perform the same function. +* Backport source column identifiable and sortable + improvements. + Removals -------- diff --git a/mayan/apps/appearance/templates/appearance/generic_list_items_subtemplate.html b/mayan/apps/appearance/templates/appearance/generic_list_items_subtemplate.html index 69b323c114..ca837c505d 100644 --- a/mayan/apps/appearance/templates/appearance/generic_list_items_subtemplate.html +++ b/mayan/apps/appearance/templates/appearance/generic_list_items_subtemplate.html @@ -50,24 +50,25 @@
@@ -77,8 +78,9 @@
{% if not hide_columns %} - {% for column in object|get_source_columns %} -
{% source_column_resolve column=column %}{{ column_result }}
+ {% navigation_get_source_columns source=object exclude_identifier=True as source_columns %} + {% for column in source_columns %} +
{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}{{ column.label }}: {% endif %}{{ column_value }}{% endif %}
{% endfor %} {% endif %} diff --git a/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html b/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html index 77b7cf589d..35d6be257d 100644 --- a/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html +++ b/mayan/apps/appearance/templates/appearance/generic_list_subtemplate.html @@ -38,7 +38,7 @@
- +
{% if not hide_header %} @@ -48,11 +48,37 @@ {% if not hide_object %} + {% else %} + {% navigation_get_source_columns source=object_list only_identifier=True as source_column %} + {% if source_column %} + + {% endif %} {% endif %} {% if not hide_columns %} - {% for column in object_list|get_source_columns %} - + {% navigation_get_source_columns source=object_list exclude_identifier=True as source_columns %} + {% for column in source_columns %} + {% endfor %} {% endif %} @@ -68,60 +94,68 @@ {% for object in object_list %} - {% if multi_item_actions %} - {% endif %} - - {% endif %} - {% if not hide_object %} - {% if main_object %} - {% with object|object_property:main_object as object %} - - {% endwith %} - {% else %} + + {% if not hide_object %} + {% else %} + {% navigation_get_source_columns source=object only_identifier=True as source_column %} + {% navigation_source_column_resolve column=source_column as column_value %} + {% if column_value %} + + {% endif %} {% endif %} - {% endif %} - {% if not hide_columns %} - {% for column in object|get_source_columns %} - - {% endfor %} - {% endif %} - {% for column in extra_columns %} - - {% endfor %} - {% if not hide_links %} - {% endfor %} - {% navigation_resolve_menu name='object' source=object as object_menus_results %} - {% for object_menu_results in object_menus_results %} - {% for link_group in object_menu_results.link_groups %} - {% with link_group.links as object_navigation_links %} - {% with 'true' as horizontal %} - {% with 'true' as hide_icon %} - {% include 'navigation/generic_navigation.html' %} - {% endwith %} - {% endwith %} - {% endwith %} + {% endif %} + + {% if not hide_links %} + - {% endif %} + {% navigation_resolve_menu name='object' source=object as object_menus_results %} + {% for object_menu_results in object_menus_results %} + {% for link_group in object_menu_results.link_groups %} + {% with link_group.links as object_navigation_links %} + {% with 'true' as horizontal %} + {% with 'true' as hide_icon %} + {% include 'navigation/generic_navigation.html' %} + {% endwith %} + {% endwith %} + {% endwith %} + {% endfor %} + {% endfor %} + + {% endif %} {% empty %} diff --git a/mayan/apps/common/generics.py b/mayan/apps/common/generics.py index a73374a724..e53d497cbd 100644 --- a/mayan/apps/common/generics.py +++ b/mayan/apps/common/generics.py @@ -22,7 +22,15 @@ from django_downloadview import ( from pure_pagination.mixins import PaginationMixin from .forms import ChoiceForm -from .icons import icon_assign_remove_add, icon_assign_remove_remove +from .icons import ( + icon_assign_remove_add, icon_assign_remove_remove, icon_sort_down, + icon_sort_up +) +from .literals import ( + TEXT_SORT_FIELD_PARAMETER, TEXT_SORT_FIELD_VARIABLE_NAME, + TEXT_SORT_ORDER_CHOICE_ASCENDING, TEXT_SORT_ORDER_PARAMETER, + TEXT_SORT_ORDER_VARIABLE_NAME +) from .mixins import ( DeleteExtraDataMixin, DynamicFormViewMixin, ExtraContextMixin, FormExtraKwargsMixin, MultipleObjectMixin, ObjectActionMixin, @@ -495,12 +503,52 @@ class SingleObjectListView(PaginationMixin, ViewPermissionCheckMixin, ObjectList return result + def get_context_data(self, **kwargs): + context = super(SingleObjectListView, self).get_context_data(**kwargs) + + context.update( + { + TEXT_SORT_FIELD_VARIABLE_NAME: self.get_sort_field(), + TEXT_SORT_ORDER_VARIABLE_NAME: self.get_sort_order(), + 'icon_sort': self.get_sort_icon(), + } + ) + return context + def get_paginate_by(self, queryset): return setting_paginate_by.value def get_queryset(self): try: - return super(SingleObjectListView, self).get_queryset() + queryset = super(SingleObjectListView, self).get_queryset() except ImproperlyConfigured: self.queryset = self.get_object_list() - return super(SingleObjectListView, self).get_queryset() + queryset = super(SingleObjectListView, self).get_queryset() + + self.field_name = self.get_sort_field() + if self.get_sort_order() == TEXT_SORT_ORDER_CHOICE_ASCENDING: + sort_order = '' + else: + sort_order = '-' + + if self.field_name: + queryset = queryset.order_by( + '{}{}'.format(sort_order, self.field_name) + ) + + return queryset + + def get_sort_field(self): + return self.request.GET.get(TEXT_SORT_FIELD_PARAMETER) + + def get_sort_icon(self): + sort_order = self.get_sort_order() + if not sort_order: + return + elif sort_order == TEXT_SORT_ORDER_CHOICE_ASCENDING: + return icon_sort_down + else: + return icon_sort_up + + def get_sort_order(self): + return self.request.GET.get(TEXT_SORT_ORDER_PARAMETER) diff --git a/mayan/apps/common/icons.py b/mayan/apps/common/icons.py index fb0b33b7ad..5c2ed58c50 100644 --- a/mayan/apps/common/icons.py +++ b/mayan/apps/common/icons.py @@ -52,6 +52,8 @@ icon_social_paypal = Icon( icon_social_twitter = Icon( driver_name='fontawesomecss', css_classes='fab fa-twitter' ) +icon_sort_down = Icon(driver_name='fontawesome', symbol='sort-down') +icon_sort_up = Icon(driver_name='fontawesome', symbol='sort-up') icon_source_code = Icon(driver_name='fontawesome', symbol='code-branch') icon_support = Icon( driver_name='fontawesome', symbol='phone' diff --git a/mayan/apps/common/literals.py b/mayan/apps/common/literals.py index 7513a2d33c..ac4f04c35d 100644 --- a/mayan/apps/common/literals.py +++ b/mayan/apps/common/literals.py @@ -11,6 +11,14 @@ MESSAGE_SQLITE_WARNING = _( 'for development and testing, not for production.' ) PYPI_URL = 'https://pypi.python.org/pypi' + +TEXT_SORT_FIELD_PARAMETER = '_sort_field' +TEXT_SORT_FIELD_VARIABLE_NAME = 'sort_field' +TEXT_SORT_ORDER_CHOICE_ASCENDING = 'asc' +TEXT_SORT_ORDER_CHOICE_DESCENDING = 'desc' +TEXT_SORT_ORDER_PARAMETER = '_sort_order' +TEXT_SORT_ORDER_VARIABLE_NAME = 'sort_order' + TIME_DELTA_UNIT_DAYS = 'days' TIME_DELTA_UNIT_HOURS = 'hours' TIME_DELTA_UNIT_MINUTES = 'minutes' diff --git a/mayan/apps/navigation/classes.py b/mayan/apps/navigation/classes.py index 3e16c142f5..f053ba288b 100644 --- a/mayan/apps/navigation/classes.py +++ b/mayan/apps/navigation/classes.py @@ -14,6 +14,11 @@ from django.urls import resolve, reverse from django.utils.encoding import force_str, force_text from django.utils.module_loading import import_string +from mayan.apps.common.literals import ( + TEXT_SORT_FIELD_PARAMETER, TEXT_SORT_FIELD_VARIABLE_NAME, + TEXT_SORT_ORDER_CHOICE_ASCENDING, TEXT_SORT_ORDER_CHOICE_DESCENDING, + TEXT_SORT_ORDER_PARAMETER, TEXT_SORT_ORDER_VARIABLE_NAME +) from mayan.apps.common.settings import setting_home_view from mayan.apps.common.utils import resolve_attribute from mayan.apps.permissions import Permission @@ -528,38 +533,90 @@ class SourceColumn(object): return sorted(columns, key=lambda x: x.order) @classmethod - def get_for_source(cls, source): + def get_for_source(cls, context, source, exclude_identifier=False, only_identifier=False): try: - return SourceColumn.sort(columns=cls._registry[source]) + result = cls._registry[source] except KeyError: try: - # Try it as a queryset - return SourceColumn.sort(columns=cls._registry[source.model]) - except AttributeError: + # Might be an instance, try its class + result = cls._registry[source.__class__] + except KeyError: try: - # It seems to be an instance, try its class - return SourceColumn.sort(columns=cls._registry[source.__class__]) - except KeyError: + # Might be an inherited class insance, try its source class + result = cls._registry[source.source_ptr.__class__] + except (KeyError, AttributeError): try: - # Special case for queryset items produced from - # .defer() or .only() optimizations - return SourceColumn.sort(columns=cls._registry[list(source._meta.parents.items())[0][0]]) - except (AttributeError, KeyError, IndexError): - return () + # Try it as a queryset + result = cls._registry[source.model] + except AttributeError: + try: + # Special case for queryset items produced from + # .defer() or .only() optimizations + result = cls._registry[list(source._meta.parents.items())[0][0]] + except (AttributeError, KeyError, IndexError): + result = () except TypeError: # unhashable type: list - return () + result = () - def __init__(self, source, label=None, attribute=None, func=None, kwargs=None, order=None, widget=None): + result = SourceColumn.sort(columns=result) + + if exclude_identifier: + result = [item for item in result if not item.is_identifier] + else: + if only_identifier: + for item in result: + if item.is_identifier: + return item + return None + + final_result = [] + + try: + request = context.request + except AttributeError: + # Simple request extraction failed. Might not be a view context. + # Try alternate method. + try: + request = Variable('request').resolve(context) + except VariableDoesNotExist: + # There is no request variable, most probable a 500 in a test + # view. Don't return any resolved request. + logger.warning( + 'No request variable, aborting request resolution' + ) + return result + + current_view_name = get_current_view_name(request=request) + for item in result: + if item.views: + if current_view_name in item.views: + final_result.append(item) + else: + final_result.append(item) + + return final_result + + def __init__( + self, source, label=None, attribute=None, func=None, + is_absolute_url=False, is_identifier=False, is_sortable=False, + kwargs=None, order=None, sort_field=None, views=None, widget=None + ): self.source = source self._label = label self.attribute = attribute self.func = func + self.is_absolute_url = is_absolute_url + self.is_identifier = is_identifier + self.is_sortable = is_sortable self.kwargs = kwargs or {} self.order = order or 0 + self.sort_field = sort_field + self.views = views or [] + self.widget = widget + self.__class__._registry.setdefault(source, []) self.__class__._registry[source].append(self) - self.widget = widget @property def label(self): @@ -574,7 +631,39 @@ class SourceColumn(object): return self._label + def get_sort_field(self): + if self.sort_field: + return self.sort_field + else: + return self.attribute + + def get_sort_field_querystring(self, context): + # We do this to get an mutable copy we can modify + querystring = context.request.GET.copy() + + previous_sort_field = context.get(TEXT_SORT_FIELD_VARIABLE_NAME, None) + previous_sort_order = context.get( + TEXT_SORT_ORDER_VARIABLE_NAME, TEXT_SORT_ORDER_CHOICE_DESCENDING + ) + + if previous_sort_field != self.get_sort_field(): + sort_order = TEXT_SORT_ORDER_CHOICE_ASCENDING + else: + if previous_sort_order == TEXT_SORT_ORDER_CHOICE_DESCENDING: + sort_order = TEXT_SORT_ORDER_CHOICE_ASCENDING + else: + sort_order = TEXT_SORT_ORDER_CHOICE_DESCENDING + + querystring[TEXT_SORT_FIELD_PARAMETER] = self.get_sort_field() + querystring[TEXT_SORT_ORDER_PARAMETER] = sort_order + + return '?{}'.format(querystring.urlencode()) + def resolve(self, context): + if self.views: + if get_current_view_name(request=context.request) not in self.views: + return + if self.attribute: result = resolve_attribute( attribute=self.attribute, kwargs=self.kwargs, diff --git a/mayan/apps/navigation/templatetags/navigation_tags.py b/mayan/apps/navigation/templatetags/navigation_tags.py index e88af49d25..f4d5666a7e 100644 --- a/mayan/apps/navigation/templatetags/navigation_tags.py +++ b/mayan/apps/navigation/templatetags/navigation_tags.py @@ -48,8 +48,13 @@ def get_multi_item_links_form(context, object_list): return '' -@register.filter -def get_source_columns(source): +@register.simple_tag(takes_context=True) +def navigation_get_sort_field_querystring(context, column): + return column.get_sort_field_querystring(context=context) + + +@register.simple_tag(takes_context=True) +def navigation_get_source_columns(context, source, exclude_identifier=False, only_identifier=False): try: # Is it a query set? source = source.model @@ -68,7 +73,10 @@ def get_source_columns(source): # It a list and it's empty pass - return SourceColumn.get_for_source(source) + return SourceColumn.get_for_source( + context=context, source=source, exclude_identifier=exclude_identifier, + only_identifier=only_identifier + ) @register.simple_tag(takes_context=True) @@ -107,6 +115,10 @@ def resolve_link(context, link): @register.simple_tag(takes_context=True) -def source_column_resolve(context, column): - context['column_result'] = column.resolve(context=context) - return '' +def navigation_source_column_resolve(context, column): + if column: + result = column.resolve(context=context) + return result + else: + return '' +
{% trans 'Identifier' %} + {% if source_column.is_sortable %} + {{ source_column.label }} + {% if navigation_source_column.get_sort_field == sort_field %} + {% if icon_sort %}{{ icon_sort.render }}{% endif %} + {% endif %} + + {% else %} + {{ source_column.label }} + {% endif %} + {{ column.label }} + {% if column.is_sortable %} + {{ column.label }} + {% if column.get_sort_field == sort_field %} + {% if icon_sort %}{{ icon_sort.render }}{% endif %} + {% endif %} + + {% else %} + {{ column.label }} + {% endif %} +
- {% if multi_select_item_properties %} - - {% else %} - + {% if multi_item_actions %} + + + {% if not hide_link %}{{ object }}{% else %}{{ object }}{% endif %}{% if not hide_link %}{{ object }}{% else %}{{ object }}{% endif %} + {% if source_column.is_absolute_url %} + {{ column_value }} + {% else %} + {{ column_value }} + {% endif %} + {% source_column_resolve column=column %}{{ column_result }}{{ object|object_property:column.attribute }} - {% navigation_resolve_menu name='list facet' sort_results=True source=object as facet_menus_results %} - {% for facet_menu_results in facet_menus_results %} - {% for link_group in facet_menu_results.link_groups %} - {% with link_group.links as object_navigation_links %} - {% with 'true' as horizontal %} - {% with 'true' as hide_icon %} - {% include 'navigation/generic_navigation.html' %} - {% endwith %} - {% endwith %} - {% endwith %} - {% endfor %} + + {% if not hide_columns %} + {% navigation_get_source_columns source=object exclude_identifier=True as source_columns %} + {% for column in source_columns %} + +
+ {% navigation_source_column_resolve column=column as column_value %}{{ column_value }} + {# Use explicit 'as column_value ' to force date rendering #} +
+
+ {% navigation_resolve_menu name='list facet' sort_results=True source=object as facet_menus_results %} + {% for facet_menu_results in facet_menus_results %} + {% for link_group in facet_menu_results.link_groups %} + {% with link_group.links as object_navigation_links %} + {% with 'true' as horizontal %} + {% with 'true' as hide_icon %} + {% include 'navigation/generic_navigation.html' %} + {% endwith %} + {% endwith %} + {% endwith %} + {% endfor %} {% endfor %} - {% endfor %} -