style: run black formatter

This commit is contained in:
Mike 2021-10-16 10:43:02 -07:00
parent e35258dc31
commit b1badf8dc5
16 changed files with 445 additions and 268 deletions

View file

@ -18,16 +18,16 @@ sys.path.insert(0, os.path.abspath('../../'))
# Pass settings into configure.
settings.configure(
INSTALLED_APPS = [
INSTALLED_APPS=[
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'eav'
'eav',
],
SECRET_KEY=os.environ.get("DJANGO_SECRET_KEY", "this-is-not-s3cur3")
SECRET_KEY=os.environ.get("DJANGO_SECRET_KEY", "this-is-not-s3cur3"),
)
# Call django.setup to load installed apps and other stuff.
@ -56,10 +56,7 @@ extensions = [
]
html_theme_options = dict(
show_powered_by = False,
show_related = True,
fixed_sidebar = True,
font_family = 'Roboto'
show_powered_by=False, show_related=True, fixed_sidebar=True, font_family='Roboto'
)
templates_path = ['_templates']
@ -111,17 +108,8 @@ def setup(app):
html_sidebars = {
'index': [
'sidebarintro.html',
'localtoc.html'
],
'**': [
'sidebarintro.html',
'localtoc.html',
'relations.html',
'searchbox.html'
]
'index': ['sidebarintro.html', 'localtoc.html'],
'**': ['sidebarintro.html', 'localtoc.html', 'relations.html', 'searchbox.html'],
}
@ -137,15 +125,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
@ -155,8 +140,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'DjangoEAV2.tex', 'Django EAV 2 Documentation',
'-', 'manual'),
(master_doc, 'DjangoEAV2.tex', 'Django EAV 2 Documentation', '-', 'manual'),
]
@ -164,10 +148,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'djangoeav2', 'Django EAV 2 Documentation',
[author], 1)
]
man_pages = [(master_doc, 'djangoeav2', 'Django EAV 2 Documentation', [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
@ -176,9 +157,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'DjangoEAV2', 'Django EAV 2 Documentation',
author, 'DjangoEAV2', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
'DjangoEAV2',
'Django EAV 2 Documentation',
author,
'DjangoEAV2',
'One line description of project.',
'Miscellaneous',
),
]
# -- Extension configuration -------------------------------------------------

View file

@ -1,7 +1,10 @@
def register(model_cls, config_cls=None):
from .registry import Registry
Registry.register(model_cls, config_cls)
def unregister(model_cls):
from .registry import Registry
Registry.unregister(model_cls)

View file

@ -36,6 +36,7 @@ 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)
@ -59,6 +60,7 @@ class BaseEntityInline(InlineModelAdmin):
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):

View file

@ -3,6 +3,7 @@ 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

View file

@ -1,2 +1,2 @@
class IllegalAssignmentException(Exception):
pass
pass

View file

