From 1cdc831bd1406b70be7e0cc062c54fd1fcfdd9dc Mon Sep 17 00:00:00 2001 From: ksamuel Date: Wed, 8 Sep 2010 15:24:14 +0000 Subject: [PATCH 1/2] Fixed cache bug --- models.py | 60 ++++++++++++++++++++++++++--------- tests/set_and_get.py | 74 ++++++++++++++++++++++++++++++++++++++++++-- utils.py | 21 +++++++++++-- 3 files changed, 136 insertions(+), 19 deletions(-) diff --git a/models.py b/models.py index d367d66..93c50c5 100644 --- a/models.py +++ b/models.py @@ -106,6 +106,7 @@ class EavAttribute(models.Model): ''' ct = ContentType.objects.get_for_model(entity) qs = self.eavvalue_set.filter(entity_ct=ct, entity_id=entity.pk) + print entity count = qs.count() if count > 1: raise AttributeError(u"You should have one and only one value "\ @@ -134,6 +135,7 @@ class EavAttribute(models.Model): Use attribute if you want to use something else than the current one """ + print "save_value()" ct = ContentType.objects.get_for_model(entity) attribute = attribute or self try: @@ -144,7 +146,10 @@ class EavAttribute(models.Model): eavvalue = self.eavvalue_set.model(entity_ct=ct, entity_id=entity.pk, attribute=attribute) + print "value", value + print "eavvalue.value", eavvalue.value if value != eavvalue.value: + print value eavvalue.value = value eavvalue.save() @@ -223,13 +228,21 @@ class EavEntity(object): # TODO: memoize def __getattr__(self, name): + print self, name + + print "__getattr__" + if not name.startswith('_'): - for slug in self.get_all_attribute_slugs(): - attribute = self.get_attribute_by_slug(name) - if attribute: - value = attribute.get_value_for_entity(self.model) - if value: - return attribute.get_value_for_entity(self.model).value + attribute = self.get_attribute_by_slug(name) + print attribute + print "if attribute" + if attribute: + print attribute + value = attribute.get_value_for_entity(self.model) + print value + print "if value" + if value: + return attribute.get_value_for_entity(self.model).value return None raise AttributeError(_(u"%s EAV does not have attribute " \ @@ -237,12 +250,22 @@ class EavEntity(object): (self.model._meta.object_name, name)) + @classmethod + def get_eav_attributes_for_model(cls, model_cls): + """ + Return the attributes for this 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(cls), {}) + return cls._cache.setdefault(get_unique_class_identifier(model_cls), {}) @classmethod @@ -251,7 +274,8 @@ class EavEntity(object): 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().select_related() + 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 @@ -265,6 +289,13 @@ class EavEntity(object): 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): """ @@ -291,11 +322,12 @@ class EavEntity(object): def save(self): - for attribute in self.get_all_attributes(): + for attribute in self.get_eav_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): """ @@ -308,9 +340,7 @@ class EavEntity(object): return cache['attributes'] - def get_all_attributes(self): - m_cls = self.__class__ - return self.__class__.get_all_attributes_for_model(m_cls) + def get_values(self): @@ -346,8 +376,8 @@ class EavEntity(object): cache = cls.get_attr_cache_for_model(model_cls) if not cache: cache = EavEntity.update_attr_cache_for_model(model_cls) - if slug in cache['slug_mapping']: - return cache['slug_mapping'][slug] + return cache['slug_mapping'].get(slug, None) + def get_attribute_by_slug(self, slug): m_cls = self.model.__class__ @@ -363,10 +393,12 @@ class EavEntity(object): if attr.pk == attribute_id: return attr + def __iter__(self): "Iterates over non-empty EAV attributes. Normal fields are not included." return iter(self.get_values()) + @staticmethod def save_handler(sender, *args, **kwargs): kwargs['instance'].eav.save() diff --git a/tests/set_and_get.py b/tests/set_and_get.py index baebed3..1b6cce7 100644 --- a/tests/set_and_get.py +++ b/tests/set_and_get.py @@ -1,6 +1,7 @@ from datetime import datetime from django.test import TestCase +from django.contrib.auth.models import User from ..models import * from ..utils import EavRegistry, EavConfig @@ -147,6 +148,35 @@ class EavSetterAndGetterTests(TestCase): # remove a label that is not attach does nothing self.attribute.remove_label('a') self.attribute.remove_label('x') + + + def test_attributes_are_filtered_according_to_config_class(self): + attribute = EavAttribute.objects\ + .create(datatype=EavAttribute.TYPE_TEXT, + name='Country', slug='country') + + EavRegistry.unregister(Patient) + + class PatientEav(EavConfig): + + @classmethod + def get_eav_attributes(cls): + return EavAttribute.objects.filter(slug='country') + + class UserEav(EavConfig): + + @classmethod + def get_eav_attributes(cls): + return EavAttribute.objects.filter(slug='city') + + EavRegistry.register(Patient, PatientEav) + EavRegistry.register(User, UserEav) + + self.assertEqual(list(Patient.eav.get_eav_attributes()), + list(EavAttribute.objects.filter(slug='country'))) + + self.assertEqual(list(User.eav.get_eav_attributes()), + list(EavAttribute.objects.filter(slug='city'))) def test_can_filter_attribute_availability_for_entity(self): @@ -177,8 +207,48 @@ class EavSetterAndGetterTests(TestCase): self.assertFalse(p.eav.city, 'Paris') self.assertEqual(p.eav.country, 'USA') - p = Patient() + + def test_can_have_differente_attribute_filter(self): + + attribute = EavAttribute.objects\ + .create(datatype=EavAttribute.TYPE_TEXT, + name='Country', slug='country') + + EavRegistry.unregister(Patient) + + class PatientEav(EavConfig): + + @classmethod + def get_eav_attributes(cls): + return EavAttribute.objects.filter(slug='country') + + class UserEav(EavConfig): + + @classmethod + def get_eav_attributes(cls): + return EavAttribute.objects.filter(slug='city') + + EavRegistry.register(Patient, PatientEav) + EavRegistry.register(User, UserEav) + + p = Patient.objects.create(name='Patrick') + u = User.objects.create(username='John') + + p.eav.city = 'Paris' + p.eav.country = 'USA' + p.save() + u.eav.city = 'Paris' + u.eav.country = 'USA' + u.save() + + p = Patient.objects.get(pk=p.pk) + u = User.objects.get(pk=u.pk) + + self.assertFalse(p.eav.city) + self.assertEqual(p.eav.country, 'USA') + + self.assertFalse(u.eav.country) + self.assertEqual(u.eav.city, 'Paris') - # todo: test multiple children diff --git a/utils.py b/utils.py index b39dab3..be131d3 100644 --- a/utils.py +++ b/utils.py @@ -5,7 +5,7 @@ from .managers import EntityManager from .models import (EavEntity, EavAttribute, EavValue, get_unique_class_identifier) -class EavConfig(object): +class EavConfig(EavEntity): proxy_field_name = 'eav' manager_field_name ='objects' @@ -17,6 +17,8 @@ class EavConfig(object): By default, all attributes apply to an entity, unless otherwise specified. """ + print 'Generic' + print cls return EavAttribute.objects.all() @@ -57,6 +59,17 @@ class EavRegistry(object): for cache in EavRegistry.cache.itervalues(): EavEntity.update_attr_cache_for_model(cache['model_cls']) + + @staticmethod + def wrap_config_class(model_cls, config_cls): + """ + Check if the config class is EavConfig, and create a subclass if + it is + """ + if config_cls is EavConfig: + return type("%sConfig" % model_cls.__name__, (EavConfig,), {}) + return config_cls + @staticmethod def register(model_cls, config_cls=EavConfig): @@ -70,17 +83,19 @@ class EavRegistry(object): if cls_id in EavRegistry.cache: return + config_cls = EavRegistry.wrap_config_class(model_cls, config_cls) post_init.connect(EavRegistry.attach, sender=model_cls) post_save.connect(EavEntity.save_handler, sender=model_cls) EavRegistry.cache[cls_id] = { 'config_cls': config_cls, 'model_cls': model_cls } + print config_cls if hasattr(model_cls, config_cls.manager_field_name): mgr = getattr(model_cls, config_cls.manager_field_name) EavRegistry.cache[cls_id]['old_mgr'] = mgr - setattr(model_cls, config_cls.proxy_field_name, EavEntity) - + setattr(model_cls, config_cls.proxy_field_name, config_cls) + setattr(getattr(model_cls, config_cls.proxy_field_name), 'get_eav_attributes', config_cls.get_eav_attributes) From 1b596733e558b31b107a46160e5ab931c8438bf4 Mon Sep 17 00:00:00 2001 From: ksamuel Date: Wed, 8 Sep 2010 16:09:05 +0000 Subject: [PATCH 2/2] Remove prints, added test to value acccessors and object setters / getters --- models.py | 27 ++++++++--------- tests/basics.py | 3 -- tests/set_and_get.py | 72 ++++++++++++++++++++++++++++++++++++++++++++ utils.py | 3 -- 4 files changed, 84 insertions(+), 21 deletions(-) diff --git a/models.py b/models.py index 93c50c5..5cbf603 100644 --- a/models.py +++ b/models.py @@ -106,7 +106,6 @@ class EavAttribute(models.Model): ''' ct = ContentType.objects.get_for_model(entity) qs = self.eavvalue_set.filter(entity_ct=ct, entity_id=entity.pk) - print entity count = qs.count() if count > 1: raise AttributeError(u"You should have one and only one value "\ @@ -126,6 +125,7 @@ class EavAttribute(models.Model): """ 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 @@ -135,7 +135,6 @@ class EavAttribute(models.Model): Use attribute if you want to use something else than the current one """ - print "save_value()" ct = ContentType.objects.get_for_model(entity) attribute = attribute or self try: @@ -146,13 +145,11 @@ class EavAttribute(models.Model): eavvalue = self.eavvalue_set.model(entity_ct=ct, entity_id=entity.pk, attribute=attribute) - print "value", value - print "eavvalue.value", eavvalue.value if value != eavvalue.value: - print value eavvalue.value = value eavvalue.save() + def __unicode__(self): return u"%s (%s)" % (self.name, self.get_datatype_display()) @@ -194,19 +191,29 @@ class EavValue(models.Model): attribute = models.ForeignKey(EavAttribute) + def save(self, *args, **kwargs): self.full_clean() super(EavValue, self).save(*args, **kwargs) + def _blank(self): + """ + Set all the field to none + """ self.value_bool = False for field in self._meta.fields: if field.name.startswith('value_') and field.null == True: setattr(self, field.name, None) + def _get_value(self): + """ + Get returns the Python object hold by this EavValue object. + """ return getattr(self, 'value_%s' % self.attribute.datatype) + def _set_value(self, new_value): self._blank() setattr(self, 'value_%s' % self.attribute.datatype, new_value) @@ -227,20 +234,10 @@ class EavEntity(object): # TODO: memoize def __getattr__(self, name): - - print self, name - - print "__getattr__" - if not name.startswith('_'): attribute = self.get_attribute_by_slug(name) - print attribute - print "if attribute" if attribute: - print attribute value = attribute.get_value_for_entity(self.model) - print value - print "if value" if value: return attribute.get_value_for_entity(self.model).value return None diff --git a/tests/basics.py b/tests/basics.py index 9a75053..1660978 100644 --- a/tests/basics.py +++ b/tests/basics.py @@ -55,9 +55,6 @@ class EavBasicTests(TestCase): self.assertEqual(unicode(self.value), "Doe - City: \"Denver\"") - def test_value_unicode(self): - self.assertEqual(unicode(self.value), "Doe - City: \"Denver\"") - def test_value_types(self): _text = EavAttribute.objects.create(datatype=EavAttribute.TYPE_TEXT, diff --git a/tests/set_and_get.py b/tests/set_and_get.py index 1b6cce7..2f4d9c3 100644 --- a/tests/set_and_get.py +++ b/tests/set_and_get.py @@ -87,6 +87,8 @@ class EavSetterAndGetterTests(TestCase): def test_you_can_create_several_type_of_attributes(self): + self.patient = Patient(name='test') + EavAttribute.objects.create(datatype=EavAttribute.TYPE_TEXT, name='text', slug='text') EavAttribute.objects.create(datatype=EavAttribute.TYPE_FLOAT, @@ -97,6 +99,8 @@ class EavSetterAndGetterTests(TestCase): name='date', slug='date') EavAttribute.objects.create(datatype=EavAttribute.TYPE_BOOLEAN, name='bool', slug='bool') + EavAttribute.objects.create(datatype=EavAttribute.TYPE_OBJECT, + name='object', slug='object') now = datetime.today() self.patient.eav.text = 'a' @@ -104,6 +108,7 @@ class EavSetterAndGetterTests(TestCase): self.patient.eav.int = 1 self.patient.eav.date = now self.patient.eav.bool = True + self.patient.eav.object = User.objects.create(username='Bob') self.patient.save() @@ -112,6 +117,8 @@ class EavSetterAndGetterTests(TestCase): self.assertEqual(self.patient.eav.int, 1) self.assertEqual(self.patient.eav.date, now) self.assertEqual(self.patient.eav.bool, True) + self.assertEqual(self.patient.eav.object, + User.objects.get(username='Bob')) def test_assign_a_value_that_is_not_an_eav_attribute_does_nothing(self): @@ -119,6 +126,11 @@ class EavSetterAndGetterTests(TestCase): self.patient.eav.no_an_attribute = 'Woot' self.patient.save() self.assertFalse(EavValue.objects.filter(value_text='Paris').count()) + + + def test_get_a_value_that_does_not_exists_returns_none(self): + + self.assertEqual(self.patient.eav.impossible_value, None) def test_attributes_can_be_labelled(self): @@ -210,6 +222,7 @@ class EavSetterAndGetterTests(TestCase): def test_can_have_differente_attribute_filter(self): + attribute = EavAttribute.objects\ .create(datatype=EavAttribute.TYPE_TEXT, name='Country', slug='country') @@ -251,4 +264,63 @@ class EavSetterAndGetterTests(TestCase): self.assertEqual(u.eav.city, 'Paris') + def test_can_have_a_subclass_for_config_class(self): + + attribute = EavAttribute.objects\ + .create(datatype=EavAttribute.TYPE_TEXT, + name='Country', slug='country') + + EavRegistry.unregister(Patient) + + class PatientEav(EavConfig): + + @classmethod + def get_eav_attributes(cls): + return EavAttribute.objects.filter(slug='country') + + class SubPatientEav(PatientEav): + @classmethod + def get_eav_attributes(cls): + return EavAttribute.objects.filter(slug='country') + + EavRegistry.register(Patient, SubPatientEav) + + + self.patient.eav.city = 'Paris' + self.patient.eav.country = 'USA' + self.patient.save() + + p = Patient.objects.get(pk=self.patient.pk) + + self.assertFalse(p.eav.city) + self.assertEqual(p.eav.country, 'USA') + + + def test_blank_set_all_value_field_with_a_null_default_to_none(self): + self.value._blank() + self.assertEqual(self.value.value_text, None) + self.assertEqual(self.value.value_int, None) + self.assertEqual(self.value.value_float, None) + self.assertEqual(self.value.value_date, None) + self.assertEqual(self.value.value_object, None) + + def test_blank_set_all_value_field_with_a_default_to_default(self): + self.value._blank() + self.assertEqual(self.value.value_bool, False) + + + def test_get_value_on_eavvalue_return_python_object(self): + self.assertEqual(self.value._get_value(), 'Denver') + self.assertEqual(self.value.value, self.value._get_value()) + + def test_set_value_store_the_python_object_and_blank_other_fields(self): + + self.value._set_value('Bamako') + self.assertEqual(self.value.value, 'Bamako') + self.assertEqual(self.value.value_text, 'Bamako') + self.assertEqual(self.value.value_int, None) + self.assertEqual(self.value.value_float, None) + self.assertEqual(self.value.value_date, None) + self.assertEqual(self.value.value_bool, False) + self.assertEqual(self.value.value_object, None) diff --git a/utils.py b/utils.py index be131d3..181fc10 100644 --- a/utils.py +++ b/utils.py @@ -17,8 +17,6 @@ class EavConfig(EavEntity): By default, all attributes apply to an entity, unless otherwise specified. """ - print 'Generic' - print cls return EavAttribute.objects.all() @@ -89,7 +87,6 @@ class EavRegistry(object): EavRegistry.cache[cls_id] = { 'config_cls': config_cls, 'model_cls': model_cls } - print config_cls if hasattr(model_cls, config_cls.manager_field_name): mgr = getattr(model_cls, config_cls.manager_field_name) EavRegistry.cache[cls_id]['old_mgr'] = mgr