mirror of
https://github.com/jazzband/django-eav2.git
synced 2026-03-16 22:40:26 +00:00
Reorganized dir
This commit is contained in:
parent
0264919f88
commit
8744714574
19 changed files with 1 additions and 1612 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@
|
|||
*.orig
|
||||
*.db
|
||||
*.sqlite*
|
||||
_build
|
||||
|
|
|
|||
35
__init__.py
35
__init__.py
|
|
@ -1,35 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
|
||||
#
|
||||
# This software is derived from EAV-Django originally written and
|
||||
# copyrighted by Andrey Mikhaylenko <http://pypi.python.org/pypi/eav-django>
|
||||
#
|
||||
# This is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with EAV-Django. If not, see <http://gnu.org/licenses/>.
|
||||
VERSION = (0, 9, 1)
|
||||
|
||||
def get_version():
|
||||
version = "%s.%s" % (VERSION[0], VERSION[1])
|
||||
if VERSION[2] != 0:
|
||||
version = "%s.%s" % (version, VERSION[2])
|
||||
return version
|
||||
|
||||
__version__ = get_version()
|
||||
|
||||
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)
|
||||
105
admin.py
105
admin.py
|
|
@ -1,105 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
|
||||
#
|
||||
# This software is derived from EAV-Django originally written and
|
||||
# copyrighted by Andrey Mikhaylenko <http://pypi.python.org/pypi/eav-django>
|
||||
#
|
||||
# This is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with EAV-Django. If not, see <http://gnu.org/licenses/>.
|
||||
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.options import (
|
||||
ModelAdmin, InlineModelAdmin, StackedInline
|
||||
)
|
||||
from django.forms.models import BaseInlineFormSet
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from .models import Attribute, Value, EnumValue, EnumGroup
|
||||
|
||||
|
||||
class BaseEntityAdmin(ModelAdmin):
|
||||
|
||||
def render_change_form(self, request, context, **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.
|
||||
"""
|
||||
form = context['adminform'].form
|
||||
|
||||
# infer correct data from the form
|
||||
fieldsets = [(None, {'fields': form.fields.keys()})]
|
||||
adminform = admin.helpers.AdminForm(form, fieldsets,
|
||||
self.prepopulated_fields)
|
||||
media = mark_safe(self.media + adminform.media)
|
||||
|
||||
context.update(adminform=adminform, media=media)
|
||||
|
||||
super_meth = super(BaseEntityAdmin, self).render_change_form
|
||||
return super_meth(request, context, **kwargs)
|
||||
|
||||
|
||||
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)
|
||||
form._build_dynamic_fields()
|
||||
super(BaseEntityInlineFormSet, self).add_fields(form, index)
|
||||
|
||||
|
||||
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.::
|
||||
|
||||
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).
|
||||
|
||||
"""
|
||||
formset = BaseEntityInlineFormSet
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
if self.declared_fieldsets:
|
||||
return self.declared_fieldsets
|
||||
|
||||
formset = self.get_formset(request)
|
||||
fk_name = self.fk_name or formset.fk.name
|
||||
kw = {fk_name: obj} if obj else {}
|
||||
instance = self.model(**kw)
|
||||
form = formset.form(request.POST, instance=instance)
|
||||
|
||||
return [(None, {'fields': form.fields.keys()})]
|
||||
|
||||
class AttributeAdmin(ModelAdmin):
|
||||
list_display = ('name', 'slug', 'datatype', 'description')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
|
||||
admin.site.register(Attribute, AttributeAdmin)
|
||||
admin.site.register(Value)
|
||||
admin.site.register(EnumValue)
|
||||
admin.site.register(EnumGroup)
|
||||
|
||||
0
docs/_static/.gitignore
vendored
Normal file
0
docs/_static/.gitignore
vendored
Normal file
0
docs/_templates/.gitignore
vendored
Normal file
0
docs/_templates/.gitignore
vendored
Normal file
75
fields.py
75
fields.py
|
|
@ -1,75 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
|
||||
#
|
||||
# This software is derived from EAV-Django originally written and
|
||||
# copyrighted by Andrey Mikhaylenko <http://pypi.python.org/pypi/eav-django>
|
||||
#
|
||||
# This is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with EAV-Django. If not, see <http://gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
import re
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class EavSlugField(models.SlugField):
|
||||
|
||||
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 db.
|
||||
"""
|
||||
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."))
|
||||
|
||||
@staticmethod
|
||||
def create_slug_from_name(name):
|
||||
'''
|
||||
Creates a slug based on the name
|
||||
'''
|
||||
name = name.strip().lower()
|
||||
|
||||
# Change spaces to underscores
|
||||
name = '_'.join(name.split())
|
||||
|
||||
# Remove non alphanumeric characters
|
||||
return re.sub('[^\w]', '', name)
|
||||
|
||||
|
||||
class EavDatatypeField(models.CharField):
|
||||
"""
|
||||
This holds checks for the attributes datatypes.
|
||||
"""
|
||||
|
||||
def validate(self, value, instance):
|
||||
"""
|
||||
We don't want them to be able to change the attribute type
|
||||
once it have been created.
|
||||
"""
|
||||
super(EavDatatypeField, self).validate(value, instance)
|
||||
from .models import Attribute
|
||||
if not instance.pk:
|
||||
return
|
||||
if instance.value_set.count():
|
||||
raise ValidationError(_(u"You cannot change the datatype of an "
|
||||
u"attribute that is already in use."))
|
||||
|
||||
121
forms.py
121
forms.py
|
|
@ -1,121 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
|
||||
#
|
||||
# This software is derived from EAV-Django originally written and
|
||||
# copyrighted by Andrey Mikhaylenko <http://pypi.python.org/pypi/eav-django>
|
||||
#
|
||||
# This is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with EAV-Django. If not, see <http://gnu.org/licenses/>.
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
# django
|
||||
from django.forms import BooleanField, CharField, DateTimeField, FloatField, \
|
||||
IntegerField, ModelForm, ChoiceField, ValidationError
|
||||
from django.contrib.admin.widgets import AdminSplitDateTime
|
||||
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), only static
|
||||
fields are used. However, on form validation the schema will be retrieved
|
||||
and EAV fields dynamically added to the form, so when the validation is
|
||||
actually done, all EAV fields are present in it (unless Rubric is not defined).
|
||||
"""
|
||||
|
||||
FIELD_CLASSES = {
|
||||
'text': CharField,
|
||||
'float': FloatField,
|
||||
'int': IntegerField,
|
||||
'date': DateTimeField,
|
||||
'bool': BooleanField,
|
||||
'enum': ChoiceField,
|
||||
}
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
super(BaseDynamicEntityForm, self).__init__(data, *args, **kwargs)
|
||||
config_cls = self.instance._eav_config_cls
|
||||
self.entity = getattr(self.instance, config_cls.eav_attr)
|
||||
self._build_dynamic_fields()
|
||||
|
||||
def _build_dynamic_fields(self):
|
||||
# reset form fields
|
||||
self.fields = deepcopy(self.base_fields)
|
||||
|
||||
for attribute in self.entity.get_all_attributes():
|
||||
value = getattr(self.entity, attribute.slug)
|
||||
|
||||
defaults = {
|
||||
'label': attribute.name.capitalize(),
|
||||
'required': attribute.required,
|
||||
'help_text': attribute.help_text,
|
||||
'validators': attribute.get_validators()
|
||||
}
|
||||
|
||||
datatype = attribute.datatype
|
||||
if datatype == attribute.TYPE_ENUM:
|
||||
enums = attribute.get_choices() \
|
||||
.values_list('id', 'value')
|
||||
|
||||
choices = [('', '-----')] + list(enums)
|
||||
|
||||
defaults.update({'choices': choices})
|
||||
if value:
|
||||
defaults.update({'initial': value.pk})
|
||||
|
||||
elif datatype == attribute.TYPE_DATE:
|
||||
defaults.update({'widget': AdminSplitDateTime})
|
||||
elif datatype == attribute.TYPE_OBJECT:
|
||||
continue
|
||||
|
||||
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: # m2m is already done above
|
||||
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``.
|
||||
"""
|
||||
|
||||
if self.errors:
|
||||
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)
|
||||
|
||||
# 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.enums.get(pk=value)
|
||||
else:
|
||||
value = None
|
||||
|
||||
setattr(self.entity, attribute.slug, value)
|
||||
|
||||
# save entity and its attributes
|
||||
if commit:
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
153
managers.py
153
managers.py
|
|
@ -1,153 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
|
||||
#
|
||||
# This software is derived from EAV-Django originally written and
|
||||
# copyrighted by Andrey Mikhaylenko <http://pypi.python.org/pypi/eav-django>
|
||||
#
|
||||
# This is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with EAV-Django. If not, see <http://gnu.org/licenses/>.
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from django.db import models
|
||||
|
||||
from .models import Attribute, Value
|
||||
|
||||
def eav_filter(func):
|
||||
'''
|
||||
Decorator used to wrap filter and exlclude methods. Passes args through
|
||||
expand_q_filters and kwargs through expand_eav_filter. Returns the
|
||||
called function (filter or exclude) .distinct()
|
||||
'''
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
new_args = []
|
||||
for arg in args:
|
||||
if isinstance(arg, models.Q):
|
||||
# modify Q objects (warning: recursion ahead)
|
||||
arg = expand_q_filters(arg, self.model)
|
||||
new_args.append(arg)
|
||||
|
||||
new_kwargs = {}
|
||||
for key, value in kwargs.items():
|
||||
# modify kwargs (warning: recursion ahead)
|
||||
new_key, new_value = expand_eav_filter(self.model, key, value)
|
||||
new_kwargs.update({new_key: new_value})
|
||||
|
||||
return func(self, *new_args, **new_kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def expand_q_filters(q, root_cls):
|
||||
'''
|
||||
Takes a Q object and a model class.
|
||||
Recursivley passes each filter / value in the Q object tree leaf nodes
|
||||
through expand_eav_filter
|
||||
'''
|
||||
new_children = []
|
||||
for qi in q.children:
|
||||
if type(qi) is tuple:
|
||||
# this child is a leaf node: in Q this is a 2-tuple of:
|
||||
# (filter parameter, value)
|
||||
key, value = expand_eav_filter(root_cls, *qi)
|
||||
new_children.append(models.Q(**{key: value}))
|
||||
else:
|
||||
# this child is another Q node: recursify!
|
||||
new_children.append(expand_q_filters(qi, root_cls))
|
||||
q.children = new_children
|
||||
return q
|
||||
|
||||
|
||||
def expand_eav_filter(model_cls, key, value):
|
||||
'''
|
||||
Accepts a model class and a key, value.
|
||||
Recurisively replaces any eav filter with a subquery.
|
||||
|
||||
For example:
|
||||
key = 'eav__height'
|
||||
value = 5
|
||||
Would return:
|
||||
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)
|
||||
if len(fields) > 1 and config_cls and \
|
||||
fields[0] == config_cls.eav_attr:
|
||||
slug = fields[1]
|
||||
gr_name = config_cls.generic_relation_attr
|
||||
datatype = Attribute.objects.get(slug=slug).datatype
|
||||
|
||||
lookup = '__%s' % fields[2] if len(fields) > 2 else ''
|
||||
kwargs = {'value_%s%s' % (datatype, lookup): value,
|
||||
'attribute__slug': slug}
|
||||
value = Value.objects.filter(**kwargs)
|
||||
|
||||
return '%s__in' % gr_name, value
|
||||
|
||||
try:
|
||||
field, m, direct, m2m = model_cls._meta.get_field_by_name(fields[0])
|
||||
except models.FieldDoesNotExist:
|
||||
return key, value
|
||||
|
||||
if direct:
|
||||
return key, value
|
||||
else:
|
||||
sub_key = '__'.join(fields[1:])
|
||||
key, value = expand_eav_filter(field.model, sub_key, value)
|
||||
return '%s__%s' % (fields[0], key), value
|
||||
|
||||
|
||||
class EntityManager(models.Manager):
|
||||
@eav_filter
|
||||
def filter(self, *args, **kwargs):
|
||||
return super(EntityManager, self).filter(*args, **kwargs).distinct()
|
||||
|
||||
@eav_filter
|
||||
def exclude(self, *args, **kwargs):
|
||||
return super(EntityManager, self).exclude(*args, **kwargs).distinct()
|
||||
|
||||
@eav_filter
|
||||
def get(self, *args, **kwargs):
|
||||
return super(EntityManager, self).get(*args, **kwargs)
|
||||
|
||||
def create(self, **kwargs):
|
||||
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.iteritems():
|
||||
if key.startswith(prefix):
|
||||
eav_kwargs.update({key[len(prefix):]: value})
|
||||
else:
|
||||
new_kwargs.update({key: value})
|
||||
|
||||
obj = self.model(**new_kwargs)
|
||||
obj_eav = getattr(obj, config_cls.eav_attr)
|
||||
for key, value in eav_kwargs.iteritems():
|
||||
setattr(obj_eav, key, value)
|
||||
obj.save()
|
||||
return obj
|
||||
|
||||
def get_or_create(self, **kwargs):
|
||||
try:
|
||||
return self.get(**kwargs)
|
||||
except self.model.DoesNotExist:
|
||||
return self.create(**kwargs)
|
||||
361
models.py
361
models.py
|
|
@ -1,361 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
|
||||
#
|
||||
# This software is derived from EAV-Django originally written and
|
||||
# copyrighted by Andrey Mikhaylenko <http://pypi.python.org/pypi/eav-django>
|
||||
#
|
||||
# This is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with EAV-Django. If not, see <http://gnu.org/licenses/>.
|
||||
'''
|
||||
.. automodule:: models
|
||||
:members:
|
||||
|
||||
This is my models file
|
||||
'''
|
||||
|
||||
import inspect
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.conf import settings
|
||||
|
||||
from .validators import *
|
||||
from .fields import EavSlugField, EavDatatypeField
|
||||
|
||||
|
||||
class EnumValue(models.Model):
|
||||
value = models.CharField(_(u"value"), db_index=True,
|
||||
unique=True, max_length=50)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class EnumGroup(models.Model):
|
||||
name = models.CharField(_(u"name"), unique=True, max_length=100)
|
||||
|
||||
enums = models.ManyToManyField(EnumValue, verbose_name=_(u"enum group"))
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Attribute(models.Model):
|
||||
'''
|
||||
The A model in E-A-V. This holds the 'concepts' along with the data type
|
||||
something like:
|
||||
|
||||
>>> Attribute.objects.create(name='Height', datatype='float')
|
||||
<Attribute: Height (Float)>
|
||||
|
||||
>>> Attribute.objects.create(name='Color', datatype='text', slug='color')
|
||||
<Attribute: Color (Text)>
|
||||
'''
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
TYPE_TEXT = 'text'
|
||||
TYPE_FLOAT = 'float'
|
||||
TYPE_INT = 'int'
|
||||
TYPE_DATE = 'date'
|
||||
TYPE_BOOLEAN = 'bool'
|
||||
TYPE_OBJECT = 'object'
|
||||
TYPE_ENUM = 'enum'
|
||||
|
||||
DATATYPE_CHOICES = (
|
||||
(TYPE_TEXT, _(u"Text")),
|
||||
(TYPE_FLOAT, _(u"Float")),
|
||||
(TYPE_INT, _(u"Integer")),
|
||||
(TYPE_DATE, _(u"Date")),
|
||||
(TYPE_BOOLEAN, _(u"True / False")),
|
||||
(TYPE_OBJECT, _(u"Django Object")),
|
||||
(TYPE_ENUM, _(u"Multiple Choice")),
|
||||
)
|
||||
|
||||
name = models.CharField(_(u"name"), max_length=100,
|
||||
help_text=_(u"User-friendly attribute name"))
|
||||
|
||||
slug = EavSlugField(_(u"slug"), max_length=50, db_index=True,
|
||||
help_text=_(u"Short unique attribute label"),
|
||||
unique=True)
|
||||
|
||||
description = models.CharField(_(u"description"), max_length=256,
|
||||
blank=True, null=True,
|
||||
help_text=_(u"Short description"))
|
||||
|
||||
enum_group = models.ForeignKey(EnumGroup, verbose_name=_(u"choice group"),
|
||||
blank=True, null=True)
|
||||
|
||||
@property
|
||||
def help_text(self):
|
||||
return self.description
|
||||
|
||||
datatype = EavDatatypeField(_(u"data type"), max_length=6,
|
||||
choices=DATATYPE_CHOICES)
|
||||
|
||||
created = models.DateTimeField(_(u"created"), default=datetime.now,
|
||||
editable=False)
|
||||
|
||||
modified = models.DateTimeField(_(u"modified"), auto_now=True)
|
||||
|
||||
required = models.BooleanField(_(u"required"), default=False)
|
||||
|
||||
|
||||
def get_validators(self):
|
||||
DATATYPE_VALIDATORS = {
|
||||
'text': validate_text,
|
||||
'float': validate_float,
|
||||
'int': validate_int,
|
||||
'date': validate_date,
|
||||
'bool': validate_bool,
|
||||
'object': validate_object,
|
||||
'enum': validate_enum,
|
||||
}
|
||||
|
||||
validation_function = DATATYPE_VALIDATORS[self.datatype]
|
||||
return [validation_function]
|
||||
|
||||
def validate_value(self, value):
|
||||
for validator in self.get_validators():
|
||||
validator(value)
|
||||
if self.datatype == self.TYPE_ENUM:
|
||||
if value not in self.enum_group.enums.all():
|
||||
raise ValidationError(_(u"%(enum)s is not a valid choice "
|
||||
u"for %(attr)s") % \
|
||||
{'enum': value, 'attr': self})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = EavSlugField.create_slug_from_name(self.name)
|
||||
self.full_clean()
|
||||
super(Attribute, self).save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
if self.datatype == self.TYPE_ENUM and not self.enum_group:
|
||||
raise ValidationError(_(
|
||||
u"You must set the choice group for multiple choice" \
|
||||
u"attributes"))
|
||||
|
||||
if self.datatype != self.TYPE_ENUM and self.enum_group:
|
||||
raise ValidationError(_(
|
||||
u"You can only assign a choice group to multiple choice " \
|
||||
u"attributes"))
|
||||
|
||||
def get_choices(self):
|
||||
'''
|
||||
Returns the avilable choices for enums.
|
||||
'''
|
||||
if not self.datatype == Attribute.TYPE_ENUM:
|
||||
return None
|
||||
return self.enum_group.enums.all()
|
||||
|
||||
def save_value(self, entity, value):
|
||||
ct = ContentType.objects.get_for_model(entity)
|
||||
try:
|
||||
value_obj = self.value_set.get(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)
|
||||
if value == None or value == '':
|
||||
value_obj.delete()
|
||||
return
|
||||
|
||||
if value != value_obj.value:
|
||||
value_obj.value = value
|
||||
value_obj.save()
|
||||
|
||||
|
||||
def __unicode__(self):
|
||||
return u"%s (%s)" % (self.name, self.get_datatype_display())
|
||||
|
||||
|
||||
class Value(models.Model):
|
||||
'''
|
||||
The V model in E-A-V. This holds the 'value' for an attribute and an
|
||||
entity:
|
||||
|
||||
>>> from django.db import models
|
||||
>>> from django.contrib.auth.models import User
|
||||
>>> from .registry import Registry
|
||||
>>> Registry.register(User)
|
||||
>>> u = User.objects.create(username='crazy_dev_user')
|
||||
>>> a = Attribute.objects.create(name='Favorite Drink', datatype='text',
|
||||
... slug='fav_drink')
|
||||
>>> Value.objects.create(entity=u, attribute=a, value_text='red bull')
|
||||
<Value: crazy_dev_user - Favorite Drink: "red bull">
|
||||
|
||||
'''
|
||||
|
||||
class Meta:
|
||||
unique_together = ('entity_ct', 'entity_id', 'attribute')
|
||||
|
||||
|
||||
entity_ct = models.ForeignKey(ContentType, related_name='value_entities')
|
||||
entity_id = models.IntegerField()
|
||||
entity = generic.GenericForeignKey(ct_field='entity_ct',
|
||||
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.NullBooleanField(blank=True, null=True)
|
||||
value_enum = models.ForeignKey(EnumValue, blank=True, null=True,
|
||||
related_name='eav_values')
|
||||
|
||||
generic_value_id = models.IntegerField(blank=True, null=True)
|
||||
generic_value_ct = models.ForeignKey(ContentType, blank=True, null=True,
|
||||
related_name='value_values')
|
||||
value_object = generic.GenericForeignKey(ct_field='generic_value_ct',
|
||||
fk_field='generic_value_id')
|
||||
|
||||
created = models.DateTimeField(_(u"created"), default=datetime.now)
|
||||
modified = models.DateTimeField(_(u"modified"), auto_now=True)
|
||||
|
||||
attribute = models.ForeignKey(Attribute, db_index=True,
|
||||
verbose_name=_(u"attribute"))
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
super(Value, self).save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
if self.attribute.datatype == Attribute.TYPE_ENUM and \
|
||||
self.value_enum:
|
||||
if self.value_enum not in self.attribute.enum_group.enums.all():
|
||||
raise ValidationError(_(u"%(choice)s is not a valid " \
|
||||
u"choice for %s(attribute)") % \
|
||||
{'choice': self.value_enum,
|
||||
'attribute': self.attribute})
|
||||
|
||||
# TODO: Remove
|
||||
def _blank(self):
|
||||
"""
|
||||
Set all the field to none
|
||||
"""
|
||||
for field in self._meta.fields:
|
||||
if field.name.startswith('value_') and field.null == True:
|
||||
setattr(self, field.name, None)
|
||||
|
||||
def _get_value(self):
|
||||
"""
|
||||
Get returns the Python object hold by this Value object.
|
||||
"""
|
||||
return getattr(self, 'value_%s' % self.attribute.datatype)
|
||||
|
||||
|
||||
def _set_value(self, new_value):
|
||||
self._blank()
|
||||
setattr(self, 'value_%s' % self.attribute.datatype, new_value)
|
||||
|
||||
value = property(_get_value, _set_value)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"%s - %s: \"%s\"" % (self.entity, self.attribute.name, self.value)
|
||||
|
||||
|
||||
class Entity(object):
|
||||
|
||||
def __init__(self, instance):
|
||||
self.model = instance
|
||||
self.ct = ContentType.objects.get_for_model(instance)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if not name.startswith('_'):
|
||||
try:
|
||||
attribute = self.get_attribute_by_slug(name)
|
||||
except Attribute.DoesNotExist:
|
||||
raise AttributeError(_(u"%(obj)s has no EAV attribute named " \
|
||||
u"'%(attr)s'") % \
|
||||
{'obj':self.model, 'attr':name})
|
||||
try:
|
||||
return self.get_value_by_attribute(attribute).value
|
||||
except Value.DoesNotExist:
|
||||
return None
|
||||
return getattr(super(Entity, self), name)
|
||||
|
||||
def get_all_attributes(self):
|
||||
return self.model._eav_config_cls.get_attributes()
|
||||
|
||||
def save(self):
|
||||
for attribute in self.get_all_attributes():
|
||||
if hasattr(self, attribute.slug):
|
||||
attribute_value = getattr(self, attribute.slug)
|
||||
attribute.save_value(self.model, attribute_value)
|
||||
|
||||
def validate_attributes(self):
|
||||
for attribute in self.get_all_attributes():
|
||||
value = getattr(self, attribute.slug, None)
|
||||
if value is None:
|
||||
if attribute.required:
|
||||
raise ValidationError(_(u"%(attr)s EAV field cannot " \
|
||||
u"be blank") % \
|
||||
{'attr': attribute.slug})
|
||||
else:
|
||||
try:
|
||||
attribute.validate_value(value)
|
||||
except ValidationError, e:
|
||||
raise ValidationError(_(u"%(attr)s EAV field %(err)s") % \
|
||||
{'attr': attribute.slug,
|
||||
'err': e})
|
||||
|
||||
def get_values(self):
|
||||
'''
|
||||
Get all set EAV Value objects for self.model
|
||||
'''
|
||||
return Value.objects.filter(entity_ct=self.ct,
|
||||
entity_id=self.model.pk).select_related()
|
||||
|
||||
def get_all_attribute_slugs(self):
|
||||
return self.get_all_attributes().values_list('slug', Flat=True)
|
||||
|
||||
def get_attribute_by_slug(self, slug):
|
||||
return self.get_all_attributes().get(slug=slug)
|
||||
|
||||
def get_value_by_attribute(self, attribute):
|
||||
return self.get_values().get(attribute=attribute)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.get_values())
|
||||
|
||||
@staticmethod
|
||||
def post_save_handler(sender, *args, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
entity = getattr(instance, instance._eav_config_cls.eav_attr)
|
||||
entity.save()
|
||||
|
||||
@staticmethod
|
||||
def pre_save_handler(sender, *args, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
entity = getattr(kwargs['instance'], instance._eav_config_cls.eav_attr)
|
||||
entity.validate_attributes()
|
||||
|
||||
if 'django_nose' in settings.INSTALLED_APPS:
|
||||
'''
|
||||
The django_nose test runner won't automatically create our Patient model
|
||||
database table which is required for tests, unless we import it here.
|
||||
|
||||
Please, someone tell me a better way to do this.
|
||||
'''
|
||||
from .tests.models import Patient, Encounter
|
||||
146
registry.py
146
registry.py
|
|
@ -1,146 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
|
||||
#
|
||||
# This software is derived from EAV-Django originally written and
|
||||
# copyrighted by Andrey Mikhaylenko <http://pypi.python.org/pypi/eav-django>
|
||||
#
|
||||
# This is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with EAV-Django. If not, see <http://gnu.org/licenses/>.
|
||||
|
||||
from django.db.utils import DatabaseError
|
||||
from django.db.models.signals import pre_init, post_init, pre_save, post_save
|
||||
from django.contrib.contenttypes import generic
|
||||
|
||||
from .managers import EntityManager
|
||||
from .models import Entity, Attribute, Value
|
||||
|
||||
|
||||
class EavConfig(Entity):
|
||||
|
||||
manager_attr ='objects'
|
||||
manager_only = False
|
||||
eav_attr = 'eav'
|
||||
generic_relation_attr = 'eav_values'
|
||||
generic_relation_related_name = None
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
"""
|
||||
By default, all attributes apply to an entity,
|
||||
unless otherwise specified.
|
||||
"""
|
||||
return Attribute.objects.all()
|
||||
|
||||
|
||||
class Registry(object):
|
||||
@staticmethod
|
||||
def register(model_cls, config_cls=None):
|
||||
if hasattr(model_cls, '_eav_config_cls'):
|
||||
return
|
||||
|
||||
if config_cls is EavConfig or config_cls is None:
|
||||
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)
|
||||
|
||||
reg = Registry(model_cls)
|
||||
reg._register_self()
|
||||
|
||||
@staticmethod
|
||||
def unregister(model_cls):
|
||||
if not getattr(model_cls, '_eav_config_cls', None):
|
||||
return
|
||||
reg = Registry(model_cls)
|
||||
reg._unregister_self()
|
||||
|
||||
delattr(model_cls, '_eav_config_cls')
|
||||
|
||||
|
||||
@staticmethod
|
||||
def attach_eav_attr(sender, *args, **kwargs):
|
||||
'''
|
||||
Attache 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):
|
||||
self.model_cls = model_cls
|
||||
self.config_cls = model_cls._eav_config_cls
|
||||
|
||||
def _attach_manager(self):
|
||||
# save the old manager if the attribute name conflict with the new one
|
||||
if hasattr(self.model_cls, self.config_cls.manager_attr):
|
||||
mgr = getattr(self.model_cls, self.config_cls.manager_attr)
|
||||
self.config_cls.old_mgr = mgr
|
||||
|
||||
# attache the new manager to the model
|
||||
mgr = EntityManager()
|
||||
mgr.contribute_to_class(self.model_cls, self.config_cls.manager_attr)
|
||||
|
||||
def _detach_manager(self):
|
||||
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)
|
||||
|
||||
def _attach_signals(self):
|
||||
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):
|
||||
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):
|
||||
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_name=rel_name)
|
||||
generic_relation.contribute_to_class(self.model_cls, gr_name)
|
||||
|
||||
|
||||
def _detach_generic_relation(self):
|
||||
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:
|
||||
self.model_cls._meta.local_many_to_many.remove(field)
|
||||
break
|
||||
|
||||
delattr(self.model_cls, gen_rel_field)
|
||||
|
||||
|
||||
def _register_self(self):
|
||||
self._attach_manager()
|
||||
|
||||
if not self.config_cls.manager_only:
|
||||
self._attach_signals()
|
||||
self._attach_generic_relation()
|
||||
|
||||
def _unregister_self(self):
|
||||
self._detach_manager()
|
||||
|
||||
if not self.config_cls.manager_only:
|
||||
self._detach_signals()
|
||||
self._detach_generic_relation()
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from .registry import *
|
||||
from .limiting_attributes import *
|
||||
from .data_validation import *
|
||||
from .misc_models import *
|
||||
from .queries import *
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
from datetime import datetime, date
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
import eav
|
||||
from ..registry import EavConfig
|
||||
from ..models import Attribute, Value, EnumValue, EnumGroup
|
||||
|
||||
from .models import Patient, Encounter
|
||||
|
||||
|
||||
class DataValidation(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
eav.register(Patient)
|
||||
|
||||
Attribute.objects.create(name='Age', datatype=Attribute.TYPE_INT)
|
||||
Attribute.objects.create(name='DoB', datatype=Attribute.TYPE_DATE)
|
||||
Attribute.objects.create(name='Height', datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name='City', datatype=Attribute.TYPE_TEXT)
|
||||
Attribute.objects.create(name='Pregnant?', datatype=Attribute.TYPE_BOOLEAN)
|
||||
Attribute.objects.create(name='User', datatype=Attribute.TYPE_OBJECT)
|
||||
|
||||
def tearDown(self):
|
||||
eav.unregister(Patient)
|
||||
|
||||
def test_required_field(self):
|
||||
p = Patient(name='Bob')
|
||||
p.eav.age = 5
|
||||
p.save()
|
||||
|
||||
Attribute.objects.create(name='Weight', datatype=Attribute.TYPE_INT, required=True)
|
||||
p.eav.age = 6
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p = Patient.objects.get(name='Bob')
|
||||
self.assertEqual(p.eav.age, 5)
|
||||
p.eav.weight = 23
|
||||
p.save()
|
||||
p = Patient.objects.get(name='Bob')
|
||||
self.assertEqual(p.eav.weight, 23)
|
||||
|
||||
def test_create_required_field(self):
|
||||
Attribute.objects.create(name='Weight', datatype=Attribute.TYPE_INT, required=True)
|
||||
self.assertRaises(ValidationError,
|
||||
Patient.objects.create,
|
||||
name='Joe', eav__age=5)
|
||||
self.assertEqual(Patient.objects.count(), 0)
|
||||
self.assertEqual(Value.objects.count(), 0)
|
||||
|
||||
p = Patient.objects.create(name='Joe', eav__weight=2, eav__age=5)
|
||||
self.assertEqual(Patient.objects.count(), 1)
|
||||
self.assertEqual(Value.objects.count(), 2)
|
||||
|
||||
def test_validation_error_create(self):
|
||||
self.assertRaises(ValidationError,
|
||||
Patient.objects.create,
|
||||
name='Joe', eav__age='df')
|
||||
self.assertEqual(Patient.objects.count(), 0)
|
||||
self.assertEqual(Value.objects.count(), 0)
|
||||
|
||||
def test_bad_slug(self):
|
||||
a = Attribute.objects.create(name='color', datatype=Attribute.TYPE_TEXT)
|
||||
a.slug = 'Color'
|
||||
self.assertRaises(ValidationError, a.save)
|
||||
a.slug = '1st'
|
||||
self.assertRaises(ValidationError, a.save)
|
||||
a.slug = '_st'
|
||||
self.assertRaises(ValidationError, a.save)
|
||||
|
||||
def test_changing_datatypes(self):
|
||||
a = Attribute.objects.create(name='Color', datatype=Attribute.TYPE_INT)
|
||||
a.datatype = Attribute.TYPE_TEXT
|
||||
a.save()
|
||||
Patient.objects.create(name='Bob', eav__color='brown')
|
||||
a.datatype = Attribute.TYPE_INT
|
||||
self.assertRaises(ValidationError, a.save)
|
||||
|
||||
def test_int_validation(self):
|
||||
p = Patient.objects.create(name='Joe')
|
||||
p.eav.age = 'bad'
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.age = 15
|
||||
p.save()
|
||||
self.assertEqual(Patient.objects.get(pk=p.pk).eav.age, 15)
|
||||
|
||||
def test_date_validation(self):
|
||||
p = Patient.objects.create(name='Joe')
|
||||
p.eav.dob = 'bad'
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.dob = 15
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
now = datetime.now()
|
||||
now = datetime(year=now.year, month=now.month, day=now.day,
|
||||
hour=now.hour, minute=now.minute, second=now.second)
|
||||
p.eav.dob = now
|
||||
p.save()
|
||||
self.assertEqual(Patient.objects.get(pk=p.pk).eav.dob, now)
|
||||
today = date.today()
|
||||
p.eav.dob = today
|
||||
p.save()
|
||||
self.assertEqual(Patient.objects.get(pk=p.pk).eav.dob.date(), today)
|
||||
|
||||
def test_float_validation(self):
|
||||
p = Patient.objects.create(name='Joe')
|
||||
p.eav.height = 'bad'
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.height = 15
|
||||
p.save()
|
||||
self.assertEqual(Patient.objects.get(pk=p.pk).eav.height, 15)
|
||||
p.eav.height='2.3'
|
||||
p.save()
|
||||
self.assertEqual(Patient.objects.get(pk=p.pk).eav.height, 2.3)
|
||||
|
||||
def test_text_validation(self):
|
||||
p = Patient.objects.create(name='Joe')
|
||||
p.eav.city = 5
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.city = 'El Dorado'
|
||||
p.save()
|
||||
self.assertEqual(Patient.objects.get(pk=p.pk).eav.city, 'El Dorado')
|
||||
|
||||
def test_bool_validation(self):
|
||||
p = Patient.objects.create(name='Joe')
|
||||
p.eav.pregnant = 5
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.pregnant = True
|
||||
p.save()
|
||||
self.assertEqual(Patient.objects.get(pk=p.pk).eav.pregnant, True)
|
||||
|
||||
def test_object_validation(self):
|
||||
p = Patient.objects.create(name='Joe')
|
||||
p.eav.user = 5
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.user = object
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.user = User(username='joe')
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
u = User.objects.create(username='joe')
|
||||
p.eav.user = u
|
||||
p.save()
|
||||
self.assertEqual(Patient.objects.get(pk=p.pk).eav.user, u)
|
||||
|
||||
def test_enum_validation(self):
|
||||
yes = EnumValue.objects.create(value='yes')
|
||||
no = EnumValue.objects.create(value='no')
|
||||
unkown = EnumValue.objects.create(value='unkown')
|
||||
green = EnumValue.objects.create(value='green')
|
||||
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
|
||||
ynu.enums.add(yes)
|
||||
ynu.enums.add(no)
|
||||
ynu.enums.add(unkown)
|
||||
Attribute.objects.create(name='Fever?', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
|
||||
|
||||
p = Patient.objects.create(name='Joe')
|
||||
p.eav.fever = 5
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.fever = object
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.fever = 'yes'
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.fever = green
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.fever = EnumValue(value='yes')
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.fever = no
|
||||
p.save()
|
||||
self.assertEqual(Patient.objects.get(pk=p.pk).eav.fever, no)
|
||||
|
||||
def test_enum_datatype_without_enum_group(self):
|
||||
a = Attribute(name='Age Bracket', datatype=Attribute.TYPE_ENUM)
|
||||
self.assertRaises(ValidationError, a.save)
|
||||
yes = EnumValue.objects.create(value='yes')
|
||||
no = EnumValue.objects.create(value='no')
|
||||
unkown = EnumValue.objects.create(value='unkown')
|
||||
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
|
||||
ynu.enums.add(yes)
|
||||
ynu.enums.add(no)
|
||||
ynu.enums.add(unkown)
|
||||
a = Attribute(name='Age Bracket', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
|
||||
a.save()
|
||||
|
||||
def test_enum_group_on_other_datatype(self):
|
||||
yes = EnumValue.objects.create(value='yes')
|
||||
no = EnumValue.objects.create(value='no')
|
||||
unkown = EnumValue.objects.create(value='unkown')
|
||||
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
|
||||
ynu.enums.add(yes)
|
||||
ynu.enums.add(no)
|
||||
ynu.enums.add(unkown)
|
||||
a = Attribute(name='color', datatype=Attribute.TYPE_TEXT, enum_group=ynu)
|
||||
self.assertRaises(ValidationError, a.save)
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
import eav
|
||||
from ..registry import EavConfig
|
||||
from ..models import Attribute, Value
|
||||
|
||||
from .models import Patient, Encounter
|
||||
|
||||
|
||||
class LimittingAttributes(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
class EncounterEavConfig(EavConfig):
|
||||
manager_attr = 'eav_objects'
|
||||
eav_attr = 'eav_field'
|
||||
generic_relation_attr = 'encounter_eav_values'
|
||||
generic_relation_related_name = 'encounters'
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
return Attribute.objects.filter(slug__contains='a')
|
||||
|
||||
eav.register(Encounter, EncounterEavConfig)
|
||||
eav.register(Patient)
|
||||
|
||||
Attribute.objects.create(name='age', datatype=Attribute.TYPE_INT)
|
||||
Attribute.objects.create(name='height', datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name='weight', datatype=Attribute.TYPE_FLOAT)
|
||||
|
||||
def tearDown(self):
|
||||
eav.unregister(Encounter)
|
||||
eav.unregister(Patient)
|
||||
|
||||
def test_get_attribute_querysets(self):
|
||||
self.assertEqual(Patient._eav_config_cls \
|
||||
.get_attributes().count(), 3)
|
||||
self.assertEqual(Encounter._eav_config_cls \
|
||||
.get_attributes().count(), 1)
|
||||
|
||||
def test_setting_attributes(self):
|
||||
p = Patient.objects.create(name='Jon')
|
||||
e = Encounter.objects.create(patient=p, num=1)
|
||||
p.eav.age = 3
|
||||
p.eav.height = 2.3
|
||||
p.save()
|
||||
e.eav_field.age = 4
|
||||
e.eav_field.height = 4.5
|
||||
e.save()
|
||||
self.assertEqual(Value.objects.count(), 3)
|
||||
p = Patient.objects.get(name='Jon')
|
||||
self.assertEqual(p.eav.age, 3)
|
||||
self.assertEqual(p.eav.height, 2.3)
|
||||
e = Encounter.objects.get(num=1)
|
||||
self.assertEqual(e.eav_field.age, 4)
|
||||
self.assertFalse(hasattr(e.eav_field, 'height'))
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from ..models import EnumGroup, Attribute, Value
|
||||
|
||||
import eav
|
||||
from .models import Patient
|
||||
|
||||
|
||||
class MiscModels(TestCase):
|
||||
|
||||
def test_enumgroup_unicode(self):
|
||||
name = 'Yes / No'
|
||||
e = EnumGroup.objects.create(name=name)
|
||||
self.assertEqual(unicode(e), name)
|
||||
|
||||
def test_attribute_help_text(self):
|
||||
desc = 'Patient Age'
|
||||
a = Attribute.objects.create(name='age', description=desc, datatype=Attribute.TYPE_INT)
|
||||
self.assertEqual(a.help_text, desc)
|
||||
|
||||
def test_setting_to_none_deletes_value(self):
|
||||
eav.register(Patient)
|
||||
Attribute.objects.create(name='age', datatype=Attribute.TYPE_INT)
|
||||
p = Patient.objects.create(name='Bob', eav__age=5)
|
||||
self.assertEqual(Value.objects.count(), 1)
|
||||
p.eav.age = None
|
||||
p.save()
|
||||
self.assertEqual(Value.objects.count(), 0)
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
class Patient(models.Model):
|
||||
class Meta:
|
||||
app_label = 'eav'
|
||||
|
||||
name = models.CharField(max_length=12)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class Encounter(models.Model):
|
||||
class Meta:
|
||||
app_label = 'eav'
|
||||
|
||||
num = models.PositiveSmallIntegerField()
|
||||
patient = models.ForeignKey(Patient)
|
||||
|
||||
def __unicode__(self):
|
||||
return '%s: encounter num %d' % (self.patient, self.num)
|
||||
|
||||
112
tests/queries.py
112
tests/queries.py
|
|
@ -1,112 +0,0 @@
|
|||
from django.test import TestCase
|
||||
from django.db.models import Q
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from ..registry import EavConfig
|
||||
from ..models import EnumValue, EnumGroup, Attribute, Value
|
||||
|
||||
import eav
|
||||
from .models import Patient, Encounter
|
||||
|
||||
|
||||
class Queries(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
eav.register(Encounter)
|
||||
eav.register(Patient)
|
||||
|
||||
Attribute.objects.create(name='age', datatype=Attribute.TYPE_INT)
|
||||
Attribute.objects.create(name='height', datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name='weight', datatype=Attribute.TYPE_FLOAT)
|
||||
Attribute.objects.create(name='city', datatype=Attribute.TYPE_TEXT)
|
||||
Attribute.objects.create(name='country', datatype=Attribute.TYPE_TEXT)
|
||||
|
||||
self.yes = EnumValue.objects.create(value='yes')
|
||||
self.no = EnumValue.objects.create(value='no')
|
||||
self.unkown = EnumValue.objects.create(value='unkown')
|
||||
ynu = EnumGroup.objects.create(name='Yes / No / Unknown')
|
||||
ynu.enums.add(self.yes)
|
||||
ynu.enums.add(self.no)
|
||||
ynu.enums.add(self.unkown)
|
||||
Attribute.objects.create(name='fever', datatype=Attribute.TYPE_ENUM, enum_group=ynu)
|
||||
|
||||
def tearDown(self):
|
||||
eav.unregister(Encounter)
|
||||
eav.unregister(Patient)
|
||||
|
||||
def test_get_or_create_with_eav(self):
|
||||
p = Patient.objects.get_or_create(name='Bob', eav__age=5)
|
||||
self.assertEqual(Patient.objects.count(), 1)
|
||||
self.assertEqual(Value.objects.count(), 1)
|
||||
p = Patient.objects.get_or_create(name='Bob', eav__age=5)
|
||||
self.assertEqual(Patient.objects.count(), 1)
|
||||
self.assertEqual(Value.objects.count(), 1)
|
||||
p = Patient.objects.get_or_create(name='Bob', eav__age=6)
|
||||
self.assertEqual(Patient.objects.count(), 2)
|
||||
self.assertEqual(Value.objects.count(), 2)
|
||||
|
||||
def test_get_with_eav(self):
|
||||
p1 = Patient.objects.get_or_create(name='Bob', eav__age=6)
|
||||
self.assertEqual(Patient.objects.get(eav__age=6), p1)
|
||||
p2 = Patient.objects.get_or_create(name='Fred', eav__age=6)
|
||||
self.assertRaises(Patient.MultipleObjectsReturned,
|
||||
Patient.objects.get, eav__age=6)
|
||||
|
||||
def test_filtering_on_normal_and_eav_fields(self):
|
||||
yes = self.yes
|
||||
no = self.no
|
||||
data = [
|
||||
# Name Age Fever City Country
|
||||
[ 'Bob', 12, no, 'New York', 'USA' ],
|
||||
[ 'Fred', 15, no, 'Bamako', 'Mali' ],
|
||||
[ 'Jose', 15, yes, 'Kisumu', 'Kenya' ],
|
||||
[ 'Joe', 2, no, 'Nice', 'France'],
|
||||
[ 'Beth', 21, yes, 'France', 'Nice' ]
|
||||
]
|
||||
for row in data:
|
||||
Patient.objects.create(name=row[0], eav__age=row[1],
|
||||
eav__fever=row[2], eav__city=row[3],
|
||||
eav__country=row[4])
|
||||
|
||||
self.assertEqual(Patient.objects.count(), 5)
|
||||
self.assertEqual(Value.objects.count(), 20)
|
||||
|
||||
self.assertEqual(Patient.objects.filter(eav__city__contains='Y').count(), 1)
|
||||
self.assertEqual(Patient.objects.exclude(eav__city__contains='Y').count(), 4)
|
||||
|
||||
# Bob
|
||||
self.assertEqual(Patient.objects.filter(Q(eav__city__contains='Y')).count(), 1)
|
||||
|
||||
# Everyone except Bob
|
||||
#self.assertEqual(Patient.objects.exclude(Q(eav__city__contains='Y')).count(), 4)
|
||||
|
||||
|
||||
# Bob, Fred, Joe
|
||||
q1 = Q(eav__city__contains='Y') | Q(eav__fever=no)
|
||||
self.assertEqual(Patient.objects.filter(q1).count(), 3)
|
||||
|
||||
# Joe
|
||||
q2 = Q(eav__age=2)
|
||||
self.assertEqual(Patient.objects.filter(q2).count(), 1)
|
||||
|
||||
# Joe
|
||||
#self.assertEqual(Patient.objects.filter(q1 & q2).count(), 1)
|
||||
|
||||
# Jose
|
||||
self.assertEqual(Patient.objects.filter(name__contains='J', eav__fever=yes).count(), 1)
|
||||
|
||||
def test_eav_through_foreign_key(self):
|
||||
Patient.objects.create(name='Fred', eav__age=15)
|
||||
p = Patient.objects.create(name='Jon', eav__age=15)
|
||||
e = Encounter.objects.create(num=1, patient=p, eav__fever=self.yes)
|
||||
|
||||
self.assertEqual(Patient.objects.filter(eav__age=15, encounter__eav__fever=self.yes).count(), 1)
|
||||
|
||||
|
||||
def test_manager_only_create(self):
|
||||
class UserEavConfig(EavConfig):
|
||||
manager_only = True
|
||||
|
||||
eav.register(User, UserEavConfig)
|
||||
|
||||
c = User.objects.create(username='joe')
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
import eav
|
||||
from ..registry import Registry, EavConfig
|
||||
from ..managers import EntityManager
|
||||
from ..models import Attribute
|
||||
|
||||
from .models import Patient, Encounter
|
||||
|
||||
|
||||
class RegistryTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def register_encounter(self):
|
||||
class EncounterEav(EavConfig):
|
||||
manager_attr = 'eav_objects'
|
||||
eav_attr = 'eav_field'
|
||||
generic_relation_attr = 'encounter_eav_values'
|
||||
generic_relation_related_name = 'encounters'
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
return 'testing'
|
||||
|
||||
eav.register(Encounter, EncounterEav)
|
||||
|
||||
|
||||
def test_registering_with_defaults(self):
|
||||
eav.register(Patient)
|
||||
self.assertTrue(hasattr(Patient, '_eav_config_cls'))
|
||||
self.assertEqual(Patient._eav_config_cls.manager_attr, 'objects')
|
||||
self.assertFalse(Patient._eav_config_cls.manager_only)
|
||||
self.assertEqual(Patient._eav_config_cls.eav_attr, 'eav')
|
||||
self.assertEqual(Patient._eav_config_cls.generic_relation_attr,
|
||||
'eav_values')
|
||||
self.assertEqual(Patient._eav_config_cls.generic_relation_related_name,
|
||||
None)
|
||||
eav.unregister(Patient)
|
||||
|
||||
def test_registering_overriding_defaults(self):
|
||||
eav.register(Patient)
|
||||
self.register_encounter()
|
||||
self.assertTrue(hasattr(Patient, '_eav_config_cls'))
|
||||
self.assertEqual(Patient._eav_config_cls.manager_attr, 'objects')
|
||||
self.assertEqual(Patient._eav_config_cls.eav_attr, 'eav')
|
||||
|
||||
self.assertTrue(hasattr(Encounter, '_eav_config_cls'))
|
||||
self.assertEqual(Encounter._eav_config_cls.get_attributes(), 'testing')
|
||||
self.assertEqual(Encounter._eav_config_cls.manager_attr, 'eav_objects')
|
||||
self.assertEqual(Encounter._eav_config_cls.eav_attr, 'eav_field')
|
||||
eav.unregister(Patient)
|
||||
eav.unregister(Encounter)
|
||||
|
||||
def test_unregistering(self):
|
||||
old_mgr = Patient.objects
|
||||
eav.register(Patient)
|
||||
self.assertTrue(Patient.objects.__class__.__name__ == 'EntityManager')
|
||||
eav.unregister(Patient)
|
||||
self.assertFalse(Patient.objects.__class__.__name__ == 'EntityManager')
|
||||
self.assertEqual(Patient.objects, old_mgr)
|
||||
self.assertFalse(hasattr(Patient, '_eav_config_cls'))
|
||||
|
||||
def test_unregistering_unregistered_model_proceeds_silently(self):
|
||||
eav.unregister(Patient)
|
||||
|
||||
def test_double_registering_model_is_harmless(self):
|
||||
eav.register(Patient)
|
||||
eav.register(Patient)
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
import eav
|
||||
from ..registry import Registry, EavConfig
|
||||
from ..managers import EntityManager
|
||||
|
||||
from .models import Patient, Encounter
|
||||
|
||||
|
||||
class RegistryTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def register_encounter(self):
|
||||
class EncounterEav(EavConfig):
|
||||
manager_attr = 'eav_objects'
|
||||
eav_attr = 'eav_field'
|
||||
generic_relation_attr = 'encounter_eav_values'
|
||||
generic_relation_related_name = 'encounters'
|
||||
eav.register(Encounter, EncounterEav)
|
||||
|
||||
|
||||
def test_registering_with_defaults(self):
|
||||
eav.register(Patient)
|
||||
self.assertTrue(hasattr(Patient, '_eav_config_cls'))
|
||||
self.assertEqual(Patient._eav_config_cls.manager_attr, 'objects')
|
||||
self.assertFalse(Patient._eav_config_cls.manager_only)
|
||||
self.assertEqual(Patient._eav_config_cls.eav_attr, 'eav')
|
||||
self.assertEqual(Patient._eav_config_cls.generic_relation_attr,
|
||||
'eav_values')
|
||||
self.assertEqual(Patient._eav_config_cls.generic_relation_related_name,
|
||||
None)
|
||||
eav.unregister(Patient)
|
||||
|
||||
def test_registering_overriding_defaults(self):
|
||||
eav.register(Patient)
|
||||
self.register_encounter()
|
||||
self.assertTrue(hasattr(Patient, '_eav_config_cls'))
|
||||
self.assertEqual(Patient._eav_config_cls.manager_attr, 'objects')
|
||||
self.assertEqual(Patient._eav_config_cls.eav_attr, 'eav')
|
||||
|
||||
self.assertTrue(hasattr(Encounter, '_eav_config_cls'))
|
||||
self.assertEqual(Encounter._eav_config_cls.manager_attr, 'eav_objects')
|
||||
self.assertEqual(Encounter._eav_config_cls.eav_attr, 'eav_field')
|
||||
eav.unregister(Patient)
|
||||
eav.unregister(Encounter)
|
||||
|
||||
def test_unregistering(self):
|
||||
old_mgr = Patient.objects
|
||||
eav.register(Patient)
|
||||
self.assertTrue(Patient.objects.__class__.__name__ == 'EntityManager')
|
||||
eav.unregister(Patient)
|
||||
self.assertFalse(Patient.objects.__class__.__name__ == 'EntityManager')
|
||||
self.assertEqual(Patient.objects, old_mgr)
|
||||
self.assertFalse(hasattr(Patient, '_eav_config_cls'))
|
||||
|
||||
def test_unregistering_unregistered_model_proceeds_silently(self):
|
||||
eav.unregister(Patient)
|
||||
|
||||
def test_double_registering_model_is_harmless(self):
|
||||
eav.register(Patient)
|
||||
eav.register(Patient)
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: ai ts=4 sts=4 et sw=4 coding=utf-8
|
||||
#
|
||||
# This software is derived from EAV-Django originally written and
|
||||
# copyrighted by Andrey Mikhaylenko <http://pypi.python.org/pypi/eav-django>
|
||||
#
|
||||
# This is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with EAV-Django. If not, see <http://gnu.org/licenses/>.
|
||||
from datetime import datetime, date
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
def validate_text(value):
|
||||
'''
|
||||
Validates text
|
||||
'''
|
||||
if not (type(value) == unicode or type(value) == str):
|
||||
raise ValidationError(_(u"Must be str or unicode"))
|
||||
|
||||
def validate_float(value):
|
||||
try:
|
||||
float(value)
|
||||
except ValueError:
|
||||
raise ValidationError(_(u"Must be a float"))
|
||||
|
||||
def validate_int(value):
|
||||
try:
|
||||
int(value)
|
||||
except ValueError:
|
||||
raise ValidationError(_(u"Must be an integer"))
|
||||
|
||||
def validate_date(value):
|
||||
if not (isinstance(value, datetime) or isinstance(value, date)):
|
||||
raise ValidationError(_(u"Must be a date or datetime"))
|
||||
|
||||
def validate_bool(value):
|
||||
if not type(value) == bool:
|
||||
raise ValidationError(_(u"Must be a boolean"))
|
||||
|
||||
def validate_object(value):
|
||||
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):
|
||||
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