Skip to content

Commit fd2411f

Browse files
committed
fully test django-filters integration and resolve a few minor issues
1 parent 4f4757b commit fd2411f

19 files changed

+1288
-62
lines changed

src/django_enum/filters.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,23 +138,28 @@ def __init__(
138138
**kwargs,
139139
):
140140
self.enum = enum
141-
self.lookup_expr = "has_all" if conjoined else "has_any"
142141
super().__init__(
143142
enum=enum,
144143
choices=kwargs.pop("choices", choices(self.enum)),
145144
strict=strict,
146145
conjoined=conjoined,
147146
**kwargs,
148147
)
148+
self.lookup_expr = "has_all" if conjoined else "has_any"
149149

150150
def filter(self, qs, value):
151+
if value in {None, ""} or self.is_noop(qs, value):
152+
return qs
153+
151154
if value == self.null_value:
152-
value = None
155+
return self.get_method(qs)(Q(**{f"{self.field_name}__isnull": True}))
153156

157+
# special case of no activate flags, performs an exact lookup
158+
# the form cleans unsupplied fields into 0s so we make sure this was supplied
159+
# before filtering on it
154160
if not value:
155-
return qs
156-
157-
if self.is_noop(qs, value):
161+
if not self.parent or self.field_name in self.parent.form.data:
162+
return self.get_method(qs)(Q(**{f"{self.field_name}": 0}))
158163
return qs
159164

160165
qs = self.get_method(qs)(Q(**self.get_filter_predicate(value)))

src/django_enum/forms.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,17 @@ def format_value(self, value):
155155
# choice tuple to the string conversion of the value
156156
# to determine selected options
157157
if self.enum:
158-
return [str(en.value) for en in decompose(self.enum(value))]
158+
named = [str(en.value) for en in decompose(self.enum(value))]
159+
named_set = set(named)
160+
unnamed = [
161+
str(val)
162+
for val in get_set_values(value)
163+
if str(val) not in named_set
164+
]
165+
return [*named, *unnamed]
159166
if isinstance(value, int):
160167
# automagically work for IntFlags even if we weren't given the enum
161-
return [str(bit) for bit in get_set_values(value)]
168+
return [str(val) for val in get_set_values(value)]
162169
return value
163170

164171

@@ -483,7 +490,9 @@ def _coerce(self, value: t.Any) -> t.Any:
483490
"""Combine the values into a single flag using |"""
484491
if self.enum and isinstance(value, self.enum):
485492
return value
486-
values = TypedMultipleChoiceField._coerce(self, value) # type: ignore[attr-defined]
493+
values = TypedMultipleChoiceField._coerce( # type: ignore[attr-defined]
494+
self, [value] if value and not isinstance(value, list) else value
495+
)
487496
if values:
488497
return reduce(or_, values)
489498
return self.empty_value

tests/djenum/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,3 +395,14 @@ class AltWidgetTester(models.Model):
395395
GNSSConstellation, null=True, blank=True, default=None
396396
)
397397
constellation_non_strict = EnumField(GNSSConstellation, strict=False)
398+
399+
400+
class FlagFilterTester(models.Model):
401+
small_flag = EnumField(enum=SmallPositiveFlagEnum, null=True, default=None)
402+
flag = EnumField(enum=PositiveFlagEnum)
403+
flag_no_coerce = EnumField(enum=PositiveFlagEnum, coerce=False)
404+
big_flag = EnumField(enum=BigPositiveFlagEnum, strict=False)
405+
# extra_big_flag = EnumField(enum=ExtraBigPositiveFlagEnum)
406+
407+
def get_absolute_url(self):
408+
return reverse("tests_djenum:flag-detail", kwargs={"pk": self.pk})

tests/djenum/urls.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
EnumTesterDetailView,
88
EnumTesterListView,
99
EnumTesterUpdateView,
10+
FlagTesterCreateView,
11+
FlagTesterDeleteView,
12+
FlagTesterDetailView,
13+
FlagTesterListView,
14+
FlagTesterUpdateView,
1015
)
1116

