Skip to content

Commit df8ce7b

Browse files
committed
closes #5, EnumChoiceField is now used by default in ModelForms to represent EnumFields
1 parent 309da69 commit df8ce7b

File tree

15 files changed

+352
-372
lines changed

15 files changed

+352
-372
lines changed

django_enum/drf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Support for django rest framework symmetric serialization"""
22

3+
__all__ = ['EnumField']
34

45
try:
56
from typing import Any, Type, Union

django_enum/fields.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
SmallIntegerField,
2828
)
2929
from django.db.models.query_utils import DeferredAttribute
30+
from django_enum.forms import EnumChoiceField, NonStrictSelect
3031

3132
T = TypeVar('T') # pylint: disable=C0103
3233

@@ -55,7 +56,7 @@ class ToPythonDeferredAttribute(DeferredAttribute):
5556
def __set__(self, instance: Model, value: Any):
5657
try:
5758
instance.__dict__[self.field.name] = self.field.to_python(value)
58-
except ValidationError:
59+
except (ValidationError, ValueError):
5960
# Django core fields allow assignment of any value, we do the same
6061
instance.__dict__[self.field.name] = value
6162

@@ -131,8 +132,8 @@ def _try_coerce(
131132
value = self.enum(value)
132133
except (TypeError, ValueError) as err:
133134
if self.strict or not isinstance(
134-
value,
135-
type(self.enum.values[0])
135+
value,
136+
type(self.enum.values[0])
136137
):
137138
raise ValueError(
138139
f"'{value}' is not a valid {self.enum.__name__} "
@@ -273,8 +274,25 @@ def validate(self, value: Any, model_instance: Model):
273274
params={'value': value}
274275
) from err
275276

276-
# def formfield(self, form_class=None, choices_form_class=None, **kwargs):
277-
# pass
277+
def formfield(self, form_class=None, choices_form_class=None, **kwargs):
278+
279+
# super().formfield deletes anything unrecognized from kwargs that
280+
# we try to pass in. Very annoying because we have to
281+
# un-encapsulate some of this initialization logic, this makes our
282+
# EnumChoiceField pretty ugly!
283+
284+
if not self.strict:
285+
kwargs.setdefault('widget', NonStrictSelect)
286+
287+
form_field = super().formfield(
288+
form_class=form_class,
289+
choices_form_class=choices_form_class or EnumChoiceField,
290+
**kwargs
291+
)
292+
293+
form_field.enum = self.enum
294+
form_field.strict = self.strict
295+
return form_field
278296

279297

280298
class EnumCharField(EnumMixin, CharField):

django_enum/forms.py

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Enumeration support for django model forms"""
2-
from typing import Any, Iterable, List, Tuple, Type, Union
2+
from typing import Any, Iterable, List, Optional, Tuple, Type, Union
33

44
from django.core.exceptions import ValidationError
55
from django.db.models import Choices
6-
from django.forms.fields import ChoiceField
6+
from django.forms.fields import TypedChoiceField
77
from django.forms.widgets import Select
88

99
__all__ = ['NonStrictSelect', 'EnumChoiceField']
@@ -41,7 +41,7 @@ def render(self, *args, **kwargs):
4141
return super().render(*args, **kwargs)
4242

4343

44-
class EnumChoiceField(ChoiceField):
44+
class EnumChoiceField(TypedChoiceField):
4545
"""
4646
The default ``ChoiceField`` will only accept the base enumeration values.
4747
Use this field on forms to accept any value mappable to an enumeration
@@ -60,68 +60,105 @@ class EnumChoiceField(ChoiceField):
6060
:param kwargs: Any additional parameters to pass to ChoiceField base class.
6161
"""
6262

