mirror of
https://github.com/jazzband/django-eav2.git
synced 2026-03-16 22:40:26 +00:00
Move remaining docstrings to Google-stype format (#10)
This commit is contained in:
parent
e0a76f86b2
commit
99254cd578
11 changed files with 232 additions and 251 deletions
|
|
@ -2,4 +2,3 @@
|
|||
omit =
|
||||
*/migrations/*
|
||||
eav/__init__.py
|
||||
eav/utils.py
|
||||
|
|
|
|||
36
eav/admin.py
36
eav/admin.py
|
|
@ -1,4 +1,4 @@
|
|||
'''This module contains classes used for admin integration.'''
|
||||
"""This module contains classes used for admin integration."""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.options import InlineModelAdmin, ModelAdmin
|
||||
|
|
@ -11,12 +11,12 @@ from .models import Attribute, EnumGroup, EnumValue, Value
|
|||
class BaseEntityAdmin(ModelAdmin):
|
||||
def render_change_form(self, request, context, *args, **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.
|
||||
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
|
||||
|
||||
|
|
@ -33,9 +33,9 @@ class BaseEntityAdmin(ModelAdmin):
|
|||
|
||||
|
||||
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)
|
||||
|
|
@ -45,20 +45,20 @@ class BaseEntityInlineFormSet(BaseInlineFormSet):
|
|||
|
||||
|
||||
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.::
|
||||
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).
|
||||
'''
|
||||
.. 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):
|
||||
|
|
@ -80,6 +80,6 @@ class AttributeAdmin(ModelAdmin):
|
|||
|
||||
|
||||
admin.site.register(Attribute, AttributeAdmin)
|
||||
admin.site.register(Value)
|
||||
admin.site.register(EnumValue)
|
||||
admin.site.register(EnumGroup)
|
||||
admin.site.register(Value)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
'''
|
||||
"""
|
||||
This module contains pure wrapper functions used as decorators.
|
||||
Functions in this module should be simple and not involve complex logic.
|
||||
'''
|
||||
"""
|
||||
|
||||
def register_eav(**kwargs):
|
||||
'''
|
||||
"""
|
||||
Registers the given model(s) classes and wrapped ``Model`` class with
|
||||
Django EAV 2::
|
||||
|
||||
@register_eav
|
||||
class Author(models.Model):
|
||||
pass
|
||||
'''
|
||||
"""
|
||||
from . import register
|
||||
from django.db.models import Model
|
||||
|
||||
|
|
|
|||
|
|
@ -6,54 +6,58 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
|
||||
class EavSlugField(models.SlugField):
|
||||
'''
|
||||
"""
|
||||
The slug field used by :class:`~eav.models.Attribute`
|
||||
'''
|
||||
"""
|
||||
|
||||
def validate(self, value, instance):
|
||||
'''
|
||||
"""
|
||||
Slugs are used to convert the Python attribute name to a database
|
||||
lookup and vice versa. We need it to be a valid Python identifier.
|
||||
We don't want it to start with a '_', underscore will be used
|
||||
var variables we don't want to be saved in the database.
|
||||
'''
|
||||
lookup and vice versa. We need it to be a valid Python identifier. We
|
||||
don't want it to start with a '_', underscore will be used in
|
||||
variables we don't want to be saved in the database.
|
||||
"""
|
||||
super(EavSlugField, self).validate(value, instance)
|
||||
slug_regex = r'[a-z][a-z0-9_]*'
|
||||
|
||||
if not re.match(slug_regex, value):
|
||||
raise ValidationError(_(u"Must be all lower case, " \
|
||||
u"start with a letter, and contain " \
|
||||
u"only letters, numbers, or underscores."))
|
||||
raise ValidationError(_(
|
||||
'Must be all lower case, start with a letter, and contain '
|
||||
'only letters, numbers, or underscores.'
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def create_slug_from_name(name):
|
||||
'''
|
||||
Creates a slug based on the name
|
||||
'''
|
||||
"""Creates a slug based on the name."""
|
||||
name = name.strip().lower()
|
||||
|
||||
# Change spaces to underscores
|
||||
# Change spaces to underscores.
|
||||
name = '_'.join(name.split())
|
||||
|
||||
# Remove non alphanumeric characters
|
||||
# Remove non alphanumeric characters.
|
||||
return re.sub('[^\w]', '', name)
|
||||
|
||||
|
||||
class EavDatatypeField(models.CharField):
|
||||
'''
|
||||
The datatype field used by :class:`~eav.models.Attribute`
|
||||
'''
|
||||
"""
|
||||
The datatype field used by :class:`~eav.models.Attribute`.
|
||||
"""
|
||||
|
||||
def validate(self, value, instance):
|
||||
'''
|
||||
"""
|
||||
Raise ``ValidationError`` if they try to change the datatype of an
|
||||
:class:`~eav.models.Attribute` that is already used by
|
||||
:class:`~eav.models.Value` objects.
|
||||
'''
|
||||
"""
|
||||
super(EavDatatypeField, self).validate(value, instance)
|
||||
|
||||
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."))
|
||||
raise ValidationError(_(
|
||||
'You cannot change the datatype of an attribute that is already in use.'
|
||||
))
|
||||
|
|
|
|||
31
eav/forms.py
31
eav/forms.py
|
|
@ -1,4 +1,4 @@
|
|||
'''This module contains forms used for admin integration.'''
|
||||
"""This module contains forms used for admin integration."""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
|
||||
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),
|
||||
|
|
@ -29,7 +29,7 @@ class BaseDynamicEntityForm(ModelForm):
|
|||
bool BooleanField
|
||||
enum ChoiceField
|
||||
===== =============
|
||||
'''
|
||||
"""
|
||||
FIELD_CLASSES = {
|
||||
'text': CharField,
|
||||
'float': FloatField,
|
||||
|
|
@ -46,7 +46,7 @@ class BaseDynamicEntityForm(ModelForm):
|
|||
self._build_dynamic_fields()
|
||||
|
||||
def _build_dynamic_fields(self):
|
||||
# reset form fields
|
||||
# Reset form fields.
|
||||
self.fields = deepcopy(self.base_fields)
|
||||
|
||||
for attribute in self.entity.get_all_attributes():
|
||||
|
|
@ -60,13 +60,12 @@ class BaseDynamicEntityForm(ModelForm):
|
|||
}
|
||||
|
||||
datatype = attribute.datatype
|
||||
|
||||
if datatype == attribute.TYPE_ENUM:
|
||||
values = attribute.get_choices() \
|
||||
.values_list('id', 'value')
|
||||
|
||||
values = attribute.get_choices().values_list('id', 'value')
|
||||
choices = [('', '-----')] + list(values)
|
||||
|
||||
defaults.update({'choices': choices})
|
||||
|
||||
if value:
|
||||
defaults.update({'initial': value.pk})
|
||||
|
||||
|
|
@ -78,21 +77,20 @@ class BaseDynamicEntityForm(ModelForm):
|
|||
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: #enum done above
|
||||
# Fill initial data (if attribute was already defined).
|
||||
if value and not datatype == attribute.TYPE_ENUM:
|
||||
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``.
|
||||
``self.instance`` and related EAV attributes. Returns ``instance``.
|
||||
"""
|
||||
if self.errors:
|
||||
raise ValueError(_(u"The %s could not be saved because the data"
|
||||
u"didn't validate.") % \
|
||||
self.instance._meta.object_name)
|
||||
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)
|
||||
|
|
@ -100,6 +98,7 @@ class BaseDynamicEntityForm(ModelForm):
|
|||
# Assign attributes.
|
||||
for attribute in self.entity.get_all_attributes():
|
||||
value = self.cleaned_data.get(attribute.slug)
|
||||
|
||||
if attribute.datatype == attribute.TYPE_ENUM:
|
||||
if value:
|
||||
value = attribute.enum_group.values.get(pk=value)
|
||||
|
|
|
|||
|
|
@ -1,34 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
"""
|
||||
This module contains the custom manager used by entities registered with eav.
|
||||
'''
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
from .queryset import EavQuerySet
|
||||
|
||||
|
||||
class EntityManager(models.Manager):
|
||||
'''
|
||||
"""
|
||||
Our custom manager, overrides ``models.Manager``.
|
||||
'''
|
||||
"""
|
||||
_queryset_class = EavQuerySet
|
||||
|
||||
def create(self, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Parse eav attributes out of *kwargs*, then try to create and save
|
||||
the object, then assign and save it's eav attributes.
|
||||
'''
|
||||
"""
|
||||
config_cls = getattr(self.model, '_eav_config_cls', None)
|
||||
|
||||
if not config_cls or config_cls.manager_only:
|
||||
return super(EntityManager, self).create(**kwargs)
|
||||
|
||||
#attributes = config_cls.get_attributes()
|
||||
prefix = '%s__' % config_cls.eav_attr
|
||||
|
||||
new_kwargs = {}
|
||||
eav_kwargs = {}
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key.startswith(prefix):
|
||||
eav_kwargs.update({key[len(prefix):]: value})
|
||||
|
|
@ -37,15 +35,17 @@ class EntityManager(models.Manager):
|
|||
|
||||
obj = self.model(**new_kwargs)
|
||||
obj_eav = getattr(obj, config_cls.eav_attr)
|
||||
|
||||
for key, value in eav_kwargs.items():
|
||||
setattr(obj_eav, key, value)
|
||||
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
def get_or_create(self, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Reproduces the behavior of get_or_create, eav friendly.
|
||||
'''
|
||||
"""
|
||||
try:
|
||||
return self.get(**kwargs), False
|
||||
except self.model.DoesNotExist:
|
||||
|
|
|
|||
161
eav/models.py
161
eav/models.py
|
|
@ -1,4 +1,4 @@
|
|||
'''
|
||||
"""
|
||||
This module defines the four concrete, non-abstract models:
|
||||
* :class:`Value`
|
||||
* :class:`Attribute`
|
||||
|
|
@ -6,7 +6,8 @@ This module defines the four concrete, non-abstract models:
|
|||
* :class:`EnumGroup`
|
||||
|
||||
Along with the :class:`Entity` helper class.
|
||||
'''
|
||||
"""
|
||||
|
||||
from copy import copy
|
||||
|
||||
from django.contrib.contenttypes import fields as generic
|
||||
|
|
@ -22,11 +23,11 @@ from .validators import *
|
|||
|
||||
|
||||
class EnumValue(models.Model):
|
||||
'''
|
||||
*EnumValue* objects are the value 'choices' to multiple choice
|
||||
*TYPE_ENUM* :class:`Attribute` objects.
|
||||
"""
|
||||
*EnumValue* objects are the value 'choices' to multiple choice *TYPE_ENUM*
|
||||
:class:`Attribute` objects. They have only one field, *value*, a
|
||||
``CharField`` that must be unique.
|
||||
|
||||
They have only one field, *value*, a ``CharField`` that must be unique.
|
||||
For example::
|
||||
|
||||
yes = EnumValue.objects.create(value='Yes') # doctest: SKIP
|
||||
|
|
@ -36,17 +37,17 @@ class EnumValue(models.Model):
|
|||
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
|
||||
ynu.values.add(yes, no, unknown)
|
||||
|
||||
Attribute.objects.create(name='has fever?', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
|
||||
Attribute.objects.create(name='has fever?',
|
||||
datatype=Attribute.TYPE_ENUM, enum_group=ynu)
|
||||
# = <Attribute: has fever? (Multiple Choice)>
|
||||
|
||||
.. note::
|
||||
The same *EnumValue* objects should be reused within multiple
|
||||
*EnumGroups*. For example, if you have one *EnumGroup*
|
||||
called: *Yes / No / Unknown* and another called *Yes / No /
|
||||
Not applicable*, you should only have a total of four *EnumValues*
|
||||
objects, as you should have used the same *Yes* and *No* *EnumValues*
|
||||
for both *EnumGroups*.
|
||||
'''
|
||||
*EnumGroups*. For example, if you have one *EnumGroup* called: *Yes /
|
||||
No / Unknown* and another called *Yes / No / Not applicable*, you should
|
||||
only have a total of four *EnumValues* objects, as you should have used
|
||||
the same *Yes* and *No* *EnumValues* for both *EnumGroups*.
|
||||
"""
|
||||
value = models.CharField(_('value'), db_index=True, unique=True, max_length=50)
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -54,13 +55,13 @@ class EnumValue(models.Model):
|
|||
|
||||
|
||||
class EnumGroup(models.Model):
|
||||
'''
|
||||
"""
|
||||
*EnumGroup* objects have two fields - a *name* ``CharField`` and *values*,
|
||||
a ``ManyToManyField`` to :class:`EnumValue`. :class:`Attribute` classes
|
||||
with datatype *TYPE_ENUM* have a ``ForeignKey`` field to *EnumGroup*.
|
||||
|
||||
See :class:`EnumValue` for an example.
|
||||
'''
|
||||
"""
|
||||
name = models.CharField(_('name'), unique = True, max_length = 100)
|
||||
values = models.ManyToManyField(EnumValue, verbose_name = _('enum group'))
|
||||
|
||||
|
|
@ -69,10 +70,10 @@ class EnumGroup(models.Model):
|
|||
|
||||
|
||||
class Attribute(models.Model):
|
||||
'''
|
||||
"""
|
||||
Putting the **A** in *EAV*. This holds the attributes, or concepts.
|
||||
Examples of possible *Attributes*: color, height, weight,
|
||||
number of children, number of patients, has fever?, etc...
|
||||
Examples of possible *Attributes*: color, height, weight, number of
|
||||
children, number of patients, has fever?, etc...
|
||||
|
||||
Each attribute has a name, and a description, along with a slug that must
|
||||
be unique. If you don't provide a slug, a default slug (derived from
|
||||
|
|
@ -115,7 +116,7 @@ class Attribute(models.Model):
|
|||
|
||||
.. warning:: Once an Attribute has been used by an entity, you can not
|
||||
change it's datatype.
|
||||
'''
|
||||
"""
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
|
|
@ -151,11 +152,11 @@ class Attribute(models.Model):
|
|||
help_text = _('User-friendly attribute name')
|
||||
)
|
||||
|
||||
'''
|
||||
"""
|
||||
Main identifer for the attribute.
|
||||
Upon creation, slug is autogenerated from the name.
|
||||
(see :meth:`~eav.fields.EavSlugField.create_slug_from_name`).
|
||||
'''
|
||||
"""
|
||||
slug = EavSlugField(
|
||||
verbose_name = _('Slug'),
|
||||
max_length = 50,
|
||||
|
|
@ -164,12 +165,12 @@ class Attribute(models.Model):
|
|||
help_text = _('Short unique attribute label')
|
||||
)
|
||||
|
||||
'''
|
||||
"""
|
||||
.. warning::
|
||||
This attribute should be used with caution. Setting this to *True*
|
||||
means that *all* entities that *can* have this attribute will
|
||||
be required to have a value for it.
|
||||
'''
|
||||
"""
|
||||
required = models.BooleanField(verbose_name = _('Required'), default = False)
|
||||
|
||||
enum_group = models.ForeignKey(
|
||||
|
|
@ -211,7 +212,7 @@ class Attribute(models.Model):
|
|||
return self.description
|
||||
|
||||
def get_validators(self):
|
||||
'''
|
||||
"""
|
||||
Returns the appropriate validator function from :mod:`~eav.validators`
|
||||
as a list (of length one) for the datatype.
|
||||
|
||||
|
|
@ -219,7 +220,7 @@ class Attribute(models.Model):
|
|||
The reason it returns it as a list, is eventually we may want this
|
||||
method to look elsewhere for additional attribute specific
|
||||
validators to return as well as the default, built-in one.
|
||||
'''
|
||||
"""
|
||||
DATATYPE_VALIDATORS = {
|
||||
'text': validate_text,
|
||||
'float': validate_float,
|
||||
|
|
@ -233,10 +234,10 @@ class Attribute(models.Model):
|
|||
return [DATATYPE_VALIDATORS[self.datatype]]
|
||||
|
||||
def validate_value(self, value):
|
||||
'''
|
||||
"""
|
||||
Check *value* against the validators returned by
|
||||
:meth:`get_validators` for this attribute.
|
||||
'''
|
||||
"""
|
||||
for validator in self.get_validators():
|
||||
validator(value)
|
||||
|
||||
|
|
@ -248,10 +249,10 @@ class Attribute(models.Model):
|
|||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Saves the Attribute and auto-generates a slug field
|
||||
if one wasn't provided.
|
||||
'''
|
||||
"""
|
||||
if not self.slug:
|
||||
self.slug = EavSlugField.create_slug_from_name(self.name)
|
||||
|
||||
|
|
@ -259,11 +260,11 @@ class Attribute(models.Model):
|
|||
super(Attribute, self).save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
'''
|
||||
Validates the attribute. Will raise ``ValidationError`` if
|
||||
the attribute's datatype is *TYPE_ENUM* and enum_group is not set,
|
||||
or if the attribute is not *TYPE_ENUM* and the enum group is set.
|
||||
'''
|
||||
"""
|
||||
Validates the attribute. Will raise ``ValidationError`` if the
|
||||
attribute's datatype is *TYPE_ENUM* and enum_group is not set, or if
|
||||
the attribute is not *TYPE_ENUM* and the enum group is set.
|
||||
"""
|
||||
if self.datatype == self.TYPE_ENUM and not self.enum_group:
|
||||
raise ValidationError(
|
||||
_('You must set the choice group for multiple choice attributes')
|
||||
|
|
@ -275,14 +276,14 @@ class Attribute(models.Model):
|
|||
)
|
||||
|
||||
def get_choices(self):
|
||||
'''
|
||||
"""
|
||||
Returns a query set of :class:`EnumValue` objects for this attribute.
|
||||
Returns None if the datatype of this attribute is not *TYPE_ENUM*.
|
||||
'''
|
||||
"""
|
||||
return self.enum_group.values.all() if self.datatype == Attribute.TYPE_ENUM else None
|
||||
|
||||
def save_value(self, entity, value):
|
||||
'''
|
||||
"""
|
||||
Called with *entity*, any Django object registered with eav, and
|
||||
*value*, the :class:`Value` this attribute for *entity* should
|
||||
be set to.
|
||||
|
|
@ -293,7 +294,7 @@ class Attribute(models.Model):
|
|||
.. note::
|
||||
If *value* is None and a :class:`Value` object exists for this
|
||||
Attribute and *entity*, it will delete that :class:`Value` object.
|
||||
'''
|
||||
"""
|
||||
ct = ContentType.objects.get_for_model(entity)
|
||||
|
||||
try:
|
||||
|
|
@ -325,7 +326,7 @@ class Attribute(models.Model):
|
|||
|
||||
|
||||
class Value(models.Model):
|
||||
'''
|
||||
"""
|
||||
Putting the **V** in *EAV*. This model stores the value for one particular
|
||||
:class:`Attribute` for some entity.
|
||||
|
||||
|
|
@ -344,7 +345,7 @@ class Value(models.Model):
|
|||
|
||||
Value.objects.create(entity = u, attribute = a, value_text = 'red bull')
|
||||
# = <Value: crazy_dev_user - Fav Drink: "red bull">
|
||||
'''
|
||||
"""
|
||||
|
||||
entity_ct = models.ForeignKey(
|
||||
ContentType,
|
||||
|
|
@ -395,17 +396,17 @@ class Value(models.Model):
|
|||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Validate and save this value.
|
||||
'''
|
||||
"""
|
||||
self.full_clean()
|
||||
super(Value, self).save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
'''
|
||||
"""
|
||||
Raises ``ValidationError`` if this value's attribute is *TYPE_ENUM*
|
||||
and value_enum is not a valid choice for this value's attribute.
|
||||
'''
|
||||
"""
|
||||
if self.attribute.datatype == Attribute.TYPE_ENUM and self.value_enum:
|
||||
if self.value_enum not in self.attribute.enum_group.values.all():
|
||||
raise ValidationError(
|
||||
|
|
@ -414,15 +415,15 @@ class Value(models.Model):
|
|||
)
|
||||
|
||||
def _get_value(self):
|
||||
'''
|
||||
"""
|
||||
Return the python object this value is holding
|
||||
'''
|
||||
"""
|
||||
return getattr(self, 'value_%s' % self.attribute.datatype)
|
||||
|
||||
def _set_value(self, new_value):
|
||||
'''
|
||||
"""
|
||||
Set the object this value is holding
|
||||
'''
|
||||
"""
|
||||
setattr(self, 'value_%s' % self.attribute.datatype, new_value)
|
||||
|
||||
value = property(_get_value, _set_value)
|
||||
|
|
@ -435,41 +436,41 @@ class Value(models.Model):
|
|||
|
||||
|
||||
class Entity(object):
|
||||
'''
|
||||
"""
|
||||
The helper class that will be attached to any entity
|
||||
registered with eav.
|
||||
'''
|
||||
"""
|
||||
@staticmethod
|
||||
def pre_save_handler(sender, *args, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Pre save handler attached to self.instance. Called before the
|
||||
model instance we are attached to is saved. This allows us to call
|
||||
:meth:`validate_attributes` before the entity is saved.
|
||||
'''
|
||||
"""
|
||||
instance = kwargs['instance']
|
||||
entity = getattr(kwargs['instance'], instance._eav_config_cls.eav_attr)
|
||||
entity.validate_attributes()
|
||||
|
||||
@staticmethod
|
||||
def post_save_handler(sender, *args, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Post save handler attached to self.instance. Calls :meth:`save` when
|
||||
the model instance we are attached to is saved.
|
||||
'''
|
||||
"""
|
||||
instance = kwargs['instance']
|
||||
entity = getattr(instance, instance._eav_config_cls.eav_attr)
|
||||
entity.save()
|
||||
|
||||
def __init__(self, instance):
|
||||
'''
|
||||
"""
|
||||
Set self.instance equal to the instance of the model that we're attached
|
||||
to. Also, store the content type of that instance.
|
||||
'''
|
||||
"""
|
||||
self.instance = instance
|
||||
self.ct = ContentType.objects.get_for_model(instance)
|
||||
|
||||
def __getattr__(self, name):
|
||||
'''
|
||||
"""
|
||||
Tha magic getattr helper. This is called whenever user invokes::
|
||||
|
||||
instance.<attribute>
|
||||
|
|
@ -479,7 +480,7 @@ class Entity(object):
|
|||
attribute slug. If there is one, it returns the value of the
|
||||
class:`Value` object, otherwise it hasn't been set, so it returns
|
||||
None.
|
||||
'''
|
||||
"""
|
||||
if not name.startswith('_'):
|
||||
try:
|
||||
attribute = self.get_attribute_by_slug(name)
|
||||
|
|
@ -497,43 +498,43 @@ class Entity(object):
|
|||
return getattr(super(Entity, self), name)
|
||||
|
||||
def get_all_attributes(self):
|
||||
'''
|
||||
"""
|
||||
Return a query set of all :class:`Attribute` objects that can be set
|
||||
for this entity.
|
||||
'''
|
||||
"""
|
||||
return self.instance._eav_config_cls.get_attributes().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 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 getting the value a user set for one of our
|
||||
attributes, without going to the db to check.
|
||||
'''
|
||||
"""
|
||||
return self.__dict__[attribute_slug]
|
||||
|
||||
def save(self):
|
||||
'''
|
||||
"""
|
||||
Saves all the EAV values that have been set on this entity.
|
||||
'''
|
||||
"""
|
||||
for attribute in self.get_all_attributes():
|
||||
if self._hasattr(attribute.slug):
|
||||
attribute_value = self._getattr(attribute.slug)
|
||||
attribute.save_value(self.instance, attribute_value)
|
||||
|
||||
def validate_attributes(self):
|
||||
'''
|
||||
"""
|
||||
Called before :meth:`save`, first validate all the entity values to
|
||||
make sure they can be created / saved cleanly.
|
||||
Raises ``ValidationError`` if they can't be.
|
||||
'''
|
||||
"""
|
||||
values_dict = self.get_values_dict()
|
||||
|
||||
for attribute in self.get_all_attributes():
|
||||
|
|
@ -574,43 +575,43 @@ class Entity(object):
|
|||
return {v.attribute.slug: v.value for v in self.get_values()}
|
||||
|
||||
def get_values(self):
|
||||
'''
|
||||
"""
|
||||
Get all set :class:`Value` objects for self.instance
|
||||
'''
|
||||
"""
|
||||
return Value.objects.filter(
|
||||
entity_ct = self.ct,
|
||||
entity_id = self.instance.pk
|
||||
).select_related()
|
||||
|
||||
def get_all_attribute_slugs(self):
|
||||
'''
|
||||
"""
|
||||
Returns a list of slugs for all attributes available to this entity.
|
||||
'''
|
||||
"""
|
||||
return set(self.get_all_attributes().values_list('slug', flat=True))
|
||||
|
||||
def get_attribute_by_slug(self, slug):
|
||||
'''
|
||||
"""
|
||||
Returns a single :class:`Attribute` with *slug*.
|
||||
'''
|
||||
"""
|
||||
return self.get_all_attributes().get(slug=slug)
|
||||
|
||||
def get_value_by_attribute(self, attribute):
|
||||
'''
|
||||
"""
|
||||
Returns a single :class:`Value` for *attribute*.
|
||||
'''
|
||||
"""
|
||||
return self.get_values().get(attribute=attribute)
|
||||
|
||||
def get_object_attributes(self):
|
||||
'''
|
||||
"""
|
||||
Returns entity instance attributes, except for
|
||||
``instance`` and ``ct`` which are used internally.
|
||||
'''
|
||||
"""
|
||||
return set(copy(self.__dict__).keys()) - set(['instance', 'ct'])
|
||||
|
||||
def __iter__(self):
|
||||
'''
|
||||
"""
|
||||
Iterate over set eav values. This would allow you to do::
|
||||
|
||||
for i in m.eav: print(i)
|
||||
'''
|
||||
"""
|
||||
return iter(self.get_values())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
"""
|
||||
This module contains custom :class:`EavQuerySet` class used for overriding
|
||||
relational operators and pure functions for rewriting Q-expressions.
|
||||
Q-expressions need to be rewritten for two reasons:
|
||||
|
|
@ -19,7 +18,7 @@ Q-expressions need to be rewritten for two reasons:
|
|||
|
||||
2. To ensure that Q-expression tree is compiled to valid SQL.
|
||||
For details see: :func:`rewrite_q_expr`.
|
||||
'''
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
|
||||
|
|
@ -31,7 +30,7 @@ from .models import Attribute, Value
|
|||
|
||||
|
||||
def is_eav_and_leaf(expr, gr_name):
|
||||
'''
|
||||
"""
|
||||
Checks whether Q-expression is an EAV AND leaf.
|
||||
|
||||
Args:
|
||||
|
|
@ -40,14 +39,16 @@ def is_eav_and_leaf(expr, gr_name):
|
|||
|
||||
Returns:
|
||||
bool
|
||||
'''
|
||||
return (getattr(expr, 'connector', None) == 'AND' and
|
||||
len(expr.children) == 1 and
|
||||
expr.children[0][0] in ['pk__in', '{}__in'.format(gr_name)])
|
||||
"""
|
||||
return (
|
||||
getattr(expr, 'connector', None) == 'AND' and
|
||||
len(expr.children) == 1 and
|
||||
expr.children[0][0] in ['pk__in', '{}__in'.format(gr_name)]
|
||||
)
|
||||
|
||||
|
||||
def rewrite_q_expr(model_cls, expr):
|
||||
'''
|
||||
"""
|
||||
Rewrites Q-expression to safe form, in order to ensure that
|
||||
generated SQL is valid.
|
||||
|
||||
|
|
@ -90,7 +91,7 @@ IGNORE
|
|||
|
||||
Returns:
|
||||
Union[Q, tuple]
|
||||
'''
|
||||
"""
|
||||
# Node in a Q-expr can be a Q or an attribute-value tuple (leaf).
|
||||
# We are only interested in Qs.
|
||||
|
||||
|
|
@ -153,11 +154,11 @@ IGNORE
|
|||
|
||||
|
||||
def eav_filter(func):
|
||||
'''
|
||||
"""
|
||||
Decorator used to wrap filter and exclude methods. Passes args through
|
||||
:func:`expand_q_filters` and kwargs through :func:`expand_eav_filter`. Returns the
|
||||
called function (filter or exclude).
|
||||
'''
|
||||
"""
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
nargs = []
|
||||
|
|
@ -187,11 +188,11 @@ def eav_filter(func):
|
|||
|
||||
|
||||
def expand_q_filters(q, root_cls):
|
||||
'''
|
||||
"""
|
||||
Takes a Q object and a model class.
|
||||
Recursively passes each filter / value in the Q object tree leaf nodes
|
||||
through :func:`expand_eav_filter`.
|
||||
'''
|
||||
"""
|
||||
new_children = []
|
||||
|
||||
for qi in q.children:
|
||||
|
|
@ -209,7 +210,7 @@ def expand_q_filters(q, root_cls):
|
|||
|
||||
|
||||
def expand_eav_filter(model_cls, key, value):
|
||||
'''
|
||||
"""
|
||||
Accepts a model class and a key, value.
|
||||
Recurisively replaces any eav filter with a subquery.
|
||||
|
||||
|
|
@ -222,7 +223,7 @@ def expand_eav_filter(model_cls, key, value):
|
|||
|
||||
key = 'eav_values__in'
|
||||
value = Values.objects.filter(value_int=5, attribute__slug='height')
|
||||
'''
|
||||
"""
|
||||
fields = key.split('__')
|
||||
config_cls = getattr(model_cls, '_eav_config_cls', None)
|
||||
|
||||
|
|
@ -254,30 +255,30 @@ def expand_eav_filter(model_cls, key, value):
|
|||
|
||||
|
||||
class EavQuerySet(QuerySet):
|
||||
'''
|
||||
"""
|
||||
Overrides relational operators for EAV models.
|
||||
'''
|
||||
"""
|
||||
|
||||
@eav_filter
|
||||
def filter(self, *args, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Pass *args* and *kwargs* through :func:`eav_filter`, then pass to
|
||||
the ``Manager`` filter method.
|
||||
'''
|
||||
"""
|
||||
return super(EavQuerySet, self).filter(*args, **kwargs)
|
||||
|
||||
@eav_filter
|
||||
def exclude(self, *args, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Pass *args* and *kwargs* through :func:`eav_filter`, then pass to
|
||||
the ``Manager`` exclude method.
|
||||
'''
|
||||
"""
|
||||
return super(EavQuerySet, self).exclude(*args, **kwargs)
|
||||
|
||||
@eav_filter
|
||||
def get(self, *args, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Pass *args* and *kwargs* through :func:`eav_filter`, then pass to
|
||||
the ``Manager`` get method.
|
||||
'''
|
||||
"""
|
||||
return super(EavQuerySet, self).get(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
'''This modules contains the registry classes.'''
|
||||
"""This modules contains the registry classes."""
|
||||
|
||||
from django.contrib.contenttypes import fields as generic
|
||||
from django.db.models.signals import post_init, post_save, pre_save
|
||||
|
|
@ -8,7 +8,7 @@ from .models import Attribute, Entity, Value
|
|||
|
||||
|
||||
class EavConfig(object):
|
||||
'''
|
||||
"""
|
||||
The default ``EavConfig`` class used if it is not overriden on registration.
|
||||
This is where all the default eav attribute names are defined.
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ class EavConfig(object):
|
|||
5. generic_relation_related_name - Name of the related name for
|
||||
GenericRelation from Entity to Value. None by default. Therefore,
|
||||
if not overridden, it is not possible to query Values by Entities.
|
||||
'''
|
||||
"""
|
||||
manager_attr = 'objects'
|
||||
manager_only = False
|
||||
eav_attr = 'eav'
|
||||
|
|
@ -34,28 +34,28 @@ class EavConfig(object):
|
|||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
'''
|
||||
"""
|
||||
By default, all :class:`~eav.models.Attribute` object apply to an
|
||||
entity, unless you provide a custom EavConfig class overriding this.
|
||||
'''
|
||||
"""
|
||||
return Attribute.objects.all()
|
||||
|
||||
|
||||
class Registry(object):
|
||||
'''
|
||||
"""
|
||||
Handles registration through the
|
||||
:meth:`register` and :meth:`unregister` methods.
|
||||
'''
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def register(model_cls, config_cls=None):
|
||||
'''
|
||||
"""
|
||||
Registers *model_cls* with eav. You can pass an optional *config_cls*
|
||||
to override the EavConfig defaults.
|
||||
|
||||
.. note::
|
||||
Multiple registrations for the same entity are harmlessly ignored.
|
||||
'''
|
||||
"""
|
||||
if hasattr(model_cls, '_eav_config_cls'):
|
||||
return
|
||||
|
||||
|
|
@ -71,12 +71,12 @@ class Registry(object):
|
|||
|
||||
@staticmethod
|
||||
def unregister(model_cls):
|
||||
'''
|
||||
"""
|
||||
Unregisters *model_cls* with eav.
|
||||
|
||||
.. note::
|
||||
Unregistering a class not already registered is harmlessly ignored.
|
||||
'''
|
||||
"""
|
||||
if not getattr(model_cls, '_eav_config_cls', None):
|
||||
return
|
||||
reg = Registry(model_cls)
|
||||
|
|
@ -86,24 +86,24 @@ class Registry(object):
|
|||
|
||||
@staticmethod
|
||||
def attach_eav_attr(sender, *args, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Attach EAV Entity toolkit to an instance after init.
|
||||
'''
|
||||
"""
|
||||
instance = kwargs['instance']
|
||||
config_cls = instance.__class__._eav_config_cls
|
||||
setattr(instance, config_cls.eav_attr, Entity(instance))
|
||||
|
||||
def __init__(self, model_cls):
|
||||
'''
|
||||
"""
|
||||
Set the *model_cls* and its *config_cls*
|
||||
'''
|
||||
"""
|
||||
self.model_cls = model_cls
|
||||
self.config_cls = model_cls._eav_config_cls
|
||||
|
||||
def _attach_manager(self):
|
||||
'''
|
||||
"""
|
||||
Attach the manager to *manager_attr* specified in *config_cls*
|
||||
'''
|
||||
"""
|
||||
# Save the old manager if the attribute name conflicts with the new one.
|
||||
if hasattr(self.model_cls, self.config_cls.manager_attr):
|
||||
mgr = getattr(self.model_cls, self.config_cls.manager_attr)
|
||||
|
|
@ -116,9 +116,9 @@ class Registry(object):
|
|||
mgr.contribute_to_class(self.model_cls, self.config_cls.manager_attr)
|
||||
|
||||
def _detach_manager(self):
|
||||
'''
|
||||
"""
|
||||
Detach the manager and restore the previous one (if there was one).
|
||||
'''
|
||||
"""
|
||||
mgr = getattr(self.model_cls, self.config_cls.manager_attr)
|
||||
self.model_cls._meta.local_managers.remove(mgr)
|
||||
self.model_cls._meta._expire_cache()
|
||||
|
|
@ -130,28 +130,28 @@ class Registry(object):
|
|||
self.config_cls.manager_attr)
|
||||
|
||||
def _attach_signals(self):
|
||||
'''
|
||||
"""
|
||||
Attach pre- and post- save signals from model class
|
||||
to Entity helper. This way, Entity instance will be
|
||||
able to prepare and clean-up before and after creation /
|
||||
update of the user's model class instance.
|
||||
'''
|
||||
"""
|
||||
post_init.connect(Registry.attach_eav_attr, sender = self.model_cls)
|
||||
pre_save.connect(Entity.pre_save_handler, sender = self.model_cls)
|
||||
post_save.connect(Entity.post_save_handler, sender = self.model_cls)
|
||||
|
||||
def _detach_signals(self):
|
||||
'''
|
||||
"""
|
||||
Detach all signals for eav.
|
||||
'''
|
||||
"""
|
||||
post_init.disconnect(Registry.attach_eav_attr, sender = self.model_cls)
|
||||
pre_save.disconnect(Entity.pre_save_handler, sender = self.model_cls)
|
||||
post_save.disconnect(Entity.post_save_handler, sender = self.model_cls)
|
||||
|
||||
def _attach_generic_relation(self):
|
||||
'''
|
||||
"""
|
||||
Set up the generic relation for the entity
|
||||
'''
|
||||
"""
|
||||
rel_name = self.config_cls.generic_relation_related_name or \
|
||||
self.model_cls.__name__
|
||||
|
||||
|
|
@ -164,9 +164,9 @@ class Registry(object):
|
|||
generic_relation.contribute_to_class(self.model_cls, gr_name)
|
||||
|
||||
def _detach_generic_relation(self):
|
||||
'''
|
||||
"""
|
||||
Remove the generic relation from the entity
|
||||
'''
|
||||
"""
|
||||
gen_rel_field = self.config_cls.generic_relation_attr.lower()
|
||||
for field in self.model_cls._meta.local_many_to_many:
|
||||
if field.name == gen_rel_field:
|
||||
|
|
@ -176,9 +176,9 @@ class Registry(object):
|
|||
delattr(self.model_cls, gen_rel_field)
|
||||
|
||||
def _register_self(self):
|
||||
'''
|
||||
"""
|
||||
Call the necessary registration methods
|
||||
'''
|
||||
"""
|
||||
self._attach_manager()
|
||||
|
||||
if not self.config_cls.manager_only:
|
||||
|
|
@ -186,9 +186,9 @@ class Registry(object):
|
|||
self._attach_generic_relation()
|
||||
|
||||
def _unregister_self(self):
|
||||
'''
|
||||
"""
|
||||
Call the necessary unregistration methods
|
||||
'''
|
||||
"""
|
||||
self._detach_manager()
|
||||
|
||||
if not self.config_cls.manager_only:
|
||||
|
|
|
|||
24
eav/utils.py
24
eav/utils.py
|
|
@ -1,24 +0,0 @@
|
|||
'''This module contains non-essential helper methods.'''
|
||||
|
||||
import sys
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
def print_q_expr(expr, indent="", is_tail=True):
|
||||
'''
|
||||
Simple print method for debugging Q-expressions' trees.
|
||||
'''
|
||||
sys.stdout.write(indent)
|
||||
sa, sb = (' └── ', ' ') if is_tail else (' ├── ', ' │ ')
|
||||
|
||||
if isinstance(expr, Q):
|
||||
sys.stdout.write('{}{}\n'.format(sa, expr.connector))
|
||||
for child in expr.children:
|
||||
print_q_expr(child, indent + sb, expr.children[-1] == child)
|
||||
else:
|
||||
try:
|
||||
queryset = ', '.join(repr(v) for v in expr[1])
|
||||
except TypeError:
|
||||
queryset = repr(expr[1])
|
||||
sys.stdout.write(' └── {} {}\n'.format(expr[0], queryset))
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
"""
|
||||
This module contains a validator for each :class:`~eav.models.Attribute` datatype.
|
||||
|
||||
A validator is a callable that takes a value and raises a ``ValidationError``
|
||||
if it doesn’t meet some criteria (see `Django validators
|
||||
if it doesn't meet some criteria (see `Django validators
|
||||
<http://docs.djangoproject.com/en/dev/ref/validators/>`_).
|
||||
|
||||
These validators are called by the
|
||||
:meth:`~eav.models.Attribute.validate_value` method in the
|
||||
:class:`~eav.models.Attribute` model.
|
||||
'''
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
|
|
@ -20,17 +18,17 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
|
||||
def validate_text(value):
|
||||
'''
|
||||
"""
|
||||
Raises ``ValidationError`` unless *value* type is ``str`` or ``unicode``
|
||||
'''
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
raise ValidationError(_(u"Must be str or unicode"))
|
||||
|
||||
|
||||
def validate_float(value):
|
||||
'''
|
||||
"""
|
||||
Raises ``ValidationError`` unless *value* can be cast as a ``float``
|
||||
'''
|
||||
"""
|
||||
try:
|
||||
float(value)
|
||||
except ValueError:
|
||||
|
|
@ -38,9 +36,9 @@ def validate_float(value):
|
|||
|
||||
|
||||
def validate_int(value):
|
||||
'''
|
||||
"""
|
||||
Raises ``ValidationError`` unless *value* can be cast as an ``int``
|
||||
'''
|
||||
"""
|
||||
try:
|
||||
int(value)
|
||||
except ValueError:
|
||||
|
|
@ -48,40 +46,43 @@ def validate_int(value):
|
|||
|
||||
|
||||
def validate_date(value):
|
||||
'''
|
||||
"""
|
||||
Raises ``ValidationError`` unless *value* is an instance of ``datetime``
|
||||
or ``date``
|
||||
'''
|
||||
"""
|
||||
if not isinstance(value, datetime.datetime) and not isinstance(value, datetime.date):
|
||||
raise ValidationError(_(u"Must be a date or datetime"))
|
||||
|
||||
|
||||
def validate_bool(value):
|
||||
'''
|
||||
"""
|
||||
Raises ``ValidationError`` unless *value* type is ``bool``
|
||||
'''
|
||||
"""
|
||||
if not isinstance(value, bool):
|
||||
raise ValidationError(_(u"Must be a boolean"))
|
||||
|
||||
|
||||
def validate_object(value):
|
||||
'''
|
||||
"""
|
||||
Raises ``ValidationError`` unless *value* is a saved
|
||||
django model instance.
|
||||
'''
|
||||
"""
|
||||
if not isinstance(value, models.Model):
|
||||
raise ValidationError(_(u"Must be a django model object instance"))
|
||||
|
||||
if not value.pk:
|
||||
raise ValidationError(_(u"Model has not been saved yet"))
|
||||
|
||||
|
||||
def validate_enum(value):
|
||||
'''
|
||||
"""
|
||||
Raises ``ValidationError`` unless *value* is a saved
|
||||
:class:`~eav.models.EnumValue` model instance.
|
||||
'''
|
||||
"""
|
||||
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"))
|
||||
|
|
|
|||
Loading…
Reference in a new issue