From 0cc260c5b1caa618db8b07bac7ee586d72874abb Mon Sep 17 00:00:00 2001 From: ksamuel Date: Wed, 8 Sep 2010 01:18:27 +0000 Subject: [PATCH] New cache reloading only for models on startup and at ever attribute addition --- models.py | 152 +++++++++++++++++++++++++++++++++++-------- tests/basics.py | 2 + tests/set_and_get.py | 10 +-- utils.py | 76 ++++++++++++++-------- 4 files changed, 183 insertions(+), 57 deletions(-) diff --git a/models.py b/models.py index 4da3c5d..ccd6e56 100644 --- a/models.py +++ b/models.py @@ -1,3 +1,4 @@ +import inspect import re from datetime import datetime @@ -9,6 +10,12 @@ from django.contrib.contenttypes import generic from .fields import EavSlugField, EavDatatypeField +def get_unique_class_identifier(cls): + """ + Return a unique identifier for a class + """ + return '.'.join((inspect.getfile(cls), cls.__name__)) + class EavAttributeLabel(models.Model): name = models.CharField(_(u"name"), db_index=True, @@ -89,6 +96,7 @@ class EavAttribute(models.Model): return self.labels.remove(label_obj) + def get_value_for_entity(self, entity): ''' Passed any object that may be used as an 'entity' object (is linked @@ -99,12 +107,17 @@ class EavAttribute(models.Model): ''' ct = ContentType.objects.get_for_model(entity) qs = self.eavvalue_set.filter(entity_ct=ct, entity_id=entity.pk) - if qs.count() == 1: - return qs[0] - raise AttributeError(u"You should have one and only one value"\ + 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 @@ -157,10 +170,11 @@ class EavValue(models.Model): ''' - class Meta: - unique_together = ('entity_ct', 'entity_id', 'attribute', - 'value_text', 'value_float', 'value_date', - 'value_bool') + #class Meta: + # we can't a TextField on mysql with a unique constraint + #unique_together = ('entity_ct', 'entity_id', 'attribute', + # 'value_text', 'value_float', 'value_date', + # 'value_bool') entity_ct = models.ForeignKey(ContentType, related_name='value_entities') entity_id = models.IntegerField() entity = generic.GenericForeignKey(ct_field='entity_ct', fk_field='entity_id') @@ -202,20 +216,83 @@ class EavValue(models.Model): class EavEntity(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('_'): for slug in self.get_all_attribute_slugs(): attribute = self.get_attribute_by_slug(name) - value = attribute.get_value_for_entity(self.model) - return value.value if value else None + if attribute: + value = attribute.get_value_for_entity(self.model) + if value: + return attribute.get_value_for_entity(self.model).value + return None + raise AttributeError(_(u"%s EAV does not have attribute " \ u"named \"%s\".") % \ (self.model._meta.object_name, name)) + + @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), {}) + + + @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().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 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): for attribute in self.get_all_attributes(): if hasattr(self, attribute.slug): @@ -223,39 +300,62 @@ class EavEntity(object): attribute.save_value(self.model, attribute_value) def get_all_attributes(self): - try: - if self._attributes_cache is not None: - return self._attributes_cache - except AttributeError: - pass - - self._attributes_cache = self.__class__.get_eav_attributes().select_related() - self._attributes_cache_dict = dict((s.slug, s) for s in self._attributes_cache) - return self._attributes_cache + """ + Return the current cache or if it doesn't exists, update it + and returns it. + """ + + cache = self.get_attr_cache() + if not cache: + cache = self.update_attr_cache() + return cache['attributes'] def get_values(self): return EavValue.objects.filter(entity_ct=self.ct, entity_id=self.model.pk).select_related() def get_all_attribute_slugs(self): - if not hasattr(self, '_attributes_cache_dict'): - self.get_all_attributes() - return self._attributes_cache_dict.keys() + """ + Returns all attributes slugs for the entity + class linked to the current instance. + """ + cache = self.get_attr_cache() + if not cache: + cache = self.update_attr_cache() + + return cache['slug_mapping'] def get_attribute_by_slug(self, slug): - if not hasattr(self, '_attributes_cache_dict'): - self.get_all_attributes() - return self._attributes_cache_dict[slug] + """ + Returns an attribute object knowing the slug for the entity + class linked to the current instance. + """ + return self.get_all_attribute_slugs().get(slug, None) def get_attribute_by_id(self, attribute_id): + """ + Returns an attribute object knowing the pk for the entity + class linked to the current instance. + """ for attr in self.get_all_attributes(): if attr.pk == attribute_id: return attr def __iter__(self): "Iterates over non-empty EAV attributes. Normal fields are not included." - return self.get_values().__iter__() + return iter(self.get_values()) @staticmethod def save_handler(sender, *args, **kwargs): kwargs['instance'].eav.save() + +# TODO: remove that, it's just for testing with nose + +class Patient(models.Model): + class Meta: + app_label = 'eav_ng' + + name = models.CharField(max_length=20) + + def __unicode__(self): + return self.name diff --git a/tests/basics.py b/tests/basics.py index 9a75053..7d0bcc4 100644 --- a/tests/basics.py +++ b/tests/basics.py @@ -124,7 +124,9 @@ class EavBasicTests(TestCase): def test_eavregistry_ataches_and_detaches_eav_attribute(self): EavRegistry.unregister(Patient) + print "hello" p = Patient() + print "hello" self.assertFalse(hasattr(p, 'eav')) EavRegistry.register(Patient) diff --git a/tests/set_and_get.py b/tests/set_and_get.py index 469677c..f52a96d 100644 --- a/tests/set_and_get.py +++ b/tests/set_and_get.py @@ -150,15 +150,13 @@ class EavSetterAndGetterTests(TestCase): def test_can_filter_attribute_availability_for_entity(self): - attribute = EavAttribute.objects\ .create(datatype=EavAttribute.TYPE_TEXT, name='Country', slug='country') - - self.patient.eav.city = 'Paris' + self.patient.eav.city = 'Tunis' self.patient.save() self.assertEqual(Patient.objects.get(pk=self.patient.pk).eav.city, - 'Paris') + 'Tunis') EavRegistry.unregister(Patient) @@ -174,11 +172,13 @@ class EavSetterAndGetterTests(TestCase): p.eav.city = 'Paris' p.eav.country = 'USA' p.save() - p = Patient.objects.get(pk=self.patient.pk) + p = Patient.objects.get(pk=p.pk) self.assertFalse(p.eav.city) self.assertEqual(p.eav.country, 'USA') p = Patient() + + # todo: test multiple children diff --git a/utils.py b/utils.py index b3963a2..81afd7a 100644 --- a/utils.py +++ b/utils.py @@ -1,6 +1,6 @@ -from django.db.models.signals import post_init, post_save +from django.db.models.signals import post_init, post_save, post_delete from .managers import EntityManager -from .models import EavEntity, EavAttribute +from .models import EavEntity, EavAttribute, get_unique_class_identifier class EavConfig(object): @@ -27,38 +27,53 @@ class EavRegistry(object): """ Attache EAV toolkit to an instance after init. """ + cls_id = get_unique_class_identifier(sender) instance = kwargs['instance'] - cls = instance.__class__ - admin_cls = EavRegistry.cache[cls.__name__]['admin_cls'] + config_cls = EavRegistry.cache[cls_id]['config_cls'] - setattr(instance, admin_cls.proxy_field_name, EavEntity(instance)) + setattr(instance, config_cls.proxy_field_name, EavEntity(instance)) @staticmethod - def register(model_cls, admin_cls=EavConfig): + 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(): + EavEntity.update_attr_cache_for_model(cache['model_cls']) + + + @staticmethod + def register(model_cls, config_cls=EavConfig): """ Inject eav features into the given model and attach a signal listener to it for setup. """ - if model_cls.__name__ in EavRegistry.cache: + + cls_id = get_unique_class_identifier(model_cls) + + if cls_id in EavRegistry.cache: return - + post_init.connect(EavRegistry.attach, sender=model_cls) post_save.connect(EavEntity.save_handler, sender=model_cls) - EavRegistry.cache[model_cls.__name__] = { 'admin_cls': - admin_cls } + EavRegistry.cache[cls_id] = { 'config_cls': config_cls, + 'model_cls': model_cls } - if hasattr(model_cls, admin_cls.manager_field_name): - mgr = getattr(model_cls, admin_cls.manager_field_name) - EavRegistry.cache[model_cls.__name__]['old_mgr'] = mgr + 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, admin_cls.proxy_field_name, EavEntity) + setattr(model_cls, config_cls.proxy_field_name, EavEntity) - setattr(getattr(model_cls, admin_cls.proxy_field_name), - 'get_eav_attributes', admin_cls.get_eav_attributes) + setattr(getattr(model_cls, config_cls.proxy_field_name), + 'get_eav_attributes', config_cls.get_eav_attributes) mgr = EntityManager() - mgr.contribute_to_class(model_cls, admin_cls.manager_field_name) + mgr.contribute_to_class(model_cls, config_cls.manager_field_name) + + EavEntity.update_attr_cache_for_model(model_cls) @staticmethod @@ -67,28 +82,37 @@ class EavRegistry(object): Inject eav features into the given model and attach a signal listener to it for setup. """ - if not model_cls.__name__ in EavRegistry.cache: + cls_id = get_unique_class_identifier(model_cls) + + if not cls_id in EavRegistry.cache: return - cache = EavRegistry.cache[model_cls.__name__] - admin_cls = cache['admin_cls'] - + cache = EavRegistry.cache[cls_id] + config_cls = cache['config_cls'] post_init.disconnect(EavRegistry.attach, sender=model_cls) post_save.disconnect(EavEntity.save_handler, sender=model_cls) try: - delattr(model_cls, admin_cls.manager_field_name) + delattr(model_cls, config_cls.manager_field_name) except AttributeError: pass if 'old_mgr' in cache: cache['old_mgr'].contribute_to_class(model_cls, - admin_cls.manager_field_name) + config_cls.manager_field_name) try: - delattr(model_cls, admin_cls.proxy_field_name) + delattr(model_cls, config_cls.proxy_field_name) except AttributeError: pass - EavRegistry.cache.pop(model_cls.__name__) - + EavEntity.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=EavAttribute) +post_delete.connect(EavRegistry.update_attribute_cache, sender=EavAttribute)