Skip to content

Commit 8bb0bd2

Browse files
committed
fix #25
1 parent 523549e commit 8bb0bd2

File tree

11 files changed

+273
-39
lines changed

11 files changed

+273
-39
lines changed

doc/source/changelog.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Change Log
55
==========
66

7-
v2.2.0 (2025-03-XX)
7+
v2.2.0 (2025-03-23)
88
===================
99

1010
* Fixed `Enum types that resolve to primitives of str or int but that do not inherit from those types can result in validation errors. <https://github.com/bckohan/django-enum/issues/109>`_
@@ -18,7 +18,8 @@ v2.2.0 (2025-03-XX)
1818
* Implemented `Upgrade to enum-properties >=2.2 <https://github.com/bckohan/django-enum/issues/95>`_
1919
* Implemented `Move form imports to locally scoped imports where needed in fields.py <https://github.com/bckohan/django-enum/issues/79>`_
2020
* Implemented `Reorganize documentation using diataxis <https://github.com/bckohan/django-enum/issues/72>`_
21-
* Implemented `Provide an EnumMultipleChoiceField <https://github.com/bckohan/django-enum/issues/72>`_
21+
* Implemented `Provide a MultipleEnumChoiceFilter <https://github.com/bckohan/django-enum/issues/25>`_
22+
* Implemented `Provide an EnumMultipleChoiceField <https://github.com/bckohan/django-enum/issues/24>`_
2223

2324
v2.1.0 (2025-02-24)
2425
===================

src/django_enum/fields.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1241,16 +1241,16 @@ def formfield(self, form_class=None, choices_form_class=None, **kwargs):
12411241
from django_enum.forms import (
12421242
ChoiceFieldMixin,
12431243
EnumFlagField,
1244+
FlagNonStrictSelectMultiple,
12441245
FlagSelectMultiple,
1245-
NonStrictSelectMultiple,
12461246
)
12471247

12481248
kwargs["empty_value"] = None if self.default is None else self.enum(0)
12491249
kwargs.setdefault(
12501250
"widget",
12511251
FlagSelectMultiple(enum=self.enum)
12521252
if self.strict
1253-
else NonStrictSelectMultiple(enum=self.enum),
1253+
else FlagNonStrictSelectMultiple(enum=self.enum),
12541254
)
12551255

12561256
form_field = Field.formfield(

src/django_enum/filters.py

+48-7
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,27 @@
22
Support for :doc:`django-filter <django-filter:index>`.
33
"""
44

5-
from typing import Tuple, Type
5+
import typing as t
6+
from enum import Enum
67

78
from django.db.models import Field as ModelField
8-
from django_filters import Filter, TypedChoiceFilter, filterset
9-
10-
from django_enum.forms import EnumChoiceField
9+
from django_filters import (
10+
Filter,
11+
TypedChoiceFilter,
12+
TypedMultipleChoiceFilter,
13+
filterset,
14+
)
15+
16+
from django_enum.fields import EnumField
17+
from django_enum.forms import EnumChoiceField, EnumMultipleChoiceField
1118
from django_enum.utils import choices
1219

20+
__all__ = [
21+
"EnumFilter",
22+
"MultipleEnumFilter",
23+
"FilterSet",
24+
]
25+
1326

1427
class EnumFilter(TypedChoiceFilter):
1528
"""
@@ -44,9 +57,37 @@ class Color(TextChoices):
4457
:param kwargs: Any additional arguments for base classes
4558
"""
4659

60+
enum: t.Type[Enum]
4761
field_class = EnumChoiceField
4862

49-
def __init__(self, *, enum, strict=False, **kwargs):
63+
def __init__(self, *, enum: t.Type[Enum], strict: bool = False, **kwargs):
64+
self.enum = enum
65+
super().__init__(
66+
enum=enum,
67+
choices=kwargs.pop("choices", choices(self.enum)),
68+
strict=strict,
69+
**kwargs,
70+
)
71+
72+
73+
class MultipleEnumFilter(TypedMultipleChoiceFilter):
74+
"""
75+
Use this filter class instead of
76+
:ref:`MultipleChoiceFilter <django-filter:multiple-choice-filter>`
77+
to get filters to accept multiple :class:`~enum.Enum` labels and symmetric
78+
properties.
79+
80+
:param enum: The class of the enumeration containing the values to
81+
filter on
82+
:param strict: If False (default), values not in the enumeration will
83+
be searchable.
84+
:param kwargs: Any additional arguments for base classes
85+
"""
86+
87+
enum: t.Type[Enum]
88+
field_class = EnumMultipleChoiceField
89+
90+
def __init__(self, *, enum: t.Type[Enum], strict: bool = False, **kwargs):
5091
self.enum = enum
5192
super().__init__(
5293
enum=enum,
@@ -68,9 +109,9 @@ class FilterSet(filterset.FilterSet):
68109
@classmethod
69110
def filter_for_lookup(
70111
cls, field: ModelField, lookup_type: str
71-
) -> Tuple[Type[Filter], dict]:
112+
) -> t.Tuple[t.Optional[t.Type[Filter]], t.Dict[str, t.Any]]:
72113
"""For EnumFields use the EnumFilter class by default"""
73-
if hasattr(field, "enum"):
114+
if isinstance(field, EnumField):
74115
return EnumFilter, {
75116
"enum": field.enum,
76117
"strict": getattr(field, "strict", False),

src/django_enum/forms.py

+21-8
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
TypedChoiceField,
1414
TypedMultipleChoiceField,
1515
)
16-
from django.forms.widgets import Select, SelectMultiple
16+
from django.forms.widgets import ChoiceWidget, Select, SelectMultiple
1717

1818
from django_enum.utils import choices as get_choices
1919
from django_enum.utils import (
@@ -27,6 +27,7 @@
2727
"NonStrictSelect",
2828
"NonStrictSelectMultiple",
2929
"FlagSelectMultiple",
30+
"FlagNonStrictSelectMultiple",
3031
"ChoiceFieldMixin",
3132
"EnumChoiceField",
3233
"EnumMultipleChoiceField",
@@ -118,7 +119,14 @@ def format_value(self, value):
118119
return value
119120

120121

121-
class NonStrictSelectMultiple(NonStrictMixin, FlagSelectMultiple):
122+
class NonStrictSelectMultiple(NonStrictMixin, SelectMultiple):
123+
"""
124+
A SelectMultiple widget for non-strict EnumFlagFields that includes any
125+
existing non-conforming value as a choice option.
126+
"""
127+
128+
129+
class FlagNonStrictSelectMultiple(NonStrictMixin, FlagSelectMultiple):
122130
"""
123131
A SelectMultiple widget for non-strict EnumFlagFields that includes any
124132
existing non-conforming value as a choice option.
@@ -157,6 +165,8 @@ class ChoiceFieldMixin(
157165

158166
choices: _ChoicesParameter
159167

168+
non_strict_widget: Type[ChoiceWidget] = NonStrictSelect
169+
160170
def __init__(
161171
self,
162172
enum: Optional[Type[Enum]] = _enum_,
@@ -172,7 +182,7 @@ def __init__(
172182
self._strict_ = strict
173183
self._primitive_ = primitive
174184
if not self.strict:
175-
kwargs.setdefault("widget", NonStrictSelect)
185+
kwargs.setdefault("widget", self.non_strict_widget)
176186

177187
if empty_values is _Unspecified:
178188
self.empty_values = copy(list(TypedChoiceField.empty_values))
@@ -323,7 +333,7 @@ def validate(self, value):
323333

324334
class EnumChoiceField(ChoiceFieldMixin, TypedChoiceField): # type: ignore
325335
"""
326-
The default :class:`~django.forms.fields.ChoiceField` will only accept the base
336+
The default :class:`~django.forms.ChoiceField` will only accept the base
327337
enumeration values. Use this field on forms to accept any value mappable to an
328338
enumeration including any labels, symmetric properties, of values accepted in
329339
:meth:`~enum.Enum._missing_`.
@@ -334,20 +344,22 @@ class EnumMultipleChoiceField( # type: ignore
334344
ChoiceFieldMixin, TypedMultipleChoiceField
335345
):
336346
"""
337-
The default :class:`~django.forms.fields.MultipleChoiceField` will only accept the
347+
The default :class:`~django.forms.MultipleChoiceField` will only accept the
338348
base enumeration values. Use this field on forms to accept multiple values mappable
339349
to an enumeration including any labels, symmetric properties, of values accepted in
340350
:meth:`~enum.Enum._missing_`.
341351
"""
342352

353+
non_strict_widget = NonStrictSelectMultiple
354+
343355

344356
class EnumFlagField(ChoiceFieldMixin, TypedMultipleChoiceField): # type: ignore
345357
"""
346358
A generic form field for :class:`~enum.Flag` derived enumerations. By default the
347359
:class:`~django_enum.forms.FlagSelectMultiple` widget will be used.
348360
349-
After cleaning the value stored in the cleaned data will be a combined enum instance.
350-
(e.g. all input flags will be or-ed together)
361+
After cleaning the value stored in the cleaned data will be a combined enum
362+
instance. (e.g. all input flags will be or-ed together)
351363
352364
.. note::
353365
@@ -356,6 +368,7 @@ class EnumFlagField(ChoiceFieldMixin, TypedMultipleChoiceField): # type: ignore
356368
"""
357369

358370
widget = FlagSelectMultiple
371+
non_strict_widget = FlagNonStrictSelectMultiple
359372

360373
def __init__(
361374
self,
@@ -369,7 +382,7 @@ def __init__(
369382
):
370383
kwargs.setdefault(
371384
"widget",
372-
self.widget(enum=enum) if strict else NonStrictSelectMultiple(enum=enum),
385+
self.widget(enum=enum) if strict else self.non_strict_widget(enum=enum), # type: ignore[call-arg]
373386
)
374387
super().__init__(
375388
enum=enum,

tests/djenum/urls.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@
3737
try:
3838
from django_filters.views import FilterView
3939

40-
from tests.djenum.views import EnumTesterFilterViewSet
40+
from tests.djenum.views import (
41+
EnumTesterFilterViewSet,
42+
EnumTesterMultipleFilterViewSet,
43+
)
4144

4245
urlpatterns.extend(
4346
[
@@ -55,6 +58,11 @@
5558
EnumTesterFilterViewSet.as_view(),
5659
name="enum-filter-symmetric",
5760
),
61+
path(
62+
"enum/filter/multiple/",
63+
EnumTesterMultipleFilterViewSet.as_view(),
64+
name="enum-filter-multiple",
65+
),
5866
]
5967
)
6068
except ImportError: # pragma: no cover

tests/djenum/views.py

+73-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,26 @@ class DRFView(viewsets.ModelViewSet):
9595
try:
9696
from django_filters.views import FilterView
9797

98-
from django_enum.filters import FilterSet as EnumFilterSet
98+
from django_enum.filters import FilterSet as EnumFilterSet, MultipleEnumFilter
99+
100+
from .enums import (
101+
BigIntEnum,
102+
BigPosIntEnum,
103+
Constants,
104+
DateEnum,
105+
DateTimeEnum,
106+
DecimalEnum,
107+
DJIntEnum,
108+
DJTextEnum,
109+
DurationEnum,
110+
ExternEnum,
111+
IntEnum,
112+
PosIntEnum,
113+
SmallIntEnum,
114+
SmallPosIntEnum,
115+
TextEnum,
116+
TimeEnum,
117+
)
99118

100119
class EnumTesterFilterViewSet(URLMixin, FilterView):
101120
class EnumTesterFilter(EnumFilterSet):
@@ -107,5 +126,58 @@ class Meta:
107126
model = EnumTester
108127
template_name = "enumtester_list.html"
109128

129+
class EnumTesterMultipleFilterViewSet(URLMixin, FilterView):
130+
class EnumTesterMultipleFilter(EnumFilterSet):
131+
small_pos_int = MultipleEnumFilter(enum=SmallPosIntEnum)
132+
small_int = MultipleEnumFilter(enum=SmallIntEnum)
133+
pos_int = MultipleEnumFilter(enum=PosIntEnum)
134+
int = MultipleEnumFilter(enum=IntEnum)
135+
big_pos_int = MultipleEnumFilter(enum=BigPosIntEnum)
136+
big_int = MultipleEnumFilter(enum=BigIntEnum)
137+
constant = MultipleEnumFilter(enum=Constants)
138+
text = MultipleEnumFilter(enum=TextEnum)
139+
extern = MultipleEnumFilter(enum=ExternEnum)
140+
141+
dj_int_enum = MultipleEnumFilter(enum=DJIntEnum)
142+
dj_text_enum = MultipleEnumFilter(enum=DJTextEnum)
143+
144+
# Non-strict
145+
non_strict_int = MultipleEnumFilter(enum=SmallPosIntEnum, strict=False)
146+
non_strict_text = MultipleEnumFilter(enum=TextEnum, strict=False)
147+
no_coerce = MultipleEnumFilter(enum=SmallPosIntEnum, strict=False)
148+
149+
# eccentric enums
150+
date_enum = MultipleEnumFilter(enum=DateEnum)
151+
datetime_enum = MultipleEnumFilter(enum=DateTimeEnum)
152+
time_enum = MultipleEnumFilter(enum=TimeEnum)
153+
duration_enum = MultipleEnumFilter(enum=DurationEnum)
154+
decimal_enum = MultipleEnumFilter(enum=DecimalEnum)
155+
156+
class Meta:
157+
fields = [
158+
"small_pos_int",
159+
"small_int",
160+
"pos_int",
161+
"int",
162+
"big_pos_int",
163+
"big_int",
164+
"constant",
165+
"text",
166+
"extern",
167+
"non_strict_int",
168+
"non_strict_text",
169+
"no_coerce",
170+
"date_enum",
171+
"datetime_enum",
172+
"time_enum",
173+
"duration_enum",
174+
"decimal_enum",
175+
]
176+
model = EnumTester
177+
178+
filterset_class = EnumTesterMultipleFilter
179+
model = EnumTester
180+
template_name = "enumtester_list.html"
181+
110182
except ImportError: # pragma: no cover
111183
pass

tests/enum_prop/urls.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
try:
3535
from django_filters.views import FilterView
3636

37-
from tests.enum_prop.views import EnumTesterFilterViewSet
37+
from tests.enum_prop.views import (
38+
EnumTesterPropFilterViewSet,
39+
EnumTesterPropMultipleFilterViewSet,
40+
)
3841

3942
urlpatterns.extend(
4043
[
@@ -49,9 +52,14 @@
4952
),
5053
path(
5154
"enum/filter/symmetric/",
52-
EnumTesterFilterViewSet.as_view(),
55+
EnumTesterPropFilterViewSet.as_view(),
5356
name="enum-filter-symmetric",
5457
),
58+
path(
59+
"enum/filter/multiple/",
60+
EnumTesterPropMultipleFilterViewSet.as_view(),
61+
name="enum-filter-multiple",
62+
),
5563
]
5664
)
5765
except (ImportError, ModuleNotFoundError): # pragma: no cover

0 commit comments

Comments
 (0)