Major refactor

This commit is contained in:
David Gelvin 2010-09-22 15:28:19 +03:00
parent 515cdf07ee
commit a53bc53daa
7 changed files with 104 additions and 334 deletions

View file

@ -53,9 +53,6 @@ class BaseEntityAdmin(ModelAdmin):
return super_meth(request, context, **kwargs)
class BaseEntityInlineFormSet(BaseInlineFormSet):
"""
An inline formset that correctly initializes EAV forms.
@ -99,7 +96,7 @@ class BaseEntityInline(InlineModelAdmin):
class AttributeAdmin(ModelAdmin):
list_display = ('name', 'slug', 'datatype', 'description')
prepopulated_fields = {'name': ('slug',)}
prepopulated_fields = {'slug': ('name',)}
admin.site.register(Attribute, AttributeAdmin)
admin.site.register(Value)

View file

@ -28,22 +28,6 @@ from django.utils.translation import ugettext_lazy as _
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):
"""
@ -69,15 +53,15 @@ class BaseDynamicEntityForm(ModelForm):
def __init__(self, data=None, *args, **kwargs):
super(BaseDynamicEntityForm, self).__init__(data, *args, **kwargs)
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()
def _build_dynamic_fields(self):
# reset form fields
self.fields = deepcopy(self.base_fields)
for attribute in self.proxy.get_eav_attributes():
value = getattr(self.proxy, attribute.slug)
for attribute in self.entity.get_all_attributes():
value = getattr(self.entity, attribute.slug)
defaults = {
'label': attribute.name.capitalize(),
@ -120,7 +104,7 @@ class BaseDynamicEntityForm(ModelForm):
instance = super(BaseDynamicEntityForm, self).save(commit=False)
# 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)
if attribute.datatype == attribute.TYPE_ENUM:
if value:
@ -128,7 +112,7 @@ class BaseDynamicEntityForm(ModelForm):
else:
value = None
setattr(self.proxy, attribute.slug, value)
setattr(self.entity, attribute.slug, value)
# save entity and its attributes
if commit:

253
models.py
View file

@ -31,9 +31,9 @@ from .fields import EavSlugField, EavDatatypeField
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__))
@ -86,13 +86,13 @@ class Attribute(models.Model):
(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,
help_text=_(u"Short unique attribute label"),
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,
blank=True, null=True,
help_text=_(u"Short description"))
@ -133,59 +133,33 @@ class Attribute(models.Model):
return None
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):
"""
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)
attribute = attribute or self
try:
eavvalue = self.value_set.get(entity_ct=ct,
entity_id=entity.pk,
attribute=attribute)
value_obj = self.value_set.get(entity_ct=ct,
entity_id=entity.pk,
attribute=self)
except Value.DoesNotExist:
eavvalue = self.value_set.model(entity_ct=ct,
entity_id=entity.pk,
attribute=attribute)
if value != eavvalue.value:
eavvalue.value = value
eavvalue.save()
if value == None:
return
value_obj = Value.objects.create(entity_ct=ct,
entity_id=entity.pk,
attribute=self)
if value == None:
value_obj.delete()
return
if value != value_obj.value:
value_obj.value = value
value_obj.save()
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_id = models.IntegerField()
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,
verbose_name=_(u"attribute"))
def save(self, *args, **kwargs):
self.full_clean()
@ -276,174 +253,64 @@ class Value(models.Model):
class Entity(object):
_cache = {}
def __init__(self, instance):
self.model = instance
self.ct = ContentType.objects.get_for_model(instance)
# TODO: memoize
def __getattr__(self, name):
if not name.startswith('_'):
attribute = self.get_attribute_by_slug(name)
if attribute:
value_obj = attribute.get_value_for_entity(self.model)
if value_obj:
return value_obj.value
return None
try:
attribute = self.get_attribute_by_slug(name)
except Attribute.DoesNotExist:
raise AttributeError(_(u"%(obj)s has no EAV attribute named " \
u"'%(attr)s'") % \
{'obj':self.model, 'attr':name})
try:
return self.get_value_by_attribute(attribute).value
except Value.DoesNotExist:
return None
return object.__getattr__(self, name)
@classmethod
def get_eav_attributes_for_model(cls, model_cls):
"""
Return the attributes for this model
"""
def get_all_attributes(self):
'''
Return all the attributes that are applicable to self.model.
'''
from .utils import EavRegistry
config_cls = EavRegistry.get_config_cls_for_model(model_cls)
return config_cls.get_eav_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__)
config_cls = EavRegistry.get_config_cls_for_model(self.model.__class__)
return config_cls.get_attributes()
def save(self):
for attribute in self.get_eav_attributes():
for attribute in self.get_all_attributes():
if hasattr(self, attribute.slug):
attribute_value = getattr(self, attribute.slug)
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):
'''
Get all set EAV Value objects for self.model
'''
return Value.objects.filter(entity_ct=self.ct,
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']
entity_id=self.model.pk).select_related()
def get_all_attribute_slugs(self):
"""
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)
return self.get_all_attributes().values_list('slug', Flat=True)
def get_attribute_by_slug(self, slug):
m_cls = self.model.__class__
return self.__class__.get_attribute_by_slug_for_model(m_cls, slug)
return self.get_all_attributes().get(slug=slug)
def get_value_by_attribute(self, attribute):
return self.get_values().get(attribute=attribute)
def __iter__(self):
"""
Iterates over non-empty EAV attributes. Normal fields are not included.
"""
return iter(self.get_values())
# todo: cache all changed value in EAV and check against existing attribtue
@staticmethod
def save_handler(sender, *args, **kwargs):
from .utils import EavRegistry
config_cls = EavRegistry.get_config_cls_for_model(sender)
instance_eav = getattr(kwargs['instance'], config_cls.eav_attr)
instance_eav.save()
entity = getattr(kwargs['instance'], config_cls.eav_attr)
entity.save()

