Source code for superdjango.forms.forms

# Imports

from django import forms
from django.utils.translation import gettext_lazy as _
from superdjango.db.eav.constants import DATA_TYPE
from superdjango.db.eav.mappings import CUSTOM_FIELD_MAPPINGS
from superdjango.exceptions import IMustBeMissingSomething
from superdjango.html.library.forms import FieldGroup

# Exports

__all__ = (
    "EAVFormMixin",
    "EAVModelForm",
    "RequestEnabledModelForm",
    "SearchForm",
)

# Mixins


[docs]class EAVFormMixin(object): """A mixin for implementing an entity-attribute-value model with a standard model form.""" attribute_model = None """The concrete model used to define the available custom attributes.""" attribute_name = "attribute" """The name of the field used to identify the attribute model.""" entity_name = None """The field name of the model to which extra attributes are added via the ``value_model``.""" value_model = None """The concrete model used to store values for custom attributes."""
[docs] def __init__(self, *args, **kwargs): # First initialize the form normally before processing custom fields. super().__init__(*args, **kwargs) # Add custom attributes as fields. for attribute in self.get_attributes(): value = self.get_attribute_value(attribute) if value: initial = value.raw_value else: initial = attribute.get_default() kwargs = { 'help_text': attribute.help_text, 'initial': initial, 'label': attribute.label, 'required': attribute.is_required, } self._add_custom_field(attribute, kwargs)
[docs] def get_attribute_model(self): """Get the model class used to define available attributes. :rtype: BaseType[superdjango.db.eav.models.AttributeModel] :raise: IMustBeMissingSomething """ if self.attribute_model is None: raise IMustBeMissingSomething(self.__class__.__name__, "attribute_model", method_name="get_attribute_model") return self.attribute_model
[docs] def get_attribute_name(self): """Get the field name used to identify the attribute model. Defaults to ``attribute``. :rtype: str """ return self.attribute_name or "attribute"
[docs] def get_attribute_value(self, attribute): """Get the value instance for the given attribute and current instance. :param attribute: The name of the attribute whose value is to be returned. :type attribute: BaseType[superdjango.db.eav.models.AttributeModel] :rtype: BaseType[superdjango.db.eav.models.ValueModel] | None :returns: The value record instance or ``None`` if no value has been provided. """ attribute_name = self.get_attribute_name() entity_name = self.get_entity_name() value_model = self.get_value_model() # Criteria operates against the attribute record to which the value is assigned (attribute_name, attribute) and # the actual entity to which the value is attached (entity_name, self.instance). # noinspection PyUnresolvedReferences criteria = { attribute_name: attribute, entity_name: self.instance, } try: return value_model.objects.get(**criteria) except value_model.DoesNotExist: return None
[docs] def get_attributes(self): """Get the attributes to be used in the form. :rtype: django.db.models.QuerySet :returns: A queryset using ``attribute_model``. .. note:: This method is called during ``__init__()``. .. tip:: By default all enabled attributes are returned. If you need to, you may filter these by overriding this method. """ attribute_model = self.get_attribute_model() return attribute_model.objects.filter(is_enabled=True).order_by("sort_order")
[docs] def get_custom_fields(self): """Get custom (possibly bound) fields for display in form output. :rtype: list """ a = list() # noinspection PyUnresolvedReferences for field_name, field_instance in list(self.fields.items()): # self[field] is how django.forms.form._html_output calls the form to get a bound field. Otherwise, you may # get a <django.forms.fields.ChoiceField object at ...> in the output. if field_name.startswith('custom_'): # noinspection PyUnresolvedReferences a.append(self[field_name]) return a
# noinspection PyMethodMayBeStatic
[docs] def get_custom_field_mappings(self): """Get a mapping of data types to form fields. :rtype: dict """ return CUSTOM_FIELD_MAPPINGS
[docs] def get_entity_name(self): """Get the field name of the object to which extra attributes are attached. :rtype: str :raise: IMustBeMissingSomething """ if self.entity_name is None: raise IMustBeMissingSomething(self.__class__.__name__, "entity_name", "get_entity_name") return self.entity_name
[docs] def get_value_model(self): """Get the model class used to define attribute values. :rtype: BaseType[superdjango.db.eav.models.ValueModel] :raise: IMustBeMissingSomething """ if self.value_model is None: raise IMustBeMissingSomething(self.__class__.__name__, "value_model", "get_value_model")
def _add_custom_field(self, field, kwargs): """Add a custom field to the form. :param field: The dynamic field (attribute) instance to be added. :type field: BaseType[superdjango.db.eav.models.AttributeModel] The keyword arguments to be passed to the field constructor. """ mappings = self.get_custom_field_mappings() # The form field is based on the attribute's selected data_type. try: field_class = mappings[field.data_type] except KeyError: raise NameError("Unrecognized data_type %s" % field.data_type) # Instantiate the field. field_instance = self._get_custom_field_instance(field, field_class, **kwargs) # This is how we know which fields have been added dynamically. field_name = 'custom_%s' % field.name # noinspection PyUnresolvedReferences self.fields[field_name] = field_instance # noinspection PyMethodMayBeStatic def _get_custom_field_instance(self, field, field_class, **kwargs): """Initialize the custom field instance. :param field: The dynamic field (attribute) instance to be added. :type field: BaseType[superdjango.db.eav.models.AttributeModel] :param field_class: The class used to instantiate the field. The keyword arguments to be passed to the field constructor. """ # Set keyword arguments specific to the attribute's data_type. if field.data_type == DATA_TYPE.DECIMAL: kwargs['decimal_places'] = field.decimal_places kwargs['max_digits'] = field.max_digits elif field.data_type == DATA_TYPE.EMAIL: kwargs['max_length'] = None kwargs['min_length'] = None elif field.data_type in (DATA_TYPE.FLOAT, DATA_TYPE.INTEGER): kwargs['widget'] = forms.NumberInput elif field.data_type == DATA_TYPE.LIST: kwargs['choices'] = field.get_choices() elif field.data_type == DATA_TYPE.TEXT: kwargs['widget'] = forms.Textarea kwargs['max_length'] = field.max_length elif field.data_type == DATA_TYPE.VARCHAR: kwargs['max_length'] = field.max_length kwargs['min_length'] = field.min_length else: pass # Override the widget if the field is hidden. if field.is_hidden: kwargs['widget'] = forms.HiddenInput # Create the custom field instance. return field_class(**kwargs)
# Forms
[docs]class EAVModelForm(EAVFormMixin, forms.ModelForm): """An implementation of entity-value-attribute model form."""
[docs] def save(self, commit=True): """Save the form, handling dynamic/custom fields. .. important:: When ``commit`` is false, you *must* work with the unsaved custom values stored in ``unsaved_values``. """ # Save the model first. # noinspection PyUnresolvedReferences obj = super().save(commit=commit) # Then save custom fields. # noinspection PyAttributeOutsideInit self.unsaved_values = self.save_custom_fields(commit=commit) # Return the model instance. return obj
[docs] def save_custom_fields(self, commit=True): """Save the custom fields.""" attribute_model = self.get_attribute_model() attribute_name = self.get_attribute_name() entity_name = self.get_entity_name() value_model = self.get_value_model() values = list() for field, clean_value in list(self.cleaned_data.items()): # Get the attribute record. if field.startswith("custom_"): field_name = field.replace("custom_", "", 1) attribute = attribute_model.objects.get(name=field_name) # Try getting the value record, creating it if it does not exist. criteria = { attribute_name: attribute, entity_name: self.instance, } try: value = value_model.objects.get(**criteria) except value_model.DoesNotExist: value = value_model(**criteria) # Set the new (or updated) value and save. value.raw_value = clean_value if commit: value.save() values.append(values) return values
[docs]class RequestEnabledModelForm(forms.ModelForm): """Incorporates the current request and enables fieldset/tab support. Used by SuperDjango UI. """
[docs] def __init__(self, fieldsets=None, request=None, tabs=None, **kwargs): """Add support for fieldsets and current request.""" self.request = request self._fieldsets = fieldsets self._tabs = tabs super().__init__(**kwargs)
@property def fieldsets(self): """Alias for ``get_fieldsets()``.""" return self.get_fieldsets()
[docs] def get_fieldsets(self): """Get the form's fieldsets including field instances. :rtype: list[superdjango.html.library.Fieldset] """ if self._fieldsets is None: return list() for fieldset in self._fieldsets: _fields = list() for field_name in fieldset.fields: # Handle superdjango.ui.options.utils.FieldGroup instances. try: subfields = getattr(field_name, "fields") _subfields = list() for f in subfields: _subfields.append(self[f]) fg = FieldGroup(*_subfields, label=field_name.label, size=field_name.size) _fields.append(fg) except AttributeError: _fields.append(self[field_name]) fieldset._fields = _fields return self._fieldsets
[docs] def get_tabs(self): """Get the form's tabs including field instances. :rtype: list[Tab] """ if self._tabs is None: return list() for tab in self._tabs: # Inline tabs contain a formset rather than fields. if tab.inline: continue # Build the list of fields from each tab, checking for the existence of field groups. _fields = list() for field_name in tab.fields: # Handle superdjango.ui.options.utils.FieldGroup instances. try: subfields = getattr(field_name, "fields") _subfields = list() for f in subfields: _subfields.append(self[f]) fg = FieldGroup(*_subfields, label=field_name.label, size=field_name.size) _fields.append(fg) except AttributeError: _fields.append(self[field_name]) # from superdjango.ui.options.utils import FieldGroup # if isinstance(field_name, FieldGroup): # subfields = list() # for f in field_name.fields: # subfields.append(self[f]) # else: # _fields.append(self[field_name]) tab._fields = _fields return self._tabs
@property def has_fieldsets(self): """Indicates whether the form has defined fieldsets. :rtype: bool """ return self._fieldsets is not None @property def has_tabs(self): """Indicates whether the form has defined tabs. :rtype: bool """ return self._tabs is not None @property def tabs(self): """Alias for ``get_tabs()``.""" return self.get_tabs()
[docs]class SearchForm(forms.Form): """Standard search form, used by UI search views.""" keywords = forms.CharField( label=_("Keywords"), help_text=_("Enter the keyword(s) for which you'd like to search."), required=True ) case_sensitive = forms.BooleanField( label=_("Match Case"), help_text=_("Results should match the case of the keywords."), required=False ) exact_matching = forms.BooleanField( label=_("Exact Match"), help_text=_("Results must exactly match search terms."), required=False )