diff --git a/README.rst b/README.rst index c0aac2d..52ca12f 100644 --- a/README.rst +++ b/README.rst @@ -5,25 +5,98 @@ django-eav Introduction ------------ -django-eav provides an Entity-Attribute-Value storage model for django apps. - -For a decent explanation of what an Entity-Attribute-Value storage model is, -check `Wikipedia -`_. - -.. note:: - This software was inspired / derived from the excellent `eav-django - `_ written by Andrey - Mikhaylenko. - - There are a few notable differences between this implementation and the - eav-django implementation. - - * This one is called django-eav, whereas the other is called eav-django. - * This app allows you to to 'attach' EAV attributes to any existing django - model (even from third-party apps) without making any changes to the those - models' code. - * This app has slightly more robust (but still not perfect) filtering. +This is a fork from the repository: https://github.com/mvpdev/django-eav . +Please read the README their to understand the purpose of this repository. + +This README only explains the feature added in this fork: + + +Feature added in this fork: +--------------------------- + +This fork adds a feature to restrict the scope of attributes. Django-eav only +provides the possibility to enable attributes for selected models using advanced +registration: :: + + class MyEavConfigClass(EavConfig): + @classmethod + def get_attributes(cls): + return Attribute.objects.filter(type='person') + + eav.register(MyModel, MyEavConfigClass) + +The feature added with this fork enhances the possibility to restrict the scope +of attributes to arbitrary models referenced by the registered model. + +The registered model has to implement one additional attribute and the configuration +class has to consider this attribute in the get_attributes method. See example source +code below: + +A use case for this feature could be: + +A generic motorbike retail backend which implements a motorbike model having a +ForeignKey to a manufacturer. The backend allows to add custom fields to the motorbike +model via django-eav. Additionally the backend allows the user to add custom +attributes which are not only restricted to the model motorbike, but also restricted +to the motorbike manufacturer, allowing the user to add manufacturer specific fields +to the motorbike model. + + +Source code for use case: + +Model Definition: :: + + class Motorbike(models.Model): + """ + Representation of a motorbike + """ + name = models.CharField(max_length=100) + manufacturer = models.ForeignKey(Manufacturer) + + 'add eav restrictions - enables eav to restrict attributes to defined foreignkeys' + eav_restriction_object = models.ForeignKey(Manufacturer, blank=True, editable=False, + related_name='eav_attribute_restrictions') + + def save(self, *args, **kwargs): + self.eav_restriction_object=self.manufacturer + super(Motorbike, self).save(*args, **kwargs) + +Config Class Definition: :: + + class EavTaskConfigClass(EavConfig): + """ + configClass to restrict the created attributes to motorbikes assigned to a speific + maufacturer + """ + + @classmethod + def get_attributes(cls, eavRestriction=None): + """ + return the attributes and consider the provided eavRestriction + the eavRestriction is a ForeignKey to an arbitrary model instance + """ + if eavRestriction is not None: + #return only attributes which have their restriction_object set to the provided + #eavRestriction + eavRestrictionType = ContentType.objects.get_for_model(eavRestriction) + return Attribute.objects.filter(type='Motorbike', + generic_restriction_id=eavRestriction.id, + generic_restriction_ct__pk=eavRestrictionType.id) + else: + return Attribute.objects.filter(type='Motorbike') + + eav.register(Motorbike, EavTaskConfigClass) + +Attribute Filtering: + +Since the eav internal restriction_object is implemented as a GenericForeignKey to support arbitrary +eav_restriction_object in a registered model, filtering is slightly more complex and has to be done +using the ContentType model. :: + + >>> from django.contrib.contenttypes.models import ContentType + >>> manufacturerCT = ContentType.objects.get_for_model(manufacturerInstance) + >>> Attribute.objects.filter(type="motorbike", generic_restriction_id=manfacturerInstance.id, + generic_restriction_ct__pk=manufacturerCT.id) Installation @@ -31,63 +104,7 @@ Installation From Github ~~~~~~~~~~~ -You can install django-eav directly from guthub:: +You can install this django-eav fork directly from guthub:: - pip install -e git+git://github.com/mvpdev/django-eav.git#egg=django-eav + pip install -e git+git://github.com/mr-stateradio/django-eav.git#egg=django-eav -Usage ------ - -Edit settings.py -~~~~~~~~~~~~~~~~ -Add ``eav`` to your ``INSTALLED_APPS`` in your project's ``settings.py`` file. - -Register your model(s) -~~~~~~~~~~~~~~~~~~~~~~ -Before you can attach eav attributes to your model, you must register your -model with eav:: - - >>> import eav - >>> eav.register(MyModel) - -Generally you would do this in your ``models.py`` immediate after your model -declarations. - -Create some attributes -~~~~~~~~~~~~~~~~~~~~~~ -:: - - >>> from eav.models import Attribute - >>> Attribute.objects.create(name='Weight', datatype=Attribute.TYPE_FLOAT) - >>> Attribute.objects.create(name='Color', datatype=Attribute.TYPE_TEXT) - - -Assign eav values -~~~~~~~~~~~~~~~~~ -:: - - >>> m = MyModel() - >>> m.eav.weight = 15.4 - >>> m.eav.color = 'blue' - >>> m.save() - >>> m = MyModel.objects.get(pk=m.pk) - >>> m.eav.weight - 15.4 - >>> m.eav.color - blue - - >>> p = MyModel.objects.create(eav__weight = 12, eav__color='red') - -Filter on eav values -~~~~~~~~~~~~~~~~~~~~ -:: - - >>> MyModel.objects.filter(eav__weight=15.4) - - >>> MyModel.objects.exclude(name='bob', eav__weight=15.4, eav__color='red') - - -Documentation and Examples --------------------------- - -``_ diff --git a/eav/fields.py b/eav/fields.py index 94e9740..b17d7cb 100644 --- a/eav/fields.py +++ b/eav/fields.py @@ -85,6 +85,23 @@ def validate(self, value, instance): from .models import Attribute if not instance.pk: return - if instance.value_set.count(): - raise ValidationError(_(u"You cannot change the datatype of an " - u"attribute that is already in use.")) + else: + #raise validation error if save command tries to change fields which are not subject to change + old_instance = Attribute.objects.get(pk=instance.pk) + if old_instance.datatype != instance.datatype: + raise ValidationError(_(u"You cannot change the datatype of an" + u"attribute.")) + elif old_instance.datatype != instance.datatype: + raise ValidationError(_(u"You cannot change the type of an" + u"attribute.")) + elif old_instance.generic_restriction_id != instance.generic_restriction_id: + raise ValidationError(_(u"You cannot change the generic" + u"restriction of an attribute.")) + elif old_instance.generic_restriction_ct != instance.generic_restriction_ct: + raise ValidationError(_(u"You cannot change the generic restriction content type of an attribute.")) + elif old_instance.slug != instance.slug: + raise ValidationError(_(u"You cannot change the slug of an attribute.")) + elif old_instance.name != instance.name: + raise ValidationError(_(u"You cannot change the name of an attribute.")) + else: + return diff --git a/eav/forms.py b/eav/forms.py index 26e5785..cb4dbc7 100644 --- a/eav/forms.py +++ b/eav/forms.py @@ -28,7 +28,8 @@ ''' from copy import deepcopy -from django.forms import BooleanField, CharField, DateTimeField, FloatField, \ +from django.conf import settings +from django.forms import BooleanField, CharField, DateField, DateTimeField, FloatField, \ IntegerField, ModelForm, ChoiceField, ValidationError from django.contrib.admin.widgets import AdminSplitDateTime from django.utils.translation import ugettext_lazy as _ @@ -49,7 +50,8 @@ class BaseDynamicEntityForm(ModelForm): 'text': CharField, 'float': FloatField, 'int': IntegerField, - 'date': DateTimeField, + 'date': DateField, + 'datetime': DateTimeField, 'bool': BooleanField, 'enum': ChoiceField, } @@ -59,11 +61,32 @@ def __init__(self, data=None, *args, **kwargs): config_cls = self.instance._eav_config_cls self.entity = getattr(self.instance, config_cls.eav_attr) self._build_dynamic_fields() + use_l10n = getattr(settings, 'USE_L10N') + if use_l10n == True: + self.localize_fields() + + def localize_fields(self): + """ + set localization to True for all fields + """ + for name, field in self.fields.items(): + widget_type = field.widget.__class__.__name__ + if widget_type != "DateInput" and widget_type != "DateTimeInput" and widget_type != "TimeInput": #do not localize date / time input widgets + field.localize = True + field.widget.is_localized = True + else: + field.localize = False + field.widget.is_localized = False + if widget_type == "DateInput": + field.widget.format=settings.DATE_FORMAT + if widget_type == "DateTimeInput": + field.widget.format=settings.DATETIME_FORMAT + if widget_type == "TimeInput": + field.widget.format=settings.TIME_FORMAT def _build_dynamic_fields(self): # reset form fields self.fields = deepcopy(self.base_fields) - for attribute in self.entity.get_all_attributes(): value = getattr(self.entity, attribute.slug) @@ -84,8 +107,7 @@ def _build_dynamic_fields(self): defaults.update({'choices': choices}) if value: defaults.update({'initial': value.pk}) - - elif datatype == attribute.TYPE_DATE: + elif datatype == attribute.TYPE_DATE_TIME: defaults.update({'widget': AdminSplitDateTime}) elif datatype == attribute.TYPE_OBJECT: continue diff --git a/eav/models.py b/eav/models.py index 049607b..a400264 100644 --- a/eav/models.py +++ b/eav/models.py @@ -118,12 +118,16 @@ class Attribute(models.Model): to save or create any entity object for which this attribute applies, without first setting this EAV attribute. - There are 7 possible values for datatype: + The generic restriction_object field enables the user to restrict the scope + of the attribute to an arbitrary model, referenced by ID and Content Type + + There are 8 possible values for datatype: * int (TYPE_INT) * float (TYPE_FLOAT) * text (TYPE_TEXT) * date (TYPE_DATE) + * datetime (TYPE_DATE_TIME) * bool (TYPE_BOOLEAN) * object (TYPE_OBJECT) * enum (TYPE_ENUM) @@ -152,12 +156,13 @@ class Attribute(models.Model): class Meta: ordering = ['name'] - unique_together = ('site', 'slug') + unique_together = ('generic_restriction_id', 'slug') TYPE_TEXT = 'text' TYPE_FLOAT = 'float' TYPE_INT = 'int' TYPE_DATE = 'date' + TYPE_DATE_TIME = 'datetime' TYPE_BOOLEAN = 'bool' TYPE_OBJECT = 'object' TYPE_ENUM = 'enum' @@ -167,6 +172,7 @@ class Meta: (TYPE_FLOAT, _(u"Float")), (TYPE_INT, _(u"Integer")), (TYPE_DATE, _(u"Date")), + (TYPE_DATE_TIME, _(u"Date and Time")), (TYPE_BOOLEAN, _(u"True / False")), (TYPE_OBJECT, _(u"Django Object")), (TYPE_ENUM, _(u"Multiple Choice")), @@ -190,11 +196,19 @@ class Meta: type = models.CharField(_(u"type"), max_length=20, blank=True, null=True) + generic_restriction_id = models.IntegerField(blank=True, null=True) + generic_restriction_ct = models.ForeignKey(ContentType, blank=True, + null=True, + related_name='attribute_restrictions') + restriction_object = generic.GenericForeignKey( + ct_field='generic_restriction_ct', + fk_field='generic_restriction_id') + @property def help_text(self): return self.description - datatype = EavDatatypeField(_(u"data type"), max_length=6, + datatype = EavDatatypeField(_(u"data type"), max_length=8, choices=DATATYPE_CHOICES) created = models.DateTimeField(_(u"created"), default=datetime.now, @@ -222,6 +236,7 @@ def get_validators(self): 'float': validate_float, 'int': validate_int, 'date': validate_date, + 'datetime': validate_date, 'bool': validate_bool, 'object': validate_object, 'enum': validate_enum, @@ -334,6 +349,9 @@ class Value(models.Model): ''' + class Meta: + unique_together = ('entity_ct', 'entity_id', 'attribute') + entity_ct = models.ForeignKey(ContentType, related_name='value_entities') entity_id = models.IntegerField() entity = generic.GenericForeignKey(ct_field='entity_ct', @@ -342,7 +360,8 @@ class Value(models.Model): 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_date = models.DateField(blank=True, null=True) + value_datetime = models.DateTimeField(blank=True, null=True) value_bool = models.NullBooleanField(blank=True, null=True) value_enum = models.ForeignKey(EnumValue, blank=True, null=True, related_name='eav_values') @@ -407,14 +426,19 @@ class Entity(object): def __init__(self, instance): ''' Set self.model equal to the instance of the model that we're attached - to. Also, store the content type of that instance. + to. Also, store the content type of that instance and the restriction + attribute of the model if it has been restricted. ''' self.model = instance self.ct = ContentType.objects.get_for_model(instance) + if hasattr(instance, 'eav_restriction_object'): + self.restriction = instance.eav_restriction_object + else: + self.restriction = None def __getattr__(self, name): ''' - Tha magic getattr helper. This is called whenevery you do + Tha magic getattr helper. This is called whenever you do this_instance. Checks if *name* is a valid slug for attributes available to this @@ -441,7 +465,10 @@ 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() + if self.restriction is not None: + return self.model._eav_config_cls.get_attributes(self.restriction) + else: + return self.model._eav_config_cls.get_attributes() def save(self): ''' diff --git a/setup.py b/setup.py index b4727b7..e6e56fe 100755 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ 'Django app.', long_description=open('README.rst').read(), - url='http://github.com/mvpdev/django-eav', + url='http://github.com/mr-stateradio/django-eav', packages=['eav', 'eav.tests'],