from copy import copy
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models.base import ModelBase
from django.utils.translation import gettext_lazy as _
from eav import register
from eav.exceptions import IllegalAssignmentException
from eav.logic.entity_pk import get_entity_pk_type
from .attribute import Attribute
from .enum_value import EnumValue
from .value import Value
[docs]class Entity:
"""Helper class that will be attached to entities registered with eav."""
[docs] @staticmethod
def pre_save_handler(sender, *args, **kwargs):
"""
Pre save handler attached to self.instance. Called before the
model instance we are attached to is saved. This allows us to call
:meth:`validate_attributes` before the entity is saved.
"""
instance = kwargs['instance']
entity = getattr(kwargs['instance'], instance._eav_config_cls.eav_attr)
entity.validate_attributes()
[docs] @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()
def __init__(self, instance) -> None:
"""
Set self.instance equal to the instance of the model that we're attached
to. Also, store the content type of that instance.
"""
self.instance = instance
self.ct = ContentType.objects.get_for_model(instance)
def __getattr__(self, name):
"""
The magic getattr helper. This is called whenever user invokes::
instance.<attribute>
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.
"""
if not name.startswith('_'):
try:
attribute = self.get_attribute_by_slug(name)
except Attribute.DoesNotExist:
raise AttributeError(
_('%(obj)s has no EAV attribute named %(attr)s')
% {'obj': self.instance, 'attr': name},
)
try:
return self.get_value_by_attribute(attribute).value
except Value.DoesNotExist:
return None
return getattr(super(), name)
[docs] 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(
instance=self.instance,
).order_by('display_order')
def _hasattr(self, attribute_slug):
"""
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):
"""
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]
[docs] def save(self):
"""Saves all the EAV values that have been set on this entity."""
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,
)
and attribute_value is not None
):
attribute_value = EnumValue.objects.get(value=attribute_value)
attribute.save_value(self.instance, attribute_value)
[docs] def validate_attributes(self):
"""
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()
for attribute in self.get_all_attributes():
value = None
# 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)
if value is None:
if attribute.required:
raise ValidationError(
_(f'{attribute.slug} EAV field cannot be blank'),
)
else:
try:
attribute.validate_value(value)
except ValidationError as e:
raise ValidationError(
_('%(attr)s EAV field %(err)s')
% {'attr': attribute.slug, 'err': e},
)
illegal = values_dict or (
self.get_object_attributes() - self.get_all_attribute_slugs()
)
if illegal:
raise IllegalAssignmentException(
'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()}
[docs] def get_values(self):
"""Get all set :class:`Value` objects for self.instance."""
entity_filter = {
'entity_ct': self.ct,
f'{get_entity_pk_type(self.instance)}': self.instance.pk,
}
return Value.objects.filter(**entity_filter).select_related()
[docs] def get_all_attribute_slugs(self):
"""Returns a list of slugs for all attributes available to this entity."""
return set(self.get_all_attributes().values_list('slug', flat=True))
[docs] def get_attribute_by_slug(self, slug):
"""Returns a single :class:`Attribute` with *slug*."""
return self.get_all_attributes().get(slug=slug)
[docs] def get_value_by_attribute(self, attribute):
"""Returns a single :class:`Value` for *attribute*."""
return self.get_values().get(attribute=attribute)
[docs] def get_object_attributes(self):
"""
Returns entity instance attributes, except for
``instance`` and ``ct`` which are used internally.
"""
return set(copy(self.__dict__).keys()) - {'instance', 'ct'}
def __iter__(self):
"""
Iterate over set eav values. This would allow you to do::
for i in m.eav: print(i)
"""
return iter(self.get_values())