Skip to content

Commit

Permalink
feat: Allow serializer customization for JSONField (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dmytro Kabakchei authored and kabakchey committed Feb 10, 2017
1 parent 73d5535 commit 60d0ef7
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 11 deletions.
1 change: 1 addition & 0 deletions annoying/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def wrapper(**kwargs):
def register_signal(self, signal, name):
self._signals[name] = signal


signals = Signals()


Expand Down
31 changes: 26 additions & 5 deletions annoying/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.db import models
from django.db.models import OneToOneField
from django.db.models.fields import NOT_PROVIDED
from django.db.transaction import atomic
from django.core.serializers.json import DjangoJSONEncoder
try:
Expand Down Expand Up @@ -55,6 +56,11 @@ class JSONField(models.TextField):
JSON objects seamlessly.
Django snippet #1478
Custom serializer/deserializer functions can be used to customize field's behavior.
Defaults:
- serializer: json.dumps(value, cls=DjangoJSONEncoder, sort_keys=True, indent=2, separators=(',', ': '))
- deserializer: json.loads(value)
example:
class Page(models.Model):
data = JSONField(blank=True, null=True)
Expand All @@ -65,6 +71,21 @@ class Page(models.Model):
page.save()
"""

def __init__(self, *args, **kwargs):
def dumps(value):
return json.dumps(value, cls=DjangoJSONEncoder, sort_keys=True, indent=2, separators=(',', ': '))

self.serializer = kwargs.pop('serializer', dumps)
self.deserializer = kwargs.pop('deserializer', json.loads)

super(JSONField, self).__init__(*args, **kwargs)

def deconstruct(self):
name, path, args, kwargs = super(JSONField, self).deconstruct()
kwargs['serializer'] = self.serializer
kwargs['deserializer'] = self.deserializer
return name, path, args, kwargs

def to_python(self, value):
"""
Convert a string from the database to a Python value.
Expand All @@ -74,9 +95,9 @@ def to_python(self, value):

try:
if isinstance(value, six.string_types):
return json.loads(value)
return self.deserializer(value)
elif isinstance(value, bytes):
return json.loads(value.decode('utf8'))
return self.deserializer(value.decode('utf8'))
except ValueError:
pass
return value
Expand All @@ -88,7 +109,7 @@ def get_prep_value(self, value):
if value == "":
return None
if isinstance(value, dict) or isinstance(value, list):
return json.dumps(value, cls=DjangoJSONEncoder, sort_keys=True, indent=2, separators=(',', ': '))
return self.serializer(value)
return super(JSONField, self).get_prep_value(value)

def from_db_value(self, value, *args, **kwargs):
Expand All @@ -106,12 +127,12 @@ def get_db_prep_save(self, value, *args, **kwargs):
if value == "":
return None
if isinstance(value, dict) or isinstance(value, list):
return json.dumps(value, cls=DjangoJSONEncoder, sort_keys=True, indent=2, separators=(',', ': '))
return self.serializer(value)
else:
return super(JSONField, self).get_db_prep_save(value, *args, **kwargs)

def value_from_object(self, obj):
value = super(JSONField, self).value_from_object(obj)
if self.null and value is None:
return None
return json.dumps(value, sort_keys=True, indent=2, separators=(',', ': '))
return self.serializer(value)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from annoying import __version__

from setuptools import setup, find_packages
from setuptools import setup
setup(
name="django-annoying",
version=__version__,
Expand Down
12 changes: 12 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from json import loads, dumps

from django.db import models

from annoying.fields import AutoOneToOneField
from annoying.fields import JSONField

Expand All @@ -8,6 +11,15 @@ class SuperVillain(models.Model):
stats = JSONField(default=None, blank=True, null=True)


class Minion(models.Model):
_PREFIX = 'I can: '

name = models.CharField(max_length="20", default="Igor")
skills = JSONField(default=None, blank=True, null=True,
serializer=lambda value: '{0}{1}'.format(Minion._PREFIX, dumps(value)),
deserializer=lambda value: loads(value[len(Minion._PREFIX):]))


class SuperHero(models.Model):
name = models.CharField(max_length="20", default="Captain Hammer")
mortal_enemy = AutoOneToOneField(SuperVillain, on_delete=models.CASCADE, related_name='mortal_enemy')
2 changes: 1 addition & 1 deletion tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import json

from django.test import TestCase, override_settings
from django.test import TestCase


class AJAXRequestTestCase(TestCase):
Expand Down
23 changes: 19 additions & 4 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json

from django.test import TestCase

from . import models


Expand All @@ -26,8 +27,8 @@ def test_json_field_create(self):
# Refresh from DB
super_villain = models.SuperVillain.objects.get(pk=sv.pk)

self.assertEqual(super_villain.stats['strength'], 100)
self.assertEqual(super_villain.stats['defence'], 50)
self.assertEqual(super_villain.stats['strength'], stats['strength'])
self.assertEqual(super_villain.stats['defence'], stats['defence'])
self.assertEqual(dump_dict(super_villain.stats), dump_dict(stats))

def test_json_field_update(self):
Expand All @@ -42,6 +43,20 @@ def test_json_field_update(self):
# Refresh from DB
super_villain = models.SuperVillain.objects.get(pk=super_villain.pk)

self.assertEqual(super_villain.stats['strength'], 100)
self.assertEqual(super_villain.stats['defence'], 50)
self.assertEqual(super_villain.stats['strength'], stats['strength'])
self.assertEqual(super_villain.stats['defence'], stats['defence'])
self.assertEqual(dump_dict(super_villain.stats), dump_dict(stats))

def test_json_field_custom_serializer_deserializer(self):
skills = {
'make': ['atomic_bomb', 'coffee'],
'understand': ['string_theory', 'women'],
}
minion = models.Minion.objects.create(skills=skills)

# Refresh from DB
minion = models.Minion.objects.get(pk=minion.pk)

self.assertEqual(minion.skills['make'], skills['make'])
self.assertEqual(minion.skills['understand'], skills['understand'])
self.assertEqual(dump_dict(minion.skills), dump_dict(skills))

0 comments on commit 60d0ef7

Please sign in to comment.