diff --git a/admin.py b/admin.py new file mode 100644 index 0000000..b06756e --- /dev/null +++ b/admin.py @@ -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 +# +# 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 . + + +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) + diff --git a/fields.py b/fields.py index d2866cd..783e7fd 100644 --- a/fields.py +++ b/fields.py @@ -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.")) + diff --git a/forms.py b/forms.py new file mode 100644 index 0000000..2fc58a2 --- /dev/null +++ b/forms.py @@ -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 +# +# 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 . + +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 diff --git a/managers.py b/managers.py index 2d75c28..e4e39ad 100644 --- a/managers.py +++ b/managers.py @@ -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 diff --git a/models.py b/models.py index a2a7656..ab00ff3 100644 --- a/models.py +++ b/models.py @@ -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') - + >>> Attribute.objects.create(name='Height', datatype='float') + - >>> EavAttribute.objects.create(name='Color', datatype='text', slug='color') - + >>> Attribute.objects.create(name='Color', datatype='text', slug='color') + ''' 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') - + >>> Value.objects.create(entity=u, attribute=a, value_text='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() diff --git a/tests/basics.py b/tests/basics.py index 078c1fd..ad2a6de 100644 --- a/tests/basics.py +++ b/tests/basics.py @@ -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 diff --git a/tests/filters.py b/tests/filters.py index cf94436..a29057a 100644 --- a/tests/filters.py +++ b/tests/filters.py @@ -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 diff --git a/tests/set_and_get.py b/tests/set_and_get.py index 30463ab..0bdc7a5 100644 --- a/tests/set_and_get.py +++ b/tests/set_and_get.py @@ -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)) diff --git a/utils.py b/utils.py index 15bb25f..bb13ba3 100644 --- a/utils.py +++ b/utils.py @@ -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)