django-eav2/eav/forms.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

159 lines
5 KiB
Python
Raw Permalink Normal View History

"""This module contains forms used for admin integration."""
2010-09-27 13:28:52 +00:00
from __future__ import annotations
2010-09-27 13:28:52 +00:00
from copy import deepcopy
from typing import ClassVar
2010-09-27 13:28:52 +00:00
from django.contrib.admin.widgets import AdminSplitDateTime
2021-10-16 17:43:20 +00:00
from django.core.exceptions import ValidationError
2021-10-16 17:43:02 +00:00
from django.forms import (
BooleanField,
CharField,
ChoiceField,
Field,
2021-10-16 17:43:02 +00:00
FloatField,
IntegerField,
JSONField,
2021-10-16 17:43:02 +00:00
ModelForm,
SplitDateTimeField,
2021-10-16 17:43:02 +00:00
)
from django.utils.translation import gettext_lazy as _
2010-09-27 13:28:52 +00:00
from eav.widgets import CSVWidget
class CSVFormField(Field):
message = _("Enter comma-separated-values. eg: one;two;three.")
code = "invalid"
widget = CSVWidget
default_separator = ";"
2021-10-12 05:49:25 +00:00
def __init__(self, *args, **kwargs):
kwargs.pop("max_length", None)
self.separator = kwargs.pop("separator", self.default_separator)
2021-10-12 05:49:25 +00:00
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)
2021-04-05 02:53:45 +00:00
2010-09-27 13:28:52 +00:00
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.
2010-09-27 13:28:52 +00:00
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
2021-04-05 02:53:45 +00:00
json JSONField
csv CSVField
===== =============
"""
2021-10-16 17:43:02 +00:00
FIELD_CLASSES: ClassVar[dict[str, Field]] = {
"text": CharField,
"float": FloatField,
"int": IntegerField,
"date": SplitDateTimeField,
"bool": BooleanField,
"enum": ChoiceField,
"json": JSONField,
"csv": CSVFormField,
2010-09-27 13:28:52 +00:00
}
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
config_cls = self.instance._eav_config_cls # noqa: SLF001
2010-09-27 13:28:52 +00:00
self.entity = getattr(self.instance, config_cls.eav_attr)
self._build_dynamic_fields()
def _build_dynamic_fields(self):
# Reset form fields.
2010-09-27 13:28:52 +00:00
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(),
2010-09-27 13:28:52 +00:00
}
datatype = attribute.datatype
if datatype == attribute.TYPE_ENUM:
values = attribute.get_choices().values_list("id", "value")
choices = [("", ""), ("-----", "-----"), *list(values)]
defaults.update({"choices": choices})
2010-09-27 13:28:52 +00:00
if value:
defaults.update({"initial": value.pk})
2010-09-27 13:28:52 +00:00
elif datatype == attribute.TYPE_DATE:
defaults.update({"widget": AdminSplitDateTime})
2010-09-27 13:28:52 +00:00
elif datatype == attribute.TYPE_OBJECT:
continue
MappedField = self.FIELD_CLASSES[datatype] # noqa: N806
2010-09-27 13:28:52 +00:00
self.fields[attribute.slug] = MappedField(**defaults)
# Fill initial data (if attribute was already defined).
if value and datatype != attribute.TYPE_ENUM:
2010-09-27 13:28:52 +00:00
self.initial[attribute.slug] = value
def save(self, *, commit=True):
2010-09-27 13:28:52 +00:00
"""
Saves this ``form``'s cleaned_data into model instance
``self.instance`` and related EAV attributes. Returns ``instance``.
2010-09-27 13:28:52 +00:00
"""
if self.errors:
2021-10-16 17:43:02 +00:00
raise ValueError(
_(
"The %s could not be saved because the data didn't validate.",
2021-10-16 17:43:02 +00:00
)
% self.instance._meta.object_name, # noqa: SLF001
2021-10-16 17:43:02 +00:00
)
2010-09-27 13:28:52 +00:00
2018-07-13 11:50:50 +00:00
# Create entity instance, don't save yet.
instance = super().save(commit=False)
2010-09-27 13:28:52 +00:00
2018-07-13 11:50:50 +00:00
# Assign attributes.
2010-09-27 13:28:52 +00:00
for attribute in self.entity.get_all_attributes():
value = self.cleaned_data.get(attribute.slug)
2010-09-27 13:28:52 +00:00
if attribute.datatype == attribute.TYPE_ENUM:
value = attribute.enum_group.values.get(pk=value) if value else None
2010-09-27 13:28:52 +00:00
setattr(self.entity, attribute.slug, value)
2018-07-13 11:50:50 +00:00
# Save entity and its attributes.
2010-09-27 13:28:52 +00:00
if commit:
instance.save()
2018-07-13 14:06:21 +00:00
self._save_m2m()
2010-09-27 13:28:52 +00:00
return instance