django-eav2/eav/forms.py
Mike c4d7cedeb8 refactor: apply ruff linter rules and standardize code style
Replace flake8 with ruff and apply consistent linting rules across the entire codebase. Update type annotations, quotation marks, and other style-related changes to comply with the new standards.
2024-09-01 08:21:47 -07:00

158 lines
5 KiB
Python

"""This module contains forms used for admin integration."""
from __future__ import annotations
from copy import deepcopy
from typing import ClassVar
from django.contrib.admin.widgets import AdminSplitDateTime
from django.core.exceptions import ValidationError
from django.forms import (
BooleanField,
CharField,
ChoiceField,
Field,
FloatField,
IntegerField,
JSONField,
ModelForm,
SplitDateTimeField,
)
from django.utils.translation import gettext_lazy as _
from eav.widgets import CSVWidget
class CSVFormField(Field):
message = _("Enter comma-separated-values. eg: one;two;three.")
code = "invalid"
widget = CSVWidget
default_separator = ";"
def __init__(self, *args, **kwargs):
kwargs.pop("max_length", None)
self.separator = kwargs.pop("separator", self.default_separator)
super().__init__(*args, **kwargs)
def to_python(self, value):
if not value:
return []
return [v.strip() for v in value.split(self.separator) if v]
def validate(self, field_value):
super().validate(field_value)
if not isinstance(field_value, list):
raise ValidationError(self.message, code=self.code)
class BaseDynamicEntityForm(ModelForm):
"""
``ModelForm`` for entity with support for EAV attributes. Form fields are
created on the fly depending on schema defined for given entity instance.
If no schema is defined (i.e. the entity instance has not been saved yet),
only static fields are used. However, on form validation the schema will be
retrieved and EAV fields dynamically added to the form, so when the
validation is actually done, all EAV fields are present in it (unless
Rubric is not defined).
Mapping between attribute types and field classes is as follows:
===== =============
Type Field
===== =============
text CharField
float IntegerField
int DateTimeField
date SplitDateTimeField
bool BooleanField
enum ChoiceField
json JSONField
csv CSVField
===== =============
"""
FIELD_CLASSES: ClassVar[dict[str, Field]] = {
"text": CharField,
"float": FloatField,
"int": IntegerField,
"date": SplitDateTimeField,
"bool": BooleanField,
"enum": ChoiceField,
"json": JSONField,
"csv": CSVFormField,
}
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
config_cls = self.instance._eav_config_cls # noqa: SLF001
self.entity = getattr(self.instance, config_cls.eav_attr)
self._build_dynamic_fields()
def _build_dynamic_fields(self):
# Reset form fields.
self.fields = deepcopy(self.base_fields)
for attribute in self.entity.get_all_attributes():
value = getattr(self.entity, attribute.slug)
defaults = {
"label": attribute.name.capitalize(),
"required": attribute.required,
"help_text": attribute.help_text,
"validators": attribute.get_validators(),
}
datatype = attribute.datatype
if datatype == attribute.TYPE_ENUM:
values = attribute.get_choices().values_list("id", "value")
choices = ["", "-----", *list(values)]
defaults.update({"choices": choices})
if value:
defaults.update({"initial": value.pk})
elif datatype == attribute.TYPE_DATE:
defaults.update({"widget": AdminSplitDateTime})
elif datatype == attribute.TYPE_OBJECT:
continue
MappedField = self.FIELD_CLASSES[datatype] # noqa: N806
self.fields[attribute.slug] = MappedField(**defaults)
# Fill initial data (if attribute was already defined).
if value and datatype != attribute.TYPE_ENUM:
self.initial[attribute.slug] = value
def save(self, *, commit=True):
"""
Saves this ``form``'s cleaned_data into model instance
``self.instance`` and related EAV attributes. Returns ``instance``.
"""
if self.errors:
raise ValueError(
_(
"The %s could not be saved because the data didn't validate.",
)
% self.instance._meta.object_name, # noqa: SLF001
)
# Create entity instance, don't save yet.
instance = super().save(commit=False)
# Assign attributes.
for attribute in self.entity.get_all_attributes():
value = self.cleaned_data.get(attribute.slug)
if attribute.datatype == attribute.TYPE_ENUM:
value = attribute.enum_group.values.get(pk=value) if value else None
setattr(self.entity, attribute.slug, value)
# Save entity and its attributes.
if commit:
instance.save()
self._save_m2m()
return instance