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
)