Skip to content

Commit c8bcc33

Browse files
author
Lukas
committed
initial
1 parent c02bdd9 commit c8bcc33

File tree

12 files changed

+734
-0
lines changed

12 files changed

+734
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,5 @@ venv.bak/
102102

103103
# mypy
104104
.mypy_cache/
105+
.DS_Store
106+
.idea

admin_numeric_filter/__init__.py

Whitespace-only changes.

admin_numeric_filter/admin.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from django.contrib import admin
2+
from django.contrib.admin.utils import reverse_field_path
3+
from django.db.models import Min, Max
4+
5+
from .forms import RangeNumericForm, SingleNumericForm, SliderNumericForm
6+
7+
8+
class SingleNumericFilter(admin.FieldListFilter):
9+
parameter_name = None
10+
template = 'admin/filter_numeric_single.html'
11+
12+
def __init__(self, field, request, params, model, model_admin, field_path):
13+
super().__init__(field, request, params, model, model_admin, field_path)
14+
15+
if self.parameter_name is None:
16+
self.parameter_name = self.field.name
17+
18+
if self.parameter_name in params:
19+
value = params.pop(self.parameter_name)
20+
self.used_parameters[self.parameter_name] = value
21+
22+
def queryset(self, request, queryset):
23+
if self.value():
24+
return queryset.filter(**{self.parameter_name: self.value()})
25+
26+
def value(self):
27+
return self.used_parameters.get(self.parameter_name, None)
28+
29+
def expected_parameters(self):
30+
return [self.parameter_name]
31+
32+
def choices(self, changelist):
33+
return ({
34+
'form': SingleNumericForm(name=self.parameter_name, data={self.parameter_name: self.value()}),
35+
}, )
36+
37+
38+
class RangeNumericFilter(admin.FieldListFilter):
39+
parameter_name = None
40+
template = 'admin/filter_numeric_range.html'
41+
42+
def __init__(self, field, request, params, model, model_admin, field_path):
43+
super().__init__(field, request, params, model, model_admin, field_path)
44+
45+
if self.parameter_name is None:
46+
self.parameter_name = self.field.name
47+
48+
if self.parameter_name + '_from' in params:
49+
value = params.pop(self.parameter_name + '_from')
50+
self.used_parameters[self.parameter_name + '_from'] = value
51+
52+
if self.parameter_name + '_to' in params:
53+
value = params.pop(self.parameter_name + '_to')
54+
self.used_parameters[self.parameter_name + '_to'] = value
55+
56+
def queryset(self, request, queryset):
57+
filters = {}
58+
59+
if self.used_parameters.get(self.parameter_name + '_from', None) is not None:
60+
filters.update({
61+
self.parameter_name + '__gte': self.used_parameters.get(self.parameter_name + '_from', None),
62+
})
63+
64+
if self.used_parameters.get(self.parameter_name + '_to', None) is not None:
65+
filters.update({
66+
self.parameter_name + '__lte': self.used_parameters.get(self.parameter_name + '_to', None),
67+
})
68+
69+
return queryset.filter(**filters)
70+
71+
def expected_parameters(self):
72+
return [
73+
'{}_from'.format(self.parameter_name),
74+
'{}_to'.format(self.parameter_name),
75+
]
76+
77+
def choices(self, changelist):
78+
return ({
79+
'form': RangeNumericForm(name=self.parameter_name, data={
80+
self.parameter_name + '_from': self.used_parameters.get(self.parameter_name + '_from', None),
81+
self.parameter_name + '_to': self.used_parameters.get(self.parameter_name + '_to', None),
82+
}),
83+
}, )
84+
85+
86+
class SliderNumericFilter(RangeNumericFilter):
87+
template = 'admin/filter_numeric_slider.html'
88+
89+
def __init__(self, field, request, params, model, model_admin, field_path):
90+
super().__init__(field, request, params, model, model_admin, field_path)
91+
92+
parent_model, reverse_path = reverse_field_path(model, field_path)
93+
94+
if model == parent_model:
95+
self.q = model_admin.get_queryset(request)
96+
else:
97+
self.q = parent_model._default_manager.all()
98+
99+
def choices(self, changelist):
100+
min = self.q.all().aggregate(min=Min(self.parameter_name)).get('min', 0)
101+
max = self.q.all().aggregate(max=Max(self.parameter_name)).get('max', 0)
102+
103+
return ({
104+
'min': min,
105+
'max': max,
106+
'value_from': self.used_parameters.get(self.parameter_name + '_from', min),
107+
'value_to': self.used_parameters.get(self.parameter_name + '_to', max),
108+
'form': SliderNumericForm(name=self.parameter_name, data={
109+
self.parameter_name + '_from': self.used_parameters.get(self.parameter_name + '_from', min),
110+
self.parameter_name + '_to': self.used_parameters.get(self.parameter_name + '_to', max),
111+
})
112+
}, )

