Skip to content

Commit ae06133

Browse files
authored
Add support for collapsable filter (Django >= 4.1) (#120)
The markup for change list filters changed between 4.0 and 4.1
1 parent 2fdb039 commit ae06133

File tree

5 files changed

+247
-8
lines changed

5 files changed

+247
-8
lines changed

rangefilter/filters.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ def get_template(self):
141141
if django.VERSION[:2] <= (1, 8):
142142
return "rangefilter/date_filter_1_8.html"
143143

144+
if django.VERSION[:2] <= (4, 0):
145+
return "rangefilter/date_filter_4_0.html"
146+
144147
return "rangefilter/date_filter.html"
145148

146149
template = property(get_template)
@@ -309,6 +312,9 @@ def get_facet_counts(self, pk_attname, filtered_qs):
309312
return {}
310313

311314
def get_template(self):
315+
if django.VERSION[:2] <= (4, 0):
316+
return "rangefilter/numeric_filter_4_0.html"
317+
312318
return "rangefilter/numeric_filter.html"
313319

314320
template = property(get_template)

rangefilter/templates/rangefilter/date_filter.html

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{% load i18n rangefilter_compat %}
2-
<h3>{{ title }}</h3>
2+
<details data-filter-title="{{ title }}" open>
3+
<summary>{% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}</summary>
34
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/widgets.css' %}">
45
<style nonce="{{ spec.request.csp_nonce }}">
56
{% default_css_vars_if_needed %}
@@ -13,9 +14,12 @@ <h3>{{ title }}</h3>
1314
cursor: pointer;
1415
}
1516
.admindatefilter {
16-
padding-left: 15px;
17-
padding-bottom: 10px;
18-
border-bottom: 1px solid var(--border-color);
17+
margin: 5px 0;
18+
padding: 0 15px 15px;
19+
border-bottom: 1px solid var(--hairline-color);
20+
}
21+
.admindatefilter:last-child {
22+
border-bottom: none;
1923
}
2024
.admindatefilter p {
2125
padding-left: 0px;
@@ -157,3 +161,4 @@ <h3>{{ title }}</h3>
157161
</div>
158162
</form>
159163
</div>
164+
</details>
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
{% load i18n rangefilter_compat %}
2+
<h3>{{ title }}</h3>
3+
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/widgets.css' %}">
4+
<style nonce="{{ spec.request.csp_nonce }}">
5+
{% default_css_vars_if_needed %}
6+
.admindatefilter .button, .admindatefilter input[type=submit], .admindatefilter input[type=button], .admindatefilter .submit-row input, .admindatefilter a.button,
7+
.admindatefilter .button, .admindatefilter input[type=reset] {
8+
background: var(--button-bg);
9+
padding: 4px 5px;
10+
border: none;
11+
border-radius: 4px;
12+
color: var(--button-fg);
13+
cursor: pointer;
14+
}
15+
.admindatefilter {
16+
padding-left: 15px;
17+
padding-bottom: 10px;
18+
border-bottom: 1px solid var(--border-color);
19+
}
20+
.admindatefilter p {
21+
padding-left: 0px;
22+
line-height: 0;
23+
}
24+
.admindatefilter p.datetime {
25+
line-height: 0;
26+
}
27+
.admindatefilter .timezonewarning {
28+
display: none;
29+
}
30+
.admindatefilter .datetimeshortcuts a:first-child {
31+
margin-right: 4px;
32+
display: none;
33+
}
34+
.calendarbox {
35+
z-index: 1100;
36+
}
37+
.clockbox {
38+
z-index: 1100;
39+
margin-left: -8em !important;
40+
margin-top: 5em !important;
41+
}
42+
.admindatefilter .datetimeshortcuts {
43+
font-size: 0;
44+
float: right;
45+
position: absolute;
46+
padding-top: 4px;
47+
}
48+
.admindatefilter a {
49+
color: #999;
50+
position: absolute;
51+
padding-top: 3px;
52+
padding-left: 4px;
53+
}
54+
@media (min-width: 768px) {
55+
.calendarbox {
56+
margin-left: -16em !important;
57+
margin-top: 9em !important;
58+
}
59+
}
60+
@media (max-width: 767px) {
61+
.calendarbox {
62+
overflow: visible;
63+
}
64+
}
65+
</style>
66+
67+
{% comment %}
68+
Force load jsi18n, issues #5
69+
https://github.com/django/django/blob/stable/1.10.x/django/contrib/admin/templates/admin/change_list.html#L7
70+
{% endcomment %}
71+
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
72+
<script type="text/javascript" nonce="{{ spec.request.csp_nonce }}">
73+
django.jQuery('document').ready(function () {
74+
django.jQuery('.admindatefilter #{{ choices.0.system_name }}-form input[type="submit"]').click(function(event) {
75+
event.preventDefault();
76+
var form = django.jQuery(this).closest('div.admindatefilter').find('form');
77+
var query_string = django.jQuery('input#{{ choices.0.system_name }}-query-string').val();
78+
var form_data = form.serialize();
79+
var amp = query_string === "?" ? "" : "&"; // avoid leading ?& combination
80+
window.location = window.location.pathname + query_string + amp + form_data;
81+
});
82+
83+
django.jQuery('.admindatefilter #{{ choices.0.system_name }}-form input[type="reset"]').click(function() {
84+
var form = django.jQuery(this).closest('div.admindatefilter').find('form');
85+
var query_string = form.find('input#{{ choices.0.system_name }}-query-string').val();
86+
window.location = window.location.pathname + query_string;
87+
});
88+
});
89+
{% comment %}
90+
// Code below makes sure that the DateTimeShortcuts.js is loaded exactly once
91+
// regardless the presence of AdminDateWidget
92+
// How it worked:
93+
// - First Django loads the model formset with predefined widgets for different
94+
// field types. If there's a date based field, then it loads the AdminDateWidget
95+
// and it's required media to context under {{media.js}} in admin/change_list.html.
96+
// (Note: it accumulates media in django.forms.widgets.Media object,
97+
// which prevents duplicates, but the DateRangeFilter is not included yet
98+
// since it's not model field related.
99+
// List of predefined widgets is in django.contrib.admin.options.FORMFIELD_FOR_DBFIELD_DEFAULTS)
100+
// - After that Django starts rendering forms, which have the {{form.media}}
101+
// tag. Only then the DjangoRangeFilter.get_media is called and rendered,
102+
// which creates the duplicates.
103+
// How it works:
104+
// - first step is the same, if there's a AdminDateWidget to be loaded then
105+
// nothing changes
106+
// - DOM gets rendered and if the AdminDateWidget was rendered then
107+
// the DateTimeShortcuts.js is initiated which sets the window.DateTimeShortcuts.
108+
// Otherwise, the window.DateTimeShortcuts is undefined.
109+
// - The lines below check if the DateTimeShortcuts has been set and if not
110+
// then the DateTimeShortcuts.js and calendar.js is rendered
111+
//
112+
// https://github.com/silentsokolov/django-admin-rangefilter/issues/9
113+
//
114+
// Django 2.1
115+
// https://github.com/silentsokolov/django-admin-rangefilter/issues/21
116+
{% endcomment %}
117+
function embedScript(url) {
118+
return new Promise(function pr(resolve, reject) {
119+
var newScript = document.createElement("script");
120+
newScript.type = "text/javascript";
121+
newScript.src = url;
122+
newScript.onload = resolve;
123+
if ("{{ spec.request.csp_nonce }}" !== "") {
124+
newScript.setAttribute("nonce", "{{ spec.request.csp_nonce }}");
125+
}
126+
document.head.appendChild(newScript);
127+
});
128+
}
129+
130+
django.jQuery('document').ready(function () {
131+
if (!('DateTimeShortcuts' in window)) {
132+
var promiseList = [];
133+
134+
{% for m in spec.form.js %}
135+
promiseList.push(embedScript("{{ m }}"));
136+
{% endfor %}
137+
138+
Promise.all(promiseList).then(function() {
139+
django.jQuery('.datetimeshortcuts').remove();
140+
if ('DateTimeShortcuts' in window) {
141+
window.DateTimeShortcuts.init();
142+
}
143+
});
144+
}
145+
});
146+
</script>
147+
{% block quick-select-choices %}{% endblock %}
148+
<div class="admindatefilter">
149+
<form method="GET" action="." id="{{ choices.0.system_name }}-form">
150+
{{ spec.form.as_p }}
151+
{% for choice in choices %}
152+
<input type="hidden" id="{{ choice.system_name }}-query-string" value="{{ choice.query_string }}">
153+
{% endfor %}
154+
<div class="controls">
155+
<input type="submit" class="button" value="{% trans "Search" %}">
156+
<input type="reset" class="button" value="{% trans "Reset" %}">
157+
</div>
158+
</form>
159+
</div>

rangefilter/templates/rangefilter/numeric_filter.html

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{% load i18n rangefilter_compat %}
2-
<h3>{{ title }}</h3>
2+
<details data-filter-title="{{ title }}" open>
3+
<summary>{% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}</summary>
34
<style nonce="{{ spec.request.csp_nonce }}">
45
{% default_css_vars_if_needed %}
56
.numericrangefilter .button, .numericrangefilter input[type=submit], .numericrangefilter input[type=button], .numericrangefilter .submit-row input, .numericrangefilter a.button,
@@ -12,9 +13,12 @@ <h3>{{ title }}</h3>
1213
cursor: pointer;
1314
}
1415
.numericrangefilter {
15-
padding-left: 15px;
16-
padding-bottom: 10px;
17-
border-bottom: 1px solid var(--border-color);
16+
margin: 5px 0;
17+
padding: 0 15px 15px;
18+
border-bottom: 1px solid var(--hairline-color);
19+
}
20+
.numericrangefilter:last-child {
21+
border-bottom: none;
1822
}
1923
.numericrangefilter p {
2024
padding-left: 0px;
@@ -62,3 +66,4 @@ <h3>{{ title }}</h3>
6266
</div>
6367
</form>
6468
</div>
69+
</details>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{% load i18n rangefilter_compat %}
2+
<h3>{{ title }}</h3>
3+
<style nonce="{{ spec.request.csp_nonce }}">
4+
{% default_css_vars_if_needed %}
5+
.numericrangefilter .button, .numericrangefilter input[type=submit], .numericrangefilter input[type=button], .numericrangefilter .submit-row input, .numericrangefilter a.button,
6+
.numericrangefilter .button, .numericrangefilter input[type=reset] {
7+
background: var(--button-bg);
8+
padding: 4px 5px;
9+
border: none;
10+
border-radius: 4px;
11+
color: var(--button-fg);
12+
cursor: pointer;
13+
}
14+
.numericrangefilter {
15+
padding-left: 15px;
16+
padding-bottom: 10px;
17+
border-bottom: 1px solid var(--border-color);
18+
}
19+
.numericrangefilter p {
20+
padding-left: 0px;
21+
display: inline;
22+
}
23+
.numericrangefilter p input {
24+
margin-bottom: 10px;
25+
width: 70px;
26+
}
27+
</style>
28+
29+
{% comment %}
30+
Force load jsi18n, issues #5
31+
https://github.com/django/django/blob/stable/1.10.x/django/contrib/admin/templates/admin/change_list.html#L7
32+
{% endcomment %}
33+
34+
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
35+
<script type="text/javascript" nonce="{{ spec.request.csp_nonce }}">
36+
django.jQuery('document').ready(function () {
37+
django.jQuery('.numericrangefilter #{{ choices.0.system_name }}-form input[type="submit"]').click(function(event) {
38+
event.preventDefault();
39+
var form = django.jQuery(this).closest('div.numericrangefilter').find('form');
40+
var query_string = django.jQuery('input#{{ choices.0.system_name }}-query-string').val();
41+
var form_data = form.serialize();
42+
var amp = query_string === "?" ? "" : "&"; // avoid leading ?& combination
43+
window.location = window.location.pathname + query_string + amp + form_data;
44+
});
45+
46+
django.jQuery('.numericrangefilter #{{ choices.0.system_name }}-form input[type="reset"]').click(function() {
47+
var form = django.jQuery(this).closest('div.numericrangefilter').find('form');
48+
var query_string = form.find('input#{{ choices.0.system_name }}-query-string').val();
49+
window.location = window.location.pathname + query_string;
50+
});
51+
});
52+
</script>
53+
<div class="numericrangefilter">
54+
<form method="GET" action="." id="{{ choices.0.system_name }}-form">
55+
{{ spec.form.as_p }}
56+
{% for choice in choices %}
57+
<input type="hidden" id="{{ choice.system_name }}-query-string" value="{{ choice.query_string }}">
58+
{% endfor %}
59+
<div class="controls">
60+
<input type="submit" class="button" value="{% trans "Search" %}">
61+
<input type="reset" class="button" value="{% trans "Reset" %}">
62+
</div>
63+
</form>
64+
</div>

0 commit comments

Comments
 (0)