1313 TypedChoiceField ,
1414 TypedMultipleChoiceField ,
1515)
16- from django .forms .widgets import ChoiceWidget , Select , SelectMultiple
16+ from django .forms .widgets import (
17+ CheckboxSelectMultiple ,
18+ ChoiceWidget ,
19+ RadioSelect ,
20+ Select ,
21+ SelectMultiple ,
22+ )
1723
1824from django_enum .utils import choices as get_choices
1925from django_enum .utils import (
2026 decompose ,
2127 determine_primitive ,
28+ get_set_bits ,
2229 get_set_values ,
2330 with_typehint ,
2431)
2734 "NonStrictSelect" ,
2835 "NonStrictSelectMultiple" ,
2936 "FlagSelectMultiple" ,
30- "FlagNonStrictSelectMultiple " ,
37+ "NonStrictRadioSelect " ,
3138 "ChoiceFieldMixin" ,
3239 "EnumChoiceField" ,
3340 "EnumMultipleChoiceField" ,
@@ -60,7 +67,7 @@ class _Unspecified:
6067 """
6168
6269
63- class NonStrictMixin ( with_typehint ( Select )): # type: ignore
70+ class NonStrictMixin :
6471 """
6572 Mixin to add non-strict behavior to a widget, this makes sure the set value
6673 appears as a choice if it is not one of the enumeration choices.
@@ -79,19 +86,53 @@ def render(self, *args, **kwargs):
7986 choice [0 ] for choice in self .choices
8087 ):
8188 self .choices = list (self .choices ) + [(value , str (value ))]
82- return super ().render (* args , ** kwargs )
89+ return super ().render (* args , ** kwargs ) # type: ignore[misc]
90+
91+
92+ class NonStrictFlagMixin :
93+ """
94+ Mixin to add non-strict behavior to a multiple choice flag widget, this makes sure
95+ that set flags outside of the enumerated flags will show up as choices. They will
96+ be displayed as the index of the set bit.
97+ """
98+
99+ choices : _SelectChoices
100+
101+ def render (self , * args , ** kwargs ):
102+ """
103+ Before rendering if we're a non-strict flag field and bits are set that are
104+ not part of our flag enumeration we add them as (integer value, bit index)
105+ to our (value, label) choice list.
106+ """
107+
108+ raw_choices = zip (
109+ get_set_values (kwargs .get ("value" )), get_set_bits (kwargs .get ("value" ))
110+ )
111+ self .choices = list (self .choices )
112+ choice_values = set (choice [0 ] for choice in self .choices )
113+ for value , label in raw_choices :
114+ if value not in choice_values :
115+ self .choices .append ((value , label ))
116+ return super ().render (* args , ** kwargs ) # type: ignore[misc]
83117
84118
85119class NonStrictSelect (NonStrictMixin , Select ):
86120 """
87- A Select widget for non-strict EnumChoiceFields that includes any existing
88- non-conforming value as a choice option .
121+ This widget renders a select box that includes an option for each value on the
122+ enumeration .
89123 """
90124
91125
92- class FlagSelectMultiple ( SelectMultiple ):
126+ class NonStrictRadioSelect ( NonStrictMixin , RadioSelect ):
93127 """
94- A SelectMultiple widget for EnumFlagFields.
128+ This widget renders a radio select field that includes an option for each value on
129+ the enumeration and for any non-value that is set.
130+ """
131+
132+
133+ class FlagMixin :
134+ """
135+ This mixin adapts a widget to work with :class:`~enum.IntFlag` types.
95136 """
96137
97138 enum : Optional [Type [Flag ]]
@@ -119,17 +160,45 @@ def format_value(self, value):
119160 return value
120161
121162
163+ class FlagSelectMultiple (FlagMixin , SelectMultiple ):
164+ """
165+ This widget will render :class:`~enum.IntFlag` types as a multi select field with
166+ an option for each flag value.
167+ """
168+
169+
170+ class FlagCheckbox (FlagMixin , CheckboxSelectMultiple ):
171+ """
172+ This widget will render :class:`~enum.IntFlag` types as checkboxes with a checkbox
173+ for each flag value.
174+ """
175+
176+
122177class NonStrictSelectMultiple (NonStrictMixin , SelectMultiple ):
123178 """
124- A SelectMultiple widget for non-strict EnumFlagFields that includes any
125- existing non-conforming value as a choice option .
179+ This widget will render a multi select box that includes an option for each
180+ value on the enumeration and for any non-value that is passed in .
126181 """
127182
128183
129- class FlagNonStrictSelectMultiple (NonStrictMixin , FlagSelectMultiple ):
184+ class NonStrictFlagSelectMultiple (NonStrictFlagMixin , FlagSelectMultiple ):
185+ """
186+ This widget will render a multi select box that includes an option for each flag
187+ on the enumeration and also for each bit lot listed in the enumeration that is set
188+ on the value.
189+
190+ Options for extra bits only appear if they are set. You should pass choices to the
191+ form field if you want additional options to always appear.
130192 """
131- A SelectMultiple widget for non-strict EnumFlagFields that includes any
132- existing non-conforming value as a choice option.
193+
194+
195+ class NonStrictFlagCheckbox (NonStrictFlagMixin , FlagCheckbox ):
196+ """
197+ This widget will render a checkbox for each flag on the enumeration and also
198+ for each bit not listed in the enumeration that is set on the value.
199+
200+ Checkboxes for extra bits only appear if they are set. You should pass choices to
201+ the form field if you want additional checkboxes to always appear.
133202 """
134203
135204
@@ -165,7 +234,7 @@ class ChoiceFieldMixin(
165234
166235 choices : _ChoicesParameter
167236
168- non_strict_widget : Type [ChoiceWidget ] = NonStrictSelect
237+ non_strict_widget : Optional [ Type [ChoiceWidget ] ] = NonStrictSelect
169238
170239 def __init__ (
171240 self ,
@@ -181,7 +250,7 @@ def __init__(
181250 ):
182251 self ._strict_ = strict
183252 self ._primitive_ = primitive
184- if not self .strict :
253+ if not self .strict and self . non_strict_widget :
185254 kwargs .setdefault ("widget" , self .non_strict_widget )
186255
187256 if empty_values is _Unspecified :
@@ -294,7 +363,7 @@ def default_coerce(self, value: Any) -> Any:
294363
295364 :param value: The value to convert
296365 :raises ValidationError: if a valid return value cannot be determined.
297- :return : An enumeration value or the canonical empty value if value is
366+ :returns : An enumeration value or the canonical empty value if value is
298367 one of our empty_values, or the value itself if this is a
299368 non-strict field and the value is of a matching primitive type
300369 """
@@ -368,7 +437,7 @@ class EnumFlagField(ChoiceFieldMixin, TypedMultipleChoiceField): # type: ignore
368437 """
369438
370439 widget = FlagSelectMultiple
371- non_strict_widget = FlagNonStrictSelectMultiple
440+ non_strict_widget = NonStrictFlagSelectMultiple
372441
373442 def __init__ (
374443 self ,
@@ -380,10 +449,12 @@ def __init__(
380449 choices : _ChoicesParameter = (),
381450 ** kwargs ,
382451 ):
383- kwargs .setdefault (
384- "widget" ,
385- self .widget (enum = enum ) if strict else self .non_strict_widget (enum = enum ), # type: ignore[call-arg]
452+ widget = kwargs .get (
453+ "widget" , self .widget if self .strict else self .non_strict_widget
386454 )
455+ if isinstance (widget , type ) and issubclass (widget , FlagMixin ):
456+ widget = widget (enum = enum )
457+ kwargs ["widget" ] = widget
387458 super ().__init__ (
388459 enum = enum ,
389460 empty_value = (
0 commit comments