admin_numeric_filter/forms.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from django import forms
2+
from django.utils.translation import ugettext_lazy as _
3+
4+
5+
class SingleNumericForm(forms.Form):
6+
def __init__(self, *args, **kwargs):
7+
name = kwargs.pop('name')
8+
super().__init__(*args, **kwargs)
9+
10+
self.fields[name] = forms.FloatField(label='', required=False,
11+
widget=forms.NumberInput(attrs={'placeholder': _('Value')}))
12+
13+
14+
class RangeNumericForm(forms.Form):
15+
name = None
16+
17+
def __init__(self, *args, **kwargs):
18+
self.name = kwargs.pop('name')
19+
super().__init__(*args, **kwargs)
20+
21+
self.fields[self.name + '_from'] = forms.FloatField(label='', required=False,
22+
widget=forms.NumberInput(attrs={'placeholder': _('From'),}))
23+
self.fields[self.name + '_to'] = forms.FloatField(label='', required=False,
24+
widget=forms.NumberInput(attrs={'placeholder': _('To'),}))
25+
26+
27+
class SliderNumericForm(RangeNumericForm):
28+
class Media:
29+
css = {
30+
'all': (
31+
'js/nouislider.min.css',
32+
'css/admin-numeric-filter.css',
33+
)
34+
}
35+
js = (
36+
'js/wnumb.js',
37+
'js/nouislider.min.js',
38+
'js/admin-numeric-filter.js',
39+
)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#changelist-filter .admin-numeric-filter-wrapper {
2+
border-bottom: 1px solid #eaeaea;
3+
overflow: hidden;
4+
padding: 0 15px 15px 15px;
5+
}
6+
7+
#changelist-filter .admin-numeric-filter-wrapper h3 {
8+
padding-left: 0;
9+
padding-right: 0;
10+
}
11+
12+
#changelist-filter .admin-numeric-filter-wrapper p {
13+
display: flex;
14+
flex-direction: column;
15+
padding-left: 0;
16+
padding-right: 0;
17+
}
18+
19+
#changelist-filter .admin-numeric-filter-wrapper p label {
20+
color: #999;
21+
display: block;
22+
font-size: 13px;
23+
margin: 0 0 3px 0;
24+
}
25+
26+
#changelist-filter .admin-numeric-filter-wrapper p input {
27+
box-sizing: border-box;
28+
width: 100%;
29+
}
30+
31+
#changelist-filter .admin-numeric-filter-wrapper p:first-child {
32+
margin-right: 5px;
33+
}
34+
35+
#changelist-filter .admin-numeric-filter-wrapper p:last-child {
36+
margin-left: 5px;
37+
}
38+
39+
#changelist-filter .admin-numeric-filter-wrapper button {
40+
background-color: transparent;
41+
border: 0;
42+
color: #447e9b;
43+
cursor: pointer;
44+
float: right;
45+
font-size: 12px;
46+
font-weight: 600;
47+
padding: 0;
48+
}
49+
50+
#changelist-filter .admin-numeric-filter-wrapper-group {
51+
display: flex;
52+
flex-direction: row;
53+
}
54+
55+
#changelist-filter .admin-numeric-filter-wrapper-group.hidden {
56+
display: none;
57+
}
58+
59+
#changelist-filter .admin-numeric-filter-slider {
60+
background-color: rgba(0, 0, 0, .12);
61+
border: 0;
62+
box-shadow: none;
63+
height: 3px;
64+
margin: 15px 0;
65+
}
66+
67+
#changelist-filter .admin-numeric-filter-slider .noUi-handle {
68+
background-color: #fff;
69+
border: 0;
70+
border-radius: 50%;
71+
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
72+
cursor: pointer;
73+
height: 14px;
74+
right: -13px;
75+
transition: all .2s ease;
76+
width: 14px;
77+
}
78+
79+
#changelist-filter .admin-numeric-filter-slider .noUi-handle:focus,
80+
#changelist-filter .admin-numeric-filter-slider .noUi-handle:hover {
81+
box-shadow: 0 1px 3px rgba(0, 0, 0, .3), 0 0 0 6px rgba(121, 174, 200, .2);
82+
outline: none;
83+
}
84+
85+
#changelist-filter .admin-numeric-filter-slider .noUi-handle-upper {
86+
right: -1px;
87+
}
88+
89+
#changelist-filter .admin-numeric-filter-slider .noUi-handle:after {
90+
content: none;
91+
}
92+
93+
#changelist-filter .admin-numeric-filter-slider .noUi-handle:before {
94+
content: none;
95+
}
96+
97+
#changelist-filter .admin-numeric-filter-slider .noUi-connect {
98+
background-color: #79aec8;
99+
}
100+
101+
#changelist-filter .admin-numeric-filter-slider-tooltips {
102+
color: #999;
103+
display: flex;
104+
font-size: 12px;
105+
flex-direction: row;
106+
margin: 10px 0;
107+
}
108+
109+
#changelist-filter .admin-numeric-filter-slider-tooltip-from {
110+
margin: 0 auto 0 0;
111+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
var sliders = document.getElementsByClassName('admin-numeric-filter-slider');
2+
3+
for (var i = 0; i < sliders.length; i++) {
4+
var slider = sliders[i];
5+
6+
if (slider.classList.contains('noUi-target')) {
7+
continue;
8+
}
9+
10+
if (slider) {
11+
var from = parseInt(slider.closest('.admin-numeric-filter-wrapper').querySelectorAll('input')[0].value);
12+
var to = parseInt(slider.closest('.admin-numeric-filter-wrapper').querySelectorAll('input')[1].value);
13+
14+
var min = parseInt(slider.getAttribute('data-min'));
15+
var max = parseInt(slider.getAttribute('data-max'));
16+
17+
noUiSlider.create(slider, {
18+
start: [from, to],
19+
step: 1,
20+
connect: true,
21+
format: wNumb({
22+
decimals: 0
23+
}),
24+
range: {
25+
'min': min,
26+
'max': max
27+
}
28+
});
29+
30+
slider.noUiSlider.on('update', function(values, handle) {
31+
var parent = this.target.closest('.admin-numeric-filter-wrapper');
32+
var from = parent.querySelectorAll('input')[0];
33+
var to = parent.querySelectorAll('input')[1];
34+
35+
parent.querySelectorAll('.admin-numeric-filter-slider-tooltip-from')[0].innerHTML = values[0];
36+
parent.querySelectorAll('.admin-numeric-filter-slider-tooltip-to')[0].innerHTML = values[1];
37+
38+
from.value = values[0];
39+
to.value = values[1];
40+
});
41+
}
42+
}

admin_numeric_filter/static/js/nouislider.min.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

admin_numeric_filter/static/js/nouislider.min.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)