@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
from .forms import CSVFormField
class EavSlugField(models.SlugField):
"""
The slug field used by :class:`~eav.models.Attribute`
@ -22,10 +23,12 @@ class EavSlugField(models.SlugField):
slug_regex = r'[a-z][a-z0-9_]*'
if not re.match(slug_regex, value):
raise ValidationError(_(
'Must be all lower case, start with a letter, and contain '
'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):
@ -59,12 +62,14 @@ class EavDatatypeField(models.CharField):
return
if instance.value_set.count():
raise ValidationError(_(
'You cannot change the datatype of an attribute that is already in use.'
))
raise ValidationError(
_(
'You cannot change the datatype of an attribute that is already in use.'
)
)
class CSVField(models.TextField): # (models.Field):
class CSVField(models.TextField): # (models.Field):
description = _("A Comma-Separated-Value field.")
default_separator = ";"

View file

@ -4,8 +4,15 @@ from copy import deepcopy
from django import forms
from django.contrib.admin.widgets import AdminSplitDateTime
from django.forms import (BooleanField, CharField, ChoiceField, DateTimeField,
FloatField, IntegerField, ModelForm)
from django.forms import (
BooleanField,
CharField,
ChoiceField,
DateTimeField,
FloatField,
IntegerField,
ModelForm,
)
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
@ -64,15 +71,16 @@ class BaseDynamicEntityForm(ModelForm):
csv CSVField
===== =============
"""
FIELD_CLASSES = {
'text': CharField,
'text': CharField,
'float': FloatField,
'int': IntegerField,
'date': DateTimeField,
'bool': BooleanField,
'enum': ChoiceField,
'json': JSONField,
'csv': CSVFormField,
'int': IntegerField,
'date': DateTimeField,
'bool': BooleanField,
'enum': ChoiceField,
'json': JSONField,
'csv': CSVFormField,
}
def __init__(self, data=None, *args, **kwargs):
@ -123,10 +131,12 @@ class BaseDynamicEntityForm(ModelForm):
``self.instance`` and related EAV attributes. Returns ``instance``.
"""
if self.errors:
raise ValueError(_(
'The %s could not be saved because the data'
'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)

View file

@ -11,6 +11,7 @@ class EntityManager(models.Manager):
"""
Our custom manager, overrides ``models.Manager``.
"""
_queryset_class = EavQuerySet
def create(self, **kwargs):
@ -29,7 +30,7 @@ class EntityManager(models.Manager):
for key, value in kwargs.items():
if key.startswith(prefix):
eav_kwargs.update({key[len(prefix):]: value})
eav_kwargs.update({key[len(prefix) :]: value})
else:
new_kwargs.update({key: value})

View file

@ -19,15 +19,79 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Attribute',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='User-friendly attribute name', max_length=100, verbose_name='Name')),
('slug', eav.fields.EavSlugField(help_text='Short unique attribute label', unique=True, verbose_name='Slug')),
('description', models.CharField(blank=True, help_text='Short description', max_length=256, null=True, verbose_name='Description')),
('datatype', eav.fields.EavDatatypeField(choices=[('text', 'Text'), ('date', 'Date'), ('float', 'Float'), ('int', 'Integer'), ('bool', 'True / False'), ('object', 'Django Object'), ('enum', 'Multiple Choice')], max_length=6, verbose_name='Data Type')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Modified')),
('required', models.BooleanField(default=False, verbose_name='Required')),
('display_order', models.PositiveIntegerField(default=1, verbose_name='Display order')),
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'name',
models.CharField(
help_text='User-friendly attribute name',
max_length=100,
verbose_name='Name',
),
),
(
'slug',
eav.fields.EavSlugField(
help_text='Short unique attribute label',
unique=True,
verbose_name='Slug',
),
),
(
'description',
models.CharField(
blank=True,
help_text='Short description',
max_length=256,
null=True,
verbose_name='Description',
),
),
(
'datatype',
eav.fields.EavDatatypeField(
choices=[
('text', 'Text'),
('date', 'Date'),
('float', 'Float'),
('int', 'Integer'),
('bool', 'True / False'),
('object', 'Django Object'),
('enum', 'Multiple Choice'),
],
max_length=6,
verbose_name='Data Type',
),
),
(
'created',
models.DateTimeField(
default=django.utils.timezone.now,
editable=False,
verbose_name='Created',
),
),
(
'modified',
models.DateTimeField(auto_now=True, verbose_name='Modified'),
),
(
'required',
models.BooleanField(default=False, verbose_name='Required'),
),
(
'display_order',
models.PositiveIntegerField(
default=1, verbose_name='Display order'
),
),
],
options={
'ordering': ['name'],
@ -36,21 +100,53 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='EnumGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True, verbose_name='Name')),
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'name',
models.CharField(max_length=100, unique=True, verbose_name='Name'),
),
],
),
migrations.CreateModel(
name='EnumValue',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='Value')),
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'value',
models.CharField(
db_index=True, max_length=50, unique=True, verbose_name='Value'
),
),
],
),
migrations.CreateModel(
name='Value',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('entity_id', models.IntegerField()),
('value_text', models.TextField(blank=True, null=True)),
('value_float', models.FloatField(blank=True, null=True)),
@ -58,12 +154,52 @@ class Migration(migrations.Migration):
('value_date', models.DateTimeField(blank=True, null=True)),
('value_bool', models.NullBooleanField()),
('generic_value_id', models.IntegerField(blank=True, null=True)),
('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Modified')),
('attribute', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='eav.Attribute', verbose_name='Attribute')),
('entity_ct', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='value_entities', to='contenttypes.ContentType')),
('generic_value_ct', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='value_values', to='contenttypes.ContentType')),
('value_enum', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='eav_values', to='eav.EnumValue')),
(
'created',
models.DateTimeField(
default=django.utils.timezone.now, verbose_name='Created'
),
),
(
'modified',
models.DateTimeField(auto_now=True, verbose_name='Modified'),
),
(
'attribute',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to='eav.Attribute',
verbose_name='Attribute',
),
),
(
'entity_ct',
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name='value_entities',
to='contenttypes.ContentType',
),
),
(
'generic_value_ct',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='value_values',
to='contenttypes.ContentType',
),
),
(
'value_enum',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name='eav_values',
to='eav.EnumValue',
),
),
],
),
migrations.AddField(
@ -74,6 +210,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='attribute',
name='enum_group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='eav.EnumGroup', verbose_name='Choice Group'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to='eav.EnumGroup',
verbose_name='Choice Group',
),
),
]

View file

@ -3,6 +3,7 @@
from django.db import migrations
import eav.fields
import django.core.serializers.json
try:
from django.db.models import JSONField
except ImportError:
@ -19,11 +20,29 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='value',
name='value_json',
field=JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True),
field=JSONField(
blank=True,
default=dict,
encoder=django.core.serializers.json.DjangoJSONEncoder,
null=True,
),
),
migrations.AlterField(
model_name='attribute',
name='datatype',
field=eav.fields.EavDatatypeField(choices=[('text', 'Text'), ('date', 'Date'), ('float', 'Float'), ('int', 'Integer'), ('bool', 'True / False'), ('object', 'Django Object'), ('enum', 'Multiple Choice'), ('json', 'JSON Object')], max_length=6, verbose_name='Data Type'),
field=eav.fields.EavDatatypeField(
choices=[
('text', 'Text'),
('date', 'Date'),
('float', 'Float'),
('int', 'Integer'),
('bool', 'True / False'),
('object', 'Django Object'),
('enum', 'Multiple Choice'),
('json', 'JSON Object'),
],
max_length=6,
verbose_name='Data Type',
),
),
]

View file

@ -19,6 +19,20 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='attribute',
name='datatype',
field=eav.fields.EavDatatypeField(choices=[('text', 'Text'), ('date', 'Date'), ('float', 'Float'), ('int', 'Integer'), ('bool', 'True / False'), ('object', 'Django Object'), ('enum', 'Multiple Choice'), ('json', 'JSON Object'), ('csv', 'Comma-Separated-Value')], max_length=6, verbose_name='Data Type'),
field=eav.fields.EavDatatypeField(
choices=[
('text', 'Text'),
('date', 'Date'),
('float', 'Float'),
('int', 'Integer'),
('bool', 'True / False'),
('object', 'Django Object'),
('enum', 'Multiple Choice'),
('json', 'JSON Object'),
('csv', 'Comma-Separated-Value'),
],
max_length=6,
verbose_name='Data Type',
),
),
]

View file

@ -67,6 +67,7 @@ class EnumValue(models.Model):
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):
@ -81,8 +82,9 @@ class EnumGroup(models.Model):
See :class:`EnumValue` for an example.
"""
name = models.CharField(_('Name'), unique = True, max_length = 100)
values = models.ManyToManyField(EnumValue, verbose_name = _('Enum group'))
name = models.CharField(_('Name'), unique=True, max_length=100)
values = models.ManyToManyField(EnumValue, verbose_name=_('Enum group'))
def __str__(self):
return '<EnumGroup {}>'.format(self.name)
@ -139,43 +141,42 @@ 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']
TYPE_TEXT = 'text'
TYPE_FLOAT = 'float'
TYPE_INT = 'int'
TYPE_DATE = 'date'
TYPE_TEXT = 'text'
TYPE_FLOAT = 'float'
TYPE_INT = 'int'
TYPE_DATE = 'date'
TYPE_BOOLEAN = 'bool'
TYPE_OBJECT = 'object'
TYPE_ENUM = 'enum'
TYPE_JSON = 'json'
TYPE_CSV = 'csv'
TYPE_OBJECT = 'object'
TYPE_ENUM = 'enum'
TYPE_JSON = 'json'
TYPE_CSV = 'csv'
DATATYPE_CHOICES = (
(TYPE_TEXT, _('Text')),
(TYPE_DATE, _('Date')),
(TYPE_FLOAT, _('Float')),
(TYPE_INT, _('Integer')),
(TYPE_TEXT, _('Text')),
(TYPE_DATE, _('Date')),
(TYPE_FLOAT, _('Float')),
(TYPE_INT, _('Integer')),
(TYPE_BOOLEAN, _('True / False')),
(TYPE_OBJECT, _('Django Object')),
(TYPE_ENUM, _('Multiple Choice')),
(TYPE_JSON, _('JSON Object')),
(TYPE_CSV, _('Comma-Separated-Value')),
(TYPE_OBJECT, _('Django Object')),
(TYPE_ENUM, _('Multiple Choice')),
(TYPE_JSON, _('JSON Object')),
(TYPE_CSV, _('Comma-Separated-Value')),
)
# Core attributes
datatype = EavDatatypeField(
verbose_name = _('Data Type'),
choices = DATATYPE_CHOICES,
max_length = 6
verbose_name=_('Data Type'), choices=DATATYPE_CHOICES, max_length=6
)
name = models.CharField(
verbose_name = _('Name'),
max_length = 100,
help_text = _('User-friendly attribute name')
verbose_name=_('Name'),
max_length=100,
help_text=_('User-friendly attribute name'),
)
"""
@ -184,11 +185,11 @@ class Attribute(models.Model):
(see :meth:`~eav.fields.EavSlugField.create_slug_from_name`).
"""
slug = EavSlugField(
verbose_name = _('Slug'),
max_length = 50,
db_index = True,
unique = True,
help_text = _('Short unique attribute label')
verbose_name=_('Slug'),
max_length=50,
db_index=True,
unique=True,
help_text=_('Short unique attribute label'),
)
"""
@ -197,7 +198,7 @@ class Attribute(models.Model):
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)
required = models.BooleanField(verbose_name=_('Required'), default=False)
entity_ct = models.ManyToManyField(ContentType, blank=True)
"""
@ -209,36 +210,30 @@ class Attribute(models.Model):
enum_group = models.ForeignKey(
EnumGroup,
verbose_name = _('Choice Group'),
on_delete = models.PROTECT,
blank = True,
null = True
verbose_name=_('Choice Group'),
on_delete=models.PROTECT,
blank=True,
null=True,
)
description = models.CharField(
verbose_name = _('Description'),
max_length = 256,
blank = True,
null = True,
help_text = _('Short description')
verbose_name=_('Description'),
max_length=256,
blank=True,
null=True,
help_text=_('Short description'),
)
# Useful meta-information
display_order = models.PositiveIntegerField(
verbose_name = _('Display order'),
default = 1
verbose_name=_('Display order'), default=1
)
modified = models.DateTimeField(
verbose_name = _('Modified'),
auto_now = True
)
modified = models.DateTimeField(verbose_name=_('Modified'), auto_now=True)
created = models.DateTimeField(
verbose_name = _('Created'),
default = timezone.now,
editable = False
verbose_name=_('Created'), default=timezone.now, editable=False
)
@property
@ -256,15 +251,15 @@ class Attribute(models.Model):
validators to return as well as the default, built-in one.
"""
DATATYPE_VALIDATORS = {
'text': validate_text,
'float': validate_float,
'int': validate_int,
'date': validate_date,
'bool': validate_bool,
'text': validate_text,
'float': validate_float,
'int': validate_int,
'date': validate_date,
'bool': validate_bool,
'object': validate_object,
'enum': validate_enum,
'json': validate_json,
'csv': validate_csv,
'enum': validate_enum,
'json': validate_json,
'csv': validate_csv,
}
return [DATATYPE_VALIDATORS[self.datatype]]
@ -283,7 +278,7 @@ class Attribute(models.Model):
if not self.enum_group.values.filter(value=value).exists():
raise ValidationError(
_('%(val)s is not a valid choice for %(attr)s')
% dict(val = value, attr = self)
% dict(val=value, attr=self)
)
def save(self, *args, **kwargs):
@ -318,7 +313,11 @@ class Attribute(models.Model):
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
return (
self.enum_group.values.all()
if self.datatype == Attribute.TYPE_ENUM
else None
)
def save_value(self, entity, value):
"""
@ -337,18 +336,14 @@ class Attribute(models.Model):
try:
value_obj = self.value_set.get(
entity_ct = ct,
entity_id = entity.pk,
attribute = self
entity_ct=ct, entity_id=entity.pk, attribute=self
)
except Value.DoesNotExist:
if value == None or value == '':
return
value_obj = Value.objects.create(
entity_ct = ct,
entity_id = entity.pk,
attribute = self
entity_ct=ct, entity_id=entity.pk, attribute=self
)
if value == None or value == '':
@ -386,53 +381,49 @@ class Value(models.Model):
"""
entity_ct = models.ForeignKey(
ContentType,
on_delete = models.PROTECT,
related_name = 'value_entities'
ContentType, on_delete=models.PROTECT, related_name='value_entities'
)
entity_id = models.IntegerField()
entity = generic.GenericForeignKey(ct_field = 'entity_ct', fk_field = 'entity_id')
entity = generic.GenericForeignKey(ct_field='entity_ct', fk_field='entity_id')
value_text = models.TextField(blank = True, null = True)
value_float = models.FloatField(blank = True, null = True)
value_int = models.IntegerField(blank = True, null = True)
value_date = models.DateTimeField(blank = True, null = True)
value_bool = models.BooleanField(blank = True, null = True)
value_json = JSONField(default=dict, encoder=DjangoJSONEncoder, blank = True, null = True)
value_csv = CSVField(blank = True, null = True)
value_text = models.TextField(blank=True, null=True)
value_float = models.FloatField(blank=True, null=True)
value_int = models.IntegerField(blank=True, null=True)
value_date = models.DateTimeField(blank=True, null=True)
value_bool = models.BooleanField(blank=True, null=True)
value_json = JSONField(
default=dict, encoder=DjangoJSONEncoder, blank=True, null=True
)
value_csv = CSVField(blank=True, null=True)
value_enum = models.ForeignKey(
value_enum = models.ForeignKey(
EnumValue,
blank = True,
null = True,
on_delete = models.PROTECT,
related_name = 'eav_values'
blank=True,
null=True,
on_delete=models.PROTECT,
related_name='eav_values',
)
generic_value_id = models.IntegerField(blank=True, null=True)
generic_value_ct = models.ForeignKey(
ContentType,
blank = True,
null = True,
on_delete = models.PROTECT,
related_name ='value_values'
blank=True,
null=True,
on_delete=models.PROTECT,
related_name='value_values',
)
value_object = generic.GenericForeignKey(
ct_field = 'generic_value_ct',
fk_field = 'generic_value_id'
ct_field='generic_value_ct', fk_field='generic_value_id'
)
created = models.DateTimeField(_('Created'), default = timezone.now)
modified = models.DateTimeField(_('Modified'), auto_now = True)
created = models.DateTimeField(_('Created'), default=timezone.now)
modified = models.DateTimeField(_('Modified'), auto_now=True)
attribute = models.ForeignKey(
Attribute,
db_index = True,
on_delete = models.PROTECT,
verbose_name = _('Attribute')
Attribute, db_index=True, on_delete=models.PROTECT, verbose_name=_('Attribute')
)
def save(self, *args, **kwargs):
@ -468,6 +459,7 @@ class Entity(object):
The helper class that will be attached to any entity
registered with eav.
"""
@staticmethod
def pre_save_handler(sender, *args, **kwargs):
"""
@ -515,7 +507,7 @@ class Entity(object):
except Attribute.DoesNotExist:
raise AttributeError(
_('%(obj)s has no EAV attribute named %(attr)s')
% dict(obj = self.instance, attr = name)
% dict(obj=self.instance, attr=name)
)
try:
@ -557,7 +549,9 @@ class Entity(object):
for attribute in self.get_all_attributes():
if self._hasattr(attribute.slug):
attribute_value = self._getattr(attribute.slug)
if attribute.datatype == Attribute.TYPE_ENUM and not isinstance(attribute_value, EnumValue):
if attribute.datatype == Attribute.TYPE_ENUM and not isinstance(
attribute_value, EnumValue
):
if attribute_value is not None:
attribute_value = EnumValue.objects.get(value=attribute_value)
attribute.save_value(self.instance, attribute_value)
@ -592,16 +586,18 @@ class Entity(object):
except ValidationError as e:
raise ValidationError(
_('%(attr)s EAV field %(err)s')
% dict(attr = attribute.slug, err = e)
% dict(attr=attribute.slug, err=e)
)
illegal = values_dict or (
self.get_object_attributes() - self.get_all_attribute_slugs())
self.get_object_attributes() - self.get_all_attribute_slugs()
)
if illegal:
raise IllegalAssignmentException(
'Instance of the class {} cannot have values for attributes: {}.'
.format(self.instance.__class__, ', '.join(illegal))
'Instance of the class {} cannot have values for attributes: {}.'.format(
self.instance.__class__, ', '.join(illegal)
)
)
def get_values_dict(self):
@ -612,8 +608,7 @@ class Entity(object):
Get all set :class:`Value` objects for self.instance
"""
return Value.objects.filter(
entity_ct = self.ct,
entity_id = self.instance.pk
entity_ct=self.ct, entity_id=self.instance.pk
).select_related()
def get_all_attribute_slugs(self):

