Skip to content

Commit 5fd351c

Browse files
authored
MultiChoiceExt without linked db field (#8)
1 parent 4ed7c0b commit 5fd351c

File tree

7 files changed

+109
-45
lines changed

7 files changed

+109
-45
lines changed

README.md

+3-4
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ Each element of the list consists of three values.
154154
- checkbox label
155155
- filtering expression applied to the DB model in the form of [Django Q-objects](https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects)
156156

157+
In the `parameter_name` attribute, you need to specify the name of the GET request parameter for sending filter data.
158+
157159
For our example, the code will look like this.
158160

159161
```python
@@ -163,6 +165,7 @@ from django_admin_filters import MultiChoiceExt
163165

164166
class ColorFilter(MultiChoiceExt):
165167
FILTER_LABEL = "By color"
168+
parameter_name = "color"
166169
options = [
167170
('red', 'Red', Q(is_online=False)),
168171
('yellow', 'Yellow', Q(is_online=True) & (Q(is_trouble1=True) | Q(is_trouble2=True))),
@@ -176,10 +179,6 @@ class Admin(admin.ModelAdmin):
176179
admin.site.register(Log, Admin)
177180
```
178181

179-
When specifying the field to which the filter is applied, you must specify the name of an existing model field (for example, 'is_online' in the example above),
180-
and not the name of the virtual property ('color').
181-
You can specify the name of any field in the model. This is necessary so that Django will recognize it as valid when creating an instance of the filter.
182-
183182
Otherwise, the behavior and settings of the `MultiChoiceExt` filter are similar to the `MultiChoice` filter described earlier.
184183

185184
## DateRange and DateRangePicker filters

READMEru.md

+3-4
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ admin.site.register(Log, Admin)
149149
- текст у чекбокса
150150
- применяемое к таблице модели в БД выражение фильтрации в виде [Q-объектов Django](https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects)
151151

152+
В атрибуте `parameter_name` нужно указать имя параметра GET-запроса, в котором будут передаваться данные фильтра.
153+
152154
Для нашего примера код будет таким.
153155

154156
```python
@@ -158,6 +160,7 @@ from django_admin_filters import MultiChoiceExt
158160

159161
class ColorFilter(MultiChoiceExt):
160162
FILTER_LABEL = "По цвету"
163+
parameter_name = "color"
161164
options = [
162165
('red', 'Red', Q(is_online=False)),
163166
('yellow', 'Yellow', Q(is_online=True) & (Q(is_trouble1=True) | Q(is_trouble2=True))),
@@ -171,10 +174,6 @@ class Admin(admin.ModelAdmin):
171174
admin.site.register(Log, Admin)
172175
```
173176

174-
При указании поля, к которому применяется фильтр, нужно указывать имя существующего поля модели (например, 'is_online' в примере выше),
175-
а не имя виртуального свойства ('color').
176-
Можно указывать имя любого поля модели. Это необходимо, чтобы Django при создании экземпляра фильтра признала его валидным.
177-
178177
В остальном поведение и настройки фильтра `MultiChoiceExt` аналогичны описанному ранее фильтру `MultiChoice`.
179178

180179
## Фильтры DateRange и DateRangePicker

django_admin_filters/base.py

+38-8
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,18 @@
22
from django.contrib import admin
33

44

5-
class Filter(admin.FieldListFilter):
6-
"""Base class for filters with title, apply button and collapsed state."""
5+
class Collapsed:
6+
"""Mixin class for filters with title, apply button and collapsed state."""
77

88
is_collapsed = False
9-
parameter_name_mask = 'adminfilter_'
9+
parameter_name = 'filter'
10+
title = None
1011

1112
FILTER_LABEL = "Admin filter"
1213
BUTTON_LABEL = "Apply"
1314

14-
def __init__(self, field, request, params, model, model_admin, field_path):
15-
"""Customize FieldListFilter functionality."""
16-
self.parameter_name = self.parameter_name_mask + field_path
17-
super().__init__(field, request, params, model, model_admin, field_path)
18-
15+
def set_title(self):
16+
"""Init title values."""
1917
self.title = {
2018
'parameter_name': self.parameter_name,
2119
'filter_name': self.FILTER_LABEL,
@@ -28,6 +26,18 @@ def collapsed_state(self):
2826
"""Return string for CSS stype."""
2927
return '' if self.is_collapsed else 'open'
3028

29+
30+
class Filter(admin.FieldListFilter, Collapsed):
31+
"""Base class for filters applied to field with title, apply button and collapsed state."""
32+
33+
parameter_name_mask = 'adminfilter_'
34+
35+
def __init__(self, field, request, params, model, model_admin, field_path):
36+
"""Customize FieldListFilter functionality."""
37+
self.parameter_name = self.parameter_name_mask + field_path
38+
super().__init__(field, request, params, model, model_admin, field_path)
39+
self.set_title()
40+
3141
def value(self):
3242
"""Return the string provided in the request's query string.
3343
@@ -42,3 +52,23 @@ def expected_parameters(self):
4252
def choices(self, changelist):
4353
"""Must be implemented in childs."""
4454
raise NotImplementedError('Method choices')
55+
56+
57+
class FilterSimple(admin.SimpleListFilter, Collapsed):
58+
"""Base class for filters without field with title, apply button and collapsed state."""
59+
60+
parameter_name = 'adminfilter'
61+
title = 'Filter'
62+
63+
def __init__(self, request, params, model, model_admin):
64+
"""Combine parents init."""
65+
super().__init__(request, params, model, model_admin)
66+
self.set_title()
67+
68+
def lookups(self, request, model_admin):
69+
"""Must be implemented in childs."""
70+
raise NotImplementedError('Method lookups')
71+
72+
def queryset(self, request, queryset):
73+
"""Must be implemented in childs."""
74+
raise NotImplementedError('Method queryset')

django_admin_filters/multi_choice.py

+46-27
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,36 @@
11
"""Django admin multi choice filter with checkboxes for db fields with choices option."""
2-
from .base import Filter as BaseFilter
2+
from .base import Filter as BaseFilter, FilterSimple as BaseFilterSimple
33

44

5-
class Filter(BaseFilter):
5+
class Choices:
6+
"""Multi choice options filter."""
7+
8+
template = None
9+
selected = []
10+
lookup_choices = []
11+
12+
FILTER_LABEL = "By choices"
13+
CHOICES_SEPARATOR = ','
14+
15+
def set_selected(self, val, title):
16+
"""Init choices according request parameter string."""
17+
self.template = 'multi_choice.html'
18+
title.update({
19+
'choices_separator': self.CHOICES_SEPARATOR,
20+
})
21+
self.selected = val.split(self.CHOICES_SEPARATOR) if val else []
22+
23+
def choices(self, _changelist):
24+
"""Define filter checkboxes."""
25+
for lookup, title in self.lookup_choices:
26+
yield {
27+
'selected': lookup in self.selected,
28+
'value': lookup,
29+
'display': title,
30+
}
31+
32+
33+
class Filter(BaseFilter, Choices):
634
"""Multi choice options filter.
735
836
For CharField and IntegerField fields with 'choices' option.
@@ -13,37 +41,19 @@ class Filter(BaseFilter):
1341
https://github.com/modlinltd/django-advanced-filters
1442
"""
1543

16-
template = 'multi_choice.html'
1744
parameter_name_mask = 'mchoice_'
1845

19-
FILTER_LABEL = "By choices"
20-
CHOICES_SEPARATOR = ','
21-
2246
def __init__(self, field, request, params, model, model_admin, field_path):
2347
"""Extend base functionality."""
2448
super().__init__(field, request, params, model, model_admin, field_path)
25-
26-
self.title.update({
27-
'choices_separator': self.CHOICES_SEPARATOR,
28-
})
29-
val = self.value()
30-
self.selected = val.split(self.CHOICES_SEPARATOR) if val else []
31-
self.lookup_choices = self.get_lookup_choices()
32-
33-
def get_lookup_choices(self):
34-
"""Return filter choices."""
49+
self.set_selected(self.value(), self.title)
3550
if self.field.get_internal_type() in ['IntegerField']:
3651
self.selected = [int(i) for i in self.selected]
37-
return self.field.flatchoices
52+
self.lookup_choices = self.field.flatchoices
3853

3954
def choices(self, changelist):
40-
"""Define filter checkboxes."""
41-
for lookup, title in self.lookup_choices:
42-
yield {
43-
'selected': lookup in self.selected,
44-
'value': lookup,
45-
'display': title,
46-
}
55+
"""Call shared implementation."""
56+
return Choices.choices(self, changelist)
4757

4858
def queryset(self, request, queryset):
4959
"""Return the filtered by selected options queryset."""
@@ -56,12 +66,21 @@ def queryset(self, request, queryset):
5666
return queryset
5767

5868

59-
class FilterExt(Filter):
60-
"""Extended variant of previous filter, that allows filtering by custom defined properties."""
69+
class FilterExt(BaseFilterSimple, Choices):
70+
"""Allows filtering by custom defined properties."""
6171

6272
options = []
6373

64-
def get_lookup_choices(self):
74+
def __init__(self, request, params, model, model_admin):
75+
"""Combine parents init."""
76+
super().__init__(request, params, model, model_admin)
77+
self.set_selected(self.value(), self.title)
78+
79+
def choices(self, changelist):
80+
"""Call shared implementation."""
81+
return Choices.choices(self, changelist)
82+
83+
def lookups(self, request, model_admin):
6584
"""Return filter choices."""
6685
return [i[:2] for i in self.options]
6786

example/admin.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class NumberFilter(MultiChoice):
2222
class ColorFilter(MultiChoiceExt):
2323
"""Property color filter."""
2424

25+
parameter_name = "color"
2526
FILTER_LABEL = "By color"
2627
is_collapsed = True
2728

@@ -61,7 +62,7 @@ class Admin(admin.ModelAdmin):
6162
('timestamp1', Timestamp1Filter),
6263
('timestamp2', Timestamp2Filter),
6364
('number', NumberFilter),
64-
('is_online', ColorFilter),
65+
ColorFilter
6566
)
6667

6768

tests/test/test_base.py

+16
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,19 @@ def test_choices(self):
1616
with pytest.raises(NotImplementedError) as err:
1717
Filter.choices(None, None)
1818
assert 'choices' in str(err.value)
19+
20+
def test_lookups(self):
21+
"""Check 'lookups' method."""
22+
from django_admin_filters.base import FilterSimple
23+
24+
with pytest.raises(NotImplementedError) as err:
25+
FilterSimple.lookups(None, None, None)
26+
assert 'lookups' in str(err.value)
27+
28+
def test_queryset(self):
29+
"""Check 'queryset' method."""
30+
from django_admin_filters.base import FilterSimple
31+
32+
with pytest.raises(NotImplementedError) as err:
33+
FilterSimple.queryset(None, None, None)
34+
assert 'queryset' in str(err.value)

tests/test/test_multi_choice.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def test_queryset_ext(self):
3333
"""Filter queryset with MultiChoiceExt."""
3434
from example.admin import ColorFilter
3535

36-
pname = ColorFilter.parameter_name_mask + 'is_online'
36+
pname = ColorFilter.parameter_name
3737
request = self.admin_get({pname: 'green' + ColorFilter.CHOICES_SEPARATOR + 'red'})
3838

3939
changelist = self.modeladmin.get_changelist_instance(request)

0 commit comments

Comments
 (0)