diff --git a/.coveragerc b/.coveragerc index 932c4dd..f204218 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,4 +2,3 @@ omit = */migrations/* eav/__init__.py - eav/utils.py diff --git a/eav/admin.py b/eav/admin.py index a9b0133..8bd8db2 100644 --- a/eav/admin.py +++ b/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) diff --git a/eav/decorators.py b/eav/decorators.py index c45a736..5c8503d 100644 --- a/eav/decorators.py +++ b/eav/decorators.py @@ -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 diff --git a/eav/fields.py b/eav/fields.py index e4c43a0..f573332 100644 --- a/eav/fields.py +++ b/eav/fields.py @@ -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.' + )) diff --git a/eav/forms.py b/eav/forms.py index 236ff7e..b1f97b1 100644 --- a/eav/forms.py +++ b/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) diff --git a/eav/managers.py b/eav/managers.py index 999abd9..b30acf9 100644 --- a/eav/managers.py +++ b/eav/managers.py @@ -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: diff --git a/eav/models.py b/eav/models.py index 2f96f24..7b26475 100644 --- a/eav/models.py +++ b/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) # = .. 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') # = - ''' + """ 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. @@ -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()) diff --git a/eav/queryset.py b/eav/queryset.py index 349c060..b6cdc47 100644 --- a/eav/queryset.py +++ b/eav/queryset.py @@ -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) diff --git a/eav/registry.py b/eav/registry.py index fec5cbe..5216081 100644 --- a/eav/registry.py +++ b/eav/registry.py @@ -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: diff --git a/eav/utils.py b/eav/utils.py deleted file mode 100644 index 38d1773..0000000 --- a/eav/utils.py +++ /dev/null @@ -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)) diff --git a/eav/validators.py b/eav/validators.py index 3759f40..b27e264 100644 --- a/eav/validators.py +++ b/eav/validators.py @@ -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 `_). 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"))