Source code for fairdm.forms.base

"""Base form classes configured through options on their ``Meta``."""

from typing import Any

from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Div, Layout
from django import forms
from django.contrib.admin.utils import flatten, flatten_fieldsets
from django.forms.forms import DeclarativeFieldsMetaclass
from django.forms.models import ModelFormMetaclass
from partial_date import fields as partial_date_fields

from fairdm.utils import fields_to_crispy_layout, fieldsets_to_crispy_layout

from .fields import PartialDateField


[docs] class BaseMetaClass: """Metaclass mixin that collects FairDM options from a form's ``Meta``. The options are merged with those of the base classes and stored on the form class as ``_custom_conf``. They are removed from ``Meta`` so Django never sees them. ``Meta.fields`` is flattened, or taken from ``Meta.fieldsets`` when set. """ def __new__(cls, name, bases, attrs): """Build the form class with its merged FairDM options attached.""" meta = attrs.get("Meta", None) meta_fields = getattr(meta, "fields", []) custom_conf = { "formfield_overrides": {}, "field_overrides": {}, "explicit_fields": None, "fieldsets": None, "form_attrs": {}, "helper_attrs": {}, "help_text": None, } for base in reversed(bases): base_conf = getattr(base, "_custom_conf", {}) for key, value in base_conf.items(): if isinstance(value, dict): custom_conf[key].update(value) elif value is not None: custom_conf[key] = value if meta := attrs.get("Meta", None): for key in custom_conf: val = cls._get_conf_and_remove(meta, key) if isinstance(custom_conf[key], dict) and isinstance(val, dict): custom_conf[key].update(val) elif val: custom_conf[key] = val # The nested form is kept for get_layout(); Meta.fields is flattened below. custom_conf["fields"] = meta_fields if custom_conf["fieldsets"]: meta.fields = flatten_fieldsets(custom_conf["fieldsets"]) else: meta.fields = flatten(meta_fields) new_class = super().__new__(cls, name, bases, attrs) new_class._custom_conf = custom_conf return new_class def _get_conf_and_remove(meta, attr): """Pop an option off ``Meta``, returning ``None`` when it is not defined.""" if hasattr(meta, attr): value = getattr(meta, attr) delattr(meta, attr) return value
[docs] class FairDMModelFormMetaclass(BaseMetaClass, ModelFormMetaclass): """Metaclass for :class:`ModelForm` that reads FairDM ``Meta`` options.""" pass
[docs] class FairDMFormMetaclass(BaseMetaClass, DeclarativeFieldsMetaclass): """Metaclass for :class:`Form` that reads FairDM ``Meta`` options.""" pass
[docs] class FairDMFormMixin: """Mixin that applies the FairDM ``Meta`` options to a form. The options are read by :class:`BaseMetaClass` into ``_custom_conf``: - ``formfield_overrides``: maps a model field type to the form field class used for it. Fields declared on the form are left alone. - ``field_overrides``: maps a field name to a dict of attributes set on that field. - ``explicit_fields``: when ``True``, drop every field not in ``Meta.fields``. - ``fieldsets``: grouped fields, used for the crispy layout and ``Meta.fields``. - ``form_attrs``: HTML attributes for the ``<form>`` element. - ``helper_attrs``: attributes set on the crispy ``FormHelper``. - ``help_text``: HTML shown above the form fields. """ _custom_conf: dict[str, Any] = {} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self._custom_conf.get("explicit_fields") is True: allowed = set(self.Meta.fields or []) for name in list(self.fields.keys()): if name not in allowed: self.fields.pop(name) if formfield_overrides := self._custom_conf["formfield_overrides"]: for name in self.fields: if name in self.declared_fields: continue model_field = self._meta.model._meta.get_field(name) for model_field_type, form_class in formfield_overrides.items(): if isinstance(model_field, model_field_type): self.fields[name] = model_field.formfield(form_class=form_class) break for name, kwargs in self._custom_conf.get("field_overrides").items(): for key, value in kwargs.items(): setattr(self.fields[name], key, value) self.helper = self._helper() def _helper(self): """Build the crispy ``FormHelper`` for this form. Returns: A helper carrying the configured attributes, form id and layout. """ helper = FormHelper() for key, value in self._custom_conf.get("helper_attrs", {}).items(): setattr(helper, key, value) if form_attrs := self._custom_conf.get("form_attrs"): helper.attrs = form_attrs if not helper.form_id: helper.form_id = self.get_form_id() helper.layout = self.get_layout() help_text = self.get_help_text() if help_text is not None: helper.layout.insert(0, help_text) return helper
[docs] def get_layout(self): """Build the crispy layout for this form. Uses ``fieldsets`` when configured, then the configured ``fields``, then ``Meta.fields``. Override it to provide a custom layout. Returns: The layout, empty when the form declares no fields. """ if fieldsets := self._custom_conf.get("fieldsets"): return fieldsets_to_crispy_layout(fieldsets) elif fields := self._custom_conf.get("fields"): return fields_to_crispy_layout(fields) if hasattr(self, "_meta") and hasattr(self._meta, "fields"): # noqa: SIM102 if isinstance(self._meta.fields, list | tuple): return fields_to_crispy_layout(self._meta.fields) return Layout()
[docs] def get_help_text(self): """Build the help text block shown above the form fields. Override it to provide custom help text. Returns: A crispy element wrapping the configured ``help_text``, or ``None`` when none is configured. """ if help_text := self._custom_conf.get("help_text"): return Div(HTML(help_text), css_class="mb-3") return None
[docs] def get_form_id(self): """Return the form's HTML id, the lowercased class name by default. Override it to provide a custom id. Returns: The id used for the crispy helper's ``form_id``. """ return f"{self.__class__.__name__.lower()}"
[docs] class Form(FairDMFormMixin, forms.Form, metaclass=FairDMFormMetaclass): """Base form for FairDM, configured through options on its ``Meta``.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs)
[docs] class Meta: formfield_overrides = { partial_date_fields.PartialDateField: PartialDateField, } explicit_fields = True form_attrs: dict[str, Any] = { "x-data": {}, }
[docs] class ModelForm(FairDMFormMixin, forms.ModelForm, metaclass=FairDMModelFormMetaclass): """Base model form for FairDM, configured through options on its ``Meta``."""
[docs] class Meta: formfield_overrides = { partial_date_fields.PartialDateField: PartialDateField, } explicit_fields = True form_attrs: dict[str, Any] = { "x-data": {}, }