From 9a56b8ac50969eef0e9a6187328524dae4c88e88 Mon Sep 17 00:00:00 2001 From: Muhammad Tahir Date: Mon, 27 Jul 2020 11:52:12 +0400 Subject: [PATCH 01/14] Add ContentType and extra field types to Attribute --- eav/migrations/0002_add_new_fields.py | 64 ++++++++++++++++++ eav/models.py | 93 ++++++++++++++++++--------- eav/registry.py | 14 +++- eav/validators.py | 23 +++++++ 4 files changed, 160 insertions(+), 34 deletions(-) create mode 100644 eav/migrations/0002_add_new_fields.py diff --git a/eav/migrations/0002_add_new_fields.py b/eav/migrations/0002_add_new_fields.py new file mode 100644 index 00000000..37aa44a8 --- /dev/null +++ b/eav/migrations/0002_add_new_fields.py @@ -0,0 +1,64 @@ +# Generated by Django 3.0.2 on 2020-07-27 07:30 + +from django.db import migrations, models +import django.db.models.deletion +import eav.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('eav', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='attribute', + name='entity_ct', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='attribute_entities', to='contenttypes.ContentType'), + ), + migrations.AddField( + model_name='attribute', + name='entity_id', + field=models.UUIDField(blank=True, null=True), + ), + migrations.AddField( + model_name='enumvalue', + name='legacy_value', + field=models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Legacy Value'), + ), + migrations.AddField( + model_name='value', + name='value_decimal', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=14, null=True), + ), + migrations.AddField( + model_name='value', + name='value_enum_multi', + field=models.ManyToManyField(related_name='eav_multi_values', to='eav.EnumValue'), + ), + migrations.AlterField( + model_name='attribute', + name='datatype', + field=eav.fields.EavDatatypeField( + choices=[('text', 'Text'), ('date', 'Date'), ('float', 'Float'), ('decimal', 'Decimal'), + ('int', 'Integer'), ('bool', 'True / False'), ('object', 'Django Object'), ('enum', 'Choice'), + ('enum_multi', 'Multiple Choice')], max_length=10, verbose_name='Data Type'), + ), + migrations.AlterField( + model_name='attribute', + name='slug', + field=eav.fields.EavSlugField(help_text='Short attribute label', verbose_name='Slug'), + ), + migrations.AlterField( + model_name='enumgroup', + name='name', + field=models.CharField(max_length=100, verbose_name='Name'), + ), + migrations.AlterField( + model_name='enumvalue', + name='value', + field=models.CharField(db_index=True, max_length=100, verbose_name='Value'), + ), + ] diff --git a/eav/models.py b/eav/models.py index 3321616c..499727ea 100644 --- a/eav/models.py +++ b/eav/models.py @@ -19,6 +19,8 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from .validators import validate_decimal +from .validators import validate_enum_multi from .validators import ( validate_text, validate_float, @@ -59,7 +61,8 @@ class EnumValue(models.Model): only have a total of four *EnumValues* objects, as you should have used the same *Yes* and *No* *EnumValues* for both *EnumGroups*. """ - value = models.CharField(_('Value'), db_index=True, unique=True, max_length=50) + value = models.CharField(_('Value'), db_index=True, max_length=100) + legacy_value = models.CharField(_('Legacy Value'), blank=True, null=True, db_index=True, max_length=100) def __str__(self): return ''.format(self.value) @@ -73,7 +76,7 @@ class EnumGroup(models.Model): See :class:`EnumValue` for an example. """ - name = models.CharField(_('Name'), unique = True, max_length = 100) + name = models.CharField(_('Name'), max_length = 100) values = models.ManyToManyField(EnumValue, verbose_name = _('Enum group')) def __str__(self): @@ -131,30 +134,49 @@ class Attribute(models.Model): class Meta: ordering = ['name'] - TYPE_TEXT = 'text' - TYPE_FLOAT = 'float' - TYPE_INT = 'int' - TYPE_DATE = 'date' - TYPE_BOOLEAN = 'bool' - TYPE_OBJECT = 'object' - TYPE_ENUM = 'enum' + TYPE_TEXT = 'text' + TYPE_FLOAT = 'float' + TYPE_DECIMAL = 'decimal' + TYPE_INT = 'int' + TYPE_DATE = 'date' + TYPE_BOOLEAN = 'bool' + TYPE_OBJECT = 'object' + TYPE_ENUM = 'enum' + TYPE_ENUM_MULTI = 'enum_multi' DATATYPE_CHOICES = ( - (TYPE_TEXT, _('Text')), - (TYPE_DATE, _('Date')), - (TYPE_FLOAT, _('Float')), - (TYPE_INT, _('Integer')), - (TYPE_BOOLEAN, _('True / False')), - (TYPE_OBJECT, _('Django Object')), - (TYPE_ENUM, _('Multiple Choice')), + (TYPE_TEXT, _('Text')), + (TYPE_DATE, _('Date')), + (TYPE_FLOAT, _('Float')), + (TYPE_DECIMAL, _('Decimal')), + (TYPE_INT, _('Integer')), + (TYPE_BOOLEAN, _('True / False')), + (TYPE_OBJECT, _('Django Object')), + (TYPE_ENUM, _('Choice')), + (TYPE_ENUM_MULTI, _('Multiple Choice')), ) # Core attributes + entity_ct = models.ForeignKey( + ContentType, + on_delete = models.PROTECT, + related_name = 'attribute_entities', + blank=True, + null=True, + ) + entity_id = models.UUIDField( + blank=True, + null=True, + ) + entity = generic.GenericForeignKey( + ct_field = 'entity_ct', + fk_field = 'entity_id' + ) datatype = EavDatatypeField( verbose_name = _('Data Type'), choices = DATATYPE_CHOICES, - max_length = 6 + max_length = 10 ) name = models.CharField( @@ -172,8 +194,7 @@ class Meta: verbose_name = _('Slug'), max_length = 50, db_index = True, - unique = True, - help_text = _('Short unique attribute label') + help_text = _('Short attribute label') ) """ @@ -233,13 +254,15 @@ def get_validators(self): validators to return as well as the default, built-in one. """ DATATYPE_VALIDATORS = { - 'text': validate_text, - 'float': validate_float, - 'int': validate_int, - 'date': validate_date, - 'bool': validate_bool, - 'object': validate_object, - 'enum': validate_enum, + 'text': validate_text, + 'float': validate_float, + 'decimal': validate_decimal, + 'int': validate_int, + 'date': validate_date, + 'bool': validate_bool, + 'object': validate_object, + 'enum': validate_enum, + 'enum_multi': validate_enum_multi, } return [DATATYPE_VALIDATORS[self.datatype]] @@ -369,11 +392,12 @@ class Value(models.Model): entity_id = models.IntegerField() entity = generic.GenericForeignKey(ct_field = 'entity_ct', fk_field = 'entity_id') - value_text = models.TextField(blank = True, null = True) - value_float = models.FloatField(blank = True, null = True) - value_int = models.IntegerField(blank = True, null = True) - value_date = models.DateTimeField(blank = True, null = True) - value_bool = models.NullBooleanField(blank = True, null = True) + value_text = models.TextField(blank = True, null = True) + value_float = models.FloatField(blank = True, null = True) + value_decimal = models.DecimalField(blank = True, null = True, max_digits = 14, decimal_places = 2) + value_int = models.IntegerField(blank = True, null = True) + value_date = models.DateTimeField(blank = True, null = True) + value_bool = models.NullBooleanField(blank = True, null = True) value_enum = models.ForeignKey( EnumValue, @@ -383,6 +407,11 @@ class Value(models.Model): related_name = 'eav_values' ) + value_enum_multi = models.ManyToManyField( + EnumValue, + related_name = 'eav_multi_values' + ) + generic_value_id = models.IntegerField(blank=True, null=True) generic_value_ct = models.ForeignKey( @@ -503,7 +532,7 @@ def get_all_attributes(self): Return a query set of all :class:`Attribute` objects that can be set for this entity. """ - return self.instance._eav_config_cls.get_attributes().order_by('display_order') + return self.instance._eav_config_cls.get_attributes(self.instance).order_by('display_order') def _hasattr(self, attribute_slug): """ diff --git a/eav/registry.py b/eav/registry.py index 52160810..99989315 100644 --- a/eav/registry.py +++ b/eav/registry.py @@ -1,7 +1,9 @@ """This modules contains the registry classes.""" from django.contrib.contenttypes import fields as generic +from django.contrib.contenttypes.models import ContentType from django.db.models.signals import post_init, post_save, pre_save +from django.db.models import Q from .managers import EntityManager from .models import Attribute, Entity, Value @@ -33,12 +35,20 @@ class EavConfig(object): generic_relation_related_name = None @classmethod - def get_attributes(cls): + def get_attributes(cls, entity=None): """ By default, all :class:`~eav.models.Attribute` object apply to an entity, unless you provide a custom EavConfig class overriding this. """ - return Attribute.objects.all() + qs = Attribute.objects.all() + if entity: + entity_ct = ContentType.objects.get_for_model(entity) + qs = qs.filter( + Q(entity_ct__isnull=True, entity_id__isnull=True) | + Q(entity_ct=entity_ct, entity_id__isnull=True) | + Q(entity_ct=entity_ct, entity_id=entity.pk) + ) + return qs class Registry(object): diff --git a/eav/validators.py b/eav/validators.py index 0186482c..999f2335 100644 --- a/eav/validators.py +++ b/eav/validators.py @@ -9,6 +9,7 @@ :meth:`~eav.models.Attribute.validate_value` method in the :class:`~eav.models.Attribute` model. """ +from decimal import Decimal import datetime @@ -35,6 +36,16 @@ def validate_float(value): raise ValidationError(_(u"Must be a float")) +def validate_decimal(value): + """ + Raises ``ValidationError`` unless *value* can be cast as a ``Decimal`` + """ + try: + Decimal(value) + except ValueError: + raise ValidationError(_(u"Must be a Decimal")) + + def validate_int(value): """ Raises ``ValidationError`` unless *value* can be cast as an ``int`` @@ -83,3 +94,15 @@ def validate_enum(value): if isinstance(value, EnumValue) and not value.pk: raise ValidationError(_(u"EnumValue has not been saved yet")) + + +def validate_enum_multi(value): + """ + Raises ``ValidationError`` unless *value* is a saved + :class:`~eav.models.EnumValue` model instance. + """ + from .models import EnumValue + + for single_value in value: + if isinstance(single_value, EnumValue) and not single_value.pk: + raise ValidationError(_(u"EnumValue has not been saved yet")) From cfdd8ada6f3d55da58613a7d4770f41a068b4e7c Mon Sep 17 00:00:00 2001 From: Muhammad Tahir Date: Wed, 12 Aug 2020 17:29:28 +0400 Subject: [PATCH 02/14] Fix enum_multi field --- eav/models.py | 52 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/eav/models.py b/eav/models.py index 499727ea..d5057d34 100644 --- a/eav/models.py +++ b/eav/models.py @@ -38,8 +38,8 @@ class EnumValue(models.Model): """ *EnumValue* objects are the value 'choices' to multiple choice *TYPE_ENUM* - :class:`Attribute` objects. They have only one field, *value*, a - ``CharField`` that must be unique. + and *TYPE_ENUM_MULTI* :class:`Attribute` objects. They have only one field, + *value*, a ``CharField`` that must be unique. For example:: @@ -72,7 +72,8 @@ class EnumGroup(models.Model): """ *EnumGroup* objects have two fields - a *name* ``CharField`` and *values*, a ``ManyToManyField`` to :class:`EnumValue`. :class:`Attribute` classes - with datatype *TYPE_ENUM* have a ``ForeignKey`` field to *EnumGroup*. + with datatype *TYPE_ENUM* or *TYPE_ENUM_MULTI* have a ``ForeignKey`` + field to *EnumGroup*. See :class:`EnumValue` for an example. """ @@ -101,15 +102,17 @@ class Attribute(models.Model): to save or create any entity object for which this attribute applies, without first setting this EAV attribute. - There are 7 possible values for datatype: + There are 9 possible values for datatype: * int (TYPE_INT) * float (TYPE_FLOAT) + * decimal (TYPE_DECIMAL) * text (TYPE_TEXT) * date (TYPE_DATE) * bool (TYPE_BOOLEAN) * object (TYPE_OBJECT) * enum (TYPE_ENUM) + * enum_multi (TYPE_ENUM_MULTI) Examples:: @@ -280,8 +283,14 @@ def validate_value(self, value): value = value.value if not self.enum_group.values.filter(value=value).exists(): raise ValidationError( - _('%(val)s is not a valid choice for %(attr)s') - % dict(val = value, attr = self) + _('{val} is not a valid choice for {attr}').format(val = value, attr = self) + ) + + if self.datatype == self.TYPE_ENUM_MULTI: + value = [v.value if isinstance(value, EnumValue) else v for v in value] + if self.enum_group.values.filter(value__in=value).count() != len(value): + raise ValidationError( + _('{val} is not a valid choice for {attr}').format(val = value, attr = self) ) def save(self, *args, **kwargs): @@ -298,15 +307,16 @@ def save(self, *args, **kwargs): def clean(self): """ Validates the attribute. Will raise ``ValidationError`` if the - attribute's datatype is *TYPE_ENUM* and enum_group is not set, or if - the attribute is not *TYPE_ENUM* and the enum group is set. + attribute's datatype is *TYPE_ENUM* or *TYPE_ENUM_MULTI* and + enum_group is not set, or if the attribute is not *TYPE_ENUM* + or *TYPE_ENUM_MULTI* and the enum group is set. """ - if self.datatype == self.TYPE_ENUM and not self.enum_group: + if self.datatype in (self.TYPE_ENUM, self.TYPE_ENUM_MULTI) and not self.enum_group: raise ValidationError( _('You must set the choice group for multiple choice attributes') ) - if self.datatype != self.TYPE_ENUM and self.enum_group: + if self.datatype not in (self.TYPE_ENUM, self.TYPE_ENUM_MULTI) and self.enum_group: raise ValidationError( _('You can only assign a choice group to multiple choice attributes') ) @@ -314,9 +324,10 @@ def clean(self): def get_choices(self): """ Returns a query set of :class:`EnumValue` objects for this attribute. - Returns None if the datatype of this attribute is not *TYPE_ENUM*. + Returns None if the datatype of this attribute is not *TYPE_ENUM* or + *TYPE_ENUM_MULTI*. """ - return self.enum_group.values.all() if self.datatype == Attribute.TYPE_ENUM else None + return self.enum_group.values.all() if self.datatype in (self.TYPE_ENUM, self.TYPE_ENUM_MULTI) else None def save_value(self, entity, value): """ @@ -340,7 +351,7 @@ def save_value(self, entity, value): attribute = self ) except Value.DoesNotExist: - if value == None or value == '': + if value in (None, '', []): return value_obj = Value.objects.create( @@ -349,13 +360,17 @@ def save_value(self, entity, value): attribute = self ) - if value == None or value == '': + if value in (None, '', []): value_obj.delete() return if value != value_obj.value: - value_obj.value = value - value_obj.save() + if self.datatype == self.TYPE_ENUM_MULTI: + value_obj.value.clear() + value_obj.value.add(*value) + else: + value_obj.value = value + value_obj.save() def __str__(self): return '{} ({})'.format(self.name, self.get_datatype_display()) @@ -559,6 +574,11 @@ def save(self): attribute_value = self._getattr(attribute.slug) if attribute.datatype == Attribute.TYPE_ENUM and not isinstance(attribute_value, EnumValue): attribute_value = EnumValue.objects.get(value=attribute_value) + if attribute.datatype == Attribute.TYPE_ENUM_MULTI: + attribute_value = [ + EnumValue.objects.get(value=v) if not isinstance(attribute_value, EnumValue) else v + for v in attribute_value + ] attribute.save_value(self.instance, attribute_value) def validate_attributes(self): From c03101620a90c672fd1676fafb7eedf3f23611ea Mon Sep 17 00:00:00 2001 From: Rag Sagar Date: Mon, 17 Aug 2020 10:11:25 +0400 Subject: [PATCH 03/14] Fix enum multi validation. --- eav/models.py | 2 +- eav/validators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eav/models.py b/eav/models.py index d5057d34..32a028ee 100644 --- a/eav/models.py +++ b/eav/models.py @@ -287,7 +287,7 @@ def validate_value(self, value): ) if self.datatype == self.TYPE_ENUM_MULTI: - value = [v.value if isinstance(value, EnumValue) else v for v in value] + value = [v.value if isinstance(v, EnumValue) else v for v in value.all()] if self.enum_group.values.filter(value__in=value).count() != len(value): raise ValidationError( _('{val} is not a valid choice for {attr}').format(val = value, attr = self) diff --git a/eav/validators.py b/eav/validators.py index 999f2335..6f1bb7dc 100644 --- a/eav/validators.py +++ b/eav/validators.py @@ -103,6 +103,6 @@ def validate_enum_multi(value): """ from .models import EnumValue - for single_value in value: + for single_value in value.all(): if isinstance(single_value, EnumValue) and not single_value.pk: raise ValidationError(_(u"EnumValue has not been saved yet")) From cbc59280b89cad9748e69f29fc6b4a2ff274ed03 Mon Sep 17 00:00:00 2001 From: Shyamala Gopalakrishnan Date: Wed, 19 Aug 2020 11:49:44 +0400 Subject: [PATCH 04/14] Add custom update method --- eav/models.py | 2 +- eav/queryset.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/eav/models.py b/eav/models.py index 32a028ee..48dc7b05 100644 --- a/eav/models.py +++ b/eav/models.py @@ -572,7 +572,7 @@ def save(self): for attribute in self.get_all_attributes(): if self._hasattr(attribute.slug): attribute_value = self._getattr(attribute.slug) - if attribute.datatype == Attribute.TYPE_ENUM and not isinstance(attribute_value, EnumValue): + if attribute.datatype == Attribute.TYPE_ENUM and not isinstance(attribute_value, EnumValue) and attribute_value: attribute_value = EnumValue.objects.get(value=attribute_value) if attribute.datatype == Attribute.TYPE_ENUM_MULTI: attribute_value = [ diff --git a/eav/queryset.py b/eav/queryset.py index ff5038f6..ed3deb58 100644 --- a/eav/queryset.py +++ b/eav/queryset.py @@ -376,3 +376,23 @@ def order_by(self, *fields): order_clauses.append(term[0]) return QuerySet.order_by(query_clause, *order_clauses) + + def update(self, **kwargs): + config_cls = getattr(self.model, '_eav_config_cls', None) + + prefix = '%s__' % config_cls.eav_attr + new_kwargs = {} + eav_kwargs = {} + + for key, value in kwargs.items(): + if key.startswith(prefix): + eav_kwargs.update({key[len(prefix):]: value}) + else: + new_kwargs.update({key: value}) + + obj = self.first() + obj_eav = getattr(obj, config_cls.eav_attr) + for key, value in eav_kwargs.items(): + setattr(obj_eav, key, value) + obj.save() + return super(EavQuerySet, self).update(**new_kwargs) From fcaccb19a3b61c238a572bc7045e6fafeff047b2 Mon Sep 17 00:00:00 2001 From: Muhammad Tahir Date: Sun, 6 Sep 2020 12:38:49 +0400 Subject: [PATCH 05/14] Fix multi_enum field save --- eav/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eav/models.py b/eav/models.py index 48dc7b05..81ca2ff1 100644 --- a/eav/models.py +++ b/eav/models.py @@ -576,7 +576,7 @@ def save(self): attribute_value = EnumValue.objects.get(value=attribute_value) if attribute.datatype == Attribute.TYPE_ENUM_MULTI: attribute_value = [ - EnumValue.objects.get(value=v) if not isinstance(attribute_value, EnumValue) else v + EnumValue.objects.get(value=v) if not isinstance(v, EnumValue) else v for v in attribute_value ] attribute.save_value(self.instance, attribute_value) From 9e0b35937b7d226815ec64902d244b3602c0abfa Mon Sep 17 00:00:00 2001 From: Muhammad Tahir Date: Sun, 13 Sep 2020 15:44:56 +0400 Subject: [PATCH 06/14] Fix EavQuerySet.update method to handle all records in queryset --- eav/queryset.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/eav/queryset.py b/eav/queryset.py index ed3deb58..aa741c88 100644 --- a/eav/queryset.py +++ b/eav/queryset.py @@ -380,6 +380,9 @@ def order_by(self, *fields): def update(self, **kwargs): config_cls = getattr(self.model, '_eav_config_cls', None) + if not config_cls or config_cls.manager_only: + return super(EavQuerySet, self).update(**kwargs) + prefix = '%s__' % config_cls.eav_attr new_kwargs = {} eav_kwargs = {} @@ -390,9 +393,17 @@ def update(self, **kwargs): else: new_kwargs.update({key: value}) - obj = self.first() - obj_eav = getattr(obj, config_cls.eav_attr) - for key, value in eav_kwargs.items(): - setattr(obj_eav, key, value) - obj.save() - return super(EavQuerySet, self).update(**new_kwargs) + return_value = 0 + + if new_kwargs: + return_value = super(EavQuerySet, self).update(**new_kwargs) + + if eav_kwargs: + for obj in self: + obj_eav = getattr(obj, config_cls.eav_attr) + for key, value in eav_kwargs.items(): + setattr(obj_eav, key, value) + obj_eav.save() + return_value = 1 + + return return_value From 074e0b8c4cdbc23a96fdeb8a989a0f3fb3afd9d3 Mon Sep 17 00:00:00 2001 From: Muhammad Tahir Date: Sun, 20 Sep 2020 15:07:38 +0400 Subject: [PATCH 07/14] Add multiple optimization to reduce the load on database --- .../0003_add_values_unique_constraint.py | 18 ++++++ eav/models.py | 55 ++++++++++--------- eav/registry.py | 1 + 3 files changed, 49 insertions(+), 25 deletions(-) create mode 100644 eav/migrations/0003_add_values_unique_constraint.py diff --git a/eav/migrations/0003_add_values_unique_constraint.py b/eav/migrations/0003_add_values_unique_constraint.py new file mode 100644 index 00000000..3d196d96 --- /dev/null +++ b/eav/migrations/0003_add_values_unique_constraint.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.2 on 2020-09-20 11:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('eav', '0002_add_new_fields'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='value', + unique_together={('entity_ct', 'entity_id', 'attribute_id')}, + ), + ] diff --git a/eav/models.py b/eav/models.py index 81ca2ff1..6ca7d0b8 100644 --- a/eav/models.py +++ b/eav/models.py @@ -344,33 +344,28 @@ def save_value(self, entity, value): """ ct = ContentType.objects.get_for_model(entity) - try: - value_obj = self.value_set.get( - entity_ct = ct, - entity_id = entity.pk, - attribute = self - ) - except Value.DoesNotExist: - if value in (None, '', []): - return - - value_obj = Value.objects.create( - entity_ct = ct, - entity_id = entity.pk, - attribute = self - ) - if value in (None, '', []): - value_obj.delete() - return - - if value != value_obj.value: + self.value_set.filter( + entity_ct=ct, + entity_id=entity.pk, + ).delete() + else: if self.datatype == self.TYPE_ENUM_MULTI: - value_obj.value.clear() + value_obj, created = self.value_set.get_or_create( + entity_ct=ct, + entity_id=entity.pk, + ) + if not created: + value_obj.value.clear() value_obj.value.add(*value) else: - value_obj.value = value - value_obj.save() + value_obj, _ = self.value_set.update_or_create( + entity_ct=ct, + entity_id=entity.pk, + defaults={ + 'value_{datatype}'.format(datatype=self.datatype): value, + } + ) def __str__(self): return '{} ({})'.format(self.name, self.get_datatype_display()) @@ -398,6 +393,11 @@ class Value(models.Model): # = """ + class Meta: + unique_together = [ + ['entity_ct', 'entity_id', 'attribute_id'], + ] + entity_ct = models.ForeignKey( ContentType, on_delete = models.PROTECT, @@ -494,7 +494,8 @@ def pre_save_handler(sender, *args, **kwargs): """ instance = kwargs['instance'] entity = getattr(kwargs['instance'], instance._eav_config_cls.eav_attr) - entity.validate_attributes() + if instance._eav_config_cls.pre_save_validation_enabled: + entity.validate_attributes() @staticmethod def post_save_handler(sender, *args, **kwargs): @@ -547,7 +548,11 @@ def get_all_attributes(self): Return a query set of all :class:`Attribute` objects that can be set for this entity. """ - return self.instance._eav_config_cls.get_attributes(self.instance).order_by('display_order') + attributes = getattr(self, '_attributes', None) + if attributes is None: + attributes = self.instance._eav_config_cls.get_attributes(self.instance).order_by('display_order') + setattr(self, '_attributes', attributes) + return attributes def _hasattr(self, attribute_slug): """ diff --git a/eav/registry.py b/eav/registry.py index 99989315..aa0ca81f 100644 --- a/eav/registry.py +++ b/eav/registry.py @@ -33,6 +33,7 @@ class EavConfig(object): eav_attr = 'eav' generic_relation_attr = 'eav_values' generic_relation_related_name = None + pre_save_validation_enabled = True @classmethod def get_attributes(cls, entity=None): From 93069b70dd4821141cf01c75eebde626f3e4f015 Mon Sep 17 00:00:00 2001 From: Muhammad Tahir Date: Wed, 7 Oct 2020 10:10:24 +0400 Subject: [PATCH 08/14] Remove validation while saving eav.Value in favour of database level validation --- eav/models.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/eav/models.py b/eav/models.py index 6ca7d0b8..861439c6 100644 --- a/eav/models.py +++ b/eav/models.py @@ -452,13 +452,6 @@ class Meta: verbose_name = _('Attribute') ) - def save(self, *args, **kwargs): - """ - Validate and save this value. - """ - self.full_clean() - super(Value, self).save(*args, **kwargs) - def _get_value(self): """ Return the python object this value is holding From e095e0eceb410e31bfdc7bc2b3d3244d3052dc13 Mon Sep 17 00:00:00 2001 From: Rag Sagar Date: Wed, 18 Nov 2020 15:22:11 +0400 Subject: [PATCH 09/14] Add json support in eav. --- eav/models.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/eav/models.py b/eav/models.py index 861439c6..0464731e 100644 --- a/eav/models.py +++ b/eav/models.py @@ -9,6 +9,8 @@ optional metaclass for each eav model class. """ +import json + from copy import copy from django.contrib.contenttypes import fields as generic @@ -146,6 +148,7 @@ class Meta: TYPE_OBJECT = 'object' TYPE_ENUM = 'enum' TYPE_ENUM_MULTI = 'enum_multi' + TYPE_JSON = 'json' DATATYPE_CHOICES = ( (TYPE_TEXT, _('Text')), @@ -157,6 +160,7 @@ class Meta: (TYPE_OBJECT, _('Django Object')), (TYPE_ENUM, _('Choice')), (TYPE_ENUM_MULTI, _('Multiple Choice')), + (TYPE_JSON, _('Text')), ) # Core attributes @@ -266,6 +270,7 @@ def get_validators(self): 'object': validate_object, 'enum': validate_enum, 'enum_multi': validate_enum_multi, + 'json': value_json, } return [DATATYPE_VALIDATORS[self.datatype]] @@ -452,6 +457,18 @@ class Meta: verbose_name = _('Attribute') ) + @property + def value_json(self): + if self.value_text: + return json.loads(self.value_text) + else: + return {} + + @value_json.setter + def value_json(self, new_value): + self.value_text = json.dumps(new_value) + + def _get_value(self): """ Return the python object this value is holding From eb748cf2cf90b3880b980fdc35981a7fdf925f69 Mon Sep 17 00:00:00 2001 From: Rag Sagar Date: Wed, 18 Nov 2020 17:15:58 +0400 Subject: [PATCH 10/14] Fix validation. --- eav/models.py | 5 +++-- eav/validators.py | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/eav/models.py b/eav/models.py index 0464731e..ec5e680a 100644 --- a/eav/models.py +++ b/eav/models.py @@ -30,7 +30,8 @@ validate_date, validate_bool, validate_object, - validate_enum + validate_enum, + validate_json, ) from .exceptions import IllegalAssignmentException from .fields import EavDatatypeField, EavSlugField @@ -270,7 +271,7 @@ def get_validators(self): 'object': validate_object, 'enum': validate_enum, 'enum_multi': validate_enum_multi, - 'json': value_json, + 'json': validate_json, } return [DATATYPE_VALIDATORS[self.datatype]] diff --git a/eav/validators.py b/eav/validators.py index 6f1bb7dc..dd0586e4 100644 --- a/eav/validators.py +++ b/eav/validators.py @@ -26,6 +26,14 @@ def validate_text(value): raise ValidationError(_(u"Must be str or unicode")) +def validate_json(value): + """ + Raises ``ValidationError`` unless *value* can be cast as a ``dict`` + """ + if not isinstance(value, dict): + raise ValidationError(_(u"Must be dict")) + + def validate_float(value): """ Raises ``ValidationError`` unless *value* can be cast as a ``float`` From 0986575e644d9ef6556132ee6b56f2251eb78780 Mon Sep 17 00:00:00 2001 From: Anas Ahmed Date: Mon, 24 Jun 2024 15:55:02 +0500 Subject: [PATCH 11/14] changed imports ugetext_lazy to gettext_lazy to make it compatible with newer djangi version. --- eav/fields.py | 2 +- eav/forms.py | 2 +- eav/models.py | 2 +- eav/validators.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eav/fields.py b/eav/fields.py index f5733323..3377730a 100644 --- a/eav/fields.py +++ b/eav/fields.py @@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class EavSlugField(models.SlugField): diff --git a/eav/forms.py b/eav/forms.py index b1f97b1c..e750b2b8 100644 --- a/eav/forms.py +++ b/eav/forms.py @@ -5,7 +5,7 @@ from django.contrib.admin.widgets import AdminSplitDateTime from django.forms import (BooleanField, CharField, ChoiceField, DateTimeField, FloatField, IntegerField, ModelForm) -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class BaseDynamicEntityForm(ModelForm): diff --git a/eav/models.py b/eav/models.py index ec5e680a..bc2df78d 100644 --- a/eav/models.py +++ b/eav/models.py @@ -19,7 +19,7 @@ from django.db import models from django.db.models.base import ModelBase from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .validators import validate_decimal from .validators import validate_enum_multi diff --git a/eav/validators.py b/eav/validators.py index dd0586e4..f01a8c72 100644 --- a/eav/validators.py +++ b/eav/validators.py @@ -15,7 +15,7 @@ from django.core.exceptions import ValidationError from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ def validate_text(value): From 257cd62a3bdff96016b69a66347401aed473950d Mon Sep 17 00:00:00 2001 From: Anas Ahmed Date: Thu, 27 Jun 2024 16:45:25 +0500 Subject: [PATCH 12/14] changed NullBoolean field to Boolean(null=True). --- eav/migrations/0001_initial.py | 2 +- eav/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eav/migrations/0001_initial.py b/eav/migrations/0001_initial.py index 7417debf..686440dc 100644 --- a/eav/migrations/0001_initial.py +++ b/eav/migrations/0001_initial.py @@ -55,7 +55,7 @@ class Migration(migrations.Migration): ('value_float', models.FloatField(blank=True, null=True)), ('value_int', models.IntegerField(blank=True, null=True)), ('value_date', models.DateTimeField(blank=True, null=True)), - ('value_bool', models.NullBooleanField()), + ('value_bool', models.BooleanField(null=True)), ('generic_value_id', models.IntegerField(blank=True, null=True)), ('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Created')), ('modified', models.DateTimeField(auto_now=True, verbose_name='Modified')), diff --git a/eav/models.py b/eav/models.py index bc2df78d..234c39da 100644 --- a/eav/models.py +++ b/eav/models.py @@ -418,7 +418,7 @@ class Meta: value_decimal = models.DecimalField(blank = True, null = True, max_digits = 14, decimal_places = 2) value_int = models.IntegerField(blank = True, null = True) value_date = models.DateTimeField(blank = True, null = True) - value_bool = models.NullBooleanField(blank = True, null = True) + value_bool = models.BooleanField(null=True) value_enum = models.ForeignKey( EnumValue, From 9e49a84cf0235193c88ccf7fbeb10af15c5db315 Mon Sep 17 00:00:00 2001 From: Alishba Date: Tue, 3 Jun 2025 11:56:56 +0500 Subject: [PATCH 13/14] Fix Value repr crash for missing entity --- eav/models.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/eav/models.py b/eav/models.py index 234c39da..cf6b1679 100644 --- a/eav/models.py +++ b/eav/models.py @@ -488,7 +488,17 @@ def __str__(self): return '{}: "{}" ({})'.format(self.attribute.name, self.value, self.entity) def __repr__(self): - return '{}: "{}" ({})'.format(self.attribute.name, self.value, self.entity.pk) + entity_pk = None + if self.entity is not None: + try: + entity_pk = self.entity.pk + except AttributeError: + entity_pk = None + return '{}: "{}" ({})'.format( + getattr(self.attribute, 'name', None), + self.value, + entity_pk, + ) class Entity(object): From 6ba4a204ec3fa5a46b4d7a106fca3f3abd147d98 Mon Sep 17 00:00:00 2001 From: Alishba Date: Tue, 3 Jun 2025 12:11:52 +0500 Subject: [PATCH 14/14] Fix Value repr crash for missing entity --- eav/models.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/eav/models.py b/eav/models.py index cf6b1679..6ff512f5 100644 --- a/eav/models.py +++ b/eav/models.py @@ -488,15 +488,10 @@ def __str__(self): return '{}: "{}" ({})'.format(self.attribute.name, self.value, self.entity) def __repr__(self): - entity_pk = None - if self.entity is not None: - try: - entity_pk = self.entity.pk - except AttributeError: - entity_pk = None + entity_pk = getattr(self.entity, 'pk', None) return '{}: "{}" ({})'.format( - getattr(self.attribute, 'name', None), - self.value, + self.attribute.name, + self.value, entity_pk, )