Added JSON Fields support

This commit is contained in:
Mikhail Silonov 2013-08-08 18:02:12 +04:00
parent 9050d60295
commit 8c6f343111
6 changed files with 222 additions and 4 deletions

View file

@ -19,3 +19,4 @@ Simon Meers <simon@simonmeers.com>
sayane
Trey Hunner <trey@treyhunner.com>
zyegfryed
Mikhail Silonov <silonov.pro>

View file

@ -7,6 +7,8 @@ master (unreleased)
* `Choices` now `__contains__` its Python identifier values. Thanks Keryn
Knight. (Merge of GH-69).
* Added JSON Fields support.
1.4.0 (2013.06.03)
------------------

View file

@ -0,0 +1,30 @@
import json
from django.db import models
from django.core.serializers.json import DjangoJSONEncoder
class SimpleJSONField(models.TextField):
__metaclass__ = models.SubfieldBase
def to_python(self, value):
if value == "":
return None
try:
if isinstance(value, basestring):
return json.loads(value)
except ValueError:
pass
return value
def get_db_prep_save(self, value, connection):
if value == "":
return None
if isinstance(value, dict):
value = json.dumps(value, cls=DjangoJSONEncoder)
return super(SimpleJSONField, self).get_db_prep_save(value, connection)

View file

@ -5,6 +5,7 @@ from model_utils.models import TimeStampedModel, StatusModel, TimeFramedModel
from model_utils.tracker import FieldTracker, ModelTracker
from model_utils.managers import QueryManager, InheritanceManager, PassThroughManager
from model_utils.fields import SplitField, MonitorField, StatusField
from model_utils.tests.fields import SimpleJSONField
from model_utils import Choices
@ -271,6 +272,15 @@ class TrackedMultiple(models.Model):
number_tracker = FieldTracker(fields=['number'])
class TrackedWithJsonField(models.Model):
name = models.CharField(max_length=20)
number = models.IntegerField()
props = SimpleJSONField()
tracker = FieldTracker()
class ModelTracked(models.Model):
name = models.CharField(max_length=20)
number = models.IntegerField()

View file

