Adding a JSON Datatype

This commit is contained in:
Mauro 2021-04-04 23:53:45 -03:00
parent 6cd8163099
commit 39e1ff35f7
6 changed files with 119 additions and 9 deletions

View file

@ -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):

View 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'),
),
]

View file

@ -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,

View file

@ -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"))

View file

@ -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")

View file

@ -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))