Validate against illegal assignments (#17)

This commit is contained in:
Iwo Herka 2018-06-21 13:38:48 +02:00
parent 6f3329d2ae
commit dcfd8e4e7e
4 changed files with 78 additions and 43 deletions

2
eav/exceptions.py Normal file
View file

@ -0,0 +1,2 @@
class IllegalAssignmentException(Exception):
pass

View file

@ -9,6 +9,7 @@ This module defines the four concrete, non-abstract models:
Along with the :class:`Entity` helper class. Along with the :class:`Entity` helper class.
''' '''
from copy import copy
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes import fields as generic
@ -18,6 +19,7 @@ from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .exceptions import IllegalAssignmentException
from .fields import EavDatatypeField, EavSlugField from .fields import EavDatatypeField, EavSlugField
from .validators import * from .validators import *
@ -440,20 +442,10 @@ class Entity(object):
The helper class that will be attached to any entity The helper class that will be attached to any entity
registered with eav. registered with eav.
''' '''
@staticmethod
def post_save_handler(sender, *args, **kwargs):
'''
Post save handler attached to self.model. 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()
@staticmethod @staticmethod
def pre_save_handler(sender, *args, **kwargs): def pre_save_handler(sender, *args, **kwargs):
''' '''
Pre save handler attached to self.model. Called before the Pre save handler attached to self.instance. Called before the
model instance we are attached to is saved. This allows us to call model instance we are attached to is saved. This allows us to call
:meth:`validate_attributes` before the entity is saved. :meth:`validate_attributes` before the entity is saved.
''' '''
@ -461,12 +453,22 @@ class Entity(object):
entity = getattr(kwargs['instance'], instance._eav_config_cls.eav_attr) entity = getattr(kwargs['instance'], instance._eav_config_cls.eav_attr)
entity.validate_attributes() entity.validate_attributes()
@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): def __init__(self, instance):
''' '''
Set self.model equal to the instance of the model that we're attached Set self.instance equal to the instance of the model that we're attached
to. Also, store the content type of that instance. to. Also, store the content type of that instance.
''' '''
self.model = instance self.instance = instance
self.ct = ContentType.objects.get_for_model(instance) self.ct = ContentType.objects.get_for_model(instance)
def __getattr__(self, name): def __getattr__(self, name):
@ -487,7 +489,7 @@ class Entity(object):
except Attribute.DoesNotExist: except Attribute.DoesNotExist:
raise AttributeError( raise AttributeError(
_('%(obj)s has no EAV attribute named %(attr)s') _('%(obj)s has no EAV attribute named %(attr)s')
% dict(obj = self.model, attr = name) % dict(obj = self.instance, attr = name)
) )
try: try:
@ -502,7 +504,7 @@ class Entity(object):
Return a query set of all :class:`Attribute` objects that can be set Return a query set of all :class:`Attribute` objects that can be set
for this entity. for this entity.
''' '''
return self.model._eav_config_cls.get_attributes().order_by('display_order') return self.instance._eav_config_cls.get_attributes().order_by('display_order')
def _hasattr(self, attribute_slug): def _hasattr(self, attribute_slug):
''' '''
@ -527,30 +529,32 @@ class Entity(object):
for attribute in self.get_all_attributes(): for attribute in self.get_all_attributes():
if self._hasattr(attribute.slug): if self._hasattr(attribute.slug):
attribute_value = self._getattr(attribute.slug) attribute_value = self._getattr(attribute.slug)
attribute.save_value(self.model, attribute_value) attribute.save_value(self.instance, attribute_value)
def validate_attributes(self): def validate_attributes(self):
''' '''
Called before :meth:`save`, first validate all the entity values to Called before :meth:`save`, first validate all the entity values to
make sure they can be created / saved cleanly. make sure they can be created / saved cleanly.
Raises ``ValidationError`` if they can't be.
Raise ``ValidationError`` if they can't be.
''' '''
values_dict = self.get_values_dict() values_dict = self.get_values_dict()
for attribute in self.get_all_attributes(): for attribute in self.get_all_attributes():
value = None value = None
# Value was assigned to this instance.
if self._hasattr(attribute.slug): if self._hasattr(attribute.slug):
value = self._getattr(attribute.slug) value = self._getattr(attribute.slug)
values_dict.pop(attribute.slug, None)
# Otherwise try pre-loaded from DB.
else: else:
value = values_dict.get(attribute.slug, None) value = values_dict.pop(attribute.slug, None)
if value is None: if value is None:
if attribute.required: if attribute.required:
raise ValidationError(_( raise ValidationError(
'{} EAV field cannot be blank'.format(attribute.slug) _('{} EAV field cannot be blank'.format(attribute.slug))
)) )
else: else:
try: try:
attribute.validate_value(value) attribute.validate_value(value)
@ -560,28 +564,32 @@ class Entity(object):
% dict(attr = attribute.slug, err = e) % dict(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): def get_values_dict(self):
values_dict = dict() return {v.attribute.slug: v.value for v in self.get_values()}
for value in self.get_values():
values_dict[value.attribute.slug] = value.value
return values_dict
def get_values(self): def get_values(self):
''' '''
Get all set :class:`Value` objects for self.model Get all set :class:`Value` objects for self.instance
''' '''
return Value.objects.filter( return Value.objects.filter(
entity_ct=self.ct, entity_ct = self.ct,
entity_id=self.model.pk entity_id = self.instance.pk
).select_related() ).select_related()
def get_all_attribute_slugs(self): def get_all_attribute_slugs(self):
''' '''
Returns a list of slugs for all attributes available to this entity. Returns a list of slugs for all attributes available to this entity.
''' '''
return self.get_all_attributes().values_list('slug', flat=True) return set(self.get_all_attributes().values_list('slug', flat=True))
def get_attribute_by_slug(self, slug): def get_attribute_by_slug(self, slug):
''' '''
@ -595,6 +603,13 @@ class Entity(object):
''' '''
return self.get_values().get(attribute=attribute) 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'])
def __iter__(self): def __iter__(self):
''' '''
Iterate over set eav values. This would allow you to do:: Iterate over set eav values. This would allow you to do::

View file

@ -87,7 +87,7 @@ class Registry(object):
@staticmethod @staticmethod
def attach_eav_attr(sender, *args, **kwargs): def attach_eav_attr(sender, *args, **kwargs):
''' '''
Attache EAV Entity toolkit to an instance after init. Attach EAV Entity toolkit to an instance after init.
''' '''
instance = kwargs['instance'] instance = kwargs['instance']
config_cls = instance.__class__._eav_config_cls config_cls = instance.__class__._eav_config_cls
@ -131,19 +131,22 @@ class Registry(object):
def _attach_signals(self): def _attach_signals(self):
''' '''
Attach all signals for eav Attach pre- and post- save signals from model class
to Entity helper. This way, Entity instance will be
able to prepare and clean-up before and after creation /
update of the user's model class instance.
''' '''
post_init.connect(Registry.attach_eav_attr, sender=self.model_cls) post_init.connect(Registry.attach_eav_attr, sender = self.model_cls)
pre_save.connect(Entity.pre_save_handler, sender=self.model_cls) pre_save.connect(Entity.pre_save_handler, sender = self.model_cls)
post_save.connect(Entity.post_save_handler, sender=self.model_cls) post_save.connect(Entity.post_save_handler, sender = self.model_cls)
def _detach_signals(self): def _detach_signals(self):
''' '''
Detach all signals for eav Detach all signals for eav.
''' '''
post_init.disconnect(Registry.attach_eav_attr, sender=self.model_cls) post_init.disconnect(Registry.attach_eav_attr, sender = self.model_cls)
pre_save.disconnect(Entity.pre_save_handler, sender=self.model_cls) pre_save.disconnect(Entity.pre_save_handler, sender = self.model_cls)
post_save.disconnect(Entity.post_save_handler, sender=self.model_cls) post_save.disconnect(Entity.post_save_handler, sender = self.model_cls)
def _attach_generic_relation(self): def _attach_generic_relation(self):
''' '''

View file

@ -4,6 +4,7 @@ from django.test import TestCase
import eav import eav
from eav.models import Attribute, Value from eav.models import Attribute, Value
from eav.registry import EavConfig from eav.registry import EavConfig
from eav.exceptions import IllegalAssignmentException
from .models import Encounter, Patient from .models import Encounter, Patient
@ -54,7 +55,6 @@ class Attributes(TestCase):
p.eav.height = 2.3 p.eav.height = 2.3
p.save() p.save()
e.eav_field.age = 4 e.eav_field.age = 4
e.eav_field.height = 4.5
e.save() e.save()
self.assertEqual(Value.objects.count(), 3) self.assertEqual(Value.objects.count(), 3)
p = Patient.objects.get(name='Jon') p = Patient.objects.get(name='Jon')
@ -62,4 +62,19 @@ class Attributes(TestCase):
self.assertEqual(p.eav.height, 2.3) self.assertEqual(p.eav.height, 2.3)
e = Encounter.objects.get(num=1) e = Encounter.objects.get(num=1)
self.assertEqual(e.eav_field.age, 4) self.assertEqual(e.eav_field.age, 4)
self.assertFalse(hasattr(e.eav_field, 'height'))
def test_illegal_assignemnt(self):
class EncounterEavConfig(EavConfig):
@classmethod
def get_attributes(cls):
return Attribute.objects.filter(datatype=Attribute.TYPE_INT)
eav.unregister(Encounter)
eav.register(Encounter, EncounterEavConfig)
p = Patient.objects.create(name='Jon')
e = Encounter.objects.create(patient=p, num=1)
with self.assertRaises(IllegalAssignmentException):
e.eav.color = 'red'
e.save()