diff --git a/CHANGES.rst b/CHANGES.rst index c961a88..5dbc02a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,7 +12,7 @@ master (unreleased) ``update_fields`` in which there are untracked fields. Thanks Mikhail Silonov. (Merge of GH-70, fixes GH-71). -* Added JSON Fields support. +* Added support for mutable fields (Merge of GH-73, fixes GH-74) 1.4.0 (2013.06.03) diff --git a/model_utils/tests/fields.py b/model_utils/tests/fields.py index b0cdcad..0c95815 100644 --- a/model_utils/tests/fields.py +++ b/model_utils/tests/fields.py @@ -1,30 +1,34 @@ -import json - from django.db import models -from django.core.serializers.json import DjangoJSONEncoder + +try: + unicode() + str_class = basestring +except NameError: + str_class = str + +def with_metaclass(meta, base=object): + return meta("NewBase", (base,), {}) -class SimpleJSONField(models.TextField): - - __metaclass__ = models.SubfieldBase +class MutableField(with_metaclass(models.SubfieldBase, models.TextField)): def to_python(self, value): - if value == "": + if value == '': return None try: - if isinstance(value, basestring): - return json.loads(value) + if isinstance(value, str_class): + return [int(i) for i in value.split(',')] except ValueError: pass return value def get_db_prep_save(self, value, connection): - if value == "": - return None + if value is None: + return '' - if isinstance(value, dict): - value = json.dumps(value, cls=DjangoJSONEncoder) + if isinstance(value, list): + value = ','.join((str(i) for i in value)) - return super(SimpleJSONField, self).get_db_prep_save(value, connection) + return super(MutableField, self).get_db_prep_save(value, connection) diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index af8ff95..a41cce7 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -5,7 +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.tests.fields import MutableField from model_utils import Choices @@ -235,6 +235,7 @@ class Spot(models.Model): class Tracked(models.Model): name = models.CharField(max_length=20) number = models.IntegerField() + mutable = MutableField() tracker = FieldTracker() @@ -272,18 +273,10 @@ 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() + mutable = MutableField() tracker = ModelTracker() diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index daeaa5d..2c1c538 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -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, TrackedWithJsonField, - TrackedNonFieldAttr, TrackedMultiple, StatusFieldDefaultFilled, - StatusFieldDefaultNotFilled) + Tracked, TrackedFK, TrackedNotDefault,TrackedNonFieldAttr, TrackedMultiple, + StatusFieldDefaultFilled,StatusFieldDefaultNotFilled + ) class GetExcerptTests(TestCase): @@ -762,52 +762,61 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.assertChanged(name=None, number=None) self.instance.name = '' self.assertChanged(name=None, number=None) + self.instance.mutable = [1,2,3] + self.assertChanged(name=None, number=None, mutable=None) def test_pre_save_has_changed(self): - self.assertHasChanged(name=True, number=False) + self.assertHasChanged(name=True, number=False, mutable=False) self.instance.name = 'new age' - self.assertHasChanged(name=True, number=False) + self.assertHasChanged(name=True, number=False, mutable=False) self.instance.number = 7 self.assertHasChanged(name=True, number=True) + self.instance.mutable = [1,2,3] + self.assertHasChanged(name=True, number=True, mutable=True) def test_first_save(self): - self.assertHasChanged(name=True, number=False) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='', number=None, id=None) + self.assertHasChanged(name=True, number=False, mutable=False) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='', number=None, id=None, mutable=None) self.assertChanged(name=None) self.instance.name = 'retro' self.instance.number = 4 - self.assertHasChanged(name=True, number=True) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='retro', number=4, id=None) - self.assertChanged(name=None, number=None) + self.instance.mutable = [1,2,3] + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertChanged(name=None, number=None, mutable=None) # Django 1.4 doesn't have update_fields if django.VERSION >= (1, 5, 0): self.instance.save(update_fields=[]) - self.assertHasChanged(name=True, number=True) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='retro', number=4, id=None) - self.assertChanged(name=None, number=None) + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) + self.assertChanged(name=None, number=None, mutable=None) with self.assertRaises(ValueError): self.instance.save(update_fields=['number']) def test_post_save_has_changed(self): - self.update_instance(name='retro', number=4) - self.assertHasChanged(name=False, number=False) + self.update_instance(name='retro', number=4, mutable=[1,2,3]) + self.assertHasChanged(name=False, number=False, mutable=False) self.instance.name = 'new age' self.assertHasChanged(name=True, number=False) self.instance.number = 8 self.assertHasChanged(name=True, number=True) + self.instance.mutable[1] = 4 + self.assertHasChanged(name=True, number=True, mutable=True) self.instance.name = 'retro' - self.assertHasChanged(name=False, number=True) + self.assertHasChanged(name=False, number=True, mutable=True) def test_post_save_previous(self): - self.update_instance(name='retro', number=4) + self.update_instance(name='retro', number=4, mutable=[1,2,3]) self.instance.name = 'new age' - self.assertPrevious(name='retro', number=4) + self.assertPrevious(name='retro', number=4, mutable=[1,2,3]) + self.instance.mutable[1] = 4 + self.assertPrevious(name='retro', number=4, mutable=[1,2,3]) def test_post_save_changed(self): - self.update_instance(name='retro', number=4) + self.update_instance(name='retro', number=4, mutable=[1,2,3]) self.assertChanged() self.instance.name = 'new age' self.assertChanged(name='retro') @@ -815,36 +824,48 @@ class FieldTrackerTests(FieldTrackerTestCase, FieldTrackerCommonTests): self.assertChanged(name='retro', number=4) self.instance.name = 'retro' self.assertChanged(number=4) + self.instance.mutable[1] = 4 + self.assertChanged(number=4, mutable=[1,2,3]) + self.instance.mutable = [1,2,3] + self.assertChanged(number=4) def test_current(self): - self.assertCurrent(id=None, name='', number=None) + self.assertCurrent(id=None, name='', number=None, mutable=None) self.instance.name = 'new age' - self.assertCurrent(id=None, name='new age', number=None) + self.assertCurrent(id=None, name='new age', number=None, mutable=None) self.instance.number = 8 - self.assertCurrent(id=None, name='new age', number=8) + self.assertCurrent(id=None, name='new age', number=8, mutable=None) + self.instance.mutable = [1,2,3] + self.assertCurrent(id=None, name='new age', number=8, mutable=[1,2,3]) + self.instance.mutable[1] = 4 + self.assertCurrent(id=None, name='new age', number=8, mutable=[1,4,3]) self.instance.save() - self.assertCurrent(id=self.instance.id, name='new age', number=8) + self.assertCurrent(id=self.instance.id, name='new age', number=8, mutable=[1,4,3]) @skipUnless( django.VERSION >= (1, 5, 0), "Django 1.4 doesn't have update_fields") def test_update_fields(self): - self.update_instance(name='retro', number=4) + self.update_instance(name='retro', number=4, mutable=[1,2,3]) self.assertChanged() self.instance.name = 'new age' self.instance.number = 8 - self.assertChanged(name='retro', number=4) + self.instance.mutable = [4,5,6] + self.assertChanged(name='retro', number=4, mutable=[1,2,3]) self.instance.save(update_fields=[]) - self.assertChanged(name='retro', number=4) + self.assertChanged(name='retro', number=4, mutable=[1,2,3]) self.instance.save(update_fields=['name']) in_db = self.tracked_class.objects.get(id=self.instance.id) self.assertEqual(in_db.name, self.instance.name) self.assertNotEqual(in_db.number, self.instance.number) - self.assertChanged(number=4) + self.assertChanged(number=4, mutable=[1,2,3]) self.instance.save(update_fields=['number']) + self.assertChanged(mutable=[1,2,3]) + self.instance.save(update_fields=['mutable']) self.assertChanged() in_db = self.tracked_class.objects.get(id=self.instance.id) self.assertEqual(in_db.name, self.instance.name) self.assertEqual(in_db.number, self.instance.number) + self.assertEqual(in_db.mutable, self.instance.mutable) class FieldTrackedModelCustomTests(FieldTrackerTestCase, @@ -929,171 +950,6 @@ class FieldTrackedModelCustomTests(FieldTrackerTestCase, self.assertChanged() -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 @@ -1291,24 +1147,27 @@ class ModelTrackerTests(FieldTrackerTests): self.assertChanged() self.instance.name = '' self.assertChanged() + self.instance.mutable = [1,2,3] + self.assertChanged() def test_first_save(self): - self.assertHasChanged(name=True, number=True) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='', number=None, id=None) + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='', number=None, id=None, mutable=None) self.assertChanged() self.instance.name = 'retro' self.instance.number = 4 - self.assertHasChanged(name=True, number=True) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='retro', number=4, id=None) + self.instance.mutable = [1,2,3] + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) self.assertChanged() # Django 1.4 doesn't have update_fields if django.VERSION >= (1, 5, 0): self.instance.save(update_fields=[]) - self.assertHasChanged(name=True, number=True) - self.assertPrevious(name=None, number=None) - self.assertCurrent(name='retro', number=4, id=None) + self.assertHasChanged(name=True, number=True, mutable=True) + self.assertPrevious(name=None, number=None, mutable=None) + self.assertCurrent(name='retro', number=4, id=None, mutable=[1,2,3]) self.assertChanged() with self.assertRaises(ValueError): self.instance.save(update_fields=['number']) diff --git a/model_utils/tracker.py b/model_utils/tracker.py index a08a958..fa43c57 100644 --- a/model_utils/tracker.py +++ b/model_utils/tracker.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from copy import deepcopy -from json import JSONEncoder from django.db import models from django.core.exceptions import FieldError @@ -23,7 +22,10 @@ class FieldInstanceTracker(object): self.saved_data = self.current() else: self.saved_data.update(**self.current(fields=fields)) - return self.saved_data + + # preventing mutable fields side effects + for field, field_value in self.saved_data.items(): + self.saved_data[field] = deepcopy(field_value) def current(self, fields=None): """Return dict of current values for all tracked fields""" @@ -82,8 +84,7 @@ 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) - saved_data = tracker.set_saved_fields() - self.prevent_json_fields_side_effects(saved_data) + tracker.set_saved_fields() self.patch_save(instance) def patch_save(self, instance): @@ -106,16 +107,6 @@ class FieldTracker(object): return ret instance.save = save - def prevent_json_fields_side_effects(self, saved_data): - for field, field_value in saved_data.items(): - if isinstance(field_value, (dict, list, tuple)): - try: - JSONEncoder().encode(field_value) - except TypeError: - pass - else: - saved_data[field] = deepcopy(field_value) - def __get__(self, instance, owner): if instance is None: return self