Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

restrict scope of attributes #15

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 94 additions & 77 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,89 +5,106 @@ 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
<http://en.wikipedia.org/wiki/Entity-attribute-value_model>`_.

.. note::
This software was inspired / derived from the excellent `eav-django
<http://pypi.python.org/pypi/eav-django/1.0.2>`_ 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
------------

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
--------------------------

`<http://mvpdev.github.com/django-eav>`_
23 changes: 20 additions & 3 deletions eav/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 27 additions & 5 deletions eav/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 _
Expand All @@ -49,7 +50,8 @@ class BaseDynamicEntityForm(ModelForm):
'text': CharField,
'float': FloatField,
'int': IntegerField,
'date': DateTimeField,
'date': DateField,
'datetime': DateTimeField,
'bool': BooleanField,
'enum': ChoiceField,
}
Expand All @@ -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)

Expand All @@ -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
Expand Down
41 changes: 34 additions & 7 deletions eav/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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'
Expand All @@ -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")),
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -334,6 +349,9 @@ class Value(models.Model):
<Value: crazy_dev_user - Favorite Drink: "red bull">
'''

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',
Expand All @@ -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')
Expand Down Expand Up @@ -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.<whatever>

Checks if *name* is a valid slug for attributes available to this
Expand All @@ -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):
'''
Expand Down
Loading