Rename models, config attrs, started admin intigration

This commit is contained in:
David Gelvin 2010-09-22 11:02:28 +03:00
parent 8a46f3938f
commit 515cdf07ee
9 changed files with 393 additions and 187 deletions

108
admin.py Normal file
View file

@ -0,0 +1,108 @@
#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
#
# This software is derived from EAV-Django originally written and
# copyrighted by Andrey Mikhaylenko <http://pypi.python.org/pypi/eav-django>
#
# This is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This software is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with EAV-Django. If not, see <http://gnu.org/licenses/>.
from django.contrib import admin
from django.contrib.admin.options import (
ModelAdmin, InlineModelAdmin, StackedInline
)
from django.forms.models import BaseInlineFormSet
from django.utils.safestring import mark_safe
from .models import Attribute, Value, EnumValue, EnumGroup
class BaseEntityAdmin(ModelAdmin):
def render_change_form(self, request, context, **kwargs):
"""
Wrapper for ModelAdmin.render_change_form. Replaces standard static
AdminForm with an EAV-friendly one. The point is that our form generates
fields dynamically and fieldsets must be inferred from a prepared and
validated form instance, not just the form class. Django does not seem
to provide hooks for this purpose, so we simply wrap the view and
substitute some data.
"""
form = context['adminform'].form
# infer correct data from the form
fieldsets = [(None, {'fields': form.fields.keys()})]
adminform = admin.helpers.AdminForm(form, fieldsets,
self.prepopulated_fields)
media = mark_safe(self.media + adminform.media)
context.update(adminform=adminform, media=media)
super_meth = super(BaseEntityAdmin, self).render_change_form
return super_meth(request, context, **kwargs)
class BaseEntityInlineFormSet(BaseInlineFormSet):
"""
An inline formset that correctly initializes EAV forms.
"""
def add_fields(self, form, index):
if self.instance:
setattr(form.instance, self.fk.name, self.instance)
form._build_dynamic_fields()
super(BaseEntityInlineFormSet, self).add_fields(form, index)
class BaseEntityInline(InlineModelAdmin):
"""
Inline model admin that works correctly with EAV attributes. You should mix
in the standard StackedInline or TabularInline classes in order to define
formset representation, e.g.::
class ItemInline(BaseEntityInline, StackedInline):
model = Item
form = forms.ItemForm
.. warning: TabularInline does *not* work out of the box. There is,
however, a patched template `admin/edit_inline/tabular.html` bundled
with EAV-Django. You can copy or symlink the `admin` directory to your
templates search path (see Django documentation).
"""
formset = BaseEntityInlineFormSet
def get_fieldsets(self, request, obj=None):
if self.declared_fieldsets:
return self.declared_fieldsets
formset = self.get_formset(request)
fk_name = self.fk_name or formset.fk.name
kw = {fk_name: obj} if obj else {}
instance = self.model(**kw)
form = formset.form(request.POST, instance=instance)
return [(None, {'fields': form.fields.keys()})]
class AttributeAdmin(ModelAdmin):
list_display = ('name', 'slug', 'datatype', 'description')
prepopulated_fields = {'name': ('slug',)}
admin.site.register(Attribute, AttributeAdmin)
admin.site.register(Value)
admin.site.register(EnumValue)
admin.site.register(EnumGroup)

View file

