1
1
"""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
3
3
4
4
from django .core .exceptions import ValidationError
5
5
from django .db .models import Choices
6
- from django .forms .fields import ChoiceField
6
+ from django .forms .fields import TypedChoiceField
7
7
from django .forms .widgets import Select
8
8
9
9
__all__ = ['NonStrictSelect' , 'EnumChoiceField' ]
@@ -41,7 +41,7 @@ def render(self, *args, **kwargs):
41
41
return super ().render (* args , ** kwargs )
42
42
43
43
44
- class EnumChoiceField (ChoiceField ):
44
+ class EnumChoiceField (TypedChoiceField ):
45
45
"""
46
46
The default ``ChoiceField`` will only accept the base enumeration values.
47
47
Use this field on forms to accept any value mappable to an enumeration
@@ -60,68 +60,105 @@ class EnumChoiceField(ChoiceField):
60
60
:param kwargs: Any additional parameters to pass to ChoiceField base class.
61
61
"""
62
62
63
- enum : Type [Choices ]
64
- strict : bool = True
63
+ enum_ : Optional [ Type [Choices ]] = None
64
+ strict_ : bool = True
65
65
empty_value : Any = ''
66
66
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
+ )
67
110
68
111
def __init__ (
69
112
self ,
70
- enum : Type [Choices ],
113
+ enum : Optional [ Type [Choices ]] = None ,
71
114
* ,
72
115
empty_value : Any = _Unspecified ,
73
- strict : bool = strict ,
116
+ strict : bool = strict_ ,
74
117
choices : Iterable [Tuple [Any , str ]] = (),
75
118
** kwargs
76
119
):
77
- self .enum = enum
78
120
self .strict = strict
79
121
if not self .strict :
80
122
kwargs .setdefault ('widget' , NonStrictSelect )
81
123
124
+ self .empty_values = kwargs .pop ('empty_values' , self .empty_values )
125
+
82
126
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 ),
84
129
** kwargs
85
130
)
86
131
87
132
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 )
89
135
self .empty_value = empty_value
90
136
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
107
139
108
140
def _coerce_to_value_type (self , value : Any ) -> Any :
109
141
"""Coerce the value to the enumerations value type"""
110
142
return type (self .enum .values [0 ])(value )
111
143
112
- def _coerce (self , value : Any ) -> Union [Choices , Any ]:
144
+ def coerce ( # pylint: disable=E0202
145
+ self , value : Any
146
+ ) -> Union [Choices , Any ]:
113
147
"""
114
148
Attempt conversion of value to an enumeration value and return it
115
149
if successful.
116
150
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
+
117
156
:param value: The value to convert
118
157
:raises ValidationError: if a valid return value cannot be determined.
119
158
:return: An enumeration value or the canonical empty value if value is
120
159
one of our empty_values, or the value itself if this is a
121
160
non-strict field and the value is of a matching primitive type
122
161
"""
123
- if value in self .empty_values :
124
- return self .empty_value
125
162
if (
126
163
self .enum is not None and
127
164
not isinstance (value , self .enum ) # pylint: disable=R0801
@@ -134,8 +171,8 @@ def _coerce(self, value: Any) -> Union[Choices, Any]:
134
171
value = self .enum (value )
135
172
except (TypeError , ValueError ) as err :
136
173
if self .strict or not isinstance (
137
- value ,
138
- type (self .enum .values [0 ])
174
+ value ,
175
+ type (self .enum .values [0 ])
139
176
):
140
177
raise ValidationError (
141
178
f'{ value } is not a valid { self .enum } .' ,
0 commit comments