@ -26,9 +26,9 @@ from model_utils.tests.models import (
StatusPlainTuple, TimeFrame, Monitored, StatusManagerAdded,
TimeFrameManagerAdded, Dude, SplitFieldAbstractParent, Car, Spot,
ModelTracked, ModelTrackedFK, ModelTrackedNotDefault, ModelTrackedMultiple,
Tracked, TrackedFK, TrackedNotDefault, TrackedNonFieldAttr,
TrackedMultiple, StatusFieldDefaultFilled, StatusFieldDefaultNotFilled)
Tracked, TrackedFK, TrackedNotDefault, TrackedWithJsonField,
TrackedNonFieldAttr, TrackedMultiple, StatusFieldDefaultFilled,
StatusFieldDefaultNotFilled)
class GetExcerptTests(TestCase):
@ -924,6 +924,171 @@ class FieldTrackedModelCustomTests(FieldTrackerTestCase,
self.assertCurrent(name='new age')
class JSONFieldTrackedModelTests(FieldTrackerTestCase):
tracked_class = TrackedWithJsonField
def setUp(self):
self.instance = self.tracked_class()
self.tracker = self.instance.tracker
def test_pre_save_changed(self):
self.assertChanged(name=None)
self.instance.name = 'new age'
self.assertChanged(name=None)
self.instance.number = 8
self.assertChanged(name=None, number=None)
self.instance.name = ''
self.assertChanged(name=None, number=None)
self.instance.props = {'attr': 1}
self.assertChanged(name=None, number=None, props=None)
def test_first_save(self):
self.assertHasChanged(name=True)
self.assertPrevious(name=None, number=None, props=None)
self.assertCurrent(name='', number=None, props=None, id=None)
self.assertChanged(name=None)
self.instance.name = 'retro'
self.instance.number = 4
self.instance.props = {'vodka': True}
self.assertHasChanged(name=True, number=True, props=True)
self.assertPrevious(name=None, number=None, props=None)
self.assertCurrent(name='retro', number=4,
props={'vodka': True}, id=None)
self.assertChanged(name=None, number=None, props=None)
def test_pre_save_has_changed(self):
self.assertHasChanged(name=True)
self.instance.name = 'new age'
self.assertHasChanged(name=True)
self.instance.number = 7
self.assertHasChanged(name=True, number=True)
self.instance.props = {'bears_on_red_square': False}
self.assertChanged(name=None, number=None, props=None)
def test_post_save_has_changed(self):
self.update_instance(
name='retro', number=4,
props={
'goodies': {
'balalaika': True,
'Topol-M': True
}
}
)
self.assertHasChanged(name=False, number=False, props=False)
self.instance.name = 'new age'
self.assertHasChanged(name=True, number=False, props=False)
self.instance.number = 8
self.assertHasChanged(name=True, number=True)
self.instance.name = 'retro'
self.assertHasChanged(name=False, number=True)
self.instance.props = {
'goodies': {
'balalaika': False,
'Topol-M': True
}
}
self.assertHasChanged(name=False, number=True, props=True)
def test_post_save_previous(self):
self.update_instance(
name='retro', number=4,
props={
'goodies': {
'balalaika': True,
'Topol-M': True
}
}
)
self.instance.name = 'new age'
self.instance.props = {
'goodies': {
'balalaika': False,
'Topol-M': True
}
}
self.assertPrevious(
name='retro',
number=4,
props={
'goodies': {
'balalaika': True,
'Topol-M': True
}
}
)
def test_post_save_changed(self):
self.update_instance(
name='retro', number=4,
props={
'goodies': {
'balalaika': True,
'Topol-M': True
}
}
)
self.assertChanged()
self.instance.name = 'new age'
self.assertChanged(name='retro')
self.instance.number = 8
self.assertChanged(name='retro', number=4)
self.instance.name = 'retro'
self.assertChanged(number=4)
self.instance.props = {
'goodies': {
'balalaika': False,
'Topol-M': True
}
}
self.assertChanged(
number=4,
props={
'goodies': {
'balalaika': True,
'Topol-M': True
}
}
)
def test_current(self):
self.assertCurrent(name='', number=None, props=None, id=None)
self.instance.name = 'new age'
self.assertCurrent(name='new age', number=None, props=None, id=None)
self.instance.number = 8
self.assertCurrent(name='new age', number=8, props=None, id=None)
self.instance.props = {
'goodies': {
'balalaika': False,
'Topol-M': True
}
}
self.assertCurrent(
name='new age',
number=8,
props={
'goodies': {
'balalaika': False,
'Topol-M': True
}
},
id=None
)
self.instance.save()
self.assertCurrent(
name='new age',
number=8,
props={
'goodies': {
'balalaika': False,
'Topol-M': True
}
},
id=self.instance.id
)
class FieldTrackedModelAttributeTests(FieldTrackerTestCase):
tracked_class = TrackedNonFieldAttr

View file

@ -1,4 +1,7 @@
from __future__ import unicode_literals
from copy import deepcopy
from django.db import models
from django.core.exceptions import FieldError
@ -19,6 +22,7 @@ class FieldInstanceTracker(object):
self.saved_data = self.current()
else:
self.saved_data.update(**self.current(fields=fields))
return self.saved_data
def current(self, fields=None):
"""Return dict of current values for all tracked fields"""
@ -76,7 +80,8 @@ class FieldTracker(object):
def initialize_tracker(self, sender, instance, **kwargs):
tracker = self.tracker_class(instance, self.fields, self.field_map)
setattr(instance, self.attname, tracker)
tracker.set_saved_fields()
saved_data = tracker.set_saved_fields()
self.prevent_side_effects(saved_data)
self.patch_save(instance)
def patch_save(self, instance):
@ -88,6 +93,11 @@ class FieldTracker(object):
return ret
instance.save = save
def prevent_side_effects(self, saved_data):
for field, field_value in saved_data.items():
if isinstance(field_value, dict):
saved_data[field] = deepcopy(field_value)
def __get__(self, instance, owner):
if instance is None:
return self