Skip to content

Commit 747df47

Browse files
committed
finish form field functionality and tests
1 parent 5c2f29c commit 747df47

File tree

14 files changed

+578
-127
lines changed

14 files changed

+578
-127
lines changed

README.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,15 @@ Installation
151151
.. note::
152152

153153
``django-enum`` *does not* need to be added to ``INSTALLED_APPS``.
154+
155+
.. note::
156+
157+
``django-enum`` has several optional dependencies that are not pulled in
158+
by default. To utilize the
159+
`enum-properties <https://pypi.org/project/enum-properties/>`_ choice types
160+
you must `pip install enum-properties` and to use the ``EnumFilter`` type
161+
for `django-filter <https://pypi.org/project/django-filter/>`_ you
162+
must `pip install django-filter`.
163+
164+
If features are utilized that require a missing optional dependency an
165+
exception will be thrown.

django_enum/fields.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class EnumMixin:
3535

3636
def _coerce_to_value_type(self, value):
3737
"""Coerce the value to the enumerations value type"""
38+
# note if enum type is int and a floating point is passed we could get
39+
# situations like X.xxx == X - this is acceptable
3840
return type(self.enum.values[0])(value)
3941

4042
def __init__(self, *args, enum=None, strict=strict, **kwargs):

django_enum/forms.py

Lines changed: 126 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,162 @@
11
"""Enumeration support for django model forms"""
22
from django.core.exceptions import ValidationError
33
from django.forms.fields import ChoiceField
4+
from django.forms.widgets import Select
5+
46
# pylint: disable=R0801
57

8+
__all__ = ['NonStrictSelect', 'EnumChoiceField']
9+
10+
11+
class _Unspecified:
12+
"""
13+
Marker used by EnumChoiceField to determine if empty_value
14+
was overridden
15+
"""
16+
17+
18+
class NonStrictSelect(Select):
19+
"""
20+
A Select widget for non-strict EnumChoiceFields that includes any existing
21+
non-conforming value as a choice option.
22+
"""
23+
24+
def render(self, *args, **kwargs):
25+
"""Before rendering if we're a non strict field and our value is """
26+
value = kwargs.get('value')
27+
if value not in self.attrs.get('empty_values', []):
28+
self.choices = list(self.choices) + [(value, value)]
29+
return super().render(*args, **kwargs)
30+
631

732
class EnumChoiceField(ChoiceField):
833
"""
934
The default ``ChoiceField`` will only accept the base enumeration values.
1035
Use this field on forms to accept any value mappable to an enumeration
1136
including any labels or symmetric properties.
37+
38+
:param enum: The Enumeration type
39+
:param empty_value: Allow users to define what empty is because some
40+
enumeration types might use an empty value (i.e. empty string) as an
41+
enumeration value. This value will be returned when any "empty" value
42+
is encountered. If unspecified the default empty value of '' is
43+
returned.
44+
:param strict: If False, values not included in the enumeration list, but
45+
of the same primitive type are acceptable.
46+
:param choices: Override choices, otherwise enumeration choices attribute
47+
will be used.
48+
:param kwargs: Any additional parameters to pass to ChoiceField base class.
1249
"""
1350

14-
def __init__(self, enum, *, empty_value='', choices=(), **kwargs):
51+
strict = True
52+
empty_value = ''
53+
54+
def __init__(
55+
self,
56+
enum,
57+
*,
58+
empty_value=_Unspecified,
59+
strict=strict,
60+
choices=(),
61+
**kwargs
62+
):
1563
self.enum = enum
16-
self.empty_value = empty_value
64+
self.strict = strict
65+
if not self.strict:
66+
kwargs.setdefault('widget', NonStrictSelect)
67+
1768
super().__init__(
1869
choices=choices or getattr(self.enum, 'choices', ()),
1970
**kwargs
2071
)
2172

73+
if empty_value is not _Unspecified:
74+
self.empty_values.insert(0, empty_value)
75+
self.empty_value = empty_value
76+
77+
# remove any of our valid enumeration values or symmetric properties
78+
# from our empty value list if there exists an equivalency
79+
for empty in self.empty_values:
80+
for enum_val in self.enum:
81+
if empty == enum_val:
82+
# copy the list instead of modifying the class's
83+
self.empty_values = [
84+
empty for empty in self.empty_values
85+
if empty != enum_val
86+
]
87+
if empty == self.empty_value:
88+
raise ValueError(
89+
f'Enumeration value {repr(enum_val)} is equivalent'
90+
f' to {self.empty_value}, you must specify a '
91+
f'non-conflicting empty_value.'
92+
)
93+
2294
def _coerce_to_value_type(self, value):
2395
"""Coerce the value to the enumerations value type"""
2496
return type(self.enum.values[0])(value)
2597

2698
def _coerce(self, value):
27-
if value == self.empty_value or value in self.empty_values:
99+
"""
100+
Attempt conversion of value to an enumeration value and return it
101+
if successful.
102+
103+
:param value The value to convert
104+
:return An enumeration value or the canonical empty value if value is
105+
one of our empty_values, or the value itself if this is a
106+
non-strict field and the value is of a matching primitive type
107+
:raises ValidationError if a valid return value cannot be determined.
108+
"""
109+
if value in self.empty_values:
28110
return self.empty_value
29111
if self.enum is not None and not isinstance(value, self.enum):
30112
try:
31113
value = self.enum(value)
32114
except (TypeError, ValueError):
33115
try:
34-
value = self.enum(self._coerce_to_value_type(value))
116+
value = self._coerce_to_value_type(value)
117+
value = self.enum(value)
35118
except (TypeError, ValueError) as err:
36-
raise ValidationError(
37-
f'{value} is not a valid {self.enum}.',
38-
code='invalid_choice',
39-
params={'value': value},
40-
) from err
119+
if self.strict or not isinstance(
120+
value,
121+
type(self.enum.values[0])
122+
):
123+
raise ValidationError(
124+
f'{value} is not a valid {self.enum}.',
125+
code='invalid_choice',
126+
params={'value': value},
127+
) from err
41128
return value
42129

130+
def widget_attrs(self, widget):
131+
attrs = super().widget_attrs(widget)
132+
attrs.setdefault('empty_value', self.empty_value)
133+
attrs.setdefault('empty_values', self.empty_values)
134+
return attrs
135+
43136
def clean(self, value):
137+
"""
138+
Validate the given value and return its "cleaned" value as an
139+
appropriate Python object. Raise ValidationError for any errors.
140+
"""
44141
return super().clean(self._coerce(value))
45142

46-
# def prepare_value(self, value):
47-
# return value
48-
#
49-
# def to_python(self, value):
50-
# return value
51-
#
52-
# def validate(self, value):
53-
# if value in self.empty_values and self.required:
54-
# raise ValidationError(
55-
# self.error_messages['required'], code='required'
56-
# )
143+
def prepare_value(self, value):
144+
"""Must return the raw enumeration value type"""
145+
value = self._coerce(value)
146+
return super().prepare_value(
147+
value.value
148+
if isinstance(value, self.enum)
149+
else value
150+
)
151+
152+
def to_python(self, value):
153+
"""Return the value as its full enumeration object"""
154+
return self._coerce(value)
155+
156+
def valid_value(self, value):
157+
"""Return false if this value is not valid"""
158+
try:
159+
self._coerce(value)
160+
return True
161+
except ValidationError:
162+
return False

django_enum/tests/djenum/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class EnumTesterForm(ModelForm):
2626
text = EnumChoiceField(TextEnum)
2727
dj_int_enum = EnumChoiceField(DJIntEnum)
2828
dj_text_enum = EnumChoiceField(DJTextEnum)
29-
non_strict_int = EnumChoiceField(SmallPosIntEnum)
29+
non_strict_int = EnumChoiceField(SmallPosIntEnum, strict=False)
3030

3131
class Meta:
3232
model = EnumTester

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Generated by Django 3.2.14 on 2022-07-26 03:42
22

3-
from django.db import migrations, models
43
import django_enum.fields
4+
from django.db import migrations, models
55

66

77
class Migration(migrations.Migration):

django_enum/tests/djenum/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
EnumTesterDeleteView,
66
EnumTesterDetailView,
77
EnumTesterFormCreateView,
8+
EnumTesterFormDeleteView,
89
EnumTesterFormView,
910
EnumTesterListView,
1011
EnumTesterUpdateView,
@@ -19,7 +20,8 @@
1920
path('enum/form/add/', EnumTesterFormCreateView.as_view(), name='enum-form-add'),
2021
path('enum/<int:pk>/', EnumTesterUpdateView.as_view(), name='enum-update'),
2122
path('enum/form/<int:pk>/', EnumTesterFormView.as_view(), name='enum-form-update'),
22-
path('enum/<int:pk>/delete/', EnumTesterDeleteView.as_view(), name='enum-delete')
23+
path('enum/<int:pk>/delete/', EnumTesterDeleteView.as_view(), name='enum-delete'),
24+
path('enum/form/<int:pk>/delete/', EnumTesterFormDeleteView.as_view(), name='enum-form-delete')
2325
]
2426

2527
try:

django_enum/tests/djenum/views.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ class EnumTesterUpdateView(URLMixin, UpdateView):
4646
fields = '__all__'
4747
template_name = 'enumtester_form.html'
4848

49+
def get_success_url(self): # pragma: no cover
50+
return reverse(f'{self.NAMESPACE}:enum-update', kwargs={'pk': self.object.pk})
51+
4952

5053
class EnumTesterFormView(URLMixin, UpdateView):
5154
form_class = EnumTesterForm
5255
model = EnumTester
5356
template_name = 'enumtester_form.html'
5457

58+
def get_success_url(self): # pragma: no cover
59+
return reverse(f'{self.NAMESPACE}:enum-update', kwargs={'pk': self.object.pk})
60+
5561

5662
class EnumTesterFormCreateView(URLMixin, CreateView):
5763
form_class = EnumTesterForm
@@ -67,6 +73,15 @@ def get_success_url(self): # pragma: no cover
6773
return reverse(f'{self.NAMESPACE}:enum-list')
6874

6975

76+
class EnumTesterFormDeleteView(URLMixin, DeleteView):
77+
form_class = EnumTesterForm
78+
model = EnumTester
79+
template_name = 'enumtester_form.html'
80+
81+
def get_success_url(self): # pragma: no cover
82+
return reverse(f'{self.NAMESPACE}:enum-list')
83+
84+
7085
try:
7186
from django_enum.filters import FilterSet as EnumFilterSet
7287
from django_filters.views import FilterView

django_enum/tests/enum_prop/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class EnumTesterForm(ModelForm):
2828
text = EnumChoiceField(TextEnum)
2929
dj_int_enum = EnumChoiceField(DJIntEnum)
3030
dj_text_enum = EnumChoiceField(DJTextEnum)
31-
non_strict_int = EnumChoiceField(SmallPosIntEnum)
31+
non_strict_int = EnumChoiceField(SmallPosIntEnum, strict=False)
3232

3333
class Meta:
3434
model = EnumTester

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Generated by Django 3.2.14 on 2022-07-26 03:42
22

3-
from django.db import migrations, models
43
import django_enum.fields
4+
from django.db import migrations, models
55

66

77
class Migration(migrations.Migration):

django_enum/tests/enum_prop/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
EnumTesterDeleteView,
77
EnumTesterDetailView,
88
EnumTesterFormCreateView,
9+
EnumTesterFormDeleteView,
910
EnumTesterFormView,
1011
EnumTesterListView,
1112
EnumTesterUpdateView,
@@ -20,7 +21,8 @@
2021
path('enum/form/add/', EnumTesterFormCreateView.as_view(), name='enum-form-add'),
2122
path('enum/<int:pk>/', EnumTesterUpdateView.as_view(), name='enum-update'),
2223
path('enum/form/<int:pk>/', EnumTesterFormView.as_view(), name='enum-form-update'),
23-
path('enum/<int:pk>/delete/', EnumTesterDeleteView.as_view(), name='enum-delete')
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')
2426
]
2527

2628
try:

0 commit comments

Comments
 (0)