1217
app_name = "tests_djenum"
@@ -18,6 +23,11 @@
1823
path("enum/add/", EnumTesterCreateView.as_view(), name="enum-add"),
1924
path("enum/<int:pk>/", EnumTesterUpdateView.as_view(), name="enum-update"),
2025
path("enum/<int:pk>/delete/", EnumTesterDeleteView.as_view(), name="enum-delete"),
26+
path("flag/<int:pk>", FlagTesterDetailView.as_view(), name="flag-detail"),
27+
path("flag/list/", FlagTesterListView.as_view(), name="flag-list"),
28+
path("flag/add/", FlagTesterCreateView.as_view(), name="flag-add"),
29+
path("flag/<int:pk>/", FlagTesterUpdateView.as_view(), name="flag-update"),
30+
path("flag/<int:pk>/delete/", FlagTesterDeleteView.as_view(), name="flag-delete"),
2131
]
2232

2333

@@ -42,6 +52,10 @@
4252
EnumTesterFilterExcludeViewSet,
4353
EnumTesterMultipleFilterViewSet,
4454
EnumTesterMultipleFilterExcludeViewSet,
55+
FlagTesterFilterViewSet,
56+
FlagTesterFilterExcludeViewSet,
57+
FlagTesterFilterConjoinedViewSet,
58+
FlagTesterFilterConjoinedExcludeViewSet,
4559
)
4660

4761
urlpatterns.extend(
@@ -75,6 +89,22 @@
7589
EnumTesterMultipleFilterExcludeViewSet.as_view(),
7690
name="enum-filter-multiple-exclude",
7791
),
92+
path("flag/filter/", FlagTesterFilterViewSet.as_view(), name="flag-filter"),
93+
path(
94+
"flag/filter/exclude",
95+
FlagTesterFilterExcludeViewSet.as_view(),
96+
name="flag-filter-exclude",
97+
),
98+
path(
99+
"flag/filter/conjoined",
100+
FlagTesterFilterConjoinedViewSet.as_view(),
101+
name="flag-filter-conjoined",
102+
),
103+
path(
104+
"flag/filter/conjoined/exclude",
105+
FlagTesterFilterConjoinedExcludeViewSet.as_view(),
106+
name="flag-filter-conjoined-exclude",
107+
),
78108
]
79109
)
80110
except ImportError: # pragma: no cover

tests/djenum/views.py

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
from django.views.generic.edit import CreateView, DeleteView, UpdateView
44

55
from django_enum import EnumField
6+
from django_enum.fields import FlagField
67
from tests.djenum import enums as dj_enums
7-
from tests.djenum.models import EnumTester
8+
from tests.djenum.models import EnumTester, FlagFilterTester
89

910

1011
class URLMixin:
@@ -36,6 +37,22 @@ def get_context_data(self, **kwargs):
3637
}
3738

3839

40+
class FlagURLMixin:
41+
NAMESPACE = "tests_djenum"
42+
enums = dj_enums
43+
44+
def get_context_data(self, **kwargs):
45+
return {
46+
**super().get_context_data(**kwargs),
47+
"SmallPositiveFlagEnum": self.enums.SmallPositiveFlagEnum,
48+
"PositiveFlagEnum": self.enums.PositiveFlagEnum,
49+
"BigPositiveFlagEnum": self.enums.BigPositiveFlagEnum,
50+
"NAMESPACE": self.NAMESPACE,
51+
"update_path": f"{self.NAMESPACE}:flag-update",
52+
"delete_path": f"{self.NAMESPACE}:flag-delete",
53+
}
54+
55+
3956
class EnumTesterDetailView(URLMixin, DetailView):
4057
model = EnumTester
4158
template_name = "enumtester_detail.html"
@@ -74,6 +91,44 @@ def get_success_url(self): # pragma: no cover
7491
return reverse(f"{self.NAMESPACE}:enum-list")
7592