63-
enum: Type[Choices]
64-
strict: bool = True
63+
enum_: Optional[Type[Choices]] = None
64+
strict_: bool = True
6565
empty_value: Any = ''
6666
empty_values: List[Any]
67+
choices: Iterable[Tuple[Any, Any]]
68+
69+
@property
70+
def strict(self):
71+
"""strict fields allow non-enumeration values"""
72+
return self.strict_
73+
74+
@strict.setter
75+
def strict(self, strict):
76+
self.strict_ = strict
77+
78+
@property
79+
def enum(self):
80+
"""the class of the enumeration"""
81+
return self.enum_
82+
83+
@enum.setter
84+
def enum(self, enum):
85+
self.enum_ = enum
86+
self.choices = self.choices or getattr(
87+
self.enum,
88+
'choices',
89+
self.choices
90+
)
91+
# remove any of our valid enumeration values or symmetric properties
92+
# from our empty value list if there exists an equivalency
93+
for empty in self.empty_values:
94+
for enum_val in self.enum:
95+
if empty == enum_val:
96+
# copy the list instead of modifying the class's
97+
self.empty_values = [
98+
empty for empty in self.empty_values
99+
if empty != enum_val
100+
]
101+
if empty == self.empty_value:
102+
if self.empty_values:
103+
self.empty_value = self.empty_values[0]
104+
else:
105+
raise ValueError(
106+
f'Enumeration value {repr(enum_val)} is'
107+
f'equivalent to {self.empty_value}, you must '
108+
f'specify a non-conflicting empty_value.'
109+
)
67110

68111
def __init__(
69112
self,
70-
enum: Type[Choices],
113+
enum: Optional[Type[Choices]] = None,
71114
*,
72115
empty_value: Any = _Unspecified,
73-
strict: bool = strict,
116+
strict: bool = strict_,
74117
choices: Iterable[Tuple[Any, str]] = (),
75118
**kwargs
76119
):
77-
self.enum = enum
78120
self.strict = strict
79121
if not self.strict:
80122
kwargs.setdefault('widget', NonStrictSelect)
81123

124+
self.empty_values = kwargs.pop('empty_values', self.empty_values)
125+
82126
super().__init__(
83-
choices=choices or getattr(self.enum, 'choices', ()),
127+
choices=choices or getattr(self.enum, 'choices', choices),
128+
coerce=kwargs.pop('coerce', self.coerce),
84129
**kwargs
85130
)
86131

87132
if empty_value is not _Unspecified:
88-
self.empty_values.insert(0, empty_value)
133+
if empty_value not in self.empty_values:
134+
self.empty_values.insert(0, empty_value)
89135
self.empty_value = empty_value
90136

91-
# remove any of our valid enumeration values or symmetric properties
92-
# from our empty value list if there exists an equivalency
93-
for empty in self.empty_values:
94-
for enum_val in self.enum:
95-
if empty == enum_val:
96-
# copy the list instead of modifying the class's
97-
self.empty_values = [
98-
empty for empty in self.empty_values
99-
if empty != enum_val
100-
]
101-
if empty == self.empty_value:
102-
raise ValueError(
103-
f'Enumeration value {repr(enum_val)} is equivalent'
104-
f' to {self.empty_value}, you must specify a '
105-
f'non-conflicting empty_value.'
106-
)
137+
if enum:
138+
self.enum = enum
107139

108140
def _coerce_to_value_type(self, value: Any) -> Any:
109141
"""Coerce the value to the enumerations value type"""
110142
return type(self.enum.values[0])(value)
111143

