From 2d6d20def70dacf4ea783e5a0c8a72266e603bb7 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 12 Jul 2007 07:45:35 +0000 Subject: [PATCH] Fixed #4459 -- Added 'raw' argument to save method, to override any pre-save processing, and modified serializers to use a raw-save. This enables serialization of DateFields with auto_now/auto_now_add. Also modified serializers to invoke save() directly on the model baseclass, to avoid any (potentially order-dependent, data modifying) behavior in a custom save() method. git-svn-id: http://code.djangoproject.com/svn/django/trunk@5658 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/serializers/base.py | 7 +++++- django/db/models/base.py | 8 +++---- docs/db-api.txt | 17 +++++++++++++ .../serializers_regress/models.py | 13 ++++++++++ .../serializers_regress/tests.py | 24 ++++++++++++------- 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index 1ef7bee472..ee9e4dd621 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -158,7 +158,12 @@ class DeserializedObject(object): return "" % smart_str(self.object) def save(self, save_m2m=True): - self.object.save() + # Call save on the Model baseclass directly. This bypasses any + # model-defined save. The save is also forced to be raw. + # This ensures that the data that is deserialized is literally + # what came from the file, not post-processed by pre_save/save + # methods. + models.Model.save(self.object, raw=True) if self.m2m_data and save_m2m: for accessor_name, object_list in self.m2m_data.items(): setattr(self.object, accessor_name, object_list) diff --git a/django/db/models/base.py b/django/db/models/base.py index d430d03a31..eb8a9b63be 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -201,7 +201,7 @@ class Model(object): _prepare = classmethod(_prepare) - def save(self): + def save(self, raw=False): dispatcher.send(signal=signals.pre_save, sender=self.__class__, instance=self) non_pks = [f for f in self._meta.fields if not f.primary_key] @@ -218,7 +218,7 @@ class Model(object): self._meta.pk.get_db_prep_lookup('exact', pk_val)) # If it does already exist, do an UPDATE. if cursor.fetchone()[0] > 0: - db_values = [f.get_db_prep_save(f.pre_save(self, False)) for f in non_pks] + db_values = [f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, False)) for f in non_pks] if db_values: cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \ (backend.quote_name(self._meta.db_table), @@ -229,11 +229,11 @@ class Model(object): record_exists = False if not pk_set or not record_exists: field_names = [backend.quote_name(f.column) for f in self._meta.fields if not isinstance(f, AutoField)] - db_values = [f.get_db_prep_save(f.pre_save(self, True)) for f in self._meta.fields if not isinstance(f, AutoField)] + db_values = [f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True)) for f in self._meta.fields if not isinstance(f, AutoField)] # If the PK has been manually set, respect that. if pk_set: field_names += [f.column for f in self._meta.fields if isinstance(f, AutoField)] - db_values += [f.get_db_prep_save(f.pre_save(self, True)) for f in self._meta.fields if isinstance(f, AutoField)] + db_values += [f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True)) for f in self._meta.fields if isinstance(f, AutoField)] placeholders = ['%s'] * len(field_names) if self._meta.order_with_respect_to: field_names.append(backend.quote_name('_order')) diff --git a/docs/db-api.txt b/docs/db-api.txt index ef3d811189..feeb708be0 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -118,6 +118,23 @@ happens. Explicitly specifying auto-primary-key values is mostly useful for bulk-saving objects, when you're confident you won't have primary-key collision. +Raw saves +--------- + +When you save an Django object, some pre-processing will occur on the the data +that is in the object. For example, if your model has a ``DateField`` with +``auto_now=True`` set, the pre-save phase will alter the data in the object +to ensure that the date field contains the current date stamp. + +Although these automated changes can be very useful, there will be times when +you just want to save the data as-is. In these cases, you can invoke a *Raw Save* +by passing ``raw=True`` as an argument to the ``save()`` method:: + + b4.save(raw=True) # Saves object, but does no pre-processing + +A raw save saves all the data in your object, but performs no pre-save processing +on the data in the object. + Saving changes to objects ========================= diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py index b441885f10..268fa052e6 100644 --- a/tests/regressiontests/serializers_regress/models.py +++ b/tests/regressiontests/serializers_regress/models.py @@ -209,3 +209,16 @@ class ComplexModel(models.Model): field1 = models.CharField(maxlength=10) field2 = models.CharField(maxlength=10) field3 = models.CharField(maxlength=10) + +# Tests for handling fields with pre_save functions, or +# models with save functions that modify data +class AutoNowDateTimeData(models.Model): + data = models.DateTimeField(null=True, auto_now=True) + +class ModifyingSaveData(models.Model): + data = models.IntegerField(null=True) + + def save(self): + "A save method that modifies the data in the object" + self.data = 666 + super(ModifyingSaveData, self).save(raw) diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py index 7c6da9d356..30fd20a80c 100644 --- a/tests/regressiontests/serializers_regress/tests.py +++ b/tests/regressiontests/serializers_regress/tests.py @@ -24,17 +24,20 @@ except ImportError: from django.utils import _decimal as decimal # A set of functions that can be used to recreate -# test data objects of various kinds +# test data objects of various kinds. +# The save method is a raw base model save, to make +# sure that the data in the database matches the +# exact test case. def data_create(pk, klass, data): instance = klass(id=pk) instance.data = data - instance.save() + models.Model.save(instance, raw=True) return instance def generic_create(pk, klass, data): instance = klass(id=pk) instance.data = data[0] - instance.save() + models.Model.save(instance, raw=True) for tag in data[1:]: instance.tags.create(data=tag) return instance @@ -42,25 +45,25 @@ def generic_create(pk, klass, data): def fk_create(pk, klass, data): instance = klass(id=pk) setattr(instance, 'data_id', data) - instance.save() + models.Model.save(instance, raw=True) return instance def m2m_create(pk, klass, data): instance = klass(id=pk) - instance.save() + models.Model.save(instance, raw=True) instance.data = data return instance def o2o_create(pk, klass, data): instance = klass() instance.data_id = data - instance.save() + models.Model.save(instance, raw=True) return instance def pk_create(pk, klass, data): instance = klass() instance.data = data - instance.save() + models.Model.save(instance, raw=True) return instance # A set of functions that can be used to compare @@ -249,6 +252,9 @@ The end."""), # (pk_obj, 770, TimePKData, datetime.time(10,42,37)), (pk_obj, 780, USStatePKData, "MA"), # (pk_obj, 790, XMLPKData, ""), + + (data_obj, 800, AutoNowDateTimeData, datetime.datetime(2006,6,16,10,42,37)), + (data_obj, 810, ModifyingSaveData, 42), ] # Because Oracle treats the empty string as NULL, Oracle is expected to fail @@ -303,7 +309,7 @@ def fieldsTest(format, self): management.flush(verbosity=0, interactive=False) obj = ComplexModel(field1='first',field2='second',field3='third') - obj.save() + obj.save(raw=True) # Serialize then deserialize the test database serialized_data = serializers.serialize(format, [obj], indent=2, fields=('field1','field3')) @@ -319,7 +325,7 @@ def streamTest(format, self): management.flush(verbosity=0, interactive=False) obj = ComplexModel(field1='first',field2='second',field3='third') - obj.save() + obj.save(raw=True) # Serialize the test database to a stream stream = StringIO()