mirror of
https://github.com/jazzband/django-eav2.git
synced 2026-03-16 22:40:26 +00:00
Adding a JSON Datatype
This commit is contained in:
parent
6cd8163099
commit
39e1ff35f7
6 changed files with 119 additions and 9 deletions
|
|
@ -8,6 +8,12 @@ from django.forms import (BooleanField, CharField, ChoiceField, DateTimeField,
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
try:
|
||||
from django.forms import JSONField
|
||||
except:
|
||||
JSONField = CharField
|
||||
|
||||
|
||||
class BaseDynamicEntityForm(ModelForm):
|
||||
"""
|
||||
``ModelForm`` for entity with support for EAV attributes. Form fields are
|
||||
|
|
@ -28,6 +34,7 @@ class BaseDynamicEntityForm(ModelForm):
|
|||
int DateTimeField
|
||||
bool BooleanField
|
||||
enum ChoiceField
|
||||
json JSONField
|
||||
===== =============
|
||||
"""
|
||||
FIELD_CLASSES = {
|
||||
|
|
@ -37,6 +44,7 @@ class BaseDynamicEntityForm(ModelForm):
|
|||
'date': DateTimeField,
|
||||
'bool': BooleanField,
|
||||
'enum': ChoiceField,
|
||||
'json': JSONField,
|
||||
}
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
|
|
|
|||
29
eav/migrations/0003_auto_20210404_2209.py
Normal file
29
eav/migrations/0003_auto_20210404_2209.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 3.1.6 on 2021-04-04 22:09
|
||||
|
||||
from django.db import migrations
|
||||
import eav.fields
|
||||
import django.core.serializers.json
|
||||
try:
|
||||
from django.db.models import JSONField
|
||||
except:
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('eav', '0002_add_entity_ct_field'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='value',
|
||||
name='value_json',
|
||||
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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -19,6 +19,16 @@ from django.db.models.base import ModelBase
|
|||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
if hasattr(models, "JSONField"):
|
||||
JSONField = models.JSONField
|
||||
else:
|
||||
try:
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
except:
|
||||
JSONField = models.TextField
|
||||
|
||||
|
||||
from .validators import (
|
||||
validate_text,
|
||||
validate_float,
|
||||
|
|
@ -26,7 +36,8 @@ from .validators import (
|
|||
validate_date,
|
||||
validate_bool,
|
||||
validate_object,
|
||||
validate_enum
|
||||
validate_enum,
|
||||
validate_json
|
||||
)
|
||||
from .exceptions import IllegalAssignmentException
|
||||
from .fields import EavDatatypeField, EavSlugField
|
||||
|
|
@ -107,6 +118,7 @@ class Attribute(models.Model):
|
|||
* bool (TYPE_BOOLEAN)
|
||||
* object (TYPE_OBJECT)
|
||||
* enum (TYPE_ENUM)
|
||||
* json (TYPE_JSON)
|
||||
|
||||
Examples::
|
||||
|
||||
|
|
@ -138,6 +150,7 @@ class Attribute(models.Model):
|
|||
TYPE_BOOLEAN = 'bool'
|
||||
TYPE_OBJECT = 'object'
|
||||
TYPE_ENUM = 'enum'
|
||||
TYPE_JSON = 'json'
|
||||
|
||||
DATATYPE_CHOICES = (
|
||||
(TYPE_TEXT, _('Text')),
|
||||
|
|
@ -147,6 +160,7 @@ class Attribute(models.Model):
|
|||
(TYPE_BOOLEAN, _('True / False')),
|
||||
(TYPE_OBJECT, _('Django Object')),
|
||||
(TYPE_ENUM, _('Multiple Choice')),
|
||||
(TYPE_JSON, _('JSON Object')),
|
||||
)
|
||||
|
||||
# Core attributes
|
||||
|
|
@ -248,6 +262,7 @@ class Attribute(models.Model):
|
|||
'bool': validate_bool,
|
||||
'object': validate_object,
|
||||
'enum': validate_enum,
|
||||
'json': validate_json,
|
||||
}
|
||||
|
||||
return [DATATYPE_VALIDATORS[self.datatype]]
|
||||
|
|
@ -382,6 +397,7 @@ class Value(models.Model):
|
|||
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_json = JSONField(default=dict, encoder=DjangoJSONEncoder, blank = True, null = True)
|
||||
|
||||
value_enum = models.ForeignKey(
|
||||
EnumValue,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ These validators are called by the
|
|||
:class:`~eav.models.Attribute` model.
|
||||
"""
|
||||
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
|
@ -83,3 +84,16 @@ def validate_enum(value):
|
|||
|
||||
if isinstance(value, EnumValue) and not value.pk:
|
||||
raise ValidationError(_(u"EnumValue has not been saved yet"))
|
||||
|
||||
|
||||
def validate_json(value):
|
||||
"""
|
||||
Raises ``ValidationError`` unless *value* can be cast as an ``json object`` (a dict)
|
||||
"""
|
||||
try:
|
||||
if isinstance(value, str):
|
||||
value = json.loads(value)
|
||||
if not isinstance(value, dict):
|
||||
raise ValidationError(_(u"Must be a JSON Serializable object"))
|
||||
except ValueError:
|
||||
raise ValidationError(_(u"Must be a JSON Serializable object"))
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class DataValidation(TestCase):
|
|||
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)
|
||||
Attribute.objects.create(name='Extra', datatype=Attribute.TYPE_JSON)
|
||||
|
||||
def tearDown(self):
|
||||
eav.unregister(Patient)
|
||||
|
|
@ -186,3 +187,11 @@ class DataValidation(TestCase):
|
|||
ynu.values.add(unkown)
|
||||
a = Attribute(name='color', datatype=Attribute.TYPE_TEXT, enum_group=ynu)
|
||||
self.assertRaises(ValidationError, a.save)
|
||||
|
||||
def test_json_validation(self):
|
||||
p = Patient.objects.create(name='Joe')
|
||||
p.eav.extra = 5
|
||||
self.assertRaises(ValidationError, p.save)
|
||||
p.eav.extra = {"eyes": "blue", "hair": "brown"}
|
||||
p.save()
|
||||
self.assertEqual(Patient.objects.get(pk=p.pk).eav.extra.get("eyes", ""), "blue")
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class Queries(TestCase):
|
|||
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)
|
||||
Attribute.objects.create(name='extras', datatype=Attribute.TYPE_JSON)
|
||||
|
||||
self.yes = EnumValue.objects.create(value='yes')
|
||||
self.no = EnumValue.objects.create(value='no')
|
||||
|
|
@ -41,12 +42,12 @@ class Queries(TestCase):
|
|||
no = self.no
|
||||
|
||||
data = [
|
||||
# Name, age, fever, city, country.
|
||||
['Anne', 3, no, 'New York', 'USA'],
|
||||
['Bob', 15, no, 'Bamako', 'Mali'],
|
||||
['Cyrill', 15, yes, 'Kisumu', 'Kenya'],
|
||||
['Daniel', 3, no, 'Nice', 'France'],
|
||||
['Eugene', 2, yes, 'France', 'Nice']
|
||||
# Name, age, fever, city, country, extras
|
||||
['Anne', 3, no, 'New York', 'USA', {"chills": "yes"}],
|
||||
['Bob', 15, no, 'Bamako', 'Mali', {}],
|
||||
['Cyrill', 15, yes, 'Kisumu', 'Kenya', {"chills": "yes", "headache": "no"}],
|
||||
['Daniel', 3, no, 'Nice', 'France', {"headache": "yes"}],
|
||||
['Eugene', 2, yes, 'France', 'Nice', {"chills": "no", "headache": "yes"}]
|
||||
]
|
||||
|
||||
for row in data:
|
||||
|
|
@ -55,7 +56,8 @@ class Queries(TestCase):
|
|||
eav__age=row[1],
|
||||
eav__fever=row[2],
|
||||
eav__city=row[3],
|
||||
eav__country=row[4]
|
||||
eav__country=row[4],
|
||||
eav__extras=row[5]
|
||||
)
|
||||
|
||||
def test_get_or_create_with_eav(self):
|
||||
|
|
@ -81,7 +83,7 @@ class Queries(TestCase):
|
|||
|
||||
# Check number of objects in DB.
|
||||
self.assertEqual(Patient.objects.count(), 5)
|
||||
self.assertEqual(Value.objects.count(), 20)
|
||||
self.assertEqual(Value.objects.count(), 25)
|
||||
|
||||
# Nobody
|
||||
q1 = Q(eav__fever=self.yes) & Q(eav__fever=self.no)
|
||||
|
|
@ -149,6 +151,38 @@ class Queries(TestCase):
|
|||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 1)
|
||||
|
||||
# Extras: Chills
|
||||
# Without
|
||||
q1 = Q(eav__extras__has_key="chills")
|
||||
p = Patient.objects.exclude(q1)
|
||||
self.assertEqual(p.count(), 2)
|
||||
|
||||
# With
|
||||
q1 = Q(eav__extras__has_key="chills")
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 3)
|
||||
|
||||
# No chills
|
||||
q1 = Q(eav__extras__chills="no")
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 1)
|
||||
|
||||
# Has chills
|
||||
q1 = Q(eav__extras__chills="yes")
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 2)
|
||||
|
||||
# Extras: Empty
|
||||
# Yes
|
||||
q1 = Q(eav__extras={})
|
||||
p = Patient.objects.filter(q1)
|
||||
self.assertEqual(p.count(), 1)
|
||||
|
||||
# No
|
||||
q1 = Q(eav__extras={})
|
||||
p = Patient.objects.exclude(q1)
|
||||
self.assertEqual(p.count(), 4)
|
||||
|
||||
def _order(self, ordering):
|
||||
query = Patient.objects.all().order_by(*ordering)
|
||||
return list(query.values_list('name', flat=True))
|
||||
|
|
|
|||
Loading…
Reference in a new issue