diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index 190636e670..9f535e3ba8 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -170,3 +170,21 @@ class DeserializedObject(object): # prevent a second (possibly accidental) call to save() from saving # the m2m data twice. self.m2m_data = None + +def build_instance(Model, data, db): + """ + Build a model instance. + + If the model instance doesn't have a primary key and the model supports + natural keys, try to retrieve it from the database. + """ + obj = Model(**data) + if obj.pk is None and hasattr(Model, 'natural_key') and\ + hasattr(Model._default_manager, 'get_by_natural_key'): + pk = obj.natural_key() + try: + obj.pk = Model._default_manager.db_manager(db)\ + .get_by_natural_key(*pk).pk + except Model.DoesNotExist: + pass + return obj diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index a68ea21f78..839fee12a9 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -27,11 +27,13 @@ class Serializer(base.Serializer): self._current = {} def end_object(self, obj): - self.objects.append({ - "model" : smart_unicode(obj._meta), - "pk" : smart_unicode(obj._get_pk_val(), strings_only=True), - "fields" : self._current - }) + data = { + "model": smart_unicode(obj._meta), + "fields": self._current + } + if not self.use_natural_keys or not hasattr(obj, 'natural_key'): + data['pk'] = smart_unicode(obj._get_pk_val(), strings_only=True) + self.objects.append(data) self._current = None def handle_field(self, obj, field): @@ -82,7 +84,9 @@ def Deserializer(object_list, **options): for d in object_list: # Look up the model and starting build a dict of data for it. Model = _get_model(d["model"]) - data = {Model._meta.pk.attname : Model._meta.pk.to_python(d["pk"])} + data = {} + if 'pk' in d: + data[Model._meta.pk.attname] = Model._meta.pk.to_python(d['pk']) m2m_data = {} # Handle each field @@ -127,7 +131,9 @@ def Deserializer(object_list, **options): else: data[field.name] = field.to_python(field_value) - yield base.DeserializedObject(Model(**data), m2m_data) + obj = base.build_instance(Model, data, db) + + yield base.DeserializedObject(obj, m2m_data) def _get_model(model_identifier): """ diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index bcf5631e00..36e3404988 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -42,16 +42,12 @@ class Serializer(base.Serializer): raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj)) self.indent(1) - obj_pk = obj._get_pk_val() - if obj_pk is None: - attrs = {"model": smart_unicode(obj._meta),} - else: - attrs = { - "pk": smart_unicode(obj._get_pk_val()), - "model": smart_unicode(obj._meta), - } - - self.xml.startElement("object", attrs) + object_data = {"model": smart_unicode(obj._meta)} + if not self.use_natural_keys or not hasattr(obj, 'natural_key'): + obj_pk = obj._get_pk_val() + if obj_pk is not None: + object_data['pk'] = smart_unicode(obj_pk) + self.xml.startElement("object", object_data) def end_object(self, obj): """ @@ -173,13 +169,10 @@ class Deserializer(base.Deserializer): Model = self._get_model_from_node(node, "model") # Start building a data dictionary from the object. - # If the node is missing the pk set it to None - if node.hasAttribute("pk"): - pk = node.getAttribute("pk") - else: - pk = None - - data = {Model._meta.pk.attname : Model._meta.pk.to_python(pk)} + data = {} + if node.hasAttribute('pk'): + data[Model._meta.pk.attname] = Model._meta.pk.to_python( + node.getAttribute('pk')) # Also start building a dict of m2m data (this is saved as # {m2m_accessor_attribute : [list_of_related_objects]}) @@ -210,8 +203,10 @@ class Deserializer(base.Deserializer): value = field.to_python(getInnerText(field_node).strip()) data[field.name] = value + obj = base.build_instance(Model, data, self.db) + # Return a DeserializedObject so that the m2m data has a place to live. - return base.DeserializedObject(Model(**data), m2m_data) + return base.DeserializedObject(obj, m2m_data) def _handle_fk_field_node(self, node, field): """ diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt index c8acc8539e..358f869a07 100644 --- a/docs/topics/serialization.txt +++ b/docs/topics/serialization.txt @@ -307,6 +307,12 @@ into the primary key of an actual ``Person`` object. fields will be effectively unique, you can still use those fields as a natural key. +.. versionchanged:: 1.3 + +Deserialization of objects with no primary key will always check whether the +model's manager has a ``get_by_natural_key()`` method and if so, use it to +populate the deserialized object's primary key. + Serialization of natural keys ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -353,6 +359,23 @@ use the `--natural` command line flag to generate natural keys. natural keys during serialization, but *not* be able to load those key values, just don't define the ``get_by_natural_key()`` method. +.. versionchanged:: 1.3 + +When ``use_natural_keys=True`` is specified, the primary key is no longer +provided in the serialized data of this object since it can be calculated +during deserialization:: + + ... + { + "model": "store.person", + "fields": { + "first_name": "Douglas", + "last_name": "Adams", + "birth_date": "1952-03-11", + } + } + ... + Dependencies during serialization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py index bec0a98202..32c55c0707 100644 --- a/tests/regressiontests/serializers_regress/models.py +++ b/tests/regressiontests/serializers_regress/models.py @@ -264,3 +264,17 @@ class LengthModel(models.Model): def __len__(self): return self.data + +#Tests for natural keys. +class BookManager(models.Manager): + def get_by_natural_key(self, isbn13): + return self.get(isbn13=isbn13) + +class Book(models.Model): + isbn13 = models.CharField(max_length=14) + title = models.CharField(max_length=100) + + objects = BookManager() + + def natural_key(self): + return (self.isbn13,) diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py index 4180143e21..12cc4f9bf2 100644 --- a/tests/regressiontests/serializers_regress/tests.py +++ b/tests/regressiontests/serializers_regress/tests.py @@ -414,8 +414,36 @@ def streamTest(format, self): self.assertEqual(string_data, stream.getvalue()) stream.close() +def naturalKeyTest(format, self): + book1 = {'isbn13': '978-1590597255', 'title': 'The Definitive Guide to ' + 'Django: Web Development Done Right'} + book2 = {'isbn13':'978-1590599969', 'title': 'Practical Django Projects'} + + # Create the books. + adrian = Book.objects.create(**book1) + james = Book.objects.create(**book2) + + # Serialize the books. + string_data = serializers.serialize(format, Book.objects.all(), indent=2, + use_natural_keys=True) + + # Delete one book (to prove that the natural key generation will only + # restore the primary keys of books found in the database via the + # get_natural_key manager method). + james.delete() + + # Deserialize and test. + books = list(serializers.deserialize(format, string_data)) + self.assertEqual(len(books), 2) + self.assertEqual(books[0].object.title, book1['title']) + self.assertEqual(books[0].object.pk, adrian.pk) + self.assertEqual(books[1].object.title, book2['title']) + self.assertEqual(books[1].object.pk, None) + for format in serializers.get_serializer_formats(): setattr(SerializerTests, 'test_' + format + '_serializer', curry(serializerTest, format)) setattr(SerializerTests, 'test_' + format + '_serializer_fields', curry(fieldsTest, format)) + setattr(SerializerTests, 'test_' + format + '_serializer_natural_key', + curry(naturalKeyTest, format)) if format != 'python': setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))