mirror of
https://github.com/jazzband/django-eav2.git
synced 2026-04-08 17:30:59 +00:00
New cache reloading only for models on startup and at ever attribute addition
This commit is contained in:
parent
5fbd515d3f
commit
0cc260c5b1
4 changed files with 183 additions and 57 deletions
152
models.py
152
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
76
utils.py
76
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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue