"""This module contains forms used for admin integration.""" from copy import deepcopy from django.contrib.admin.widgets import AdminSplitDateTime from django.core.exceptions import ValidationError from django.forms import ( BooleanField, CharField, ChoiceField, Field, FloatField, IntegerField, ModelForm, SplitDateTimeField, ) from django.utils.translation import gettext_lazy as _ from eav.widgets import CSVWidget try: from django.forms import JSONField except: JSONField = CharField 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) try: isinstance(field_value, list) except ValidationError: 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 = { 'text': CharField, 'float': FloatField, 'int': IntegerField, 'date': SplitDateTimeField, 'bool': BooleanField, 'enum': ChoiceField, 'json': JSONField, 'csv': CSVFormField, } def __init__(self, data=None, *args, **kwargs): super(BaseDynamicEntityForm, self).__init__(data, *args, **kwargs) config_cls = self.instance._eav_config_cls 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] self.fields[attribute.slug] = MappedField(**defaults) # Fill initial data (if attribute was already defined). if value and not 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 ) ) # Create entity instance, don't save yet. instance = super(BaseDynamicEntityForm, self).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: if value: value = attribute.enum_group.values.get(pk=value) else: value = None setattr(self.entity, attribute.slug, value) # Save entity and its attributes. if commit: instance.save() self._save_m2m() return instance