View file

@ -44,56 +44,56 @@ def is_eav_and_leaf(expr, gr_name):
bool
"""
return (
getattr(expr, 'connector', None) == 'AND' and
len(expr.children) == 1 and
expr.children[0][0] in ['pk__in', '{}__in'.format(gr_name)]
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.
Rewrites Q-expression to safe form, in order to ensure that
generated SQL is valid.
IGNORE:
Suppose we have the following Q-expression:
IGNORE:
Suppose we have the following Q-expression:
OR
AND
eav_values__in [1, 2, 3]
AND (1)
AND
eav_values__in [4, 5]
AND
eav_values__in [6, 7, 8]
IGNORE
OR
AND
eav_values__in [1, 2, 3]
AND (1)
AND
eav_values__in [4, 5]
AND
eav_values__in [6, 7, 8]
IGNORE
All EAV values are stored in a single table. Therefore, INNER JOIN
generated for the AND-expression (1) will always fail, i.e.
single row in a eav_values table cannot be both in two disjoint sets at
the same time (and the whole point of using AND, usually, is two have
two different sets). Therefore, we must paritially rewrite the
expression so that the generated SQL is valid::
All EAV values are stored in a single table. Therefore, INNER JOIN
generated for the AND-expression (1) will always fail, i.e.
single row in a eav_values table cannot be both in two disjoint sets at
the same time (and the whole point of using AND, usually, is two have
two different sets). Therefore, we must paritially rewrite the
expression so that the generated SQL is valid::
IGNORE:
OR
AND
eav_values__in [1, 2, 3]
AND
pk__in [1, 2]
IGNORE
IGNORE:
OR
AND
eav_values__in [1, 2, 3]
AND
pk__in [1, 2]
IGNORE
This is done by merging dangerous AND's and substituting them with
explicit ``pk__in`` filter, where pks are taken from evaluted
Q-expr branch.
This is done by merging dangerous AND's and substituting them with
explicit ``pk__in`` filter, where pks are taken from evaluted
Q-expr branch.
Args:
model_cls (TypeVar): model class used to construct :meth:`QuerySet`
from leaf attribute-value expression.
expr: (Q | tuple): Q-expression (or attr-val leaf) to be rewritten.
Args:
model_cls (TypeVar): model class used to construct :meth:`QuerySet`
from leaf attribute-value expression.
expr: (Q | tuple): Q-expression (or attr-val leaf) to be rewritten.
Returns:
Union[Q, tuple]
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.
@ -162,6 +162,7 @@ def eav_filter(func):
: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 = []
@ -244,10 +245,7 @@ def expand_eav_filter(model_cls, key, value):
else:
lookup = '__{}'.format(fields[2]) if len(fields) > 2 else ''
value_key = 'value_{}{}'.format(datatype, lookup)
kwargs = {
value_key: value,
'attribute__slug': slug
}
kwargs = {value_key: value, 'attribute__slug': slug}
value = Value.objects.filter(**kwargs)
return '%s__in' % gr_name, value
@ -317,20 +315,25 @@ class EavQuerySet(QuerySet):
field_name = 'value_%s' % attr.datatype
pks_values = Value.objects.filter(
# Retrieve pk-values pairs of the related values
# (i.e. values for the specified attribute and
# belonging to entities in the queryset).
attribute__slug=attr.slug,
entity_id__in=self
).order_by(
# Order values by their value-field of
# appriopriate attribute data-type.
field_name
).values_list(
# Retrieve only primary-keys of the entities
# in the current queryset.
'entity_id', field_name
pks_values = (
Value.objects.filter(
# Retrieve pk-values pairs of the related values
# (i.e. values for the specified attribute and
# belonging to entities in the queryset).
attribute__slug=attr.slug,
entity_id__in=self,
)
.order_by(
# Order values by their value-field of
# appriopriate attribute data-type.
field_name
)
.values_list(
# Retrieve only primary-keys of the entities
# in the current queryset.
'entity_id',
field_name,
)
)
# Retrive ordered values from pk-value list.
@ -353,29 +356,20 @@ class EavQuerySet(QuerySet):
# WHEN id = 4 THEN 3
# END
#
when_clauses = [
When(id=pk, then=i)
for pk, i in entities_pk
]
when_clauses = [When(id=pk, then=i) for pk, i in entities_pk]
order_clause = Case(
*when_clauses,
output_field=IntegerField()
)
order_clause = Case(*when_clauses, output_field=IntegerField())
clause_name = '__'.join(term)
# Use when-clause to construct
# custom order-by clause.
query_clause = query_clause.annotate(
**{clause_name: order_clause}
)
query_clause = query_clause.annotate(**{clause_name: order_clause})
order_clauses.append(clause_name)
elif len(term) >= 2 and term[0] == config_cls.eav_attr:
raise NotSupportedError(
'EAV does not support ordering through '
'foreign-key chains'
'EAV does not support ordering through ' 'foreign-key chains'
)
else:

View file

@ -26,6 +26,7 @@ class EavConfig(object):
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'
@ -60,8 +61,7 @@ class Registry(object):
return
if config_cls is EavConfig or config_cls is None:
config_cls = type("%sConfig" % model_cls.__name__,
(EavConfig,), {})
config_cls = type("%sConfig" % model_cls.__name__, (EavConfig,), {})
# set _eav_config_cls on the model so we can access it there
setattr(model_cls, '_eav_config_cls', config_cls)
@ -125,9 +125,9 @@ class Registry(object):
delattr(self.model_cls, self.config_cls.manager_attr)
if hasattr(self.config_cls, 'old_mgr'):
self.config_cls.old_mgr \
.contribute_to_class(self.model_cls,
self.config_cls.manager_attr)
self.config_cls.old_mgr.contribute_to_class(
self.model_cls, self.config_cls.manager_attr
)
def _attach_signals(self):
"""
@ -136,31 +136,33 @@ class Registry(object):
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)
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)
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__
rel_name = (
self.config_cls.generic_relation_related_name or self.model_cls.__name__
)
gr_name = self.config_cls.generic_relation_attr.lower()
generic_relation = \
generic.GenericRelation(Value,
object_id_field='entity_id',
content_type_field='entity_ct',
related_query_name=rel_name)
generic_relation = generic.GenericRelation(
Value,
object_id_field='entity_id',
content_type_field='entity_ct',
related_query_name=rel_name,
)
generic_relation.contribute_to_class(self.model_cls, gr_name)
def _detach_generic_relation(self):

View file

@ -51,7 +51,9 @@ 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):
if not isinstance(value, datetime.datetime) and not isinstance(
value, datetime.date
):
raise ValidationError(_(u"Must be a date or datetime"))

View file

@ -2,13 +2,14 @@ from django.forms.widgets import Textarea
from django.core.exceptions import ValidationError
from django.core import validators
EMPTY_VALUES = validators.EMPTY_VALUES + ('[]', )
EMPTY_VALUES = validators.EMPTY_VALUES + ('[]',)
class CSVWidget(Textarea):
is_hidden = False
def prep_value(self, value):
""" Prepare value before effectively render widget """
"""Prepare value before effectively render widget"""
if value in EMPTY_VALUES:
return ""
elif isinstance(value, str):
@ -20,4 +21,3 @@ class CSVWidget(Textarea):
def render(self, name, value, **kwargs):
value = self.prep_value(value)
return super().render(name, value, **kwargs)