From e527c0b6d808cb8e4bedf79ded3dc4ad1a7e17a8 Mon Sep 17 00:00:00 2001 From: Tai Lee Date: Wed, 1 Aug 2012 11:49:01 +1000 Subject: [PATCH] Fixed #13252 -- Added ability to serialize with natural primary keys. Added ``--natural-foreign`` and ``--natural-primary`` options and deprecated the ``--natural`` option to the ``dumpdata`` management command. Added ``use_natural_foreign_keys`` and ``use_natural_primary_keys`` arguments and deprecated the ``use_natural_keys`` argument to ``django.core.serializers.Serializer.serialize()``. Thanks SmileyChris for the suggestion. --- django/core/management/commands/dumpdata.py | 15 +++++- django/core/serializers/base.py | 23 ++++++++ django/core/serializers/python.py | 20 ++++--- django/core/serializers/xml_serializer.py | 32 +++++------ docs/internals/deprecation.txt | 6 +++ docs/ref/django-admin.txt | 27 ++++++++-- docs/releases/1.7.txt | 14 +++++ docs/topics/serialization.txt | 59 +++++++++++++++++---- tests/fixtures/tests.py | 19 ++++--- tests/fixtures_regress/tests.py | 5 +- tests/serializers_regress/models.py | 1 + tests/serializers_regress/tests.py | 40 ++++++++++++-- 12 files changed, 211 insertions(+), 50 deletions(-) diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index ed58ec79a1..e177a99b5f 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -1,3 +1,5 @@ +import warnings + from collections import OrderedDict from optparse import make_option @@ -20,6 +22,10 @@ class Command(BaseCommand): help='An appname or appname.ModelName to exclude (use multiple --exclude to exclude multiple apps/models).'), make_option('-n', '--natural', action='store_true', dest='use_natural_keys', default=False, help='Use natural keys if they are available.'), + make_option('--natural-foreign', action='store_true', dest='use_natural_foreign_keys', default=False, + help='Use natural foreign keys if they are available.'), + make_option('--natural-primary', action='store_true', dest='use_natural_primary_keys', default=False, + help='Use natural primary keys if they are available.'), make_option('-a', '--all', action='store_true', dest='use_base_manager', default=False, help="Use Django's base manager to dump all models stored in the database, including those that would otherwise be filtered or modified by a custom manager."), make_option('--pks', dest='primary_keys', help="Only dump objects with " @@ -40,6 +46,11 @@ class Command(BaseCommand): excludes = options.get('exclude') show_traceback = options.get('traceback') use_natural_keys = options.get('use_natural_keys') + if use_natural_keys: + warnings.warn("``--natural`` is deprecated; use ``--natural-foreign`` instead.", + PendingDeprecationWarning) + use_natural_foreign_keys = options.get('use_natural_foreign_keys') or use_natural_keys + use_natural_primary_keys = options.get('use_natural_primary_keys') use_base_manager = options.get('use_base_manager') pks = options.get('primary_keys') @@ -133,7 +144,9 @@ class Command(BaseCommand): try: self.stdout.ending = None serializers.serialize(format, get_objects(), indent=indent, - use_natural_keys=use_natural_keys, stream=self.stdout) + use_natural_foreign_keys=use_natural_foreign_keys, + use_natural_primary_keys=use_natural_primary_keys, + stream=self.stdout) except Exception as e: if show_traceback: raise diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index da93c3154f..96ebd5ea53 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -1,6 +1,7 @@ """ Module for abstract serializer/unserializer base classes. """ +import warnings from django.db import models from django.utils import six @@ -35,6 +36,11 @@ class Serializer(object): self.stream = options.pop("stream", six.StringIO()) self.selected_fields = options.pop("fields", None) self.use_natural_keys = options.pop("use_natural_keys", False) + if self.use_natural_keys: + warnings.warn("``use_natural_keys`` is deprecated; use ``use_natural_foreign_keys`` instead.", + PendingDeprecationWarning) + self.use_natural_foreign_keys = options.pop('use_natural_foreign_keys', False) or self.use_natural_keys + self.use_natural_primary_keys = options.pop('use_natural_primary_keys', False) self.start_serialization() self.first = True @@ -169,3 +175,20 @@ 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')): + natural_key = obj.natural_key() + try: + obj.pk = Model._default_manager.db_manager(db).get_by_natural_key(*natural_key).pk + except Model.DoesNotExist: + pass + return obj diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index cdfac5044b..89c0bdfa28 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -34,11 +34,14 @@ class Serializer(base.Serializer): self._current = None def get_dump_object(self, obj): - return { - "pk": smart_text(obj._get_pk_val(), strings_only=True), + data = { "model": smart_text(obj._meta), - "fields": self._current + "fields": self._current, } + if not self.use_natural_primary_keys or not hasattr(obj, 'natural_key'): + data["pk"] = smart_text(obj._get_pk_val(), strings_only=True) + + return data def handle_field(self, obj, field): value = field._get_val_from_obj(obj) @@ -51,7 +54,7 @@ class Serializer(base.Serializer): self._current[field.name] = field.value_to_string(obj) def handle_fk_field(self, obj, field): - if self.use_natural_keys and hasattr(field.rel.to, 'natural_key'): + if self.use_natural_foreign_keys and hasattr(field.rel.to, 'natural_key'): related = getattr(obj, field.name) if related: value = related.natural_key() @@ -63,7 +66,7 @@ class Serializer(base.Serializer): def handle_m2m_field(self, obj, field): if field.rel.through._meta.auto_created: - if self.use_natural_keys and hasattr(field.rel.to, 'natural_key'): + if self.use_natural_foreign_keys and hasattr(field.rel.to, 'natural_key'): m2m_value = lambda value: value.natural_key() else: m2m_value = lambda value: smart_text(value._get_pk_val(), strings_only=True) @@ -88,7 +91,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.get("pk", None))} + data = {} + if 'pk' in d: + data[Model._meta.pk.attname] = Model._meta.pk.to_python(d.get("pk", None)) m2m_data = {} model_fields = Model._meta.get_all_field_names() @@ -139,7 +144,8 @@ 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 70542258e4..215ae2776d 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -46,14 +46,11 @@ 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_text(obj._meta),} - else: - attrs = { - "pk": smart_text(obj._get_pk_val()), - "model": smart_text(obj._meta), - } + attrs = {"model": smart_text(obj._meta)} + if not self.use_natural_primary_keys or not hasattr(obj, 'natural_key'): + obj_pk = obj._get_pk_val() + if obj_pk is not None: + attrs['pk'] = smart_text(obj_pk) self.xml.startElement("object", attrs) @@ -91,7 +88,7 @@ class Serializer(base.Serializer): self._start_relational_field(field) related_att = getattr(obj, field.get_attname()) if related_att is not None: - if self.use_natural_keys and hasattr(field.rel.to, 'natural_key'): + if self.use_natural_foreign_keys and hasattr(field.rel.to, 'natural_key'): related = getattr(obj, field.name) # If related object has a natural key, use it related = related.natural_key() @@ -114,7 +111,7 @@ class Serializer(base.Serializer): """ if field.rel.through._meta.auto_created: self._start_relational_field(field) - if self.use_natural_keys and hasattr(field.rel.to, 'natural_key'): + if self.use_natural_foreign_keys and hasattr(field.rel.to, 'natural_key'): # If the objects in the m2m have a natural key, use it def handle_m2m(value): natural = value.natural_key() @@ -177,13 +174,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]}) @@ -217,8 +211,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/internals/deprecation.txt b/docs/internals/deprecation.txt index 498f303daf..a965627a1c 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -461,6 +461,12 @@ these changes. ``BaseMemcachedCache._get_memcache_timeout()`` method to ``get_backend_timeout()``. +* The ``--natural`` and ``-n`` options for :djadmin:`dumpdata` will be removed. + Use :djadminopt:`--natural-foreign` instead. + +* The ``use_natural_keys`` argument for ``serializers.serialize()`` will be + removed. Use ``use_natural_foreign_keys`` instead. + 2.0 --- diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 5bc9a2b83e..39cbbc8dd5 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -220,13 +220,34 @@ also mix application names and model names. The :djadminopt:`--database` option can be used to specify the database from which data will be dumped. +.. django-admin-option:: --natural-foreign + +.. versionadded:: 1.7 + +When this option is specified, Django will use the ``natural_key()`` model +method to serialize any foreign key and many-to-many relationship to objects of +the type that defines the method. If you are dumping ``contrib.auth`` +``Permission`` objects or ``contrib.contenttypes`` ``ContentType`` objects, you +should probably be using this flag. See the :ref:`natural keys +` documentation for more details on this +and the next option. + +.. django-admin-option:: --natural-primary + +.. versionadded:: 1.7 + +When this option is specified, Django will not provide the primary key in the +serialized data of this object since it can be calculated during +deserialization. + .. django-admin-option:: --natural +.. deprecated:: 1.7 + Equivalent to the :djadminopt:`--natural-foreign` option; use that instead. + Use :ref:`natural keys ` to represent any foreign key and many-to-many relationship with a model that provides -a natural key definition. If you are dumping ``contrib.auth`` ``Permission`` -objects or ``contrib.contenttypes`` ``ContentType`` objects, you should -probably be using this flag. +a natural key definition. .. versionadded:: 1.6 diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 8fbd82adc6..eeec24842e 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -294,6 +294,11 @@ Management Commands * The :djadminopt:`--no-color` option for ``django-admin.py`` allows you to disable the colorization of management command output. +* The new :djadminopt:`--natural-foreign` and :djadminopt:`--natural-primary` + options for :djadmin:`dumpdata`, and the new ``use_natural_foreign_keys`` and + ``use_natural_primary_keys`` arguments for ``serializers.serialize()``, allow + the use of natural primary keys when serializing. + Models ^^^^^^ @@ -588,3 +593,12 @@ The :class:`django.db.models.IPAddressField` and The ``BaseMemcachedCache._get_memcache_timeout()`` method has been renamed to ``get_backend_timeout()``. Despite being a private API, it will go through the normal deprecation. + +Natural key serialization options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``--natural`` and ``-n`` options for :djadmin:`dumpdata` have been +deprecated. Use :djadminopt:`--natural-foreign` instead. + +Similarly, the ``use_natural_keys`` argument for ``serializers.serialize()`` +has been deprecated. Use ``use_natural_foreign_keys`` instead. diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt index 0f24715599..5a171470ac 100644 --- a/docs/topics/serialization.txt +++ b/docs/topics/serialization.txt @@ -404,6 +404,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. +.. versionadded:: 1.7 + +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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -426,17 +432,39 @@ Firstly, you need to add another method -- this time to the model itself:: That method should always return a natural key tuple -- in this example, ``(first name, last name)``. Then, when you call -``serializers.serialize()``, you provide a ``use_natural_keys=True`` -argument:: +``serializers.serialize()``, you provide ``use_natural_foreign_keys=True`` +or ``use_natural_primary_keys=True`` arguments:: - >>> serializers.serialize('json', [book1, book2], indent=2, use_natural_keys=True) + >>> serializers.serialize('json', [book1, book2], indent=2, + ... use_natural_foreign_keys=True, use_natural_primary_keys=True) -When ``use_natural_keys=True`` is specified, Django will use the -``natural_key()`` method to serialize any reference to objects of the -type that defines the method. +When ``use_natural_foreign_keys=True`` is specified, Django will use the +``natural_key()`` method to serialize any foreign key reference to objects +of the type that defines the method. -If you are using :djadmin:`dumpdata` to generate serialized data, you -use the :djadminopt:`--natural` command line flag to generate natural keys. +When ``use_natural_primary_keys=True`` is specified, Django will not provide the +primary key 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", + } + } + ... + +This can be useful when you need to load serialized data into an existing +database and you cannot guarantee that the serialized primary key value is not +already in use, and do not need to ensure that deserialized objects retain the +same primary keys. + +If you are using :djadmin:`dumpdata` to generate serialized data, use the +:djadminopt:`--natural-foreign` and :djadminopt:`--natural-primary` command +line flags to generate natural keys. .. note:: @@ -450,6 +478,19 @@ use the :djadminopt:`--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.7 + +Previously there was only a ``use_natural_keys`` argument for +``serializers.serialize()`` and the `-n` or `--natural` command line flags. +These have been deprecated in favor of the ``use_natural_foreign_keys`` and +``use_natural_primary_keys`` arguments and the corresponding +:djadminopt:`--natural-foreign` and :djadminopt:`--natural-primary` options +for :djadmin:`dumpdata`. + +The original argument and command line flags remain for backwards +compatibility and map to the new ``use_natural_foreign_keys`` argument and +`--natural-foreign` command line flag. They'll be removed in Django 1.9. + Dependencies during serialization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -459,7 +500,7 @@ a "forward reference" with natural keys -- the data you're referencing must exist before you include a natural key reference to that data. To accommodate this limitation, calls to :djadmin:`dumpdata` that use -the :djadminopt:`--natural` option will serialize any model with a +the :djadminopt:`--natural-foreign` option will serialize any model with a ``natural_key()`` method before serializing standard primary key objects. However, this may not always be enough. If your natural key refers to diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py index a1ecf007ce..8086dc337a 100644 --- a/tests/fixtures/tests.py +++ b/tests/fixtures/tests.py @@ -37,13 +37,15 @@ class SubclassTestCaseFixtureLoadingTests(TestCaseFixtureLoadingTests): class DumpDataAssertMixin(object): - def _dumpdata_assert(self, args, output, format='json', natural_keys=False, + def _dumpdata_assert(self, args, output, format='json', + natural_foreign_keys=False, natural_primary_keys=False, use_base_manager=False, exclude_list=[], primary_keys=''): new_io = six.StringIO() management.call_command('dumpdata', *args, **{'format': format, 'stdout': new_io, 'stderr': new_io, - 'use_natural_keys': natural_keys, + 'use_natural_foreign_keys': natural_foreign_keys, + 'use_natural_primary_keys': natural_primary_keys, 'use_base_manager': use_base_manager, 'exclude': exclude_list, 'primary_keys': primary_keys}) @@ -175,14 +177,17 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): self._dumpdata_assert(['fixtures.book'], '[{"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [3, 1]}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]') # But you can get natural keys if you ask for them and they are available - self._dumpdata_assert(['fixtures.book'], '[{"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_keys=True) + self._dumpdata_assert(['fixtures.book'], '[{"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_foreign_keys=True) + + # You can also omit the primary keys for models that we can get later with natural keys. + self._dumpdata_assert(['fixtures.person'], '[{"fields": {"name": "Django Reinhardt"}, "model": "fixtures.person"}, {"fields": {"name": "Stephane Grappelli"}, "model": "fixtures.person"}, {"fields": {"name": "Artist formerly known as \\"Prince\\""}, "model": "fixtures.person"}]', natural_primary_keys=True) # Dump the current contents of the database as a JSON fixture - self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker on TV is great!", "pub_date": "2006-06-16T11:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}, {"pk": 4, "model": "fixtures.article", "fields": {"headline": "Django conquers world!", "pub_date": "2006-06-16T15:00:00"}}, {"pk": 5, "model": "fixtures.article", "fields": {"headline": "XML identified as leading cause of cancer", "pub_date": "2006-06-16T16:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "legal", "tagged_id": 3}}, {"pk": 3, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "django", "tagged_id": 4}}, {"pk": 4, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "world domination", "tagged_id": 4}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Artist formerly known as \\"Prince\\""}}, {"pk": 1, "model": "fixtures.visa", "fields": {"person": ["Django Reinhardt"], "permissions": [["add_user", "auth", "user"], ["change_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 2, "model": "fixtures.visa", "fields": {"person": ["Stephane Grappelli"], "permissions": [["add_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 3, "model": "fixtures.visa", "fields": {"person": ["Artist formerly known as \\"Prince\\""], "permissions": [["change_user", "auth", "user"]]}}, {"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_keys=True) + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker on TV is great!", "pub_date": "2006-06-16T11:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}, {"pk": 4, "model": "fixtures.article", "fields": {"headline": "Django conquers world!", "pub_date": "2006-06-16T15:00:00"}}, {"pk": 5, "model": "fixtures.article", "fields": {"headline": "XML identified as leading cause of cancer", "pub_date": "2006-06-16T16:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "legal", "tagged_id": 3}}, {"pk": 3, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "django", "tagged_id": 4}}, {"pk": 4, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "world domination", "tagged_id": 4}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Artist formerly known as \\"Prince\\""}}, {"pk": 1, "model": "fixtures.visa", "fields": {"person": ["Django Reinhardt"], "permissions": [["add_user", "auth", "user"], ["change_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 2, "model": "fixtures.visa", "fields": {"person": ["Stephane Grappelli"], "permissions": [["add_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 3, "model": "fixtures.visa", "fields": {"person": ["Artist formerly known as \\"Prince\\""], "permissions": [["change_user", "auth", "user"]]}}, {"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_foreign_keys=True) # Dump the current contents of the database as an XML fixture self._dumpdata_assert(['fixtures'], """ -News StoriesLatest news storiesPoker on TV is great!2006-06-16T11:00:00Copyright is fine the way it is2006-06-16T14:00:00Django conquers world!2006-06-16T15:00:00XML identified as leading cause of cancer2006-06-16T16:00:00copyrightfixturesarticle3legalfixturesarticle3djangofixturesarticle4world dominationfixturesarticle4Django ReinhardtStephane GrappelliArtist formerly known as "Prince"Django Reinhardtadd_userauthuserchange_userauthuserdelete_userauthuserStephane Grappelliadd_userauthuserdelete_userauthuserArtist formerly known as "Prince"change_userauthuserMusic for all agesArtist formerly known as "Prince"Django ReinhardtAchieving self-awareness of Python programs""", format='xml', natural_keys=True) +News StoriesLatest news storiesPoker on TV is great!2006-06-16T11:00:00Copyright is fine the way it is2006-06-16T14:00:00Django conquers world!2006-06-16T15:00:00XML identified as leading cause of cancer2006-06-16T16:00:00copyrightfixturesarticle3legalfixturesarticle3djangofixturesarticle4world dominationfixturesarticle4Django ReinhardtStephane GrappelliArtist formerly known as "Prince"Django Reinhardtadd_userauthuserchange_userauthuserdelete_userauthuserStephane Grappelliadd_userauthuserdelete_userauthuserArtist formerly known as "Prince"change_userauthuserMusic for all agesArtist formerly known as "Prince"Django ReinhardtAchieving self-awareness of Python programs""", format='xml', natural_foreign_keys=True) def test_dumpdata_with_excludes(self): # Load fixture1 which has a site, two articles, and a category @@ -354,11 +359,11 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): ], ordered=False) # Dump the current contents of the database as a JSON fixture - self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "law", "tagged_id": 3}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Prince"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_keys=True) + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "law", "tagged_id": 3}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Prince"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_foreign_keys=True) # Dump the current contents of the database as an XML fixture self._dumpdata_assert(['fixtures'], """ -News StoriesLatest news storiesPoker has no place on ESPN2006-06-16T12:00:00Time to reform copyright2006-06-16T13:00:00copyrightfixturesarticle3lawfixturesarticle3Django ReinhardtStephane GrappelliPrinceAchieving self-awareness of Python programs""", format='xml', natural_keys=True) +News StoriesLatest news storiesPoker has no place on ESPN2006-06-16T12:00:00Time to reform copyright2006-06-16T13:00:00copyrightfixturesarticle3lawfixturesarticle3Django ReinhardtStephane GrappelliPrinceAchieving self-awareness of Python programs""", format='xml', natural_foreign_keys=True) class FixtureTransactionTests(DumpDataAssertMixin, TransactionTestCase): diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py index 334aa6cadc..514525934b 100644 --- a/tests/fixtures_regress/tests.py +++ b/tests/fixtures_regress/tests.py @@ -546,12 +546,13 @@ class NaturalKeyFixtureTests(TestCase): 'fixtures_regress.store', verbosity=0, format='json', - use_natural_keys=True, + use_natural_foreign_keys=True, + use_natural_primary_keys=True, stdout=stdout, ) self.assertJSONEqual( stdout.getvalue(), - """[{"pk": 2, "model": "fixtures_regress.store", "fields": {"main": null, "name": "Amazon"}}, {"pk": 3, "model": "fixtures_regress.store", "fields": {"main": null, "name": "Borders"}}, {"pk": 4, "model": "fixtures_regress.person", "fields": {"name": "Neal Stephenson"}}, {"pk": 1, "model": "fixtures_regress.book", "fields": {"stores": [["Amazon"], ["Borders"]], "name": "Cryptonomicon", "author": ["Neal Stephenson"]}}]""" + """[{"fields": {"main": null, "name": "Amazon"}, "model": "fixtures_regress.store"}, {"fields": {"main": null, "name": "Borders"}, "model": "fixtures_regress.store"}, {"fields": {"name": "Neal Stephenson"}, "model": "fixtures_regress.person"}, {"pk": 1, "model": "fixtures_regress.book", "fields": {"stores": [["Amazon"], ["Borders"]], "name": "Cryptonomicon", "author": ["Neal Stephenson"]}}]""" ) def test_dependency_sorting(self): diff --git a/tests/serializers_regress/models.py b/tests/serializers_regress/models.py index 2e9971465d..144a7b2390 100644 --- a/tests/serializers_regress/models.py +++ b/tests/serializers_regress/models.py @@ -118,6 +118,7 @@ class NaturalKeyAnchor(models.Model): objects = NaturalKeyAnchorManager() data = models.CharField(max_length=100, unique=True) + title = models.CharField(max_length=100, null=True) def natural_key(self): return (self.data,) diff --git a/tests/serializers_regress/tests.py b/tests/serializers_regress/tests.py index bd71d4da6a..83561e9c46 100644 --- a/tests/serializers_regress/tests.py +++ b/tests/serializers_regress/tests.py @@ -11,6 +11,7 @@ from __future__ import unicode_literals import datetime import decimal from unittest import expectedFailure, skipUnless +import warnings try: import yaml @@ -476,9 +477,12 @@ def naturalKeySerializerTest(format, self): for klass in instance_count: instance_count[klass] = klass.objects.count() - # Serialize the test database - serialized_data = serializers.serialize(format, objects, indent=2, - use_natural_keys=True) + # use_natural_keys is deprecated and to be removed in Django 1.9 + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + # Serialize the test database + serialized_data = serializers.serialize(format, objects, indent=2, + use_natural_keys=True) for obj in serializers.deserialize(format, serialized_data): obj.save() @@ -523,6 +527,35 @@ def streamTest(format, self): else: self.assertEqual(string_data, stream.content.decode('utf-8')) + +def naturalKeyTest(format, self): + book1 = {'data': '978-1590597255', 'title': 'The Definitive Guide to ' + 'Django: Web Development Done Right'} + book2 = {'data':'978-1590599969', 'title': 'Practical Django Projects'} + + # Create the books. + adrian = NaturalKeyAnchor.objects.create(**book1) + james = NaturalKeyAnchor.objects.create(**book2) + + # Serialize the books. + string_data = serializers.serialize(format, NaturalKeyAnchor.objects.all(), + indent=2, use_natural_foreign_keys=True, + use_natural_primary_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 [ f for f in serializers.get_serializer_formats() if not isinstance(serializers.get_serializer(f), serializers.BadSerializer) @@ -530,6 +563,7 @@ for format in [ setattr(SerializerTests, 'test_' + format + '_serializer', curry(serializerTest, format)) setattr(SerializerTests, 'test_' + format + '_natural_key_serializer', curry(naturalKeySerializerTest, format)) setattr(SerializerTests, 'test_' + format + '_serializer_fields', curry(fieldsTest, format)) + setattr(SerializerTests, 'test_' + format + '_serializer_natural_keys', curry(naturalKeyTest, format)) if format != 'python': setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))