diff --git a/admin_interface/templates/admin/change_list.html b/admin_interface/templates/admin/change_list.html index 55e654d..ad40e5d 100644 --- a/admin_interface/templates/admin/change_list.html +++ b/admin_interface/templates/admin/change_list.html @@ -6,9 +6,11 @@ {% if cl.has_filters %}

{% translate 'Filter' %}

- {% if cl.has_active_filters %} + {% get_admin_interface_active_date_hierarchy cl as active_date_hierarchy %} {% get_admin_interface_setting "list_filter_removal_links" as list_filter_removal_links %} - {% if list_filter_removal_links %} + {% if cl.has_active_filters %} + {% if list_filter_removal_links %} + {% if active_date_hierarchy %}{% admin_interface_date_hierarchy_removal_link cl active_date_hierarchy %}{% endif %} {% for spec in cl.filter_specs %}{% admin_interface_filter_removal_link cl spec %}{% endfor %}

{% translate "Clear all filters" %} ✖ @@ -18,7 +20,12 @@ {# Translators: don't translate this, the django catalog already contains it #} ✖ {% translate "Clear all filters" %}

- {% endif %} + {% endif %} + {% elif active_date_hierarchy and list_filter_removal_links %} + {% admin_interface_date_hierarchy_removal_link cl active_date_hierarchy %} +

+ {% translate "Clear all filters" %} ✖ +

{% endif %} {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
diff --git a/admin_interface/templates/admin_interface/date_hierarchy_removal_link.html b/admin_interface/templates/admin_interface/date_hierarchy_removal_link.html new file mode 100644 index 0000000..bab4a3a --- /dev/null +++ b/admin_interface/templates/admin_interface/date_hierarchy_removal_link.html @@ -0,0 +1,3 @@ +
+ {{ date_label|capfirst }}: {{ date_value|date:date_format|capfirst }} ✖ +
diff --git a/admin_interface/templates/admin_interface/list_filter_removal_link.html b/admin_interface/templates/admin_interface/list_filter_removal_link.html index f86e4a4..9b54c75 100644 --- a/admin_interface/templates/admin_interface/list_filter_removal_link.html +++ b/admin_interface/templates/admin_interface/list_filter_removal_link.html @@ -1,6 +1,5 @@ -{% load admin_interface_tags %} {% if spec.lookup_val or spec.value %} -
- {{ title|capfirst }}: {{ selected_value }} ✖ +
+ {{ title|capfirst }}: {{ selected_value }} ✖
{% endif %} diff --git a/admin_interface/templatetags/admin_interface_tags.py b/admin_interface/templatetags/admin_interface_tags.py index 87e71ce..2855562 100644 --- a/admin_interface/templatetags/admin_interface_tags.py +++ b/admin_interface/templatetags/admin_interface_tags.py @@ -1,9 +1,9 @@ +import datetime import hashlib import re from django import template from django.conf import settings -from django.template.loader import get_template from django.urls import NoReverseMatch, reverse from django.utils import translation @@ -93,14 +93,23 @@ def get_admin_interface_nocache(): return hash_string(__version__) -@register.simple_tag() -def admin_interface_clear_filter_qs(changelist, list_filter): - return changelist.get_query_string(remove=list_filter.expected_parameters()) +@register.simple_tag(takes_context=False) +def get_admin_interface_active_date_hierarchy(changelist): + date_field = changelist.date_hierarchy + if not date_field: + return + + params = changelist.get_filters_params() + # link to clear all filters contains 'date_field__gte', + # only filters with specific year are really active + if f"{date_field}__year" not in params: + return + + return date_field -@register.simple_tag() +@register.inclusion_tag("admin_interface/list_filter_removal_link.html") def admin_interface_filter_removal_link(changelist, list_filter): - template = get_template("admin_interface/list_filter_removal_link.html") title = list_filter.title choices = [ choice for choice in list_filter.choices(changelist) if choice.get("selected") @@ -110,14 +119,46 @@ def admin_interface_filter_removal_link(changelist, list_filter): except (IndexError, KeyError): value = "..." - return template.render( - { - "cl": changelist, - "spec": list_filter, - "selected_value": value, - "title": title, - } - ) + removal_link = changelist.get_query_string(remove=list_filter.expected_parameters()) + + return { + "cl": changelist, + "spec": list_filter, + "selected_value": value, + "title": title, + "removal_link": removal_link, + } + + +@register.inclusion_tag("admin_interface/date_hierarchy_removal_link.html") +def admin_interface_date_hierarchy_removal_link(changelist, date_field): + date_label = changelist.model._meta.get_field(date_field).verbose_name + + params = changelist.get_filters_params() + date_params = [p for p in params if p.startswith(date_field)] + + date_args = [int(params[f"{date_field}__year"]), 1, 1] + date_format = "Y" + + if f"{date_field}__month" in params: + date_args[1] = int(params[f"{date_field}__month"]) + date_format = "YEAR_MONTH_FORMAT" + + if f"{date_field}__day" in params: + date_args[2] = int(params[f"{date_field}__day"]) + date_format = "DATE_FORMAT" + + date_value = datetime.date(*date_args) + + removal_link = changelist.get_query_string(remove=date_params) + + return { + "cl": changelist, + "date_label": date_label, + "date_value": date_value, + "date_format": date_format, + "removal_link": removal_link, + } @register.simple_tag() diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index d25c300..9cf8788 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -1,3 +1,7 @@ +from datetime import date +from unittest.mock import Mock + +from django.contrib.admin.views.main import ChangeList from django.template import Context, Template from django.test import TestCase, override_settings from django.test.client import RequestFactory @@ -137,6 +141,10 @@ class AdminInterfaceTemplateTagsTestCase(TestCase): ) self.assertEqual(rendered, "Django") + def test_get_setting(self): + title = templatetags.get_admin_interface_setting("title") + self.assertEqual(title, "Django administration") + def test_get_version(self): version = templatetags.get_admin_interface_version() self.assertEqual(version, __version__) @@ -164,3 +172,121 @@ class AdminInterfaceTemplateTagsTestCase(TestCase): "admin/edit_inline/stacked.html" ) self.assertEqual(headless_template, "admin/edit_inline/headerless_stacked.html") + + def test_get_active_date_hierarchy_none(self): + changelist = Mock() + changelist.date_hierarchy = None + + date_field = templatetags.get_admin_interface_active_date_hierarchy(changelist) + + self.assertIsNone(date_field) + + def test_get_active_date_hierarchy_inactive(self): + changelist = Mock() + changelist.date_hierarchy = "last_login" + changelist.get_filters_params.return_value = {} + + date_field = templatetags.get_admin_interface_active_date_hierarchy(changelist) + + self.assertIsNone(date_field) + + def test_get_active_date_hierarchy_active(self): + changelist = Mock() + changelist.date_hierarchy = "last_login" + params = {"some_field": 2, "last_login__year": 2022} + changelist.get_filters_params.return_value = params + + date_field = templatetags.get_admin_interface_active_date_hierarchy(changelist) + + self.assertEqual(date_field, "last_login") + + def _add_changelist_methods(self, mock, params): + def get_query_string(**kwargs): + return ChangeList.get_query_string(mock, **kwargs) + + def get_filters_params(**kwargs): + return ChangeList.get_filters_params(mock, **kwargs) + + mock.get_query_string = get_query_string + mock.get_filters_params = get_filters_params + mock.params = params + + def test_filter_removal_link(self): + changelist = Mock() + params = {"shape": "pointy", "size": "small"} + self._add_changelist_methods(changelist, params) + list_filter = Mock() + list_filter.title = "Shape filter" + choices = [{"display": "Round"}, {"display": "Pointy", "selected": True}] + list_filter.choices.return_value = choices + list_filter.expected_parameters.return_value = ("shape",) + + ctx = templatetags.admin_interface_filter_removal_link(changelist, list_filter) + + self.assertEqual(ctx["removal_link"], "?size=small") + self.assertEqual(ctx["title"], "Shape filter") + self.assertEqual(ctx["selected_value"], "Pointy") + + def test_filter_removal_link_no_display(self): + changelist = Mock() + params = {"shape": "pointy", "size": "small"} + self._add_changelist_methods(changelist, params) + list_filter = Mock() + list_filter.title = "Shape filter" + choices = [{"other": "Round"}, {"other": "Pointy", "selected": True}] + list_filter.choices.return_value = choices + list_filter.expected_parameters.return_value = ("shape",) + + ctx = templatetags.admin_interface_filter_removal_link(changelist, list_filter) + + self.assertEqual(ctx["removal_link"], "?size=small") + self.assertEqual(ctx["title"], "Shape filter") + self.assertEqual(ctx["selected_value"], "...") + + def test_date_hierarchy_removal_link_year(self): + changelist = Mock() + params = {"shape": "pointy", "last_login__year": 2022} + self._add_changelist_methods(changelist, params) + changelist.model._meta.get_field.return_value.verbose_name = "last login" + + ctx = templatetags.admin_interface_date_hierarchy_removal_link( + changelist, "last_login" + ) + + self.assertEqual(ctx["removal_link"], "?shape=pointy") + self.assertEqual(ctx["date_label"], "last login") + self.assertEqual(ctx["date_value"], date(2022, 1, 1)) + + def test_date_hierarchy_removal_link_year_month(self): + changelist = Mock() + changelist.model._meta.get_field.return_value.verbose_name = "last login" + params = {"last_login__year": 2022, "last_login__month": "11"} + self._add_changelist_methods(changelist, params) + + ctx = templatetags.admin_interface_date_hierarchy_removal_link( + changelist, "last_login" + ) + + self.assertEqual(ctx["removal_link"], "?") + self.assertEqual(ctx["date_label"], "last login") + self.assertEqual(ctx["date_value"], date(2022, 11, 1)) + + def test_date_hierarchy_removal_link_year_month_day(self): + changelist = Mock() + changelist.model._meta.get_field.return_value.verbose_name = "last login" + params = { + "last_login__year": 2022, + "last_login__month": "11", + "last_login__day": "30", + "shape": "round", + "size": "small", + } + self._add_changelist_methods(changelist, params) + + ctx = templatetags.admin_interface_date_hierarchy_removal_link( + changelist, "last_login" + ) + + self.assertEqual(ctx["removal_link"], "?shape=round&size=small") + self.assertEqual(ctx["date_label"], "last login") + self.assertEqual(ctx["date_value"], date(2022, 11, 30)) diff --git a/tox.ini b/tox.ini index 3d1deee..2471ec1 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ python = 3.11: py311 [testenv] -passenv = CI GITHUB_WORKFLOW +passenv = CI,GITHUB_WORKFLOW deps = dj22: Django == 2.2.* dj30: Django == 3.0.*