mirror of
https://github.com/jazzband/django-eav2.git
synced 2026-05-22 22:25:55 +00:00
Major refactor
This commit is contained in:
parent
515cdf07ee
commit
a53bc53daa
7 changed files with 104 additions and 334 deletions
5
admin.py
5
admin.py
|
|
@ -53,9 +53,6 @@ class BaseEntityAdmin(ModelAdmin):
|
||||||
return super_meth(request, context, **kwargs)
|
return super_meth(request, context, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BaseEntityInlineFormSet(BaseInlineFormSet):
|
class BaseEntityInlineFormSet(BaseInlineFormSet):
|
||||||
"""
|
"""
|
||||||
An inline formset that correctly initializes EAV forms.
|
An inline formset that correctly initializes EAV forms.
|
||||||
|
|
@ -99,7 +96,7 @@ class BaseEntityInline(InlineModelAdmin):
|
||||||
|
|
||||||
class AttributeAdmin(ModelAdmin):
|
class AttributeAdmin(ModelAdmin):
|
||||||
list_display = ('name', 'slug', 'datatype', 'description')
|
list_display = ('name', 'slug', 'datatype', 'description')
|
||||||
prepopulated_fields = {'name': ('slug',)}
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
|
|
||||||
admin.site.register(Attribute, AttributeAdmin)
|
admin.site.register(Attribute, AttributeAdmin)
|
||||||
admin.site.register(Value)
|
admin.site.register(Value)
|
||||||
|
|
|
||||||
26
forms.py
26
forms.py
|
|
@ -28,22 +28,6 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from .utils import EavRegistry
|
from .utils import EavRegistry
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['BaseSchemaForm', 'BaseDynamicEntityForm']
|
|
||||||
|
|
||||||
|
|
||||||
class BaseSchemaForm(ModelForm):
|
|
||||||
|
|
||||||
'''
|
|
||||||
def clean_name(self):
|
|
||||||
"Avoid name clashes between static and dynamic attributes."
|
|
||||||
name = self.cleaned_data['name']
|
|
||||||
reserved_names = self._meta.model._meta.get_all_field_names()
|
|
||||||
if name not in reserved_names:
|
|
||||||
return name
|
|
||||||
raise ValidationError(_('Attribute name must not clash with reserved names'
|
|
||||||
' ("%s")') % '", "'.join(reserved_names))
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
class BaseDynamicEntityForm(ModelForm):
|
class BaseDynamicEntityForm(ModelForm):
|
||||||
"""
|
"""
|
||||||
|
|
@ -69,15 +53,15 @@ class BaseDynamicEntityForm(ModelForm):
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
super(BaseDynamicEntityForm, self).__init__(data, *args, **kwargs)
|
super(BaseDynamicEntityForm, self).__init__(data, *args, **kwargs)
|
||||||
config_cls = EavRegistry.get_config_cls_for_model(self.instance.__class__)
|
config_cls = EavRegistry.get_config_cls_for_model(self.instance.__class__)
|
||||||
self.proxy = getattr(self.instance, config_cls.eav_attr)
|
self.entity = getattr(self.instance, config_cls.eav_attr)
|
||||||
self._build_dynamic_fields()
|
self._build_dynamic_fields()
|
||||||
|
|
||||||
def _build_dynamic_fields(self):
|
def _build_dynamic_fields(self):
|
||||||
# reset form fields
|
# reset form fields
|
||||||
self.fields = deepcopy(self.base_fields)
|
self.fields = deepcopy(self.base_fields)
|
||||||
|
|
||||||
for attribute in self.proxy.get_eav_attributes():
|
for attribute in self.entity.get_all_attributes():
|
||||||
value = getattr(self.proxy, attribute.slug)
|
value = getattr(self.entity, attribute.slug)
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
'label': attribute.name.capitalize(),
|
'label': attribute.name.capitalize(),
|
||||||
|
|
@ -120,7 +104,7 @@ class BaseDynamicEntityForm(ModelForm):
|
||||||
instance = super(BaseDynamicEntityForm, self).save(commit=False)
|
instance = super(BaseDynamicEntityForm, self).save(commit=False)
|
||||||
|
|
||||||
# assign attributes
|
# assign attributes
|
||||||
for attribute in self.proxy.get_eav_attributes():
|
for attribute in self.entity.get_all_attributes():
|
||||||
value = self.cleaned_data.get(attribute.slug)
|
value = self.cleaned_data.get(attribute.slug)
|
||||||
if attribute.datatype == attribute.TYPE_ENUM:
|
if attribute.datatype == attribute.TYPE_ENUM:
|
||||||
if value:
|
if value:
|
||||||
|
|
@ -128,7 +112,7 @@ class BaseDynamicEntityForm(ModelForm):
|
||||||
else:
|
else:
|
||||||
value = None
|
value = None
|
||||||
|
|
||||||
setattr(self.proxy, attribute.slug, value)
|
setattr(self.entity, attribute.slug, value)
|
||||||
|
|
||||||
# save entity and its attributes
|
# save entity and its attributes
|
||||||
if commit:
|
if commit:
|
||||||
|
|
|
||||||
253
models.py
253
models.py
|
|
@ -31,9 +31,9 @@ from .fields import EavSlugField, EavDatatypeField
|
||||||
|
|
||||||
|
|
||||||
def get_unique_class_identifier(cls):
|
def get_unique_class_identifier(cls):
|
||||||
"""
|
'''
|
||||||
Return a unique identifier for a class
|
Return a unique identifier for a class
|
||||||
"""
|
'''
|
||||||
return '.'.join((inspect.getfile(cls), cls.__name__))
|
return '.'.join((inspect.getfile(cls), cls.__name__))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -86,13 +86,13 @@ class Attribute(models.Model):
|
||||||
(TYPE_ENUM, _(u"Multiple Choice")),
|
(TYPE_ENUM, _(u"Multiple Choice")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
name = models.CharField(_(u"name"), max_length=100,
|
||||||
|
help_text=_(u"User-friendly attribute name"))
|
||||||
|
|
||||||
slug = EavSlugField(_(u"slug"), max_length=50, db_index=True,
|
slug = EavSlugField(_(u"slug"), max_length=50, db_index=True,
|
||||||
help_text=_(u"Short unique attribute label"),
|
help_text=_(u"Short unique attribute label"),
|
||||||
unique=True)
|
unique=True)
|
||||||
|
|
||||||
name = models.CharField(_(u"name"), max_length=100,
|
|
||||||
help_text=_(u"User-friendly attribute name"))
|
|
||||||
|
|
||||||
description = models.CharField(_(u"description"), max_length=256,
|
description = models.CharField(_(u"description"), max_length=256,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
help_text=_(u"Short description"))
|
help_text=_(u"Short description"))
|
||||||
|
|
@ -133,59 +133,33 @@ class Attribute(models.Model):
|
||||||
return None
|
return None
|
||||||
return self.enum_group.enums.all()
|
return self.enum_group.enums.all()
|
||||||
|
|
||||||
|
'''
|
||||||
|
@classmethod
|
||||||
|
def get_for_entity_class(cls, entity_cls):
|
||||||
|
from .utils import EavRegistry
|
||||||
|
config_cls = EavRegistry.get_config_cls_for_model(entity_cls)
|
||||||
|
return config_cls.get_attributes()
|
||||||
|
'''
|
||||||
|
|
||||||
def get_value_for_entity(self, entity):
|
|
||||||
'''
|
|
||||||
Passed any object that may be used as an 'entity' object (is linked
|
|
||||||
to through the generic relation from some EaveValue object. Returns
|
|
||||||
an Value object that has a foreignkey to self (attribute) and
|
|
||||||
to the entity. Returns nothing if a matching Value object
|
|
||||||
doesn't exist.
|
|
||||||
'''
|
|
||||||
ct = ContentType.objects.get_for_model(entity)
|
|
||||||
qs = self.value_set.filter(entity_ct=ct, entity_id=entity.pk)
|
|
||||||
count = qs.count()
|
|
||||||
if count > 1:
|
|
||||||
raise AttributeError(u"You should have one and only one value "\
|
|
||||||
u"for the attribute %s and the entity %s. Found "\
|
|
||||||
u"%s" % (self, entity, qs.count()))
|
|
||||||
if count:
|
|
||||||
return qs[0]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def save_value(self, entity, value):
|
def save_value(self, entity, value):
|
||||||
"""
|
|
||||||
Save any value for an entity, calling the appropriate method
|
|
||||||
according to the type of the value.
|
|
||||||
Value should not be an Value but a normal value
|
|
||||||
"""
|
|
||||||
self._save_single_value(entity, value)
|
|
||||||
|
|
||||||
|
|
||||||
def _save_single_value(self, entity, value=None, attribute=None):
|
|
||||||
"""
|
|
||||||
Save a a value of type that doesn't need special joining like
|
|
||||||
int, float, text, date, etc.
|
|
||||||
|
|
||||||
Value should not be an Value object but a normal value.
|
|
||||||
Use attribute if you want to use something else than the current
|
|
||||||
one
|
|
||||||
"""
|
|
||||||
ct = ContentType.objects.get_for_model(entity)
|
ct = ContentType.objects.get_for_model(entity)
|
||||||
attribute = attribute or self
|
|
||||||
try:
|
try:
|
||||||
eavvalue = self.value_set.get(entity_ct=ct,
|
value_obj = self.value_set.get(entity_ct=ct,
|
||||||
entity_id=entity.pk,
|
entity_id=entity.pk,
|
||||||
attribute=attribute)
|
attribute=self)
|
||||||
except Value.DoesNotExist:
|
except Value.DoesNotExist:
|
||||||
eavvalue = self.value_set.model(entity_ct=ct,
|
if value == None:
|
||||||
entity_id=entity.pk,
|
return
|
||||||
attribute=attribute)
|
value_obj = Value.objects.create(entity_ct=ct,
|
||||||
if value != eavvalue.value:
|
entity_id=entity.pk,
|
||||||
eavvalue.value = value
|
attribute=self)
|
||||||
eavvalue.save()
|
if value == None:
|
||||||
|
value_obj.delete()
|
||||||
|
return
|
||||||
|
|
||||||
|
if value != value_obj.value:
|
||||||
|
value_obj.value = value
|
||||||
|
value_obj.save()
|
||||||
|
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
|
|
@ -209,6 +183,10 @@ class Value(models.Model):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('entity_ct', 'entity_id', 'attribute')
|
||||||
|
|
||||||
|
|
||||||
entity_ct = models.ForeignKey(ContentType, related_name='value_entities')
|
entity_ct = models.ForeignKey(ContentType, related_name='value_entities')
|
||||||
entity_id = models.IntegerField()
|
entity_id = models.IntegerField()
|
||||||
entity = generic.GenericForeignKey(ct_field='entity_ct', fk_field='entity_id')
|
entity = generic.GenericForeignKey(ct_field='entity_ct', fk_field='entity_id')
|
||||||
|
|
@ -233,7 +211,6 @@ class Value(models.Model):
|
||||||
attribute = models.ForeignKey(Attribute, db_index=True,
|
attribute = models.ForeignKey(Attribute, db_index=True,
|
||||||
verbose_name=_(u"attribute"))
|
verbose_name=_(u"attribute"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.full_clean()
|
self.full_clean()
|
||||||
|
|
@ -276,174 +253,64 @@ class Value(models.Model):
|
||||||
|
|
||||||
class Entity(object):
|
class Entity(object):
|
||||||
|
|
||||||
_cache = {}
|
|
||||||
|
|
||||||
def __init__(self, instance):
|
def __init__(self, instance):
|
||||||
self.model = instance
|
self.model = instance
|
||||||
self.ct = ContentType.objects.get_for_model(instance)
|
self.ct = ContentType.objects.get_for_model(instance)
|
||||||
|
|
||||||
|
|
||||||
# TODO: memoize
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
|
|
||||||
if not name.startswith('_'):
|
if not name.startswith('_'):
|
||||||
attribute = self.get_attribute_by_slug(name)
|
try:
|
||||||
if attribute:
|
attribute = self.get_attribute_by_slug(name)
|
||||||
value_obj = attribute.get_value_for_entity(self.model)
|
except Attribute.DoesNotExist:
|
||||||
if value_obj:
|
raise AttributeError(_(u"%(obj)s has no EAV attribute named " \
|
||||||
return value_obj.value
|
u"'%(attr)s'") % \
|
||||||
return None
|
{'obj':self.model, 'attr':name})
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.get_value_by_attribute(attribute).value
|
||||||
|
except Value.DoesNotExist:
|
||||||
|
return None
|
||||||
return object.__getattr__(self, name)
|
return object.__getattr__(self, name)
|
||||||
|
|
||||||
|
def get_all_attributes(self):
|
||||||
@classmethod
|
'''
|
||||||
def get_eav_attributes_for_model(cls, model_cls):
|
Return all the attributes that are applicable to self.model.
|
||||||
"""
|
'''
|
||||||
Return the attributes for this model
|
|
||||||
"""
|
|
||||||
from .utils import EavRegistry
|
from .utils import EavRegistry
|
||||||
config_cls = EavRegistry.get_config_cls_for_model(model_cls)
|
config_cls = EavRegistry.get_config_cls_for_model(self.model.__class__)
|
||||||
return config_cls.get_eav_attributes()
|
return config_cls.get_attributes()
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_attr_cache_for_model(cls, model_cls):
|
|
||||||
"""
|
|
||||||
Return the attribute cache for this model
|
|
||||||
"""
|
|
||||||
return cls._cache.setdefault(get_unique_class_identifier(model_cls), {})
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def update_attr_cache_for_model(cls, model_cls):
|
|
||||||
"""
|
|
||||||
Create or update the attributes cache for this entity class.
|
|
||||||
"""
|
|
||||||
cache = cls.get_attr_cache_for_model(model_cls)
|
|
||||||
cache['attributes'] = cls.get_eav_attributes_for_model(model_cls)\
|
|
||||||
.select_related()
|
|
||||||
cache['slug_mapping'] = dict((s.slug, s) for s in cache['attributes'])
|
|
||||||
return cache
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def flush_attr_cache_for_model(cls, model_cls):
|
|
||||||
"""
|
|
||||||
Flush the attributes cache for this entity class
|
|
||||||
"""
|
|
||||||
cache = cls.get_attr_cache_for_model(model_cls)
|
|
||||||
cache.clear()
|
|
||||||
return cache
|
|
||||||
|
|
||||||
|
|
||||||
def get_eav_attributes(self):
|
|
||||||
"""
|
|
||||||
Return the attributes for this model
|
|
||||||
"""
|
|
||||||
return self.__class__.get_eav_attributes_for_model(self.model.__class__)
|
|
||||||
|
|
||||||
|
|
||||||
def update_attr_cache(self):
|
|
||||||
"""
|
|
||||||
Create or update the attributes cache for the entity class linked
|
|
||||||
to the current instance.
|
|
||||||
"""
|
|
||||||
return self.__class__.update_attr_cache_for_model(self.model.__class__)
|
|
||||||
|
|
||||||
|
|
||||||
def flush_attr_cache(self):
|
|
||||||
"""
|
|
||||||
Flush the attributes cache for the entity class linked
|
|
||||||
to the current instance.
|
|
||||||
"""
|
|
||||||
return self.__class__.flush_attr_cache_for_model(self.model.__class__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_attr_cache(self):
|
|
||||||
"""
|
|
||||||
Return the attribute cache for the entity class linked
|
|
||||||
to the current instance.
|
|
||||||
"""
|
|
||||||
return self.__class__.get_attr_cache_for_model(self.model.__class__)
|
|
||||||
|
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
for attribute in self.get_eav_attributes():
|
for attribute in self.get_all_attributes():
|
||||||
if hasattr(self, attribute.slug):
|
if hasattr(self, attribute.slug):
|
||||||
attribute_value = getattr(self, attribute.slug)
|
attribute_value = getattr(self, attribute.slug)
|
||||||
attribute.save_value(self.model, attribute_value)
|
attribute.save_value(self.model, attribute_value)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_all_attributes_for_model(cls, model_cls):
|
|
||||||
"""
|
|
||||||
Return the current cache or if it doesn't exists, update it
|
|
||||||
and returns it.
|
|
||||||
"""
|
|
||||||
cache = cls.get_attr_cache_for_model(model_cls)
|
|
||||||
if not cache:
|
|
||||||
cache = Entity.update_attr_cache_for_model(model_cls)
|
|
||||||
|
|
||||||
return cache['attributes']
|
|
||||||
|
|
||||||
|
|
||||||
def get_values(self):
|
def get_values(self):
|
||||||
|
'''
|
||||||
|
Get all set EAV Value objects for self.model
|
||||||
|
'''
|
||||||
return Value.objects.filter(entity_ct=self.ct,
|
return Value.objects.filter(entity_ct=self.ct,
|
||||||
entity_id=self.model.pk).select_related()
|
entity_id=self.model.pk).select_related()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_all_attribute_slugs_for_model(cls, model_cls):
|
|
||||||
"""
|
|
||||||
Returns all attributes slugs for the entity
|
|
||||||
class linked to the passed model.
|
|
||||||
"""
|
|
||||||
cache = cls.get_attr_cache_for_model(model_cls)
|
|
||||||
if not cache:
|
|
||||||
cache = Entity.update_attr_cache_for_model(model_cls)
|
|
||||||
|
|
||||||
return cache['slug_mapping']
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_attribute_slugs(self):
|
def get_all_attribute_slugs(self):
|
||||||
"""
|
return self.get_all_attributes().values_list('slug', Flat=True)
|
||||||
Returns all attributes slugs for the entity
|
|
||||||
class linked to the current instance.
|
|
||||||
"""
|
|
||||||
m_cls = self.model.__class__
|
|
||||||
return self.__class__.get_all_attribute_slugs_for_model(m_cls)
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_attribute_by_slug_for_model(cls, model_cls, slug):
|
|
||||||
"""
|
|
||||||
Returns all attributes slugs for the entity
|
|
||||||
class linked to the passed model.
|
|
||||||
"""
|
|
||||||
cache = cls.get_attr_cache_for_model(model_cls)
|
|
||||||
if not cache:
|
|
||||||
cache = Entity.update_attr_cache_for_model(model_cls)
|
|
||||||
return cache['slug_mapping'].get(slug, None)
|
|
||||||
|
|
||||||
|
|
||||||
def get_attribute_by_slug(self, slug):
|
def get_attribute_by_slug(self, slug):
|
||||||
m_cls = self.model.__class__
|
return self.get_all_attributes().get(slug=slug)
|
||||||
return self.__class__.get_attribute_by_slug_for_model(m_cls, slug)
|
|
||||||
|
|
||||||
|
def get_value_by_attribute(self, attribute):
|
||||||
|
return self.get_values().get(attribute=attribute)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""
|
|
||||||
Iterates over non-empty EAV attributes. Normal fields are not included.
|
|
||||||
"""
|
|
||||||
return iter(self.get_values())
|
return iter(self.get_values())
|
||||||
|
|
||||||
|
|
||||||
# todo: cache all changed value in EAV and check against existing attribtue
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def save_handler(sender, *args, **kwargs):
|
def save_handler(sender, *args, **kwargs):
|
||||||
from .utils import EavRegistry
|
from .utils import EavRegistry
|
||||||
config_cls = EavRegistry.get_config_cls_for_model(sender)
|
config_cls = EavRegistry.get_config_cls_for_model(sender)
|
||||||
instance_eav = getattr(kwargs['instance'], config_cls.eav_attr)
|
entity = getattr(kwargs['instance'], config_cls.eav_attr)
|
||||||
instance_eav.save()
|
entity.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ class EavBasicTests(TestCase):
|
||||||
self.entity = Patient.objects.create(name="Doe")
|
self.entity = Patient.objects.create(name="Doe")
|
||||||
|
|
||||||
self.value = Value.objects.create(entity=self.entity,
|
self.value = Value.objects.create(entity=self.entity,
|
||||||
attribute=self.attribute,
|
attribute=self.attribute,
|
||||||
value_text='Denver')
|
value_text='Denver')
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
EavRegistry.unregister(Patient)
|
EavRegistry.unregister(Patient)
|
||||||
|
|
@ -43,15 +43,11 @@ class EavBasicTests(TestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_can_eaventity_children_give_you_all_attributes_by_default(self):
|
def test_can_eaventity_children_give_you_all_attributes_by_default(self):
|
||||||
qs = Patient.eav.get_eav_attributes()
|
p = Patient.objects.create(name='bob')
|
||||||
|
qs = p.eav.get_all_attributes()
|
||||||
self.assertEqual(list(qs), list(Attribute.objects.all()))
|
self.assertEqual(list(qs), list(Attribute.objects.all()))
|
||||||
|
|
||||||
|
|
||||||
def test_value_creation(self):
|
|
||||||
Value.objects.create(entity=self.entity,
|
|
||||||
attribute=self.attribute,
|
|
||||||
value_float=1.2)
|
|
||||||
|
|
||||||
def test_value_unicode(self):
|
def test_value_unicode(self):
|
||||||
self.assertEqual(unicode(self.value), "Doe - City: \"Denver\"")
|
self.assertEqual(unicode(self.value), "Doe - City: \"Denver\"")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -217,9 +217,11 @@ class EavFilterTests(TestCase):
|
||||||
EavRegistry.register(User)
|
EavRegistry.register(User)
|
||||||
|
|
||||||
Attribute.objects.create(datatype=Attribute.TYPE_INT,
|
Attribute.objects.create(datatype=Attribute.TYPE_INT,
|
||||||
name='Height')
|
name='Height')
|
||||||
|
|
||||||
Attribute.objects.create(datatype=Attribute.TYPE_FLOAT,
|
Attribute.objects.create(datatype=Attribute.TYPE_FLOAT,
|
||||||
name='Weight')
|
name='Weight')
|
||||||
|
|
||||||
u = User.objects.create(username='Bob')
|
u = User.objects.create(username='Bob')
|
||||||
u.eav.height = 10
|
u.eav.height = 10
|
||||||
u.eav.weight = 20
|
u.eav.weight = 20
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ class EavSetterAndGetterTests(TestCase):
|
||||||
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
EavRegistry.unregister(Patient)
|
EavRegistry.unregister(Patient)
|
||||||
EavRegistry.register(Patient)
|
EavRegistry.register(Patient)
|
||||||
|
|
||||||
|
|
@ -27,8 +26,9 @@ class EavSetterAndGetterTests(TestCase):
|
||||||
self.patient = Patient.objects.create(name="Doe")
|
self.patient = Patient.objects.create(name="Doe")
|
||||||
|
|
||||||
self.value = Value.objects.create(entity=self.patient,
|
self.value = Value.objects.create(entity=self.patient,
|
||||||
attribute=self.attribute,
|
attribute=self.attribute,
|
||||||
value_text='Denver')
|
value_text='Denver')
|
||||||
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
EavRegistry.unregister(Patient)
|
EavRegistry.unregister(Patient)
|
||||||
|
|
@ -38,48 +38,36 @@ class EavSetterAndGetterTests(TestCase):
|
||||||
def additional_attribute_setup(self):
|
def additional_attribute_setup(self):
|
||||||
|
|
||||||
self.country_attr = Attribute.objects\
|
self.country_attr = Attribute.objects\
|
||||||
.create(datatype=Attribute.TYPE_TEXT,
|
.create(datatype=Attribute.TYPE_TEXT,
|
||||||
name='Country', slug='country')
|
name='Country', slug='country')
|
||||||
|
|
||||||
class PatientEav(EavConfig):
|
class PatientEav(EavConfig):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_eav_attributes(cls):
|
def get_attributes(cls):
|
||||||
return Attribute.objects.filter(slug='country')
|
return Attribute.objects.filter(slug='country')
|
||||||
|
|
||||||
self.PatientEav = PatientEav
|
self.PatientEav = PatientEav
|
||||||
|
|
||||||
class UserEav(EavConfig):
|
class UserEav(EavConfig):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_eav_attributes(cls):
|
def get_attributes(cls):
|
||||||
return Attribute.objects.filter(slug='city')
|
return Attribute.objects.filter(slug='city')
|
||||||
|
|
||||||
self.UserEav = UserEav
|
|
||||||
|
|
||||||
|
self.UserEav = UserEav
|
||||||
def test_get_value_to_entity(self):
|
|
||||||
self.assertEqual(self.attribute.get_value_for_entity(self.patient),
|
|
||||||
self.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_single_value(self):
|
def test_save_single_value(self):
|
||||||
patient = Patient.objects.create(name="x")
|
patient = Patient.objects.create(name="x")
|
||||||
attr = Attribute.objects.create(datatype=Attribute.TYPE_TEXT,
|
attr = Attribute.objects.create(datatype=Attribute.TYPE_TEXT,
|
||||||
name='a', slug='a')
|
name='a', slug='a')
|
||||||
# does nothing
|
|
||||||
attr._save_single_value(patient)
|
|
||||||
|
|
||||||
# save value
|
# save value
|
||||||
attr._save_single_value(patient, 'b')
|
attr.save_value(patient, 'b')
|
||||||
patient = Patient.objects.get(name="x")
|
patient = Patient.objects.get(name="x")
|
||||||
self.assertEqual(patient.eav.a, 'b')
|
self.assertEqual(patient.eav.a, 'b')
|
||||||
|
|
||||||
# save value on another attribute
|
|
||||||
attr._save_single_value(patient, 'Paris', self.attribute)
|
|
||||||
patient = Patient.objects.get(name="x")
|
|
||||||
self.assertEqual(patient.eav.city, 'Paris')
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_value(self):
|
def test_save_value(self):
|
||||||
# TODO: update test_save_value when multiple values are out
|
# TODO: update test_save_value when multiple values are out
|
||||||
|
|
@ -169,15 +157,7 @@ class EavSetterAndGetterTests(TestCase):
|
||||||
|
|
||||||
def test_get_a_value_that_does_not_exists(self):
|
def test_get_a_value_that_does_not_exists(self):
|
||||||
|
|
||||||
# return None for non '_' values
|
self.assertFalse(hasattr(self.patient.eav, 'impossible_value'))
|
||||||
self.assertEqual(self.patient.eav.impossible_value, None)
|
|
||||||
|
|
||||||
# normal behavior for '_' values
|
|
||||||
try:
|
|
||||||
self.patient.eav._impossible_value
|
|
||||||
self.fail()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def test_attributes_are_filtered_according_to_config_class(self):
|
def test_attributes_are_filtered_according_to_config_class(self):
|
||||||
|
|
@ -188,15 +168,18 @@ class EavSetterAndGetterTests(TestCase):
|
||||||
EavRegistry.register(Patient, self.PatientEav)
|
EavRegistry.register(Patient, self.PatientEav)
|
||||||
EavRegistry.register(User, self.UserEav)
|
EavRegistry.register(User, self.UserEav)
|
||||||
|
|
||||||
self.assertEqual(list(Patient.eav.get_eav_attributes()),
|
p = Patient.objects.create(name='Bob')
|
||||||
|
|
||||||
|
u = User.objects.create(username='hope')
|
||||||
|
|
||||||
|
self.assertEqual(list(p.eav.get_all_attributes()),
|
||||||
list(Attribute.objects.filter(slug='country')))
|
list(Attribute.objects.filter(slug='country')))
|
||||||
|
|
||||||
self.assertEqual(list(User.eav.get_eav_attributes()),
|
self.assertEqual(list(u.eav.get_all_attributes()),
|
||||||
list(Attribute.objects.filter(slug='city')))
|
list(Attribute.objects.filter(slug='city')))
|
||||||
|
|
||||||
|
|
||||||
def test_can_filter_attribute_availability_for_entity(self):
|
def test_can_filter_attribute_availability_for_entity(self):
|
||||||
|
|
||||||
self.additional_attribute_setup()
|
self.additional_attribute_setup()
|
||||||
|
|
||||||
self.patient.eav.city = 'Tunis'
|
self.patient.eav.city = 'Tunis'
|
||||||
|
|
@ -213,7 +196,7 @@ class EavSetterAndGetterTests(TestCase):
|
||||||
p.save()
|
p.save()
|
||||||
p = Patient.objects.get(pk=p.pk)
|
p = Patient.objects.get(pk=p.pk)
|
||||||
|
|
||||||
self.assertFalse(p.eav.city, 'Paris')
|
self.assertFalse(hasattr(p.eav, 'city'))
|
||||||
self.assertEqual(p.eav.country, 'USA')
|
self.assertEqual(p.eav.country, 'USA')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -238,10 +221,10 @@ class EavSetterAndGetterTests(TestCase):
|
||||||
p = Patient.objects.get(pk=p.pk)
|
p = Patient.objects.get(pk=p.pk)
|
||||||
u = User.objects.get(pk=u.pk)
|
u = User.objects.get(pk=u.pk)
|
||||||
|
|
||||||
self.assertFalse(p.eav.city)
|
self.assertFalse(hasattr(p.eav, 'city'))
|
||||||
self.assertEqual(p.eav.country, 'USA')
|
self.assertEqual(p.eav.country, 'USA')
|
||||||
|
|
||||||
self.assertFalse(u.eav.country)
|
self.assertFalse(hasattr(u.eav, 'country'))
|
||||||
self.assertEqual(u.eav.city, 'Paris')
|
self.assertEqual(u.eav.city, 'Paris')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -254,7 +237,7 @@ class EavSetterAndGetterTests(TestCase):
|
||||||
class SubPatientEav(self.PatientEav):
|
class SubPatientEav(self.PatientEav):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_eav_attributes(cls):
|
def get_attributes(cls):
|
||||||
return Attribute.objects.filter(slug='country')
|
return Attribute.objects.filter(slug='country')
|
||||||
|
|
||||||
EavRegistry.register(Patient, SubPatientEav)
|
EavRegistry.register(Patient, SubPatientEav)
|
||||||
|
|
@ -265,7 +248,7 @@ class EavSetterAndGetterTests(TestCase):
|
||||||
|
|
||||||
p = Patient.objects.get(pk=self.patient.pk)
|
p = Patient.objects.get(pk=self.patient.pk)
|
||||||
|
|
||||||
self.assertFalse(p.eav.city)
|
self.assertFalse(hasattr(p.eav, 'city'))
|
||||||
self.assertEqual(p.eav.country, 'USA')
|
self.assertEqual(p.eav.country, 'USA')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -296,18 +279,7 @@ class EavSetterAndGetterTests(TestCase):
|
||||||
self.assertEqual(self.value.value_object, None)
|
self.assertEqual(self.value.value_object, None)
|
||||||
|
|
||||||
|
|
||||||
def test_get_eav_attributes(self):
|
|
||||||
|
|
||||||
self.additional_attribute_setup()
|
|
||||||
EavRegistry.unregister(Patient)
|
|
||||||
EavRegistry.register(Patient, self.PatientEav)
|
|
||||||
|
|
||||||
assert list(self.PatientEav.get_eav_attributes_for_model(Patient))\
|
|
||||||
== list(Entity.get_eav_attributes_for_model(Patient))\
|
|
||||||
== list(self.patient.eav.get_eav_attributes())\
|
|
||||||
== list(Patient.eav.get_eav_attributes_for_model(Patient))\
|
|
||||||
== list(Attribute.objects.filter(slug='country'))
|
|
||||||
|
|
||||||
|
|
||||||
def test_values(self):
|
def test_values(self):
|
||||||
self.additional_attribute_setup()
|
self.additional_attribute_setup()
|
||||||
|
|
@ -318,33 +290,7 @@ class EavSetterAndGetterTests(TestCase):
|
||||||
self.assertEqual(list(self.patient.eav.get_values()),
|
self.assertEqual(list(self.patient.eav.get_values()),
|
||||||
list(Value.objects.exclude(value_text='Denver')))
|
list(Value.objects.exclude(value_text='Denver')))
|
||||||
|
|
||||||
|
|
||||||
def test_get_all_attribute_slugs_for_model(self):
|
|
||||||
|
|
||||||
self.country_attr = Attribute.objects\
|
|
||||||
.create(datatype=Attribute.TYPE_TEXT,
|
|
||||||
name='Street', slug='street')
|
|
||||||
|
|
||||||
self.additional_attribute_setup()
|
|
||||||
|
|
||||||
class UserEav(EavConfig):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_eav_attributes(cls):
|
|
||||||
return Attribute.objects.exclude(slug='city')
|
|
||||||
|
|
||||||
EavRegistry.register(User, UserEav)
|
|
||||||
|
|
||||||
u = User.objects.create(username='John')
|
|
||||||
|
|
||||||
slugs = dict((s.slug, s) for s in Attribute.objects\
|
|
||||||
.exclude(slug='city'))
|
|
||||||
|
|
||||||
assert slugs\
|
|
||||||
== Entity.get_all_attribute_slugs_for_model(User)\
|
|
||||||
== User.eav.get_all_attribute_slugs_for_model(User)\
|
|
||||||
== u.eav.get_all_attribute_slugs()
|
|
||||||
|
|
||||||
|
|
||||||
def test_iteration(self):
|
def test_iteration(self):
|
||||||
self.additional_attribute_setup()
|
self.additional_attribute_setup()
|
||||||
|
|
|
||||||
30
utils.py
30
utils.py
|
|
@ -34,7 +34,7 @@ class EavConfig(Entity):
|
||||||
generic_relation_related_name = None
|
generic_relation_related_name = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_eav_attributes(cls):
|
def get_attributes(cls):
|
||||||
"""
|
"""
|
||||||
By default, all attributes apply to an entity,
|
By default, all attributes apply to an entity,
|
||||||
unless otherwise specified.
|
unless otherwise specified.
|
||||||
|
|
@ -70,18 +70,7 @@ class EavRegistry(object):
|
||||||
config_cls = EavRegistry.get_config_cls_for_model(sender)
|
config_cls = EavRegistry.get_config_cls_for_model(sender)
|
||||||
|
|
||||||
setattr(instance, config_cls.eav_attr, Entity(instance))
|
setattr(instance, config_cls.eav_attr, Entity(instance))
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def update_attribute_cache(sender, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Update the attribute cache for all the models every time we
|
|
||||||
create an attribute.
|
|
||||||
"""
|
|
||||||
for cache in EavRegistry.cache.itervalues():
|
|
||||||
Entity.update_attr_cache_for_model(cache['model_cls'])
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def wrap_config_class(model_cls, config_cls):
|
def wrap_config_class(model_cls, config_cls):
|
||||||
"""
|
"""
|
||||||
|
|
@ -132,19 +121,13 @@ class EavRegistry(object):
|
||||||
|
|
||||||
# todo : not useful anymore ?
|
# todo : not useful anymore ?
|
||||||
setattr(getattr(model_cls, config_cls.eav_attr),
|
setattr(getattr(model_cls, config_cls.eav_attr),
|
||||||
'get_eav_attributes', config_cls.get_eav_attributes)
|
'get_attributes', config_cls.get_attributes)
|
||||||
|
|
||||||
# attache the new manager to the model
|
# attache the new manager to the model
|
||||||
mgr = EntityManager()
|
mgr = EntityManager()
|
||||||
mgr.contribute_to_class(model_cls, config_cls.manager_attr)
|
mgr.contribute_to_class(model_cls, config_cls.manager_attr)
|
||||||
|
|
||||||
if not manager_only:
|
if not manager_only:
|
||||||
# todo: see with david how to change that
|
|
||||||
try:
|
|
||||||
Entity.update_attr_cache_for_model(model_cls)
|
|
||||||
except DatabaseError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# todo: make that overridable
|
# todo: make that overridable
|
||||||
# attach the generic relation to the model
|
# attach the generic relation to the model
|
||||||
if config_cls.generic_relation_related_name:
|
if config_cls.generic_relation_related_name:
|
||||||
|
|
@ -200,15 +183,10 @@ class EavRegistry(object):
|
||||||
delattr(model_cls, config_cls.eav_attr)
|
delattr(model_cls, config_cls.eav_attr)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not manager_only:
|
|
||||||
Entity.flush_attr_cache_for_model(model_cls)
|
|
||||||
|
|
||||||
EavRegistry.cache.pop(cls_id)
|
EavRegistry.cache.pop(cls_id)
|
||||||
|
|
||||||
# todo : test cache
|
# todo : test cache
|
||||||
# todo : tst unique identitfier
|
# todo : tst unique identitfier
|
||||||
# todo: test update attribute cache on attribute creation
|
# todo: test update attribute cache on attribute creation
|
||||||
|
|
||||||
post_save.connect(EavRegistry.update_attribute_cache, sender=Attribute)
|
|
||||||
post_delete.connect(EavRegistry.update_attribute_cache, sender=Attribute)
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue