django-eav2/eav/models.py

715 lines
23 KiB
Python
Raw Normal View History

"""
This module defines the four concrete, non-abstract models:
* :class:`Value`
* :class:`Attribute`
* :class:`EnumValue`
* :class:`EnumGroup`
2010-09-27 13:28:52 +00:00
Along with the :class:`Entity` helper class and :class:`EAVModelMeta`
optional metaclass for each eav model class.
"""
from copy import copy
2010-09-27 13:28:52 +00:00
from django.contrib.contenttypes import fields as generic
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
2021-10-16 00:25:10 +00:00
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.db.models.base import ModelBase
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
2010-09-27 13:28:52 +00:00
from eav.logic.entity_pk import get_entity_pk_type
2021-10-16 00:25:10 +00:00
try:
from django.db.models import JSONField
except ImportError:
from django_jsonfield_backport.models import JSONField
2021-04-05 02:53:45 +00:00
2021-10-16 17:45:01 +00:00
from eav import register
from eav.exceptions import IllegalAssignmentException
from eav.fields import CSVField, EavDatatypeField, EavSlugField
from eav.validators import (
validate_bool,
2021-10-16 00:25:10 +00:00
validate_csv,
validate_date,
2021-04-05 02:53:45 +00:00
validate_enum,
2021-10-16 00:25:10 +00:00
validate_float,
validate_int,
validate_json,
2021-10-16 00:25:10 +00:00
validate_object,
validate_text,
)
2010-09-27 13:28:52 +00:00
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.
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
For example::
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
yes = EnumValue.objects.create(value='Yes') # doctest: SKIP
no = EnumValue.objects.create(value='No')
unknown = EnumValue.objects.create(value='Unknown')
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
ynu.values.add(yes, no, unknown)
2010-09-27 13:28:52 +00:00
Attribute.objects.create(name='has fever?',
datatype=Attribute.TYPE_ENUM, enum_group=ynu)
2018-06-20 12:23:30 +00:00
# = <Attribute: has fever? (Multiple Choice)>
2010-09-27 13:28:52 +00:00
.. note::
The same *EnumValue* objects should be reused within multiple
*EnumGroups*. For example, if you have one *EnumGroup* called: *Yes /
No / Unknown* and another called *Yes / No / Not applicable*, you should
only have a total of four *EnumValues* objects, as you should have used
the same *Yes* and *No* *EnumValues* for both *EnumGroups*.
"""
2021-10-16 17:43:02 +00:00
value = models.CharField(
_('Value'),
db_index=True,
unique=True,
max_length=50,
)
2010-09-27 13:28:52 +00:00
def __str__(self):
"""String representation of `EnumValue` instance."""
return str(self.value)
def __repr__(self):
"""String representation of `EnumValue` object."""
return '<EnumValue {0}>'.format(self.value)
2010-09-27 13:28:52 +00:00
class EnumGroup(models.Model):
"""
*EnumGroup* objects have two fields - a *name* ``CharField`` and *values*,
2010-09-27 13:28:52 +00:00
a ``ManyToManyField`` to :class:`EnumValue`. :class:`Attribute` classes
with datatype *TYPE_ENUM* have a ``ForeignKey`` field to *EnumGroup*.
See :class:`EnumValue` for an example.
"""
2021-10-16 17:43:02 +00:00
name = models.CharField(_('Name'), unique=True, max_length=100)
values = models.ManyToManyField(EnumValue, verbose_name=_('Enum group'))
2010-09-27 13:28:52 +00:00
2018-04-06 12:45:27 +00:00
def __str__(self):
"""String representation of `EnumGroup` instance."""
return str(self.name)
def __repr__(self):
"""String representation of `EnumGroup` object."""
return '<EnumGroup {0}>'.format(self.name)
2010-09-27 13:28:52 +00:00
class Attribute(models.Model):
"""
2010-09-27 13:28:52 +00:00
Putting the **A** in *EAV*. This holds the attributes, or concepts.
Examples of possible *Attributes*: color, height, weight, number of
children, number of patients, has fever?, etc...
2010-09-27 13:28:52 +00:00
Each attribute has a name, and a description, along with a slug that must
2012-04-04 20:57:54 +00:00
be unique. If you don't provide a slug, a default slug (derived from
2010-09-27 13:28:52 +00:00
name), will be created.
The *required* field is a boolean that indicates whether this EAV attribute
2012-04-04 20:57:54 +00:00
is required for entities to which it applies. It defaults to *False*.
2010-09-27 13:28:52 +00:00
.. warning::
Just like a normal model field that is required, you will not be able
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:
* int (TYPE_INT)
* float (TYPE_FLOAT)
* text (TYPE_TEXT)
* date (TYPE_DATE)
* bool (TYPE_BOOLEAN)
* object (TYPE_OBJECT)
* enum (TYPE_ENUM)
2021-04-05 02:53:45 +00:00
* json (TYPE_JSON)
* csv (TYPE_CSV)
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
Examples::
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
Attribute.objects.create(name='Height', datatype=Attribute.TYPE_INT)
# = <Attribute: Height (Integer)>
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
Attribute.objects.create(name='Color', datatype=Attribute.TYPE_TEXT)
# = <Attribute: Color (Text)>
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
yes = EnumValue.objects.create(value='yes')
no = EnumValue.objects.create(value='no')
unknown = EnumValue.objects.create(value='unknown')
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
ynu.values.add(yes, no, unknown)
2018-06-20 12:23:30 +00:00
Attribute.objects.create(name='has fever?', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
# = <Attribute: has fever? (Multiple Choice)>
2010-09-27 13:28:52 +00:00
.. warning:: Once an Attribute has been used by an entity, you can not
change it's datatype.
"""
2021-10-16 17:43:02 +00:00
2010-09-27 13:28:52 +00:00
class Meta:
ordering = ['name']
2010-09-27 13:28:52 +00:00
2021-10-16 17:43:02 +00:00
TYPE_TEXT = 'text'
TYPE_FLOAT = 'float'
TYPE_INT = 'int'
TYPE_DATE = 'date'
2010-09-27 13:28:52 +00:00
TYPE_BOOLEAN = 'bool'
2021-10-16 17:43:02 +00:00
TYPE_OBJECT = 'object'
TYPE_ENUM = 'enum'
TYPE_JSON = 'json'
TYPE_CSV = 'csv'
2010-09-27 13:28:52 +00:00
DATATYPE_CHOICES = (
2021-10-16 17:43:02 +00:00
(TYPE_TEXT, _('Text')),
(TYPE_DATE, _('Date')),
(TYPE_FLOAT, _('Float')),
(TYPE_INT, _('Integer')),
2018-06-20 12:23:30 +00:00
(TYPE_BOOLEAN, _('True / False')),
2021-10-16 17:43:02 +00:00
(TYPE_OBJECT, _('Django Object')),
(TYPE_ENUM, _('Multiple Choice')),
(TYPE_JSON, _('JSON Object')),
(TYPE_CSV, _('Comma-Separated-Value')),
2010-09-27 13:28:52 +00:00
)
2018-06-20 12:23:30 +00:00
# Core attributes
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
datatype = EavDatatypeField(
2021-10-16 17:43:02 +00:00
verbose_name=_('Data Type'), choices=DATATYPE_CHOICES, max_length=6
2018-06-20 12:23:30 +00:00
)
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
name = models.CharField(
2021-10-16 17:43:02 +00:00
verbose_name=_('Name'),
max_length=100,
help_text=_('User-friendly attribute name'),
2018-06-20 12:23:30 +00:00
)
2010-09-27 13:28:52 +00:00
"""
2018-06-20 12:23:30 +00:00
Main identifer for the attribute.
Upon creation, slug is autogenerated from the name.
(see :meth:`~eav.fields.EavSlugField.create_slug_from_name`).
"""
2018-06-20 12:23:30 +00:00
slug = EavSlugField(
2021-10-16 17:43:02 +00:00
verbose_name=_('Slug'),
max_length=50,
db_index=True,
unique=True,
help_text=_('Short unique attribute label'),
2018-06-20 12:23:30 +00:00
)
2010-09-27 13:28:52 +00:00
"""
2018-06-20 12:23:30 +00:00
.. warning::
This attribute should be used with caution. Setting this to *True*
means that *all* entities that *can* have this attribute will
be required to have a value for it.
"""
2021-10-16 17:43:02 +00:00
required = models.BooleanField(verbose_name=_('Required'), default=False)
2018-06-20 12:23:30 +00:00
entity_ct = models.ManyToManyField(ContentType, blank=True)
"""
This field allows you to specify a relationship with any number of content types.
This would be useful, for example, if you wanted an attribute to apply only to
a subset of entities. In that case, you could filter by content type in the
:meth:`~eav.registry.EavConfig.get_attributes` method of that entity's config.
"""
2020-10-20 16:28:14 +00:00
2018-06-20 12:23:30 +00:00
enum_group = models.ForeignKey(
EnumGroup,
2021-10-16 17:43:02 +00:00
verbose_name=_('Choice Group'),
on_delete=models.PROTECT,
blank=True,
null=True,
2018-06-20 12:23:30 +00:00
)
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
description = models.CharField(
2021-10-16 17:43:02 +00:00
verbose_name=_('Description'),
max_length=256,
blank=True,
null=True,
help_text=_('Short description'),
2018-06-20 12:23:30 +00:00
)
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
# Useful meta-information
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
display_order = models.PositiveIntegerField(
2021-10-16 17:43:02 +00:00
verbose_name=_('Display order'), default=1
2018-06-20 12:23:30 +00:00
)
2010-09-27 13:28:52 +00:00
2021-10-16 17:43:02 +00:00
modified = models.DateTimeField(verbose_name=_('Modified'), auto_now=True)
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
created = models.DateTimeField(
2021-10-16 17:43:02 +00:00
verbose_name=_('Created'), default=timezone.now, editable=False
2018-06-20 12:23:30 +00:00
)
2018-06-20 12:23:30 +00:00
@property
def help_text(self):
return self.description
2010-09-30 09:43:26 +00:00
2010-09-27 13:28:52 +00:00
def get_validators(self):
"""
2010-09-27 13:28:52 +00:00
Returns the appropriate validator function from :mod:`~eav.validators`
as a list (of length one) for the datatype.
.. note::
The reason it returns it as a list, is eventually we may want this
method to look elsewhere for additional attribute specific
validators to return as well as the default, built-in one.
"""
2010-09-27 13:28:52 +00:00
DATATYPE_VALIDATORS = {
2021-10-16 17:43:02 +00:00
'text': validate_text,
'float': validate_float,
'int': validate_int,
'date': validate_date,
'bool': validate_bool,
2010-09-27 13:28:52 +00:00
'object': validate_object,
2021-10-16 17:43:02 +00:00
'enum': validate_enum,
'json': validate_json,
'csv': validate_csv,
2010-09-27 13:28:52 +00:00
}
2018-04-06 11:59:51 +00:00
2018-06-20 12:23:30 +00:00
return [DATATYPE_VALIDATORS[self.datatype]]
2010-09-27 13:28:52 +00:00
def validate_value(self, value):
"""
2010-09-27 13:28:52 +00:00
Check *value* against the validators returned by
:meth:`get_validators` for this attribute.
"""
2010-09-27 13:28:52 +00:00
for validator in self.get_validators():
validator(value)
2018-06-20 12:23:30 +00:00
2010-09-27 13:28:52 +00:00
if self.datatype == self.TYPE_ENUM:
if isinstance(value, EnumValue):
value = value.value
if not self.enum_group.values.filter(value=value).exists():
2018-06-20 12:23:30 +00:00
raise ValidationError(
_('%(val)s is not a valid choice for %(attr)s')
2021-10-16 17:43:02 +00:00
% dict(val=value, attr=self)
2018-06-20 12:23:30 +00:00
)
2010-09-27 13:28:52 +00:00
def save(self, *args, **kwargs):
"""
2018-06-20 12:23:30 +00:00
Saves the Attribute and auto-generates a slug field
if one wasn't provided.
"""
2010-09-27 13:28:52 +00:00
if not self.slug:
self.slug = EavSlugField.create_slug_from_name(self.name)
2018-06-20 12:23:30 +00:00
2010-09-27 13:28:52 +00:00
self.full_clean()
super(Attribute, self).save(*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.
"""
2010-09-27 13:28:52 +00:00
if self.datatype == self.TYPE_ENUM and not self.enum_group:
2018-06-20 12:23:30 +00:00
raise ValidationError(
_('You must set the choice group for multiple choice attributes')
)
2010-09-27 13:28:52 +00:00
if self.datatype != self.TYPE_ENUM and self.enum_group:
2018-06-20 12:23:30 +00:00
raise ValidationError(
_('You can only assign a choice group to multiple choice attributes')
)
2010-09-27 13:28:52 +00:00
def get_choices(self):
"""
2010-09-27 13:28:52 +00:00
Returns a query set of :class:`EnumValue` objects for this attribute.
Returns None if the datatype of this attribute is not *TYPE_ENUM*.
"""
2021-10-16 17:43:02 +00:00
return (
self.enum_group.values.all()
if self.datatype == Attribute.TYPE_ENUM
else None
)
2010-09-27 13:28:52 +00:00
def save_value(self, entity, value):
"""
2018-06-20 12:23:30 +00:00
Called with *entity*, any Django object registered with eav, and
2010-09-27 13:28:52 +00:00
*value*, the :class:`Value` this attribute for *entity* should
be set to.
If a :class:`Value` object for this *entity* and attribute doesn't
exist, one will be created.
.. note::
If *value* is None and a :class:`Value` object exists for this
2018-06-20 12:23:30 +00:00
Attribute and *entity*, it will delete that :class:`Value` object.
"""
2010-09-27 13:28:52 +00:00
ct = ContentType.objects.get_for_model(entity)
2018-06-20 12:23:30 +00:00
entity_filter = {
'entity_ct': ct,
'attribute': self,
'{0}'.format(get_entity_pk_type(entity)): entity.pk,
}
2010-09-27 13:28:52 +00:00
try:
value_obj = self.value_set.get(**entity_filter)
2010-09-27 13:28:52 +00:00
except Value.DoesNotExist:
if value == None or value == '':
return
2018-06-20 12:23:30 +00:00
value_obj = Value.objects.create(**entity_filter)
2018-06-20 12:23:30 +00:00
2010-09-27 13:28:52 +00:00
if value == None or value == '':
value_obj.delete()
return
if value != value_obj.value:
value_obj.value = value
value_obj.save()
2018-06-20 12:23:30 +00:00
def __str__(self):
return '{} ({})'.format(self.name, self.get_datatype_display())
2010-09-27 13:28:52 +00:00
class Value(models.Model): # noqa: WPS110
"""Putting the **V** in *EAV*.
This model stores the value for one particular :class:`Attribute` for
some entity.
2010-09-27 13:28:52 +00:00
As with most EAV implementations, most of the columns of this model will
be blank, as onle one *value_* field will be used.
2018-06-20 12:23:30 +00:00
Example::
import eav
from django.contrib.auth.models import User
eav.register(User)
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
u = User.objects.create(username='crazy_dev_user')
a = Attribute.objects.create(name='Fav Drink', datatype='text')
Value.objects.create(entity = u, attribute = a, value_text = 'red bull')
# = <Value: crazy_dev_user - Fav Drink: "red bull">
"""
2010-09-27 13:28:52 +00:00
# Direct foreign keys
attribute = models.ForeignKey(
Attribute,
db_index=True,
on_delete=models.PROTECT,
verbose_name=_('Attribute'),
)
# Entity generic relationships. Rather than rely on database casting,
# this will instead use a separate ForeignKey field attribute that matches
# the FK type of the entity.
entity_id = models.IntegerField(blank=True, null=True)
entity_uuid = models.UUIDField(blank=True, null=True)
2018-06-20 12:23:30 +00:00
entity_ct = models.ForeignKey(
ContentType,
on_delete=models.PROTECT,
related_name='value_entities',
2018-06-20 12:23:30 +00:00
)
entity_pk_int = generic.GenericForeignKey(
ct_field='entity_ct',
fk_field='entity_id',
)
2021-10-16 17:43:02 +00:00
entity_pk_uuid = generic.GenericForeignKey(
ct_field='entity_ct',
fk_field='entity_uuid',
)
# Model attributes
created = models.DateTimeField(
_('Created'),
default=timezone.now,
)
modified = models.DateTimeField(_('Modified'), auto_now=True)
# Value attributes
value_bool = models.BooleanField(blank=True, null=True)
value_csv = CSVField(blank=True, null=True)
value_date = models.DateTimeField(blank=True, null=True)
2021-10-16 17:43:02 +00:00
value_float = models.FloatField(blank=True, null=True)
value_int = models.BigIntegerField(blank=True, null=True)
value_text = models.TextField(blank=True, null=True)
2021-10-16 17:43:02 +00:00
value_json = JSONField(
default=dict,
encoder=DjangoJSONEncoder,
blank=True,
null=True,
2021-10-16 17:43:02 +00:00
)
2018-06-20 12:23:30 +00:00
2021-10-16 17:43:02 +00:00
value_enum = models.ForeignKey(
2018-06-20 12:23:30 +00:00
EnumValue,
2021-10-16 17:43:02 +00:00
blank=True,
null=True,
on_delete=models.PROTECT,
related_name='eav_values',
2018-06-20 12:23:30 +00:00
)
2010-09-27 13:28:52 +00:00
# Value object relationship
2010-09-27 13:28:52 +00:00
generic_value_id = models.IntegerField(blank=True, null=True)
2018-06-20 12:23:30 +00:00
generic_value_ct = models.ForeignKey(
ContentType,
2021-10-16 17:43:02 +00:00
blank=True,
null=True,
on_delete=models.PROTECT,
related_name='value_values',
2018-06-20 12:23:30 +00:00
)
value_object = generic.GenericForeignKey(
ct_field='generic_value_ct',
fk_field='generic_value_id',
2018-06-20 12:23:30 +00:00
)
def __str__(self):
"""String representation of a Value."""
entity = self.entity_pk_int
if self.entity_uuid:
entity = self.entity_pk_uuid
return '{0}: "{1}" ({2})'.format(
self.attribute.name,
self.value,
entity,
)
2010-09-27 13:28:52 +00:00
def __repr__(self):
"""Representation of Value object."""
entity = self.entity_pk_int
if self.entity_uuid:
entity = self.entity_pk_uuid
return '{0}: "{1}" ({2})'.format(
self.attribute.name,
self.value,
entity.pk,
)
2010-09-27 13:28:52 +00:00
def save(self, *args, **kwargs):
"""Validate and save this value."""
2010-09-27 13:28:52 +00:00
self.full_clean()
super().save(*args, **kwargs)
2010-09-27 13:28:52 +00:00
def _get_value(self):
"""Return the python object this value is holding."""
return getattr(self, 'value_{0}'.format(self.attribute.datatype))
2010-09-27 13:28:52 +00:00
def _set_value(self, new_value):
"""Set the object this value is holding."""
setattr(self, 'value_{0}'.format(self.attribute.datatype), new_value)
2010-09-27 13:28:52 +00:00
value = property(_get_value, _set_value) # noqa: WPS110
2010-09-27 13:28:52 +00:00
class Entity(object):
"""
2018-06-20 12:23:30 +00:00
The helper class that will be attached to any entity
registered with eav.
"""
2021-10-16 17:43:02 +00:00
2018-06-20 12:23:30 +00:00
@staticmethod
def pre_save_handler(sender, *args, **kwargs):
"""
Pre save handler attached to self.instance. Called before the
2018-06-20 12:23:30 +00:00
model instance we are attached to is saved. This allows us to call
:meth:`validate_attributes` before the entity is saved.
"""
2018-06-20 12:23:30 +00:00
instance = kwargs['instance']
entity = getattr(kwargs['instance'], instance._eav_config_cls.eav_attr)
entity.validate_attributes()
2010-09-27 13:28:52 +00:00
@staticmethod
def post_save_handler(sender, *args, **kwargs):
"""
Post save handler attached to self.instance. Calls :meth:`save` when
the model instance we are attached to is saved.
"""
instance = kwargs['instance']
entity = getattr(instance, instance._eav_config_cls.eav_attr)
entity.save()
2010-09-27 13:28:52 +00:00
def __init__(self, instance):
"""
Set self.instance equal to the instance of the model that we're attached
2018-06-20 12:23:30 +00:00
to. Also, store the content type of that instance.
"""
self.instance = instance
2010-09-27 13:28:52 +00:00
self.ct = ContentType.objects.get_for_model(instance)
def __getattr__(self, name):
"""
2018-06-20 12:23:30 +00:00
Tha magic getattr helper. This is called whenever user invokes::
instance.<attribute>
2010-09-27 13:28:52 +00:00
Checks if *name* is a valid slug for attributes available to this
instances. If it is, tries to lookup the :class:`Value` with that
attribute slug. If there is one, it returns the value of the
class:`Value` object, otherwise it hasn't been set, so it returns
None.
"""
2010-09-27 13:28:52 +00:00
if not name.startswith('_'):
try:
attribute = self.get_attribute_by_slug(name)
except Attribute.DoesNotExist:
2018-06-20 12:23:30 +00:00
raise AttributeError(
_('%(obj)s has no EAV attribute named %(attr)s')
2021-10-16 17:43:02 +00:00
% dict(obj=self.instance, attr=name)
2018-06-20 12:23:30 +00:00
)
2010-09-27 13:28:52 +00:00
try:
return self.get_value_by_attribute(attribute).value
except Value.DoesNotExist:
return None
2018-06-20 12:23:30 +00:00
2010-09-27 13:28:52 +00:00
return getattr(super(Entity, self), name)
def get_all_attributes(self):
"""
2010-09-27 13:28:52 +00:00
Return a query set of all :class:`Attribute` objects that can be set
for this entity.
"""
return self.instance._eav_config_cls.get_attributes(
instance=self.instance
).order_by('display_order')
2010-09-27 13:28:52 +00:00
def _hasattr(self, attribute_slug):
"""
2018-06-20 12:23:30 +00:00
Since we override __getattr__ with a backdown to the database, this
exists as a way of checking whether a user has set a real attribute on
ourselves, without going to the db if not.
"""
return attribute_slug in self.__dict__
def _getattr(self, attribute_slug):
"""
2018-06-20 12:23:30 +00:00
Since we override __getattr__ with a backdown to the database, this
exists as a way of getting the value a user set for one of our
attributes, without going to the db to check.
"""
return self.__dict__[attribute_slug]
2010-09-27 13:28:52 +00:00
def save(self):
"""
2010-09-27 13:28:52 +00:00
Saves all the EAV values that have been set on this entity.
"""
2010-09-27 13:28:52 +00:00
for attribute in self.get_all_attributes():
if self._hasattr(attribute.slug):
attribute_value = self._getattr(attribute.slug)
2021-10-16 17:43:02 +00:00
if attribute.datatype == Attribute.TYPE_ENUM and not isinstance(
attribute_value, EnumValue
):
if attribute_value is not None:
attribute_value = EnumValue.objects.get(value=attribute_value)
attribute.save_value(self.instance, attribute_value)
2010-09-27 13:28:52 +00:00
def validate_attributes(self):
"""
2010-09-27 13:28:52 +00:00
Called before :meth:`save`, first validate all the entity values to
make sure they can be created / saved cleanly.
Raises ``ValidationError`` if they can't be.
"""
values_dict = self.get_values_dict()
2010-09-27 13:28:52 +00:00
for attribute in self.get_all_attributes():
value = None
2018-06-20 12:23:30 +00:00
# Value was assigned to this instance.
if self._hasattr(attribute.slug):
value = self._getattr(attribute.slug)
values_dict.pop(attribute.slug, None)
# Otherwise try pre-loaded from DB.
else:
value = values_dict.pop(attribute.slug, None)
2010-09-27 13:28:52 +00:00
if value is None:
if attribute.required:
raise ValidationError(
_('{} EAV field cannot be blank'.format(attribute.slug))
)
2010-09-27 13:28:52 +00:00
else:
try:
attribute.validate_value(value)
2015-06-23 11:56:03 +00:00
except ValidationError as e:
2018-06-20 12:23:30 +00:00
raise ValidationError(
_('%(attr)s EAV field %(err)s')
2021-10-16 17:43:02 +00:00
% dict(attr=attribute.slug, err=e)
2018-06-20 12:23:30 +00:00
)
illegal = values_dict or (
2021-10-16 17:43:02 +00:00
self.get_object_attributes() - self.get_all_attribute_slugs()
)
2018-06-20 12:23:30 +00:00
if illegal:
raise IllegalAssignmentException(
2021-10-16 17:43:02 +00:00
'Instance of the class {} cannot have values for attributes: {}.'.format(
self.instance.__class__, ', '.join(illegal)
)
)
def get_values_dict(self):
return {v.attribute.slug: v.value for v in self.get_values()}
2010-09-27 13:28:52 +00:00
def get_values(self):
"""Get all set :class:`Value` objects for self.instance."""
entity_filter = {
'entity_ct': self.ct,
'{0}'.format(get_entity_pk_type(self.instance)): self.instance.pk,
}
return Value.objects.filter(**entity_filter).select_related()
2010-09-27 13:28:52 +00:00
def get_all_attribute_slugs(self):
"""
2010-09-27 13:28:52 +00:00
Returns a list of slugs for all attributes available to this entity.
"""
return set(self.get_all_attributes().values_list('slug', flat=True))
2010-09-27 13:28:52 +00:00
def get_attribute_by_slug(self, slug):
"""
2018-06-20 12:23:30 +00:00
Returns a single :class:`Attribute` with *slug*.
"""
2010-09-27 13:28:52 +00:00
return self.get_all_attributes().get(slug=slug)
def get_value_by_attribute(self, attribute):
"""
2018-06-20 12:23:30 +00:00
Returns a single :class:`Value` for *attribute*.
"""
2010-09-27 13:28:52 +00:00
return self.get_values().get(attribute=attribute)
def get_object_attributes(self):
"""
Returns entity instance attributes, except for
``instance`` and ``ct`` which are used internally.
"""
return set(copy(self.__dict__).keys()) - set(['instance', 'ct'])
2010-09-27 13:28:52 +00:00
def __iter__(self):
"""
2018-06-20 12:23:30 +00:00
Iterate over set eav values. This would allow you to do::
2010-09-27 13:28:52 +00:00
2018-06-20 12:23:30 +00:00
for i in m.eav: print(i)
"""
2010-09-27 13:28:52 +00:00
return iter(self.get_values())
class EAVModelMeta(ModelBase):
def __new__(cls, name, bases, namespace, **kwds):
result = super(EAVModelMeta, cls).__new__(cls, name, bases, dict(namespace))
register(result)
return result