7693

94+
class FlagTesterDetailView(FlagURLMixin, DetailView):
95+
model = FlagFilterTester
96+
template_name = "flagtester_detail.html"
97+
fields = "__all__"
98+
99+
100+
class FlagTesterListView(FlagURLMixin, ListView):
101+
model = FlagFilterTester
102+
template_name = "flagtester_list.html"
103+
fields = "__all__"
104+
105+
106+
class FlagTesterCreateView(FlagURLMixin, CreateView):
107+
model = FlagFilterTester
108+
template_name = "flagtester_form.html"
109+
fields = "__all__"
110+
111+
def post(self, request, *args, **kwargs):
112+
return super().post(request, *args, **kwargs)
113+
114+
115+
class FlagTesterUpdateView(FlagURLMixin, UpdateView):
116+
model = FlagFilterTester
117+
template_name = "flagtester_form.html"
118+
fields = "__all__"
119+
120+
def get_success_url(self): # pragma: no cover
121+
return reverse(f"{self.NAMESPACE}:flag-update", kwargs={"pk": self.object.pk})
122+
123+
124+
class FlagTesterDeleteView(FlagURLMixin, DeleteView):
125+
model = FlagFilterTester
126+
template_name = "flagtester_form.html"
127+
128+
def get_success_url(self): # pragma: no cover
129+
return reverse(f"{self.NAMESPACE}:flag-list")
130+
131+
77132
try:
78133
from rest_framework import serializers, viewsets
79134

@@ -98,6 +153,7 @@ class DRFView(viewsets.ModelViewSet):
98153
from django_enum.filters import (
99154
FilterSet as EnumFilterSet,
100155
EnumFilter,
156+
EnumFlagFilter,
101157
MultipleEnumFilter,
102158
)
103159

@@ -254,5 +310,63 @@ class Meta:
254310
model = EnumTester
255311
template_name = "enumtester_list.html"
256312

313+
class FlagTesterFilterViewSet(FlagURLMixin, FilterView):
314+
class FlagTesterFilter(EnumFilterSet):
315+
class Meta:
316+
model = FlagFilterTester
317+
fields = "__all__"
318+
319+
filterset_class = FlagTesterFilter
320+
model = FlagFilterTester
321+
template_name = "flagtester_list.html"
322+
323+
class FlagTesterFilterExcludeViewSet(FlagURLMixin, FilterView):
324+
class FlagTesterExcludeFilter(EnumFilterSet):
325+
class Meta:
326+
model = FlagFilterTester
327+
fields = "__all__"
328+
filter_overrides = {
329+
FlagField: {
330+
"filter_class": EnumFlagFilter,
331+
"extra": lambda f: {"exclude": True},
332+
}
333+
}
334+
335+
filterset_class = FlagTesterExcludeFilter
336+
model = FlagFilterTester
337+
template_name = "flagtester_list.html"
338+
339+
class FlagTesterFilterConjoinedViewSet(FlagURLMixin, FilterView):
340+
class FlagTesterConjoinedFilter(EnumFilterSet):
341+
class Meta:
342+
model = FlagFilterTester
343+
fields = "__all__"
344+
filter_overrides = {
345+
FlagField: {
346+
"filter_class": EnumFlagFilter,
347+
"extra": lambda f: {"conjoined": True},
348+
}
349+
}
350+
351+
filterset_class = FlagTesterConjoinedFilter
352+
model = FlagFilterTester
353+
template_name = "flagtester_list.html"
354+
355+
class FlagTesterFilterConjoinedExcludeViewSet(FlagURLMixin, FilterView):
356+
class FlagTesterConjoinedExcludeFilter(EnumFilterSet):
357+
class Meta:
358+
model = FlagFilterTester
359+
fields = "__all__"
360+
filter_overrides = {
361+
FlagField: {
362+
"filter_class": EnumFlagFilter,
363+
"extra": lambda f: {"exclude": True, "conjoined": True},
364+
}
365+
}
366+
367+
filterset_class = FlagTesterConjoinedExcludeFilter
368+
model = FlagFilterTester
369+
template_name = "flagtester_list.html"
370+
257371
except ImportError: # pragma: no cover
258372
pass

