Include date hierarchy in quick removal links (#218)
* Include date hierarchy in quick removal links * use date field name in removal link * reduce diff * tweak some details * start adding tests * use mock without spec :( it's not easy to instantiate ChangeList, and speccing from the class means that many attributes are not recognized * more tests * more tests * more checks for generated URLs * better tags and tests * compat for tox 4master
parent
c414c3ad4d
commit
19cbeead58
|
|
@ -6,9 +6,11 @@
|
||||||
{% if cl.has_filters %}
|
{% if cl.has_filters %}
|
||||||
<div id="changelist-filter">
|
<div id="changelist-filter">
|
||||||
<h2>{% translate 'Filter' %}</h2>
|
<h2>{% translate 'Filter' %}</h2>
|
||||||
{% 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 %}
|
{% 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 %}
|
{% for spec in cl.filter_specs %}{% admin_interface_filter_removal_link cl spec %}{% endfor %}
|
||||||
<h3 id="changelist-filter-clear">
|
<h3 id="changelist-filter-clear">
|
||||||
<a href="{{ cl.clear_all_filters_qs }}">{% translate "Clear all filters" %} ✖</a>
|
<a href="{{ cl.clear_all_filters_qs }}">{% translate "Clear all filters" %} ✖</a>
|
||||||
|
|
@ -18,7 +20,12 @@
|
||||||
{# Translators: don't translate this, the django catalog already contains it #}
|
{# Translators: don't translate this, the django catalog already contains it #}
|
||||||
<a href="{{ cl.clear_all_filters_qs }}">✖ {% translate "Clear all filters" %}</a>
|
<a href="{{ cl.clear_all_filters_qs }}">✖ {% translate "Clear all filters" %}</a>
|
||||||
</h3>
|
</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% elif active_date_hierarchy and list_filter_removal_links %}
|
||||||
|
{% admin_interface_date_hierarchy_removal_link cl active_date_hierarchy %}
|
||||||
|
<h3 id="changelist-filter-clear">
|
||||||
|
<a href="{{ cl.clear_all_filters_qs }}">{% translate "Clear all filters" %} ✖</a>
|
||||||
|
</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
|
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="changelist-filter-clear"><a href="{{ removal_link }}">
|
||||||
|
{{ date_label|capfirst }}: <span>{{ date_value|date:date_format|capfirst }}</span> ✖
|
||||||
|
</a></div>
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
{% load admin_interface_tags %}
|
|
||||||
{% if spec.lookup_val or spec.value %}
|
{% if spec.lookup_val or spec.value %}
|
||||||
<div class="changelist-filter-clear"><a href="{% admin_interface_clear_filter_qs cl spec %}">
|
<div class="changelist-filter-clear"><a href="{{ removal_link }}">
|
||||||
{{ title|capfirst }}: <span>{{ selected_value }}</span> ✖
|
{{ title|capfirst }}: <span>{{ selected_value }}</span> ✖
|
||||||
</a></div>
|
</a></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.loader import get_template
|
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
|
|
||||||
|
|
@ -93,14 +93,23 @@ def get_admin_interface_nocache():
|
||||||
return hash_string(__version__)
|
return hash_string(__version__)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag(takes_context=False)
|
||||||
def admin_interface_clear_filter_qs(changelist, list_filter):
|
def get_admin_interface_active_date_hierarchy(changelist):
|
||||||
return changelist.get_query_string(remove=list_filter.expected_parameters())
|
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):
|
def admin_interface_filter_removal_link(changelist, list_filter):
|
||||||
template = get_template("admin_interface/list_filter_removal_link.html")
|
|
||||||
title = list_filter.title
|
title = list_filter.title
|
||||||
choices = [
|
choices = [
|
||||||
choice for choice in list_filter.choices(changelist) if choice.get("selected")
|
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):
|
except (IndexError, KeyError):
|
||||||
value = "..."
|
value = "..."
|
||||||
|
|
||||||
return template.render(
|
removal_link = changelist.get_query_string(remove=list_filter.expected_parameters())
|
||||||
{
|
|
||||||
"cl": changelist,
|
return {
|
||||||
"spec": list_filter,
|
"cl": changelist,
|
||||||
"selected_value": value,
|
"spec": list_filter,
|
||||||
"title": title,
|
"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()
|
@register.simple_tag()
|
||||||
|
|
|
||||||
|
|
@ -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.template import Context, Template
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
|
@ -137,6 +141,10 @@ class AdminInterfaceTemplateTagsTestCase(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(rendered, "Django")
|
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):
|
def test_get_version(self):
|
||||||
version = templatetags.get_admin_interface_version()
|
version = templatetags.get_admin_interface_version()
|
||||||
self.assertEqual(version, __version__)
|
self.assertEqual(version, __version__)
|
||||||
|
|
@ -164,3 +172,121 @@ class AdminInterfaceTemplateTagsTestCase(TestCase):
|
||||||
"admin/edit_inline/stacked.html"
|
"admin/edit_inline/stacked.html"
|
||||||
)
|
)
|
||||||
self.assertEqual(headless_template, "admin/edit_inline/headerless_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))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue