|
1 | 1 | """Enumeration support for django model forms"""
|
2 | 2 | from django.core.exceptions import ValidationError
|
3 | 3 | from django.forms.fields import ChoiceField
|
| 4 | +from django.forms.widgets import Select |
| 5 | + |
4 | 6 | # pylint: disable=R0801
|
5 | 7 |
|
| 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 | + |
6 | 31 |
|
7 | 32 | class EnumChoiceField(ChoiceField):
|
8 | 33 | """
|
9 | 34 | The default ``ChoiceField`` will only accept the base enumeration values.
|
10 | 35 | Use this field on forms to accept any value mappable to an enumeration
|
11 | 36 | 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. |
12 | 49 | """
|
13 | 50 |
|
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 | + ): |
15 | 63 | self.enum = enum
|
16 |
| - self.empty_value = empty_value |
| 64 | + self.strict = strict |
| 65 | + if not self.strict: |
| 66 | + kwargs.setdefault('widget', NonStrictSelect) |
| 67 | + |
17 | 68 | super().__init__(
|
18 | 69 | choices=choices or getattr(self.enum, 'choices', ()),
|
19 | 70 | **kwargs
|
20 | 71 | )
|
21 | 72 |
|
| 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 | + |
22 | 94 | def _coerce_to_value_type(self, value):
|
23 | 95 | """Coerce the value to the enumerations value type"""
|
24 | 96 | return type(self.enum.values[0])(value)
|
25 | 97 |
|
26 | 98 | 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: |
28 | 110 | return self.empty_value
|
29 | 111 | if self.enum is not None and not isinstance(value, self.enum):
|
30 | 112 | try:
|
31 | 113 | value = self.enum(value)
|
32 | 114 | except (TypeError, ValueError):
|
33 | 115 | try:
|
34 |
| - value = self.enum(self._coerce_to_value_type(value)) |
| 116 | + value = self._coerce_to_value_type(value) |
| 117 | + value = self.enum(value) |
35 | 118 | 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 |
41 | 128 | return value
|
42 | 129 |
|
| 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 | + |
43 | 136 | 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 | + """ |
44 | 141 | return super().clean(self._coerce(value))
|
45 | 142 |
|
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 |
0 commit comments