tests/enum_prop/enums.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -346,35 +346,43 @@ class ExternEnum(IntEnumProperties):
346346

347347

348348
class SmallPositiveFlagEnum(FlagChoices):
349-
ONE = 2**10, "One"
350-
TWO = 2**11, "Two"
351-
THREE = 2**12, "Three"
352-
FOUR = 2**13, "Four"
353-
FIVE = 2**14, "Five"
349+
number: Annotated[int, Symmetric()]
350+
351+
ONE = 2**10, "One", 1
352+
TWO = 2**11, "Two", 2
353+
THREE = 2**12, "Three", 3
354+
FOUR = 2**13, "Four", 4
355+
FIVE = 2**14, "Five", 5
354356

355357

356358
class PositiveFlagEnum(FlagChoices):
357-
ONE = 2**26, "One"
358-
TWO = 2**27, "Two"
359-
THREE = 2**28, "Three"
360-
FOUR = 2**29, "Four"
361-
FIVE = 2**30, "Five"
359+
number: Annotated[int, Symmetric()]
360+
361+
ONE = 2**26, "One", 1
362+
TWO = 2**27, "Two", 2
363+
THREE = 2**28, "Three", 3
364+
FOUR = 2**29, "Four", 4
365+
FIVE = 2**30, "Five", 5
362366

363367

364368
class BigPositiveFlagEnum(FlagChoices):
365-
ONE = 2**58, "One"
366-
TWO = 2**59, "Two"
367-
THREE = 2**60, "Three"
368-
FOUR = 2**61, "Four"
369-
FIVE = 2**62, "Five"
369+
version: Annotated[float, Symmetric()]
370+
371+
ONE = 2**58, "One", 1.1
372+
TWO = 2**59, "Two", 2.2
373+
THREE = 2**60, "Three", 3.3
374+
FOUR = 2**61, "Four", 4.4
375+
FIVE = 2**62, "Five", 5.5
370376

371377

372378
class ExtraBigPositiveFlagEnum(FlagChoices):
373-
ONE = 2**61, "One"
374-
TWO = 2**62, "Two"
375-
THREE = 2**63, "Three"
376-
FOUR = 2**64, "Four"
377-
FIVE = 2**65, "Five"
379+
version: Annotated[float, Symmetric()]
380+
381+
ONE = 2**61, "One", 1.1
382+
TWO = 2**62, "Two", 2.2
383+
THREE = 2**63, "Three", 3.3
384+
FOUR = 2**64, "Four", 4.4
385+
FIVE = 2**65, "Five", 5.5
378386

379387

380388
class SmallNegativeFlagEnum(FlagChoices):

tests/enum_prop/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,3 +458,14 @@ class EnumFlagPropTesterRelated(BaseEnumFlagPropTester):
458458
related_flags = models.ManyToManyField(
459459
EnumFlagPropTester, related_name="related_flags"
460460
)
461+
462+
463+
class FlagFilterTester(models.Model):
464+
small_flag = EnumField(enum=SmallPositiveFlagEnum, null=True, default=None)
465+
flag = EnumField(enum=PositiveFlagEnum)
466+
flag_no_coerce = EnumField(enum=PositiveFlagEnum, coerce=False)
467+
big_flag = EnumField(enum=BigPositiveFlagEnum, strict=False)
468+
# extra_big_flag = EnumField(enum=ExtraBigPositiveFlagEnum)
469+
470+
def get_absolute_url(self):
471+
return reverse("tests_enum_prop:flag-detail", kwargs={"pk": self.pk})

0 commit comments

Comments
 (0)