112-
def _coerce(self, value: Any) -> Union[Choices, Any]:
144+
def coerce( # pylint: disable=E0202
145+
self, value: Any
146+
) -> Union[Choices, Any]:
113147
"""
114148
Attempt conversion of value to an enumeration value and return it
115149
if successful.
116150
151+
.. note::
152+
153+
When used to represent a model field, by default the model field's
154+
to_python method will be substituted for this method.
155+
117156
:param value: The value to convert
118157
:raises ValidationError: if a valid return value cannot be determined.
119158
:return: An enumeration value or the canonical empty value if value is
120159
one of our empty_values, or the value itself if this is a
121160
non-strict field and the value is of a matching primitive type
122161
"""
123-
if value in self.empty_values:
124-
return self.empty_value
125162
if (
126163
self.enum is not None and
127164
not isinstance(value, self.enum) # pylint: disable=R0801
@@ -134,8 +171,8 @@ def _coerce(self, value: Any) -> Union[Choices, Any]:
134171
value = self.enum(value)
135172
except (TypeError, ValueError) as err:
136173
if self.strict or not isinstance(
137-
value,
138-
type(self.enum.values[0])
174+
value,
175+
type(self.enum.values[0])
139176
):
140177
raise ValidationError(
141178
f'{value} is not a valid {self.enum}.',

django_enum/tests/djenum/forms.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,8 @@
11
from django.forms import ModelForm
2-
from django_enum import EnumChoiceField
3-
from django_enum.tests.djenum.enums import (
4-
BigIntEnum,
5-
BigPosIntEnum,
6-
Constants,
7-
DJIntEnum,
8-
DJTextEnum,
9-
IntEnum,
10-
PosIntEnum,
11-
SmallIntEnum,
12-
SmallPosIntEnum,
13-
TextEnum,
14-
)
152
from django_enum.tests.djenum.models import EnumTester
163

174

185
class EnumTesterForm(ModelForm):
19-
small_pos_int = EnumChoiceField(SmallPosIntEnum)
20-
small_int = EnumChoiceField(SmallIntEnum)
21-
pos_int = EnumChoiceField(PosIntEnum)
22-
int = EnumChoiceField(IntEnum)
23-
big_pos_int = EnumChoiceField(BigPosIntEnum)
24-
big_int = EnumChoiceField(BigIntEnum)
25-
constant = EnumChoiceField(Constants)
26-
text = EnumChoiceField(TextEnum)
27-
dj_int_enum = EnumChoiceField(DJIntEnum)
28-
dj_text_enum = EnumChoiceField(DJTextEnum)
29-
non_strict_int = EnumChoiceField(SmallPosIntEnum, strict=False)
30-
non_strict_text = EnumChoiceField(TextEnum, strict=False)
31-
no_coerce = EnumChoiceField(SmallPosIntEnum)
326

337
class Meta:
348
model = EnumTester

django_enum/tests/djenum/migrations/0001_initial.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 3.2.15 on 2022-08-12 04:26
1+
# Generated by Django 3.2.15 on 2022-08-13 02:32
22

33
import django_enum.fields
44
from django.db import migrations, models
@@ -12,6 +12,13 @@ class Migration(migrations.Migration):
1212
]
1313

1414
operations = [
15+
migrations.CreateModel(
16+
name='BadDefault',
17+
fields=[
18+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('non_strict_int', django_enum.fields.EnumPositiveSmallIntegerField(blank=True, choices=[(0, 'Value 1'), (2, 'Value 2'), (32767, 'Value 32767')], default=5, null=True)),
20+
],
21+
),
1522
migrations.CreateModel(
1623
name='EnumTester',
1724
fields=[

django_enum/tests/djenum/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,14 @@ def get_absolute_url(self):
100100

101101
class Meta:
102102
ordering = ('id',)
103+
104+
105+
class BadDefault(models.Model):
106+
107+
# Non-strict
108+
non_strict_int = EnumField(
109+
SmallPosIntEnum,
110+
null=True,
111+
default=5,
112+
blank=True
113+
)

django_enum/tests/djenum/urls.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
EnumTesterCreateView,
55
EnumTesterDeleteView,
66
EnumTesterDetailView,
7-
EnumTesterFormCreateView,
8-
EnumTesterFormDeleteView,
9-
EnumTesterFormView,
107
EnumTesterListView,
118
EnumTesterUpdateView,
129
)
@@ -18,11 +15,8 @@
1815
path('enum/<int:pk>', EnumTesterDetailView.as_view(), name='enum-detail'),
1916
path('enum/list/', EnumTesterListView.as_view(), name='enum-list'),
2017
path('enum/add/', EnumTesterCreateView.as_view(), name='enum-add'),
21-
path('enum/form/add/', EnumTesterFormCreateView.as_view(), name='enum-form-add'),
2218
path('enum/<int:pk>/', EnumTesterUpdateView.as_view(), name='enum-update'),
23-
path('enum/form/<int:pk>/', EnumTesterFormView.as_view(), name='enum-form-update'),
24-
path('enum/<int:pk>/delete/', EnumTesterDeleteView.as_view(), name='enum-delete'),
25-
path('enum/form/<int:pk>/delete/', EnumTesterFormDeleteView.as_view(), name='enum-form-delete')
19+
path('enum/<int:pk>/delete/', EnumTesterDeleteView.as_view(), name='enum-delete')
2620
]
2721

2822

0 commit comments

Comments
 (0)