"""Custom form fields for the django_tenant_options app."""fromdjangoimportformsfromdjango.forms.modelsimportModelChoiceIteratorfromdjango.utils.translationimportgettext_lazyas_fromdjango_tenant_options.choicesimportOptionType# Translatable suffixes appended to option labels to indicate their type._OPTION_TYPE_SUFFIXES={OptionType.MANDATORY:_("mandatory"),OptionType.OPTIONAL:_("optional"),OptionType.CUSTOM:_("custom"),}
[docs]classOptionsModelMultipleChoiceField(forms.ModelMultipleChoiceField):"""Displays objects and shows which are mandatory."""
[docs]deflabel_from_instance(self,obj)->str:"""Return a label for each object, suffixed with its translated option type."""suffix=_OPTION_TYPE_SUFFIXES.get(obj.option_type)returnf"{obj.name} ({suffix})"ifsuffixelsestr(obj.name)
[docs]classGroupedModelChoiceIterator(ModelChoiceIterator):"""Yields choices grouped into ``<optgroup>`` tuples. The grouping key is read from the field's ``group_by`` attribute (a string, default ``"option_type"``). - When ``group_by == "option_type"``, each group's value is mapped to its human label via ``dict(OptionType.choices)`` and groups are ordered Mandatory -> Optional -> Custom. - For any other attribute, objects are grouped by ``getattr(obj, group_by, "")``. An empty/missing value is bucketed under the label ``"Uncategorized"`` and the remaining group labels are sorted alphabetically. Within each group, the queryset's own ordering is preserved. """
[docs]def__iter__(self):"""Yield an optional empty label, then ``(group_label, [choices])`` tuples."""ifself.field.empty_labelisnotNone:yield("",self.field.empty_label)group_by=getattr(self.field,"group_by","option_type")# Materialize the queryset once so we can both group and preserve order.objects=list(self.queryset)ifgroup_by=="option_type":yield fromself._iter_by_option_type(objects)else:yield fromself._iter_by_attribute(objects,group_by)
def_iter_by_option_type(self,objects):"""Group objects by their ``option_type`` in Mandatory -> Optional -> Custom order."""labels=dict(OptionType.choices)ordered_types=[OptionType.MANDATORY,OptionType.OPTIONAL,OptionType.CUSTOM]buckets={option_type:[]foroption_typeinordered_types}forobjinobjects:buckets.setdefault(obj.option_type,[]).append(obj)foroption_typeinordered_types:group_objects=buckets.get(option_type,[])ifnotgroup_objects:continuegroup_label=str(labels.get(option_type,option_type))yield(group_label,[self.choice(obj)forobjingroup_objects])def_iter_by_attribute(self,objects,group_by):"""Group objects by an arbitrary attribute, bucketing empties under ``Uncategorized``."""uncategorized_label=str(_("Uncategorized"))buckets={}forobjinobjects:value=getattr(obj,group_by,"")key=str(value)ifvaluenotin(None,"")elseuncategorized_labelbuckets.setdefault(key,[]).append(obj)# Alphabetical group order, with "Uncategorized" sorted in naturally by name.forgroup_labelinsorted(buckets.keys()):group_objects=buckets[group_label]yield(group_label,[self.choice(obj)forobjingroup_objects])
[docs]classGroupedOptionsModelMultipleChoiceField(OptionsModelMultipleChoiceField):"""A multiple-choice field that renders selectable options inside ``<optgroup>`` groups. Drop-in replacement for :class:`OptionsModelMultipleChoiceField`. Set ``group_by`` to choose the grouping key (default ``"option_type"``). The inherited ``label_from_instance`` is preserved, so option labels still show the type suffix (for example ``"High (mandatory)"``) inside each group. """iterator=GroupedModelChoiceIterator
[docs]def__init__(self,*args,group_by="option_type",**kwargs):"""Store the grouping key before delegating to the parent field."""self.group_by=group_bysuper().__init__(*args,**kwargs)