View file

@ -25,8 +25,8 @@ class EavBasicTests(TestCase):
self.entity = Patient.objects.create(name="Doe")
self.value = Value.objects.create(entity=self.entity,
attribute=self.attribute,
value_text='Denver')
attribute=self.attribute,
value_text='Denver')
def tearDown(self):
EavRegistry.unregister(Patient)
@ -43,15 +43,11 @@ class EavBasicTests(TestCase):
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()))
def test_value_creation(self):
Value.objects.create(entity=self.entity,
attribute=self.attribute,
value_float=1.2)
def test_value_unicode(self):
self.assertEqual(unicode(self.value), "Doe - City: \"Denver\"")

View file

@ -217,9 +217,11 @@ class EavFilterTests(TestCase):
EavRegistry.register(User)
Attribute.objects.create(datatype=Attribute.TYPE_INT,
name='Height')
name='Height')
Attribute.objects.create(datatype=Attribute.TYPE_FLOAT,
name='Weight')
name='Weight')
u = User.objects.create(username='Bob')
u.eav.height = 10
u.eav.weight = 20

View file

@ -16,7 +16,6 @@ class EavSetterAndGetterTests(TestCase):
def setUp(self):
EavRegistry.unregister(Patient)
EavRegistry.register(Patient)
@ -27,8 +26,9 @@ class EavSetterAndGetterTests(TestCase):
self.patient = Patient.objects.create(name="Doe")
self.value = Value.objects.create(entity=self.patient,
attribute=self.attribute,
value_text='Denver')
attribute=self.attribute,
value_text='Denver')
def tearDown(self):
EavRegistry.unregister(Patient)
@ -38,48 +38,36 @@ class EavSetterAndGetterTests(TestCase):
def additional_attribute_setup(self):
self.country_attr = Attribute.objects\
.create(datatype=Attribute.TYPE_TEXT,
name='Country', slug='country')
.create(datatype=Attribute.TYPE_TEXT,
name='Country', slug='country')
class PatientEav(EavConfig):
@classmethod
def get_eav_attributes(cls):
def get_attributes(cls):
return Attribute.objects.filter(slug='country')
self.PatientEav = PatientEav
class UserEav(EavConfig):
@classmethod
def get_eav_attributes(cls):
def get_attributes(cls):
return Attribute.objects.filter(slug='city')
self.UserEav = UserEav
def test_get_value_to_entity(self):
self.assertEqual(self.attribute.get_value_for_entity(self.patient),
self.value)
self.UserEav = UserEav
def test_save_single_value(self):
patient = Patient.objects.create(name="x")
attr = Attribute.objects.create(datatype=Attribute.TYPE_TEXT,
name='a', slug='a')
# does nothing
attr._save_single_value(patient)
# save value
attr._save_single_value(patient, 'b')
attr.save_value(patient, 'b')
patient = Patient.objects.get(name="x")
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):
# 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):
# return None for non '_' values
self.assertEqual(self.patient.eav.impossible_value, None)
# normal behavior for '_' values
try:
self.patient.eav._impossible_value
self.fail()
except AttributeError:
pass
self.assertFalse(hasattr(self.patient.eav, 'impossible_value'))
def test_attributes_are_filtered_according_to_config_class(self):
@ -188,15 +168,18 @@ class EavSetterAndGetterTests(TestCase):
EavRegistry.register(Patient, self.PatientEav)
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')))
self.assertEqual(list(User.eav.get_eav_attributes()),
self.assertEqual(list(u.eav.get_all_attributes()),
list(Attribute.objects.filter(slug='city')))
def test_can_filter_attribute_availability_for_entity(self):
self.additional_attribute_setup()
self.patient.eav.city = 'Tunis'
@ -213,7 +196,7 @@ class EavSetterAndGetterTests(TestCase):
p.save()
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')
@ -238,10 +221,10 @@ class EavSetterAndGetterTests(TestCase):
p = Patient.objects.get(pk=p.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.assertFalse(u.eav.country)
self.assertFalse(hasattr(u.eav, 'country'))
self.assertEqual(u.eav.city, 'Paris')
@ -254,7 +237,7 @@ class EavSetterAndGetterTests(TestCase):
class SubPatientEav(self.PatientEav):
@classmethod
def get_eav_attributes(cls):
def get_attributes(cls):
return Attribute.objects.filter(slug='country')
EavRegistry.register(Patient, SubPatientEav)
@ -265,7 +248,7 @@ class EavSetterAndGetterTests(TestCase):
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')
@ -296,18 +279,7 @@ class EavSetterAndGetterTests(TestCase):
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):
self.additional_attribute_setup()
@ -318,33 +290,7 @@ class EavSetterAndGetterTests(TestCase):
self.assertEqual(list(self.patient.eav.get_values()),
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):
self.additional_attribute_setup()

View file

@ -34,7 +34,7 @@ class EavConfig(Entity):
generic_relation_related_name = None
@classmethod
def get_eav_attributes(cls):
def get_attributes(cls):
"""
By default, all attributes apply to an entity,
unless otherwise specified.
@ -70,18 +70,7 @@ class EavRegistry(object):
config_cls = EavRegistry.get_config_cls_for_model(sender)
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
def wrap_config_class(model_cls, config_cls):
"""
@ -132,19 +121,13 @@ class EavRegistry(object):
# todo : not useful anymore ?
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
mgr = EntityManager()
mgr.contribute_to_class(model_cls, config_cls.manager_attr)
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
# attach the generic relation to the model
if config_cls.generic_relation_related_name:
@ -200,15 +183,10 @@ class EavRegistry(object):
delattr(model_cls, config_cls.eav_attr)
except AttributeError:
pass
if not manager_only:
Entity.flush_attr_cache_for_model(model_cls)
EavRegistry.cache.pop(cls_id)
# todo : test cache
# todo : tst unique identitfier
# 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)