@ -66,9 +66,10 @@ class EavDatatypeField(models.CharField):
once it have been created.
"""
super(EavDatatypeField, self).validate(value, instance)
from .models import EavAttribute
from .models import Attribute
if not instance.pk:
return
if value != EavAttribute.objects.get(pk=instance.pk).datatype:
if value != Attribute.objects.get(pk=instance.pk).datatype:
raise ValidationError(_(u"You cannot change the datatype of an "
u"existing attribute."))

137
forms.py Normal file
View file

@ -0,0 +1,137 @@
#!/usr/bin/env python
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
#
# This software is derived from EAV-Django originally written and
# copyrighted by Andrey Mikhaylenko <http://pypi.python.org/pypi/eav-django>
#
# This is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This software is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with EAV-Django. If not, see <http://gnu.org/licenses/>.
from copy import deepcopy
# django
from django.forms import BooleanField, CharField, DateField, FloatField, \
IntegerField, ModelForm, ChoiceField, ValidationError
from django.contrib.admin.widgets import AdminDateWidget, FilteredSelectMultiple #, RelatedFieldWidgetWrapper
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):
"""
ModelForm for entity with support for EAV attributes. Form fields are created
on the fly depending on Schema defined for given entity instance. If no schema
is defined (i.e. the entity instance has not been saved yet), only static
fields are used. However, on form validation the schema will be retrieved
and EAV fields dynamically added to the form, so when the validation is
actually done, all EAV fields are present in it (unless Rubric is not defined).
"""
FIELD_CLASSES = {
'text': CharField,
'float': FloatField,
'int': IntegerField,
'date': DateField,
'bool': BooleanField,
'enum': ChoiceField, #RelatedFieldWidgetWrapper(MultipleChoiceField),
}
FIELD_EXTRA = {
'date': {'widget': AdminDateWidget},
}
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._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)
defaults = {
'label': attribute.name.capitalize(),
'required': False,
'help_text': attribute.help_text,
}
datatype = attribute.datatype
if datatype == attribute.TYPE_ENUM:
enums = attribute.get_choices() \
.values_list('id', 'value')
choices = [('', '-----')] + list(enums)
defaults.update({'choices': choices})
if value:
defaults.update({'initial': value.pk})
MappedField = self.FIELD_CLASSES[datatype]
self.fields[attribute.slug] = MappedField(**defaults)
# fill initial data (if attribute was already defined)
if value and not datatype == attribute.TYPE_ENUM: # m2m is already done above
self.initial[attribute.slug] = value
def save(self, commit=True):
"""
Saves this ``form``'s cleaned_data into model instance ``self.instance``
and related EAV attributes.
Returns ``instance``.
"""
if self.errors:
raise ValueError("The %s could not be saved because the data didn't"
" validate." % self.instance._meta.object_name)
# create entity instance, don't save yet
instance = super(BaseDynamicEntityForm, self).save(commit=False)
# assign attributes
for attribute in self.proxy.get_eav_attributes():
value = self.cleaned_data.get(attribute.slug)
if attribute.datatype == attribute.TYPE_ENUM:
if value:
value = attribute.enum_group.enums.get(pk=value)
else:
value = None
setattr(self.proxy, attribute.slug, value)
# save entity and its attributes
if commit:
instance.save()
return instance

View file

@ -21,7 +21,7 @@ from functools import wraps
from django.db import models
from .models import EavAttribute, EavValue
from .models import Attribute, Value
def eav_filter(func):
'''
@ -78,22 +78,22 @@ def expand_eav_filter(model_cls, key, value):
value = 5
Would return:
key = 'eav_values__in'
value = EavValues.objects.filter(value_int=5, attribute__slug='height')
value = Values.objects.filter(value_int=5, attribute__slug='height')
'''
from .utils import EavRegistry
fields = key.split('__')
config_cls = EavRegistry.get_config_cls_for_model(model_cls)
if len(fields) > 1 and config_cls and \
fields[0] == config_cls.proxy_field_name:
fields[0] == config_cls.eav_attr:
slug = fields[1]
gr_name = config_cls.generic_relation_field_name
datatype = EavAttribute.objects.get(slug=slug).datatype
gr_name = config_cls.generic_relation_attr
datatype = Attribute.objects.get(slug=slug).datatype
lookup = '__%s' % fields[2] if len(fields) > 2 else ''
kwargs = {'value_%s%s' % (datatype, lookup): value,
'attribute__slug': slug}
value = EavValue.objects.filter(**kwargs)
value = Value.objects.filter(**kwargs)
return '%s__in' % gr_name, value

100
models.py
View file

@ -37,14 +37,6 @@ def get_unique_class_identifier(cls):
return '.'.join((inspect.getfile(cls), cls.__name__))
class EavAttributeLabel(models.Model):
name = models.CharField(_(u"name"), db_index=True,
unique=True, max_length=100)
def __unicode__(self):
return self.name
class EnumValue(models.Model):
value = models.CharField(_(u"value"), db_index=True,
unique=True, max_length=50)
@ -62,16 +54,16 @@ class EnumGroup(models.Model):
return self.name
class EavAttribute(models.Model):
class Attribute(models.Model):
'''
The A model in E-A-V. This holds the 'concepts' along with the data type
something like:
>>> EavAttribute.objects.create(name='Height', datatype='float')
<EavAttribute: Height (Float)>
>>> Attribute.objects.create(name='Height', datatype='float')
<Attribute: Height (Float)>
>>> EavAttribute.objects.create(name='Color', datatype='text', slug='color')
<EavAttribute: Color (Text)>
>>> Attribute.objects.create(name='Color', datatype='text', slug='color')
<Attribute: Color (Text)>
'''
class Meta:
ordering = ['name']
@ -115,47 +107,43 @@ class EavAttribute(models.Model):
datatype = EavDatatypeField(_(u"data type"), max_length=6,
choices=DATATYPE_CHOICES)
created = models.DateTimeField(_(u"created"), default=datetime.now)
created = models.DateTimeField(_(u"created"), default=datetime.now,
editable=False)
modified = models.DateTimeField(_(u"modified"), auto_now=True)
labels = models.ManyToManyField(EavAttributeLabel,
verbose_name=_(u"labels"))
def save(self, *args, **kwargs):
if not self.slug:
self.slug = EavSlugField.create_slug_from_name(self.name)
self.full_clean()
super(EavAttribute, self).save(*args, **kwargs)
super(Attribute, self).save(*args, **kwargs)
def clean(self):
if self.datatype == self.TYPE_ENUM and not enum_group:
if self.datatype == self.TYPE_ENUM and not self.enum_group:
raise ValidationError(_(
u"You must set the enum_group for TYPE_ENUM attributes"))
u"You must set the choice group for multiple choice" \
u"attributes"))
def add_label(self, label):
label, created = EavAttributeLabel.objects.get_or_create(name=label)
label.eavattribute_set.add(self)
def remove_label(self, label):
try:
label_obj = EavAttributeLabel.objects.get(name=label)
except EavAttributeLabel.DoesNotExist:
return
self.labels.remove(label_obj)
def get_choices(self):
'''
Returns the avilable choices for enums.
'''
if not self.datatype == Attribute.TYPE_ENUM:
return None
return self.enum_group.enums.all()
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 EavValue object that has a foreignkey to self (attribute) and
to the entity. Returns nothing if a matching EavValue object
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.eavvalue_set.filter(entity_ct=ct, entity_id=entity.pk)
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 "\
@ -171,7 +159,7 @@ class EavAttribute(models.Model):
"""
Save any value for an entity, calling the appropriate method
according to the type of the value.
Value should not be an EavValue but a normal value
Value should not be an Value but a normal value
"""
self._save_single_value(entity, value)
@ -181,18 +169,18 @@ class EavAttribute(models.Model):
Save a a value of type that doesn't need special joining like
int, float, text, date, etc.
Value should not be an EavValue object but a normal value.
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.eavvalue_set.get(entity_ct=ct,
eavvalue = self.value_set.get(entity_ct=ct,
entity_id=entity.pk,
attribute=attribute)
except EavValue.DoesNotExist:
eavvalue = self.eavvalue_set.model(entity_ct=ct,
except Value.DoesNotExist:
eavvalue = self.value_set.model(entity_ct=ct,
entity_id=entity.pk,
attribute=attribute)
if value != eavvalue.value:
@ -204,7 +192,7 @@ class EavAttribute(models.Model):
return u"%s (%s)" % (self.name, self.get_datatype_display())
class EavValue(models.Model):
class Value(models.Model):
'''
The V model in E-A-V. This holds the 'value' for an attribute and an
entity:
@ -214,10 +202,10 @@ class EavValue(models.Model):
>>> from .utils import EavRegistry
>>> EavRegistry.register(User)
>>> u = User.objects.create(username='crazy_dev_user')
>>> a = EavAttribute.objects.create(name='Favorite Drink', datatype='text',
>>> a = Attribute.objects.create(name='Favorite Drink', datatype='text',
... slug='fav_drink')
>>> EavValue.objects.create(entity=u, attribute=a, value_text='red bull')
<EavValue: crazy_dev_user - Favorite Drink: "red bull">
>>> Value.objects.create(entity=u, attribute=a, value_text='red bull')
<Value: crazy_dev_user - Favorite Drink: "red bull">
'''
@ -230,7 +218,8 @@ class EavValue(models.Model):
value_int = models.IntegerField(blank=True, null=True)
value_date = models.DateTimeField(blank=True, null=True)
value_bool = models.NullBooleanField(blank=True, null=True)
value_enum = models.ForeignKey(EnumValue, blank=True, null=True)
value_enum = models.ForeignKey(EnumValue, blank=True, null=True,
related_name='eav_values')
generic_value_id = models.IntegerField(blank=True, null=True)
generic_value_ct = models.ForeignKey(ContentType, blank=True, null=True,
@ -241,17 +230,19 @@ class EavValue(models.Model):
created = models.DateTimeField(_(u"created"), default=datetime.now)
modified = models.DateTimeField(_(u"modified"), auto_now=True)
attribute = models.ForeignKey(EavAttribute, db_index=True,
attribute = models.ForeignKey(Attribute, db_index=True,
verbose_name=_(u"attribute"))
def save(self, *args, **kwargs):
self.full_clean()
super(EavValue, self).save(*args, **kwargs)
super(Value, self).save(*args, **kwargs)
def clean(self):
if self.attribute.datatype == EavAttribute.TYPE_ENUM and \
if self.attribute.datatype == Attribute.TYPE_ENUM and \
self.value_enum:
if self.value_enum not in self.attribute.enumvalues:
if self.value_enum not in self.attribute.enum_group.enums.all():
raise ValidationError(_(u"%(choice)s is not a valid " \
u"choice for %s(attribute)") % \
{'choice': self.value_enum,
@ -266,10 +257,9 @@ class EavValue(models.Model):
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.
Get returns the Python object hold by this Value object.
"""
return getattr(self, 'value_%s' % self.attribute.datatype)
@ -284,7 +274,7 @@ class EavValue(models.Model):
return u"%s - %s: \"%s\"" % (self.entity, self.attribute.name, self.value)
class EavEntity(object):
class Entity(object):
_cache = {}
@ -393,13 +383,13 @@ class EavEntity(object):
"""
cache = cls.get_attr_cache_for_model(model_cls)
if not cache:
cache = EavEntity.update_attr_cache_for_model(model_cls)
cache = Entity.update_attr_cache_for_model(model_cls)
return cache['attributes']
def get_values(self):
return EavValue.objects.filter(entity_ct=self.ct,
return Value.objects.filter(entity_ct=self.ct,
entity_id=self.model.pk).select_related()
@classmethod
@ -410,7 +400,7 @@ class EavEntity(object):
"""
cache = cls.get_attr_cache_for_model(model_cls)
if not cache:
cache = EavEntity.update_attr_cache_for_model(model_cls)
cache = Entity.update_attr_cache_for_model(model_cls)
return cache['slug_mapping']
@ -432,7 +422,7 @@ class EavEntity(object):
"""
cache = cls.get_attr_cache_for_model(model_cls)
if not cache:
cache = EavEntity.update_attr_cache_for_model(model_cls)
cache = Entity.update_attr_cache_for_model(model_cls)
return cache['slug_mapping'].get(slug, None)
@ -453,7 +443,7 @@ class EavEntity(object):
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.proxy_field_name)
instance_eav = getattr(kwargs['instance'], config_cls.eav_attr)
instance_eav.save()

View file

@ -19,12 +19,12 @@ class EavBasicTests(TestCase):
EavRegistry.unregister(Patient)
EavRegistry.register(Patient)
self.attribute = EavAttribute.objects.create(datatype=EavAttribute.TYPE_TEXT,
self.attribute = Attribute.objects.create(datatype=Attribute.TYPE_TEXT,
name='City', help_text='The City', slug='city')
self.entity = Patient.objects.create(name="Doe")
self.value = EavValue.objects.create(entity=self.entity,
self.value = Value.objects.create(entity=self.entity,
attribute=self.attribute,
value_text='Denver')
@ -37,18 +37,18 @@ class EavBasicTests(TestCase):
def test_can_create_attribute(self):
EavAttribute.objects.create(datatype=EavAttribute.TYPE_TEXT,
Attribute.objects.create(datatype=Attribute.TYPE_TEXT,
name='My text test', slug='test',
help_text='My help text')
def test_can_eaventity_children_give_you_all_attributes_by_default(self):
qs = Patient.eav.get_eav_attributes()
self.assertEqual(list(qs), list(EavAttribute.objects.all()))
self.assertEqual(list(qs), list(Attribute.objects.all()))
def test_value_creation(self):
EavValue.objects.create(entity=self.entity,
Value.objects.create(entity=self.entity,
attribute=self.attribute,
value_float=1.2)
@ -58,20 +58,20 @@ class EavBasicTests(TestCase):
def test_value_types(self):
_text = EavAttribute.objects.create(datatype=EavAttribute.TYPE_TEXT,
_text = Attribute.objects.create(datatype=Attribute.TYPE_TEXT,
name='Text', slug='text',
help_text='The text')
val = EavValue.objects.create(entity=self.entity,
val = Value.objects.create(entity=self.entity,
attribute = _text)
value = "Test text"
val.value = value
val.save()
self.assertEqual(val.value, value)
_float = EavAttribute.objects.create(datatype=EavAttribute.TYPE_FLOAT,
_float = Attribute.objects.create(datatype=Attribute.TYPE_FLOAT,
name='Float', slug='float',
help_text='The float')
val = EavValue.objects.create(entity=self.entity,
val = Value.objects.create(entity=self.entity,
attribute = _float)
value = 1.22
val.value = value
@ -79,30 +79,30 @@ class EavBasicTests(TestCase):
self.assertEqual(val.value, value)
_int = EavAttribute.objects.create(datatype=EavAttribute.TYPE_INT,
_int = Attribute.objects.create(datatype=Attribute.TYPE_INT,
name='Int', slug='int',
help_text='The int')
val = EavValue.objects.create(entity=self.entity,
val = Value.objects.create(entity=self.entity,
attribute = _int)
value = 7
val.value = value
val.save()
self.assertEqual(val.value, value)
_date = EavAttribute.objects.create(datatype=EavAttribute.TYPE_DATE,
_date = Attribute.objects.create(datatype=Attribute.TYPE_DATE,
name='Date', slug='date',
help_text='The date')
val = EavValue.objects.create(entity=self.entity,
val = Value.objects.create(entity=self.entity,
attribute = _date)
value = datetime.now()
val.value = value
val.save()
self.assertEqual(val.value, value)
_bool = EavAttribute.objects.create(datatype=EavAttribute.TYPE_BOOLEAN,
_bool = Attribute.objects.create(datatype=Attribute.TYPE_BOOLEAN,
name='Bool', slug='bool',
help_text='The bool')
val = EavValue.objects.create(entity=self.entity,
val = Value.objects.create(entity=self.entity,
attribute = _bool)
value = False
val.value = value
@ -141,7 +141,7 @@ class EavBasicTests(TestCase):
@classmethod
def get_eav_attributes(self):
return EavAttribute.objects.all()
return Attribute.objects.all()
EavRegistry.register(Patient, PatientEav)
@ -159,10 +159,10 @@ class EavBasicTests(TestCase):
class PatientEav(EavConfig):
proxy_field_name = 'my_eav'
manager_field_name ='my_objects'
generic_relation_field_name = 'my_eav_values'
generic_relation_field_related_name = "patient"
eav_attr = 'my_eav'
manager_attr ='my_objects'
generic_relation_attr = 'my_eav_values'
generic_relation_related_name = "patient"
EavRegistry.register(Patient, PatientEav)
@ -174,8 +174,8 @@ class EavBasicTests(TestCase):
p2.my_eav.city = "Mbrarare"
p2.save()
value = EavValue.objects.get(value_text='Mbrarare')
name = PatientEav.generic_relation_field_related_name
value = Value.objects.get(value_text='Mbrarare')
name = PatientEav.generic_relation_related_name
self.assertTrue(value, name)
bak_registered_manager = Patient.objects

View file

@ -21,13 +21,13 @@ class EavFilterTests(TestCase):
EavRegistry.unregister(Patient)
EavRegistry.register(Patient)
self.attribute = EavAttribute.objects\
.create(datatype=EavAttribute.TYPE_TEXT,
self.attribute = Attribute.objects\
.create(datatype=Attribute.TYPE_TEXT,
name='City', slug='city')
self.patient = Patient.objects.create(name="Doe")
self.value = EavValue.objects.create(entity=self.patient,
self.value = Value.objects.create(entity=self.patient,
attribute=self.attribute,
value_text='Denver')
@ -38,15 +38,15 @@ class EavFilterTests(TestCase):
def additional_attribute_setup(self):
self.country_attr = EavAttribute.objects\
.create(datatype=EavAttribute.TYPE_TEXT,
self.country_attr = Attribute.objects\
.create(datatype=Attribute.TYPE_TEXT,
name='Country', slug='country')
class PatientEav(EavConfig):
@classmethod
def get_eav_attributes(cls):
return EavAttribute.objects.filter(slug='country')
return Attribute.objects.filter(slug='country')
self.PatientEav = PatientEav
@ -54,7 +54,7 @@ class EavFilterTests(TestCase):
@classmethod
def get_eav_attributes(cls):
return EavAttribute.objects.all()
return Attribute.objects.all()
self.UserEav = UserEav
EavRegistry.register(User, UserEav)
@ -71,7 +71,7 @@ class EavFilterTests(TestCase):
# self.patient.save()
#print EavEntity.objects.filter(eav__city='Paris')
#print Entity.objects.filter(eav__city='Paris')
def test_you_can_filter_entity_by_attribute_values(self):
@ -168,12 +168,12 @@ class EavFilterTests(TestCase):
def test_you_can_filter_entity_by_q_objects_with_lookups(self):
class UserEav(EavConfig):
manager_field_name = 'eav_objects'
manager_attr = 'eav_objects'
EavRegistry.register(User, UserEav)
EavAttribute.objects.create(datatype=EavAttribute.TYPE_INT,
Attribute.objects.create(datatype=Attribute.TYPE_INT,
name='Height')
EavAttribute.objects.create(datatype=EavAttribute.TYPE_FLOAT,
Attribute.objects.create(datatype=Attribute.TYPE_FLOAT,
name='Weight')
u = User.objects.create(username='Bob')
u.eav.height = 10
@ -213,12 +213,12 @@ class EavFilterTests(TestCase):
'''
This test demonstrates a few EAV queries that are known to be broken.
it currently fails.
''''
'''
EavRegistry.register(User)
EavAttribute.objects.create(datatype=EavAttribute.TYPE_INT,
Attribute.objects.create(datatype=Attribute.TYPE_INT,
name='Height')
EavAttribute.objects.create(datatype=EavAttribute.TYPE_FLOAT,
Attribute.objects.create(datatype=Attribute.TYPE_FLOAT,
name='Weight')
u = User.objects.create(username='Bob')
u.eav.height = 10

View file

@ -20,13 +20,13 @@ class EavSetterAndGetterTests(TestCase):
EavRegistry.unregister(Patient)
EavRegistry.register(Patient)
self.attribute = EavAttribute.objects\
.create(datatype=EavAttribute.TYPE_TEXT,
self.attribute = Attribute.objects\
.create(datatype=Attribute.TYPE_TEXT,
name='City', slug='city')
self.patient = Patient.objects.create(name="Doe")
self.value = EavValue.objects.create(entity=self.patient,
self.value = Value.objects.create(entity=self.patient,
attribute=self.attribute,
value_text='Denver')
@ -37,15 +37,15 @@ class EavSetterAndGetterTests(TestCase):
def additional_attribute_setup(self):
self.country_attr = EavAttribute.objects\
.create(datatype=EavAttribute.TYPE_TEXT,
self.country_attr = Attribute.objects\
.create(datatype=Attribute.TYPE_TEXT,
name='Country', slug='country')
class PatientEav(EavConfig):
@classmethod
def get_eav_attributes(cls):
return EavAttribute.objects.filter(slug='country')
return Attribute.objects.filter(slug='country')
self.PatientEav = PatientEav
@ -53,7 +53,7 @@ class EavSetterAndGetterTests(TestCase):
@classmethod
def get_eav_attributes(cls):
return EavAttribute.objects.filter(slug='city')
return Attribute.objects.filter(slug='city')
self.UserEav = UserEav
@ -65,7 +65,7 @@ class EavSetterAndGetterTests(TestCase):
def test_save_single_value(self):
patient = Patient.objects.create(name="x")
attr = EavAttribute.objects.create(datatype=EavAttribute.TYPE_TEXT,
attr = Attribute.objects.create(datatype=Attribute.TYPE_TEXT,
name='a', slug='a')
# does nothing
attr._save_single_value(patient)
@ -96,7 +96,7 @@ class EavSetterAndGetterTests(TestCase):
patient.save()
self.assertEqual(patient.eav.city, 'Paris')
self.assertEqual(EavValue.objects.filter(value_text='Paris').count(), 1)
self.assertEqual(Value.objects.filter(value_text='Paris').count(), 1)
def test_you_can_assign_a_value_to_a_saved_object(self):
@ -106,7 +106,7 @@ class EavSetterAndGetterTests(TestCase):
patient.save()
self.assertEqual(patient.eav.city, 'Paris')
self.assertEqual(EavValue.objects.filter(value_text='Paris').count(), 1)
self.assertEqual(Value.objects.filter(value_text='Paris').count(), 1)
def test_underscore_prevent_a_data_from_been_saved(self):
@ -121,24 +121,24 @@ class EavSetterAndGetterTests(TestCase):
self.fail()
except AttributeError:
pass
self.assertFalse(EavValue.objects.filter(value_text='Paris').count())
self.assertFalse(Value.objects.filter(value_text='Paris').count())
def test_you_can_create_several_type_of_attributes(self):
self.patient = Patient(name='test')
EavAttribute.objects.create(datatype=EavAttribute.TYPE_TEXT,
Attribute.objects.create(datatype=Attribute.TYPE_TEXT,
name='text', slug='text')
EavAttribute.objects.create(datatype=EavAttribute.TYPE_FLOAT,
Attribute.objects.create(datatype=Attribute.TYPE_FLOAT,
name='float', slug='float')
EavAttribute.objects.create(datatype=EavAttribute.TYPE_INT,
Attribute.objects.create(datatype=Attribute.TYPE_INT,
name='int', slug='int')
EavAttribute.objects.create(datatype=EavAttribute.TYPE_DATE,
Attribute.objects.create(datatype=Attribute.TYPE_DATE,
name='date', slug='date')
EavAttribute.objects.create(datatype=EavAttribute.TYPE_BOOLEAN,
Attribute.objects.create(datatype=Attribute.TYPE_BOOLEAN,
name='bool', slug='bool')
EavAttribute.objects.create(datatype=EavAttribute.TYPE_OBJECT,
Attribute.objects.create(datatype=Attribute.TYPE_OBJECT,
name='object', slug='object')
now = datetime.today()
@ -164,7 +164,7 @@ class EavSetterAndGetterTests(TestCase):
self.patient.eav.no_an_attribute = 'Woot'
self.patient.save()
self.assertFalse(EavValue.objects.filter(value_text='Paris').count())
self.assertFalse(Value.objects.filter(value_text='Paris').count())
def test_get_a_value_that_does_not_exists(self):
@ -178,36 +178,6 @@ class EavSetterAndGetterTests(TestCase):
self.fail()
except AttributeError:
pass
def test_attributes_can_be_labelled(self):
attribute = EavAttribute.objects\
.create(datatype=EavAttribute.TYPE_TEXT,
name='Country', slug='country')
# add labels
self.attribute.add_label('a')
self.attribute.add_label('c')
attribute.add_label('b')
attribute.add_label('c')
self.assertEqual(EavAttribute.objects.get(labels__name='a').name,
'City')
self.assertEqual(EavAttribute.objects.get(labels__name='b').name,
'Country')
# cross labels
self.assertEqual(EavAttribute.objects.filter(labels__name='c').count(),
2)
# remove labels
self.attribute.remove_label('a')
self.assertFalse(EavAttribute.objects.filter(labels__name='a').count())
# 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):
@ -219,10 +189,10 @@ class EavSetterAndGetterTests(TestCase):
EavRegistry.register(User, self.UserEav)
self.assertEqual(list(Patient.eav.get_eav_attributes()),
list(EavAttribute.objects.filter(slug='country')))
list(Attribute.objects.filter(slug='country')))
self.assertEqual(list(User.eav.get_eav_attributes()),
list(EavAttribute.objects.filter(slug='city')))
list(Attribute.objects.filter(slug='city')))
def test_can_filter_attribute_availability_for_entity(self):
@ -285,7 +255,7 @@ class EavSetterAndGetterTests(TestCase):
@classmethod
def get_eav_attributes(cls):
return EavAttribute.objects.filter(slug='country')
return Attribute.objects.filter(slug='country')
EavRegistry.register(Patient, SubPatientEav)
@ -333,10 +303,10 @@ class EavSetterAndGetterTests(TestCase):
EavRegistry.register(Patient, self.PatientEav)
assert list(self.PatientEav.get_eav_attributes_for_model(Patient))\
== list(EavEntity.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(EavAttribute.objects.filter(slug='country'))
== list(Attribute.objects.filter(slug='country'))
def test_values(self):
@ -346,13 +316,13 @@ class EavSetterAndGetterTests(TestCase):
self.patient.save()
self.assertEqual(list(self.patient.eav.get_values()),
list(EavValue.objects.exclude(value_text='Denver')))
list(Value.objects.exclude(value_text='Denver')))
def test_get_all_attribute_slugs_for_model(self):
self.country_attr = EavAttribute.objects\
.create(datatype=EavAttribute.TYPE_TEXT,
self.country_attr = Attribute.objects\
.create(datatype=Attribute.TYPE_TEXT,
name='Street', slug='street')
self.additional_attribute_setup()
@ -361,17 +331,17 @@ class EavSetterAndGetterTests(TestCase):
@classmethod
def get_eav_attributes(cls):
return EavAttribute.objects.exclude(slug='city')
return Attribute.objects.exclude(slug='city')
EavRegistry.register(User, UserEav)
u = User.objects.create(username='John')
slugs = dict((s.slug, s) for s in EavAttribute.objects\
slugs = dict((s.slug, s) for s in Attribute.objects\
.exclude(slug='city'))
assert slugs\
== EavEntity.get_all_attribute_slugs_for_model(User)\
== Entity.get_all_attribute_slugs_for_model(User)\
== User.eav.get_all_attribute_slugs_for_model(User)\
== u.eav.get_all_attribute_slugs()
@ -381,7 +351,7 @@ class EavSetterAndGetterTests(TestCase):
self.patient.eav.country = 'Kenya'
self.patient.eav.save()
self.assertEqual(list(EavValue.objects.all()),
self.assertEqual(list(Value.objects.all()),
list(Patient.objects.get(pk=self.patient.pk).eav))

View file

@ -21,17 +21,17 @@ from django.contrib.contenttypes import generic
from django.db.utils import DatabaseError
from django.db.models.signals import post_init, post_save, post_delete, pre_init
from .managers import EntityManager
from .models import (EavEntity, EavAttribute, EavValue,
from .models import (Entity, Attribute, Value,
get_unique_class_identifier)
#todo : rename this file in registry
class EavConfig(EavEntity):
class EavConfig(Entity):
proxy_field_name = 'eav'
manager_field_name ='objects'
generic_relation_field_name = 'eav_values'
generic_relation_field_related_name = None
eav_attr = 'eav'
manager_attr ='objects'
generic_relation_attr = 'eav_values'
generic_relation_related_name = None
@classmethod
def get_eav_attributes(cls):
@ -39,7 +39,7 @@ class EavConfig(EavEntity):
By default, all attributes apply to an entity,
unless otherwise specified.
"""
return EavAttribute.objects.all()
return Attribute.objects.all()
class EavRegistry(object):
@ -69,7 +69,7 @@ class EavRegistry(object):
instance = kwargs['instance']
config_cls = EavRegistry.get_config_cls_for_model(sender)
setattr(instance, config_cls.proxy_field_name, EavEntity(instance))
setattr(instance, config_cls.eav_attr, Entity(instance))
@staticmethod
@ -79,7 +79,7 @@ class EavRegistry(object):
create an attribute.
"""
for cache in EavRegistry.cache.itervalues():
EavEntity.update_attr_cache_for_model(cache['model_cls'])
Entity.update_attr_cache_for_model(cache['model_cls'])
@staticmethod
@ -112,7 +112,7 @@ class EavRegistry(object):
# we want to call attach and save handler on instance creation and
# saving
post_init.connect(EavRegistry.attach, sender=model_cls)
post_save.connect(EavEntity.save_handler, sender=model_cls)
post_save.connect(Entity.save_handler, sender=model_cls)
# todo: rename cache in data
EavRegistry.cache[cls_id] = { 'config_cls': config_cls,
@ -121,38 +121,38 @@ class EavRegistry(object):
# save the old manager if the attribute name conflict with the new
# one
if hasattr(model_cls, config_cls.manager_field_name):
mgr = getattr(model_cls, config_cls.manager_field_name)
if hasattr(model_cls, config_cls.manager_attr):
mgr = getattr(model_cls, config_cls.manager_attr)
EavRegistry.cache[cls_id]['old_mgr'] = mgr
if not manager_only:
# set add the config_cls as an attribute of the model
# it will allow to perform some operation directly from this model
setattr(model_cls, config_cls.proxy_field_name, config_cls)
setattr(model_cls, config_cls.eav_attr, config_cls)
# todo : not useful anymore ?
setattr(getattr(model_cls, config_cls.proxy_field_name),
setattr(getattr(model_cls, config_cls.eav_attr),
'get_eav_attributes', config_cls.get_eav_attributes)
# attache the new manager to the model
mgr = EntityManager()
mgr.contribute_to_class(model_cls, config_cls.manager_field_name)
mgr.contribute_to_class(model_cls, config_cls.manager_attr)
if not manager_only:
# todo: see with david how to change that
try:
EavEntity.update_attr_cache_for_model(model_cls)
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_field_related_name:
rel_name = config_cls.generic_relation_field_related_name
if config_cls.generic_relation_related_name:
rel_name = config_cls.generic_relation_related_name
else:
rel_name = model_cls.__name__
gr_name = config_cls.generic_relation_field_name.lower()
generic_relation = generic.GenericRelation(EavValue,
gr_name = config_cls.generic_relation_attr.lower()
generic_relation = generic.GenericRelation(Value,
object_id_field='entity_id',
content_type_field='entity_ct',
related_name=rel_name)
@ -174,15 +174,15 @@ class EavRegistry(object):
manager_only = cache['manager_only']
if not manager_only:
post_init.disconnect(EavRegistry.attach, sender=model_cls)
post_save.disconnect(EavEntity.save_handler, sender=model_cls)
post_save.disconnect(Entity.save_handler, sender=model_cls)
try:
delattr(model_cls, config_cls.manager_field_name)
delattr(model_cls, config_cls.manager_attr)
except AttributeError:
pass
# remove remaining reference to the generic relation
gen_rel_field = config_cls.generic_relation_field_name
gen_rel_field = config_cls.generic_relation_attr
for field in model_cls._meta.local_many_to_many:
if field.name == gen_rel_field:
model_cls._meta.local_many_to_many.remove(field)
@ -194,15 +194,15 @@ class EavRegistry(object):
if 'old_mgr' in cache:
cache['old_mgr'].contribute_to_class(model_cls,
config_cls.manager_field_name)
config_cls.manager_attr)
try:
delattr(model_cls, config_cls.proxy_field_name)
delattr(model_cls, config_cls.eav_attr)
except AttributeError:
pass
if not manager_only:
EavEntity.flush_attr_cache_for_model(model_cls)
Entity.flush_attr_cache_for_model(model_cls)
EavRegistry.cache.pop(cls_id)
@ -210,5 +210,5 @@ class EavRegistry(object):
# 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)
post_save.connect(EavRegistry.update_attribute_cache, sender=Attribute)
post_delete.connect(EavRegistry.update_attribute_cache, sender=Attribute)