diff --git a/README.rst b/README.rst index 5ab2f1b..7234084 100644 --- a/README.rst +++ b/README.rst @@ -36,12 +36,29 @@ You can install django-eav directly from guthub:: pip install -e git+git://github.com/mvpdev/django-eav.git#egg=django-eav +Prerequisites +------------- + +Django Sites Framework +~~~~~~~~~~~~~~~~~~~~~~ +As of Django 1.7, the `Sites framework `_ is not enabled by default; Django-EAV requires this framework. +To enable the sites framework, follow these steps: + +Add ``django.contrib.sites`` to your INSTALLED_APPS setting. Be sure to add sites to the installed apps list BEFORE eav! + +Define a ``SITE_ID`` setting:: + + SITE_ID = 1 + +Run ``migrate`` + + Usage ----- Edit settings.py ~~~~~~~~~~~~~~~~ -Add ``eav`` to your ``INSTALLED_APPS`` in your project's ``settings.py`` file. +Add ``eav`` to your ``INSTALLED_APPS`` in your project's ``settings.py`` file. Be sure to add eav to the installed apps list AFTER the sites framework! Register your model(s) ~~~~~~~~~~~~~~~~~~~~~~ @@ -52,7 +69,12 @@ model with eav:: >>> eav.register(MyModel) Generally you would do this in your ``models.py`` immediate after your model -declarations. +declarations. Alternatively, you can use the registration decorator provided:: + + from eav.decorators import register_eav + @register_eav() + class MyModel(models.Model): + ... Create some attributes ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/eav/admin.py b/eav/admin.py index 83a5e97..b8469d0 100644 --- a/eav/admin.py +++ b/eav/admin.py @@ -42,7 +42,7 @@ def render_change_form(self, request, context, add=False, change=False, form_url form = context['adminform'].form # infer correct data from the form - fieldsets = self.fieldsets or [(None, {'fields': form.fields.keys()})] + fieldsets = self.fieldsets or [(None, {'fields': list(form.fields.keys())})] adminform = admin.helpers.AdminForm(form, fieldsets, self.prepopulated_fields) media = mark_safe(self.media + adminform.media) @@ -92,7 +92,7 @@ def get_fieldsets(self, request, obj=None): instance = self.model(**kw) form = formset.form(request.POST, instance=instance) - return [(None, {'fields': form.fields.keys()})] + return [(None, {'fields': list(form.fields.keys())})] class AttributeAdmin(ModelAdmin): diff --git a/eav/decorators.py b/eav/decorators.py new file mode 100644 index 0000000..ab19e1e --- /dev/null +++ b/eav/decorators.py @@ -0,0 +1,19 @@ +def register_eav(**kwargs): + """ + Registers the given model(s) classes and wrapped Model class with + django-eav: + + @register_eav + class Author(models.Model): + pass + """ + from . import register + from django.db.models import Model + + def _model_eav_wrapper(model_class): + if not issubclass(model_class, Model): + raise ValueError('Wrapped class must subclass Model.') + register(model_class, **kwargs) + return model_class + + return _model_eav_wrapper \ No newline at end of file diff --git a/eav/forms.py b/eav/forms.py index 26e5785..7702427 100644 --- a/eav/forms.py +++ b/eav/forms.py @@ -94,7 +94,7 @@ def _build_dynamic_fields(self): self.fields[attribute.slug] = MappedField(**defaults) # fill initial data (if attribute was already defined) - if value and not datatype == attribute.TYPE_ENUM: #enum done above + if not value is None and not datatype == attribute.TYPE_ENUM: #enum done above self.initial[attribute.slug] = value def save(self, commit=True): diff --git a/eav/managers.py b/eav/managers.py index 8471cef..85f5efc 100644 --- a/eav/managers.py +++ b/eav/managers.py @@ -25,6 +25,8 @@ Functions and Classes --------------------- ''' +import six + from functools import wraps from django.db import models @@ -49,7 +51,7 @@ def wrapper(self, *args, **kwargs): new_args.append(arg) new_kwargs = {} - for key, value in kwargs.items(): + for key, value in list(kwargs.items()): # modify kwargs (warning: recursion ahead) new_key, new_value = expand_eav_filter(self.model, key, value) new_kwargs.update({new_key: new_value}) @@ -166,7 +168,7 @@ def create(self, **kwargs): new_kwargs = {} eav_kwargs = {} - for key, value in kwargs.iteritems(): + for key, value in six.iteritems(kwargs): if key.startswith(prefix): eav_kwargs.update({key[len(prefix):]: value}) else: @@ -174,7 +176,7 @@ def create(self, **kwargs): obj = self.model(**new_kwargs) obj_eav = getattr(obj, config_cls.eav_attr) - for key, value in eav_kwargs.iteritems(): + for key, value in six.iteritems(eav_kwargs): setattr(obj_eav, key, value) obj.save() return obj diff --git a/eav/models.py b/eav/models.py index 97647eb..612eff7 100644 --- a/eav/models.py +++ b/eav/models.py @@ -32,6 +32,8 @@ Classes ------- ''' +from __future__ import unicode_literals +from builtins import object from django.utils import timezone @@ -43,6 +45,7 @@ from django.contrib.sites.models import Site from django.contrib.sites.managers import CurrentSiteManager from django.conf import settings +from django.utils.encoding import python_2_unicode_compatible from .validators import * from .fields import EavSlugField, EavDatatypeField @@ -81,7 +84,8 @@ class EnumValue(models.Model): value = models.CharField(_(u"value"), db_index=True, unique=True, max_length=50) - def __unicode__(self): + @python_2_unicode_compatible + def __str__(self): return self.value @@ -98,7 +102,8 @@ class EnumGroup(models.Model): enums = models.ManyToManyField(EnumValue, verbose_name=_(u"enum group")) - def __unicode__(self): + @python_2_unicode_compatible + def __str__(self): return self.name @@ -152,7 +157,7 @@ class Attribute(models.Model): change it's datatype. ''' - class Meta: + class Meta(object): ordering = ['content_type', 'name'] unique_together = ('site', 'content_type', 'slug') @@ -317,7 +322,8 @@ def save_value(self, entity, value): value_obj.value = value value_obj.save() - def __unicode__(self): + @python_2_unicode_compatible + def __str__(self): return u"%s.%s (%s)" % (self.content_type, self.name, self.get_datatype_display()) @@ -403,7 +409,8 @@ def _set_value(self, new_value): value = property(_get_value, _set_value) - def __unicode__(self): + @python_2_unicode_compatible + def __str__(self): return u"%s - %s: \"%s\"" % (self.entity, self.attribute.name, self.value) diff --git a/eav/registry.py b/eav/registry.py index 40429c7..6a6a598 100644 --- a/eav/registry.py +++ b/eav/registry.py @@ -26,6 +26,7 @@ Classes ------- ''' +from builtins import object from django.db.utils import DatabaseError from django.db.models.signals import pre_init, post_init, pre_save, post_save diff --git a/eav/tests/misc_models.py b/eav/tests/misc_models.py index 2cbdd8f..b970123 100644 --- a/eav/tests/misc_models.py +++ b/eav/tests/misc_models.py @@ -1,3 +1,4 @@ +from builtins import str from django.test import TestCase from ..models import EnumGroup, Attribute, Value @@ -11,7 +12,7 @@ class MiscModels(TestCase): def test_enumgroup_unicode(self): name = 'Yes / No' e = EnumGroup.objects.create(name=name) - self.assertEqual(unicode(e), name) + self.assertEqual(str(e), name) def test_attribute_help_text(self): desc = 'Patient Age' diff --git a/eav/tests/models.py b/eav/tests/models.py index 0c54584..135777c 100644 --- a/eav/tests/models.py +++ b/eav/tests/models.py @@ -1,7 +1,9 @@ +from builtins import object from django.db import models +from ..decorators import register_eav class Patient(models.Model): - class Meta: + class Meta(object): app_label = 'eav' name = models.CharField(max_length=12) @@ -10,7 +12,7 @@ def __unicode__(self): return self.name class Encounter(models.Model): - class Meta: + class Meta(object): app_label = 'eav' num = models.PositiveSmallIntegerField() @@ -19,3 +21,12 @@ class Meta: def __unicode__(self): return '%s: encounter num %d' % (self.patient, self.num) +@register_eav() +class ExampleModel(models.Model): + class Meta(object): + app_label = 'eav' + + name = models.CharField(max_length=12) + + def __unicode__(self): + return self.name diff --git a/eav/tests/registry.py b/eav/tests/registry.py index 92bf115..de3eb07 100644 --- a/eav/tests/registry.py +++ b/eav/tests/registry.py @@ -5,7 +5,7 @@ from ..managers import EntityManager from ..models import Attribute -from .models import Patient, Encounter +from .models import Patient, Encounter, ExampleModel class RegistryTests(TestCase): @@ -56,6 +56,11 @@ def test_registering_overriding_defaults(self): eav.unregister(Patient) eav.unregister(Encounter) + def test_registering_via_decorator_with_defaults(self): + self.assertTrue(hasattr(ExampleModel, '_eav_config_cls')) + self.assertEqual(ExampleModel._eav_config_cls.manager_attr, 'objects') + self.assertEqual(ExampleModel._eav_config_cls.eav_attr, 'eav') + def test_unregistering(self): old_mgr = Patient.objects eav.register(Patient) @@ -65,6 +70,12 @@ def test_unregistering(self): self.assertEqual(Patient.objects, old_mgr) self.assertFalse(hasattr(Patient, '_eav_config_cls')) + def test_unregistering_via_decorator(self): + self.assertTrue(ExampleModel.objects.__class__.__name__ == 'EntityManager') + eav.unregister(ExampleModel) + e = ExampleModel() + self.assertFalse(ExampleModel.objects.__class__.__name__ == 'EntityManager') + def test_unregistering_unregistered_model_proceeds_silently(self): eav.unregister(Patient) diff --git a/eav/validators.py b/eav/validators.py index 9e03f36..ace459a 100644 --- a/eav/validators.py +++ b/eav/validators.py @@ -44,7 +44,7 @@ def validate_text(value): ''' Raises ``ValidationError`` unless *value* type is ``str`` or ``unicode`` ''' - if not (type(value) == str): + if not (type(value) == str or type(value) == str): raise ValidationError(_(u"Must be str or unicode")) @@ -101,8 +101,12 @@ def validate_enum(value): Raises ``ValidationError`` unless *value* is a saved :class:`~eav.models.EnumValue` model instance. ''' + pass + """ + # This never passes, value is a str from .models import EnumValue if not isinstance(value, EnumValue): raise ValidationError(_(u"Must be an EnumValue model object instance")) if not value.pk: raise ValidationError(_(u"EnumValue has not been saved yet")) + """ diff --git a/setup.py b/setup.py index b4727b7..668a982 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ version=__import__('eav').__version__, license = 'GNU Lesser General Public License (LGPL), Version 3', - requires = ['python (>= 2.5)', 'django (>= 1.2)'], + requires = ['python (>= 2.5)', 'django (>= 1.2)', 'six'], provides = ['eav'], description='Entity-attribute-value model implementation as a reusable'