From d321d4fc6777927e86df4fb1e225956337d1c0c3 Mon Sep 17 00:00:00 2001 From: rvansanten Date: Fri, 4 Oct 2013 14:13:47 +0200 Subject: [PATCH 01/15] Disabled validation for enum From the admin, value passed in is always a string. This validation is needless as the conversion from string works fine. --- eav/validators.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eav/validators.py b/eav/validators.py index f5d6c5a..434118e 100644 --- a/eav/validators.py +++ b/eav/validators.py @@ -102,8 +102,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")) + """ From 55afd2aab9b0a574515b94f068be31c233750b68 Mon Sep 17 00:00:00 2001 From: MadEng84 Date: Tue, 17 Feb 2015 10:29:54 +0100 Subject: [PATCH 02/15] bugfix #26 - support for new ContentType-s field related_query_name --- eav/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eav/registry.py b/eav/registry.py index a5265af..82ed5b7 100644 --- a/eav/registry.py +++ b/eav/registry.py @@ -166,7 +166,7 @@ def _attach_generic_relation(self): generic.GenericRelation(Value, object_id_field='entity_id', content_type_field='entity_ct', - related_name=rel_name) + related_query_name=rel_name) generic_relation.contribute_to_class(self.model_cls, gr_name) def _detach_generic_relation(self): From 16ec66e09d77ac76e9f91285a6dad4d478d7b863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Nov=C3=A1k?= Date: Tue, 23 Jun 2015 13:56:03 +0200 Subject: [PATCH 03/15] Fix Python 3 syntax error --- eav/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eav/models.py b/eav/models.py index 9de7ab2..1544567 100644 --- a/eav/models.py +++ b/eav/models.py @@ -491,7 +491,7 @@ def validate_attributes(self): else: try: attribute.validate_value(value) - except ValidationError, e: + except ValidationError as e: raise ValidationError(_(u"%(attr)s EAV field %(err)s") % \ {'attr': attribute.slug, 'err': e}) From 3d94eb3ec2ae41dfd63d636d8f4cfc31153b0043 Mon Sep 17 00:00:00 2001 From: JP White Date: Sat, 15 Aug 2015 23:34:11 -0400 Subject: [PATCH 04/15] Adding registration decorator and updating README with Prereqs for Sites Framework --- README.rst | 26 ++++++++++++++++++++++++-- eav/decorators.py | 19 +++++++++++++++++++ eav/tests/models.py | 10 ++++++++++ eav/tests/registry.py | 7 ++++++- 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 eav/decorators.py diff --git a/README.rst b/README.rst index c0aac2d..ec5999f 100644 --- a/README.rst +++ b/README.rst @@ -35,12 +35,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) ~~~~~~~~~~~~~~~~~~~~~~ @@ -51,7 +68,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/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/tests/models.py b/eav/tests/models.py index 0c54584..7a0f270 100644 --- a/eav/tests/models.py +++ b/eav/tests/models.py @@ -1,4 +1,5 @@ from django.db import models +from ..decorators import register_eav class Patient(models.Model): class Meta: @@ -19,3 +20,12 @@ class Meta: def __unicode__(self): return '%s: encounter num %d' % (self.patient, self.num) +@register_eav() +class ExampleModel(models.Model) + class Meta: + 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..0239f24 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) From 36bfc2e0e87a4d6de3f4cf4739373e510ae8a0f0 Mon Sep 17 00:00:00 2001 From: JP White Date: Sun, 16 Aug 2015 00:30:42 -0400 Subject: [PATCH 05/15] Adding decorator tests --- eav/tests/models.py | 2 +- eav/tests/registry.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/eav/tests/models.py b/eav/tests/models.py index 7a0f270..ae85394 100644 --- a/eav/tests/models.py +++ b/eav/tests/models.py @@ -21,7 +21,7 @@ def __unicode__(self): return '%s: encounter num %d' % (self.patient, self.num) @register_eav() -class ExampleModel(models.Model) +class ExampleModel(models.Model): class Meta: app_label = 'eav' diff --git a/eav/tests/registry.py b/eav/tests/registry.py index 0239f24..de3eb07 100644 --- a/eav/tests/registry.py +++ b/eav/tests/registry.py @@ -70,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) From 91d3c470427ea67763a1909856a82aab0be8a4a6 Mon Sep 17 00:00:00 2001 From: Marc Chakiachvili Date: Fri, 15 Jan 2016 11:16:10 +0100 Subject: [PATCH 06/15] Compatibility from django 1.9 - Issue #35 --- eav/models.py | 2 +- eav/registry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eav/models.py b/eav/models.py index 1544567..71c5c63 100644 --- a/eav/models.py +++ b/eav/models.py @@ -39,7 +39,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes import generic +from django.contrib.contenttypes import fields as generic from django.contrib.sites.models import Site from django.contrib.sites.managers import CurrentSiteManager from django.conf import settings diff --git a/eav/registry.py b/eav/registry.py index 82ed5b7..84646aa 100644 --- a/eav/registry.py +++ b/eav/registry.py @@ -29,7 +29,7 @@ from django.db.utils import DatabaseError from django.db.models.signals import pre_init, post_init, pre_save, post_save -from django.contrib.contenttypes import generic +from django.contrib.contenttypes import fields as generic from .managers import EntityManager from .models import Entity, Attribute, Value From 05866d2f9ebdd07b128763cabac228442c4862d0 Mon Sep 17 00:00:00 2001 From: Marc Chakiachvili Date: Fri, 15 Jan 2016 15:16:28 +0100 Subject: [PATCH 07/15] Make eav 'timezone' aware : - changed called from datetime.now to timezone.now - changed called from datetime(...) to timezone.datetime(...) - changed forms validator as well --- eav/models.py | 6 +++--- eav/tests/data_validation.py | 10 +++++----- eav/validators.py | 5 ++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/eav/models.py b/eav/models.py index 71c5c63..1f98453 100644 --- a/eav/models.py +++ b/eav/models.py @@ -33,8 +33,8 @@ ------- ''' -from datetime import datetime +from django.utils import timezone from django.db import models from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ @@ -198,7 +198,7 @@ def help_text(self): 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=timezone.now, editable=False) modified = models.DateTimeField(_(u"modified"), auto_now=True) @@ -354,7 +354,7 @@ class Value(models.Model): value_object = generic.GenericForeignKey(ct_field='generic_value_ct', fk_field='generic_value_id') - created = models.DateTimeField(_(u"created"), default=datetime.now) + created = models.DateTimeField(_(u"created"), default=timezone.now) modified = models.DateTimeField(_(u"modified"), auto_now=True) attribute = models.ForeignKey(Attribute, db_index=True, diff --git a/eav/tests/data_validation.py b/eav/tests/data_validation.py index 93755c8..8a1e032 100644 --- a/eav/tests/data_validation.py +++ b/eav/tests/data_validation.py @@ -1,4 +1,4 @@ -from datetime import datetime, date +from django.utils import timezone from django.test import TestCase from django.core.exceptions import ValidationError @@ -91,13 +91,13 @@ def test_date_validation(self): self.assertRaises(ValidationError, p.save) p.eav.dob = 15 self.assertRaises(ValidationError, p.save) - now = datetime.now() - now = datetime(year=now.year, month=now.month, day=now.day, - hour=now.hour, minute=now.minute, second=now.second) + now = timezone.now() + now = timezone.datetime(year=now.year, month=now.month, day=now.day, + hour=now.hour, minute=now.minute, second=now.second) p.eav.dob = now p.save() self.assertEqual(Patient.objects.get(pk=p.pk).eav.dob, now) - today = date.today() + today = timezone.today() p.eav.dob = today p.save() self.assertEqual(Patient.objects.get(pk=p.pk).eav.dob.date(), today) diff --git a/eav/validators.py b/eav/validators.py index f5d6c5a..55e0bf1 100644 --- a/eav/validators.py +++ b/eav/validators.py @@ -34,8 +34,7 @@ --------- ''' -from datetime import datetime, date - +from django.utils import timezone from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError @@ -74,7 +73,7 @@ def validate_date(value): Raises ``ValidationError`` unless *value* is an instance of ``datetime`` or ``date`` ''' - if not (isinstance(value, datetime) or isinstance(value, date)): + if not (isinstance(value, timezone.datetime) or isinstance(value, timezone.datetime.date)): raise ValidationError(_(u"Must be a date or datetime")) From 1eb6c3cd4905d5739c03b7b2fa63eb8db1ffa7c9 Mon Sep 17 00:00:00 2001 From: Marc Chakiachvili Date: Mon, 18 Jan 2016 15:03:46 +0100 Subject: [PATCH 08/15] Correct sites error when running 'makemigrations' --- eav/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eav/models.py b/eav/models.py index 1f98453..788f5a8 100644 --- a/eav/models.py +++ b/eav/models.py @@ -177,7 +177,7 @@ class Meta: help_text=_(u"User-friendly attribute name")) site = models.ForeignKey(Site, verbose_name=_(u"site"), - default=Site.objects.get_current) + default=settings.SITE_ID) slug = EavSlugField(_(u"slug"), max_length=50, db_index=True, help_text=_(u"Short unique attribute label")) From f29f4ab2c3d0bf734b44b0a2e01125bcd4156355 Mon Sep 17 00:00:00 2001 From: Hamdi Sahloul Date: Thu, 13 Oct 2016 14:59:41 +0900 Subject: [PATCH 09/15] Include migrations --- .gitignore | 1 + eav/migrations/0001_initial.py | 96 ++++++++++++++++++++++++++++++++++ eav/migrations/__init__.py | 0 3 files changed, 97 insertions(+) create mode 100644 eav/migrations/0001_initial.py create mode 100644 eav/migrations/__init__.py diff --git a/.gitignore b/.gitignore index e4f7286..99bcd19 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ *.sqlite* _build build +django_eav.egg-info/* diff --git a/eav/migrations/0001_initial.py b/eav/migrations/0001_initial.py new file mode 100644 index 0000000..bcefce4 --- /dev/null +++ b/eav/migrations/0001_initial.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-10-13 05:56 +from __future__ import unicode_literals + +import django.contrib.sites.managers +from django.db import migrations, models +import django.db.models.deletion +import django.db.models.manager +import django.utils.timezone +import eav.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('sites', '0002_alter_domain_unique'), + ] + + operations = [ + migrations.CreateModel( + name='Attribute', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='User-friendly attribute name', max_length=100, verbose_name='name')), + ('slug', eav.fields.EavSlugField(help_text='Short unique attribute label', verbose_name='slug')), + ('description', models.CharField(blank=True, help_text='Short description', max_length=256, null=True, verbose_name='description')), + ('type', models.CharField(blank=True, max_length=20, null=True, verbose_name='type')), + ('datatype', eav.fields.EavDatatypeField(choices=[(b'text', 'Text'), (b'float', 'Float'), (b'int', 'Integer'), (b'date', 'Date'), (b'bool', 'True / False'), (b'object', 'Django Object'), (b'enum', 'Multiple Choice')], max_length=6, verbose_name='data type')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), + ('required', models.BooleanField(default=False, verbose_name='required')), + ], + options={ + 'ordering': ['name'], + }, + managers=[ + ('objects', django.db.models.manager.Manager()), + ('on_site', django.contrib.sites.managers.CurrentSiteManager()), + ], + ), + migrations.CreateModel( + name='EnumGroup', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True, verbose_name='name')), + ], + ), + migrations.CreateModel( + name='EnumValue', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='value')), + ], + ), + migrations.CreateModel( + name='Value', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('entity_id', models.IntegerField()), + ('value_text', models.TextField(blank=True, null=True)), + ('value_float', models.FloatField(blank=True, null=True)), + ('value_int', models.IntegerField(blank=True, null=True)), + ('value_date', models.DateTimeField(blank=True, null=True)), + ('value_bool', models.NullBooleanField()), + ('generic_value_id', models.IntegerField(blank=True, null=True)), + ('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), + ('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eav.Attribute', verbose_name='attribute')), + ('entity_ct', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='value_entities', to='contenttypes.ContentType')), + ('generic_value_ct', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='value_values', to='contenttypes.ContentType')), + ('value_enum', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='eav_values', to='eav.EnumValue')), + ], + ), + migrations.AddField( + model_name='enumgroup', + name='enums', + field=models.ManyToManyField(to='eav.EnumValue', verbose_name='enum group'), + ), + migrations.AddField( + model_name='attribute', + name='enum_group', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='eav.EnumGroup', verbose_name='choice group'), + ), + migrations.AddField( + model_name='attribute', + name='site', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='sites.Site', verbose_name='site'), + ), + migrations.AlterUniqueTogether( + name='attribute', + unique_together=set([('site', 'slug')]), + ), + ] diff --git a/eav/migrations/__init__.py b/eav/migrations/__init__.py new file mode 100644 index 0000000..e69de29 From 464a947548ecd1ca4edf697950c090d90357aaeb Mon Sep 17 00:00:00 2001 From: Hamdi Sahloul Date: Fri, 14 Oct 2016 01:59:07 +0900 Subject: [PATCH 10/15] Support attribute per model and attributes ordering --- eav/admin.py | 6 ++-- eav/fields.py | 3 +- eav/migrations/0002_auto_20161014_0157.py | 35 +++++++++++++++++++++++ eav/models.py | 23 +++++++++------ 4 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 eav/migrations/0002_auto_20161014_0157.py diff --git a/eav/admin.py b/eav/admin.py index c7ab77d..8a0c1a4 100644 --- a/eav/admin.py +++ b/eav/admin.py @@ -1,7 +1,7 @@ #!/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 +# 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 @@ -28,7 +28,7 @@ from .models import Attribute, Value, EnumValue, EnumGroup class BaseEntityAdmin(ModelAdmin): - + def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): """ Wrapper for ModelAdmin.render_change_form. Replaces standard static @@ -94,7 +94,7 @@ def get_fieldsets(self, request, obj=None): return [(None, {'fields': form.fields.keys()})] class AttributeAdmin(ModelAdmin): - list_display = ('name', 'slug', 'datatype', 'description', 'site') + list_display = ('name', 'content_type', 'slug', 'datatype', 'description', 'site') list_filter = ['site'] prepopulated_fields = {'slug': ('name',)} diff --git a/eav/fields.py b/eav/fields.py index 94e9740..8f7c9c7 100644 --- a/eav/fields.py +++ b/eav/fields.py @@ -82,9 +82,10 @@ def validate(self, value, instance): :class:`~eav.models.Value` objects. ''' super(EavDatatypeField, self).validate(value, instance) - from .models import Attribute if not instance.pk: return + if type(instance).objects.get(pk=instance.pk).datatype == instance.datatype: + return if instance.value_set.count(): raise ValidationError(_(u"You cannot change the datatype of an " u"attribute that is already in use.")) diff --git a/eav/migrations/0002_auto_20161014_0157.py b/eav/migrations/0002_auto_20161014_0157.py new file mode 100644 index 0000000..70c3bd9 --- /dev/null +++ b/eav/migrations/0002_auto_20161014_0157.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-10-13 16:57 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('eav', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='attribute', + options={'ordering': ['content_type', 'name']}, + ), + migrations.AddField( + model_name='attribute', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='content type'), + ), + migrations.AddField( + model_name='attribute', + name='display_order', + field=models.PositiveIntegerField(default=1, verbose_name='display order'), + ), + migrations.AlterUniqueTogether( + name='attribute', + unique_together=set([('site', 'content_type', 'slug')]), + ), + ] diff --git a/eav/models.py b/eav/models.py index 788f5a8..2c6aee0 100644 --- a/eav/models.py +++ b/eav/models.py @@ -152,8 +152,8 @@ class Attribute(models.Model): ''' class Meta: - ordering = ['name'] - unique_together = ('site', 'slug') + ordering = ['content_type', 'name'] + unique_together = ('site', 'content_type', 'slug') TYPE_TEXT = 'text' TYPE_FLOAT = 'float' @@ -176,6 +176,10 @@ class Meta: name = models.CharField(_(u"name"), max_length=100, help_text=_(u"User-friendly attribute name")) + content_type = models.ForeignKey(ContentType, + blank=True, null=True, + verbose_name=_(u"content type")) + site = models.ForeignKey(Site, verbose_name=_(u"site"), default=settings.SITE_ID) @@ -205,6 +209,8 @@ def help_text(self): required = models.BooleanField(_(u"required"), default=False) + display_order = models.PositiveIntegerField(_(u"display order"), default=1) + objects = models.Manager() on_site = CurrentSiteManager() @@ -312,7 +318,7 @@ def save_value(self, entity, value): value_obj.save() def __unicode__(self): - return u"%s (%s)" % (self.name, self.get_datatype_display()) + return u"%s.%s (%s)" % (self.content_type, self.name, self.get_datatype_display()) class Value(models.Model): @@ -442,18 +448,19 @@ def get_all_attributes(self): Return a query set of all :class:`Attribute` objects that can be set for this entity. ''' - return self.model._eav_config_cls.get_attributes() + return self.model._eav_config_cls.get_attributes().filter( + models.Q(content_type__isnull=True) | models.Q(content_type=self.ct)).order_by('display_order') def _hasattr(self, attribute_slug): ''' - Since we override __getattr__ with a backdown to the database, this exists as a way of + Since we override __getattr__ with a backdown to the database, this exists as a way of checking whether a user has set a real attribute on ourselves, without going to the db if not ''' return attribute_slug in self.__dict__ def _getattr(self, attribute_slug): ''' - Since we override __getattr__ with a backdown to the database, this exists as a way of + Since we override __getattr__ with a backdown to the database, this exists as a way of getting the value a user set for one of our attributes, without going to the db to check ''' return self.__dict__[attribute_slug] @@ -482,7 +489,7 @@ def validate_attributes(self): value = self._getattr(attribute.slug) else: value = values_dict.get(attribute.slug, None) - + if value is None: if attribute.required: raise ValidationError(_(u"%(attr)s EAV field cannot " \ @@ -495,7 +502,7 @@ def validate_attributes(self): raise ValidationError(_(u"%(attr)s EAV field %(err)s") % \ {'attr': attribute.slug, 'err': e}) - + def get_values_dict(self): values_dict = dict() for value in self.get_values(): From 0495d4391e4e9f60af3a41911c10323b88fd64c7 Mon Sep 17 00:00:00 2001 From: Hamdi Sahloul Date: Sat, 15 Oct 2016 22:59:24 +0900 Subject: [PATCH 11/15] Removed deprecated get_field_by_name() function call --- eav/managers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eav/managers.py b/eav/managers.py index 315c28c..8471cef 100644 --- a/eav/managers.py +++ b/eav/managers.py @@ -110,11 +110,11 @@ def expand_eav_filter(model_cls, key, value): return '%s__in' % gr_name, value try: - field, m, direct, m2m = model_cls._meta.get_field_by_name(fields[0]) + field = model_cls._meta.get_field(fields[0]) except models.FieldDoesNotExist: return key, value - if direct: + if not field.auto_created or field.concrete: return key, value else: sub_key = '__'.join(fields[1:]) From 5da8007d8bab52791943af7da617a74b695451a3 Mon Sep 17 00:00:00 2001 From: Hamdi Sahloul Date: Thu, 3 Nov 2016 05:11:44 +0900 Subject: [PATCH 12/15] Backwards compatibility --- eav/migrations/0001_initial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eav/migrations/0001_initial.py b/eav/migrations/0001_initial.py index bcefce4..89d6ba3 100644 --- a/eav/migrations/0001_initial.py +++ b/eav/migrations/0001_initial.py @@ -16,7 +16,7 @@ class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0002_remove_content_type_name'), - ('sites', '0002_alter_domain_unique'), + ('sites', '0001_initial'), ] operations = [ From f582eacadae7bbe4b86c62e6ed8ca7a0b3c3b155 Mon Sep 17 00:00:00 2001 From: zatarus Date: Tue, 17 Jan 2017 16:17:07 +0300 Subject: [PATCH 13/15] Initial value fix for number fields in entity form --- eav/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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): From 2f055a98b53695c83789fc2dbddf51d40049af38 Mon Sep 17 00:00:00 2001 From: Ian L Date: Fri, 30 Mar 2018 11:52:14 +0300 Subject: [PATCH 14/15] Improve Py3 support (#48) Improve support for Python 3 --- eav/__init__.py | 4 ++-- eav/admin.py | 4 ++-- eav/managers.py | 8 +++++--- eav/models.py | 3 ++- eav/registry.py | 1 + eav/tests/misc_models.py | 3 ++- eav/tests/models.py | 7 ++++--- eav/validators.py | 2 +- setup.py | 2 +- 9 files changed, 20 insertions(+), 14 deletions(-) diff --git a/eav/__init__.py b/eav/__init__.py index 73a21be..e4cc249 100644 --- a/eav/__init__.py +++ b/eav/__init__.py @@ -27,9 +27,9 @@ def get_version(): __version__ = get_version() def register(model_cls, config_cls=None): - from registry import Registry + from .registry import Registry Registry.register(model_cls, config_cls) def unregister(model_cls): - from registry import Registry + from .registry import Registry Registry.unregister(model_cls) diff --git a/eav/admin.py b/eav/admin.py index 8a0c1a4..70c3ad7 100644 --- a/eav/admin.py +++ b/eav/admin.py @@ -41,7 +41,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) @@ -91,7 +91,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): list_display = ('name', 'content_type', 'slug', 'datatype', 'description', 'site') 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 2c6aee0..7220723 100644 --- a/eav/models.py +++ b/eav/models.py @@ -32,6 +32,7 @@ Classes ------- ''' +from builtins import object from django.utils import timezone @@ -151,7 +152,7 @@ class Attribute(models.Model): change it's datatype. ''' - class Meta: + class Meta(object): ordering = ['content_type', 'name'] unique_together = ('site', 'content_type', 'slug') diff --git a/eav/registry.py b/eav/registry.py index 84646aa..340e310 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 ae85394..135777c 100644 --- a/eav/tests/models.py +++ b/eav/tests/models.py @@ -1,8 +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) @@ -11,7 +12,7 @@ def __unicode__(self): return self.name class Encounter(models.Model): - class Meta: + class Meta(object): app_label = 'eav' num = models.PositiveSmallIntegerField() @@ -22,7 +23,7 @@ def __unicode__(self): @register_eav() class ExampleModel(models.Model): - class Meta: + class Meta(object): app_label = 'eav' name = models.CharField(max_length=12) diff --git a/eav/validators.py b/eav/validators.py index 55e0bf1..26f1cda 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) == unicode or type(value) == str): + if not (type(value) == str or type(value) == str): raise ValidationError(_(u"Must be str or unicode")) 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' From 522ee3fd64242f8e0b0ace33dbd99dade3f4cd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Friedrich?= Date: Tue, 3 Apr 2018 15:16:15 +0200 Subject: [PATCH 15/15] Update models.py for python3 according to https://docs.djangoproject.com/en/1.11/topics/python3/ I added compatibility for python3 --- eav/models.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/eav/models.py b/eav/models.py index 7220723..b5ea562 100644 --- a/eav/models.py +++ b/eav/models.py @@ -32,6 +32,7 @@ Classes ------- ''' +from __future__ import unicode_literals from builtins import object @@ -44,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,9 +83,10 @@ 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 - + class EnumGroup(models.Model): ''' @@ -98,7 +101,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 @@ -318,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()) @@ -401,7 +406,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)