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.
This commit is contained in:
Tai Lee 2012-08-01 11:49:01 +10:00 committed by Tim Graham
parent 945e033a69
commit e527c0b6d8
12 changed files with 211 additions and 50 deletions

View File

@ -1,3 +1,5 @@
import warnings
from collections import OrderedDict from collections import OrderedDict
from optparse import make_option 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).'), 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, make_option('-n', '--natural', action='store_true', dest='use_natural_keys', default=False,
help='Use natural keys if they are available.'), 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, 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."), 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 " make_option('--pks', dest='primary_keys', help="Only dump objects with "
@ -40,6 +46,11 @@ class Command(BaseCommand):
excludes = options.get('exclude') excludes = options.get('exclude')
show_traceback = options.get('traceback') show_traceback = options.get('traceback')
use_natural_keys = options.get('use_natural_keys') 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') use_base_manager = options.get('use_base_manager')
pks = options.get('primary_keys') pks = options.get('primary_keys')
@ -133,7 +144,9 @@ class Command(BaseCommand):
try: try:
self.stdout.ending = None self.stdout.ending = None
serializers.serialize(format, get_objects(), indent=indent, 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: except Exception as e:
if show_traceback: if show_traceback:
raise raise

View File

@ -1,6 +1,7 @@
""" """
Module for abstract serializer/unserializer base classes. Module for abstract serializer/unserializer base classes.
""" """
import warnings
from django.db import models from django.db import models
from django.utils import six from django.utils import six
@ -35,6 +36,11 @@ class Serializer(object):
self.stream = options.pop("stream", six.StringIO()) self.stream = options.pop("stream", six.StringIO())
self.selected_fields = options.pop("fields", None) self.selected_fields = options.pop("fields", None)
self.use_natural_keys = options.pop("use_natural_keys", False) 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.start_serialization()
self.first = True self.first = True
@ -169,3 +175,20 @@ class DeserializedObject(object):
# prevent a second (possibly accidental) call to save() from saving # prevent a second (possibly accidental) call to save() from saving
# the m2m data twice. # the m2m data twice.
self.m2m_data = None 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

View File

@ -34,11 +34,14 @@ class Serializer(base.Serializer):
self._current = None self._current = None
def get_dump_object(self, obj): def get_dump_object(self, obj):
return { data = {
"pk": smart_text(obj._get_pk_val(), strings_only=True),
"model": smart_text(obj._meta), "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): def handle_field(self, obj, field):
value = field._get_val_from_obj(obj) value = field._get_val_from_obj(obj)
@ -51,7 +54,7 @@ class Serializer(base.Serializer):
self._current[field.name] = field.value_to_string(obj) self._current[field.name] = field.value_to_string(obj)
def handle_fk_field(self, obj, field): 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) related = getattr(obj, field.name)
if related: if related:
value = related.natural_key() value = related.natural_key()
@ -63,7 +66,7 @@ class Serializer(base.Serializer):
def handle_m2m_field(self, obj, field): def handle_m2m_field(self, obj, field):
if field.rel.through._meta.auto_created: 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() m2m_value = lambda value: value.natural_key()
else: else:
m2m_value = lambda value: smart_text(value._get_pk_val(), strings_only=True) 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: for d in object_list:
# Look up the model and starting build a dict of data for it. # Look up the model and starting build a dict of data for it.
Model = _get_model(d["model"]) 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 = {} m2m_data = {}
model_fields = Model._meta.get_all_field_names() model_fields = Model._meta.get_all_field_names()
@ -139,7 +144,8 @@ def Deserializer(object_list, **options):
else: else:
data[field.name] = field.to_python(field_value) 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): def _get_model(model_identifier):
""" """

View File

@ -46,14 +46,11 @@ class Serializer(base.Serializer):
raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj)) raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj))
self.indent(1) self.indent(1)
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() obj_pk = obj._get_pk_val()
if obj_pk is None: if obj_pk is not None:
attrs = {"model": smart_text(obj._meta),} attrs['pk'] = smart_text(obj_pk)
else:
attrs = {
"pk": smart_text(obj._get_pk_val()),
"model": smart_text(obj._meta),
}
self.xml.startElement("object", attrs) self.xml.startElement("object", attrs)
@ -91,7 +88,7 @@ class Serializer(base.Serializer):
self._start_relational_field(field) self._start_relational_field(field)
related_att = getattr(obj, field.get_attname()) related_att = getattr(obj, field.get_attname())
if related_att is not None: 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) related = getattr(obj, field.name)
# If related object has a natural key, use it # If related object has a natural key, use it
related = related.natural_key() related = related.natural_key()
@ -114,7 +111,7 @@ class Serializer(base.Serializer):
""" """
if field.rel.through._meta.auto_created: if field.rel.through._meta.auto_created:
self._start_relational_field(field) 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 # If the objects in the m2m have a natural key, use it
def handle_m2m(value): def handle_m2m(value):
natural = value.natural_key() natural = value.natural_key()
@ -177,13 +174,10 @@ class Deserializer(base.Deserializer):
Model = self._get_model_from_node(node, "model") Model = self._get_model_from_node(node, "model")
# Start building a data dictionary from the object. # Start building a data dictionary from the object.
# If the node is missing the pk set it to None data = {}
if node.hasAttribute("pk"): if node.hasAttribute('pk'):
pk = node.getAttribute("pk") data[Model._meta.pk.attname] = Model._meta.pk.to_python(
else: node.getAttribute('pk'))
pk = None
data = {Model._meta.pk.attname : Model._meta.pk.to_python(pk)}
# Also start building a dict of m2m data (this is saved as # Also start building a dict of m2m data (this is saved as
# {m2m_accessor_attribute : [list_of_related_objects]}) # {m2m_accessor_attribute : [list_of_related_objects]})
@ -217,8 +211,10 @@ class Deserializer(base.Deserializer):
value = field.to_python(getInnerText(field_node).strip()) value = field.to_python(getInnerText(field_node).strip())
data[field.name] = value 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 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): def _handle_fk_field_node(self, node, field):
""" """

View File

@ -461,6 +461,12 @@ these changes.
``BaseMemcachedCache._get_memcache_timeout()`` method to ``BaseMemcachedCache._get_memcache_timeout()`` method to
``get_backend_timeout()``. ``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 2.0
--- ---

View File

@ -220,13 +220,34 @@ also mix application names and model names.
The :djadminopt:`--database` option can be used to specify the database The :djadminopt:`--database` option can be used to specify the database
from which data will be dumped. 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
<topics-serialization-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 .. django-admin-option:: --natural
.. deprecated:: 1.7
Equivalent to the :djadminopt:`--natural-foreign` option; use that instead.
Use :ref:`natural keys <topics-serialization-natural-keys>` to represent Use :ref:`natural keys <topics-serialization-natural-keys>` to represent
any foreign key and many-to-many relationship with a model that provides any foreign key and many-to-many relationship with a model that provides
a natural key definition. If you are dumping ``contrib.auth`` ``Permission`` a natural key definition.
objects or ``contrib.contenttypes`` ``ContentType`` objects, you should
probably be using this flag.
.. versionadded:: 1.6 .. versionadded:: 1.6

View File

@ -294,6 +294,11 @@ Management Commands
* The :djadminopt:`--no-color` option for ``django-admin.py`` allows you to * The :djadminopt:`--no-color` option for ``django-admin.py`` allows you to
disable the colorization of management command output. 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 Models
^^^^^^ ^^^^^^
@ -588,3 +593,12 @@ The :class:`django.db.models.IPAddressField` and
The ``BaseMemcachedCache._get_memcache_timeout()`` method has been renamed to The ``BaseMemcachedCache._get_memcache_timeout()`` method has been renamed to
``get_backend_timeout()``. Despite being a private API, it will go through the ``get_backend_timeout()``. Despite being a private API, it will go through the
normal deprecation. 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.

View File

@ -404,6 +404,12 @@ into the primary key of an actual ``Person`` object.
fields will be effectively unique, you can still use those fields fields will be effectively unique, you can still use those fields
as a natural key. 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 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 That method should always return a natural key tuple -- in this
example, ``(first name, last name)``. Then, when you call example, ``(first name, last name)``. Then, when you call
``serializers.serialize()``, you provide a ``use_natural_keys=True`` ``serializers.serialize()``, you provide ``use_natural_foreign_keys=True``
argument:: 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 When ``use_natural_foreign_keys=True`` is specified, Django will use the
``natural_key()`` method to serialize any reference to objects of the ``natural_key()`` method to serialize any foreign key reference to objects
type that defines the method. of the type that defines the method.
If you are using :djadmin:`dumpdata` to generate serialized data, you When ``use_natural_primary_keys=True`` is specified, Django will not provide the
use the :djadminopt:`--natural` command line flag to generate natural keys. 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:: .. 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 natural keys during serialization, but *not* be able to load those
key values, just don't define the ``get_by_natural_key()`` method. 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 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. must exist before you include a natural key reference to that data.
To accommodate this limitation, calls to :djadmin:`dumpdata` that use 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. ``natural_key()`` method before serializing standard primary key objects.
However, this may not always be enough. If your natural key refers to However, this may not always be enough. If your natural key refers to

View File

@ -37,13 +37,15 @@ class SubclassTestCaseFixtureLoadingTests(TestCaseFixtureLoadingTests):
class DumpDataAssertMixin(object): 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=''): use_base_manager=False, exclude_list=[], primary_keys=''):
new_io = six.StringIO() new_io = six.StringIO()
management.call_command('dumpdata', *args, **{'format': format, management.call_command('dumpdata', *args, **{'format': format,
'stdout': new_io, 'stdout': new_io,
'stderr': 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, 'use_base_manager': use_base_manager,
'exclude': exclude_list, 'exclude': exclude_list,
'primary_keys': primary_keys}) '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": []}}]') 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 # 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 # 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 # Dump the current contents of the database as an XML fixture
self._dumpdata_assert(['fixtures'], """<?xml version="1.0" encoding="utf-8"?> self._dumpdata_assert(['fixtures'], """<?xml version="1.0" encoding="utf-8"?>
<django-objects version="1.0"><object pk="1" model="fixtures.category"><field type="CharField" name="title">News Stories</field><field type="TextField" name="description">Latest news stories</field></object><object pk="2" model="fixtures.article"><field type="CharField" name="headline">Poker on TV is great!</field><field type="DateTimeField" name="pub_date">2006-06-16T11:00:00</field></object><object pk="3" model="fixtures.article"><field type="CharField" name="headline">Copyright is fine the way it is</field><field type="DateTimeField" name="pub_date">2006-06-16T14:00:00</field></object><object pk="4" model="fixtures.article"><field type="CharField" name="headline">Django conquers world!</field><field type="DateTimeField" name="pub_date">2006-06-16T15:00:00</field></object><object pk="5" model="fixtures.article"><field type="CharField" name="headline">XML identified as leading cause of cancer</field><field type="DateTimeField" name="pub_date">2006-06-16T16:00:00</field></object><object pk="1" model="fixtures.tag"><field type="CharField" name="name">copyright</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="2" model="fixtures.tag"><field type="CharField" name="name">legal</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="3" model="fixtures.tag"><field type="CharField" name="name">django</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">4</field></object><object pk="4" model="fixtures.tag"><field type="CharField" name="name">world domination</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">4</field></object><object pk="1" model="fixtures.person"><field type="CharField" name="name">Django Reinhardt</field></object><object pk="2" model="fixtures.person"><field type="CharField" name="name">Stephane Grappelli</field></object><object pk="3" model="fixtures.person"><field type="CharField" name="name">Artist formerly known as "Prince"</field></object><object pk="1" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Django Reinhardt</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>add_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>change_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>delete_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="2" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Stephane Grappelli</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>add_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>delete_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="3" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Artist formerly known as "Prince"</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>change_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="1" model="fixtures.book"><field type="CharField" name="name">Music for all ages</field><field to="fixtures.person" name="authors" rel="ManyToManyRel"><object><natural>Artist formerly known as "Prince"</natural></object><object><natural>Django Reinhardt</natural></object></field></object><object pk="10" model="fixtures.book"><field type="CharField" name="name">Achieving self-awareness of Python programs</field><field to="fixtures.person" name="authors" rel="ManyToManyRel"></field></object></django-objects>""", format='xml', natural_keys=True) <django-objects version="1.0"><object pk="1" model="fixtures.category"><field type="CharField" name="title">News Stories</field><field type="TextField" name="description">Latest news stories</field></object><object pk="2" model="fixtures.article"><field type="CharField" name="headline">Poker on TV is great!</field><field type="DateTimeField" name="pub_date">2006-06-16T11:00:00</field></object><object pk="3" model="fixtures.article"><field type="CharField" name="headline">Copyright is fine the way it is</field><field type="DateTimeField" name="pub_date">2006-06-16T14:00:00</field></object><object pk="4" model="fixtures.article"><field type="CharField" name="headline">Django conquers world!</field><field type="DateTimeField" name="pub_date">2006-06-16T15:00:00</field></object><object pk="5" model="fixtures.article"><field type="CharField" name="headline">XML identified as leading cause of cancer</field><field type="DateTimeField" name="pub_date">2006-06-16T16:00:00</field></object><object pk="1" model="fixtures.tag"><field type="CharField" name="name">copyright</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="2" model="fixtures.tag"><field type="CharField" name="name">legal</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="3" model="fixtures.tag"><field type="CharField" name="name">django</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">4</field></object><object pk="4" model="fixtures.tag"><field type="CharField" name="name">world domination</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">4</field></object><object pk="1" model="fixtures.person"><field type="CharField" name="name">Django Reinhardt</field></object><object pk="2" model="fixtures.person"><field type="CharField" name="name">Stephane Grappelli</field></object><object pk="3" model="fixtures.person"><field type="CharField" name="name">Artist formerly known as "Prince"</field></object><object pk="1" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Django Reinhardt</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>add_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>change_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>delete_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="2" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Stephane Grappelli</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>add_user</natural><natural>auth</natural><natural>user</natural></object><object><natural>delete_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="3" model="fixtures.visa"><field to="fixtures.person" name="person" rel="ManyToOneRel"><natural>Artist formerly known as "Prince"</natural></field><field to="auth.permission" name="permissions" rel="ManyToManyRel"><object><natural>change_user</natural><natural>auth</natural><natural>user</natural></object></field></object><object pk="1" model="fixtures.book"><field type="CharField" name="name">Music for all ages</field><field to="fixtures.person" name="authors" rel="ManyToManyRel"><object><natural>Artist formerly known as "Prince"</natural></object><object><natural>Django Reinhardt</natural></object></field></object><object pk="10" model="fixtures.book"><field type="CharField" name="name">Achieving self-awareness of Python programs</field><field to="fixtures.person" name="authors" rel="ManyToManyRel"></field></object></django-objects>""", format='xml', natural_foreign_keys=True)
def test_dumpdata_with_excludes(self): def test_dumpdata_with_excludes(self):
# Load fixture1 which has a site, two articles, and a category # Load fixture1 which has a site, two articles, and a category
@ -354,11 +359,11 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase):
], ordered=False) ], ordered=False)
# Dump the current contents of the database as a JSON fixture # 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 # Dump the current contents of the database as an XML fixture
self._dumpdata_assert(['fixtures'], """<?xml version="1.0" encoding="utf-8"?> self._dumpdata_assert(['fixtures'], """<?xml version="1.0" encoding="utf-8"?>
<django-objects version="1.0"><object pk="1" model="fixtures.category"><field type="CharField" name="title">News Stories</field><field type="TextField" name="description">Latest news stories</field></object><object pk="2" model="fixtures.article"><field type="CharField" name="headline">Poker has no place on ESPN</field><field type="DateTimeField" name="pub_date">2006-06-16T12:00:00</field></object><object pk="3" model="fixtures.article"><field type="CharField" name="headline">Time to reform copyright</field><field type="DateTimeField" name="pub_date">2006-06-16T13:00:00</field></object><object pk="1" model="fixtures.tag"><field type="CharField" name="name">copyright</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="2" model="fixtures.tag"><field type="CharField" name="name">law</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="1" model="fixtures.person"><field type="CharField" name="name">Django Reinhardt</field></object><object pk="2" model="fixtures.person"><field type="CharField" name="name">Stephane Grappelli</field></object><object pk="3" model="fixtures.person"><field type="CharField" name="name">Prince</field></object><object pk="10" model="fixtures.book"><field type="CharField" name="name">Achieving self-awareness of Python programs</field><field to="fixtures.person" name="authors" rel="ManyToManyRel"></field></object></django-objects>""", format='xml', natural_keys=True) <django-objects version="1.0"><object pk="1" model="fixtures.category"><field type="CharField" name="title">News Stories</field><field type="TextField" name="description">Latest news stories</field></object><object pk="2" model="fixtures.article"><field type="CharField" name="headline">Poker has no place on ESPN</field><field type="DateTimeField" name="pub_date">2006-06-16T12:00:00</field></object><object pk="3" model="fixtures.article"><field type="CharField" name="headline">Time to reform copyright</field><field type="DateTimeField" name="pub_date">2006-06-16T13:00:00</field></object><object pk="1" model="fixtures.tag"><field type="CharField" name="name">copyright</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="2" model="fixtures.tag"><field type="CharField" name="name">law</field><field to="contenttypes.contenttype" name="tagged_type" rel="ManyToOneRel"><natural>fixtures</natural><natural>article</natural></field><field type="PositiveIntegerField" name="tagged_id">3</field></object><object pk="1" model="fixtures.person"><field type="CharField" name="name">Django Reinhardt</field></object><object pk="2" model="fixtures.person"><field type="CharField" name="name">Stephane Grappelli</field></object><object pk="3" model="fixtures.person"><field type="CharField" name="name">Prince</field></object><object pk="10" model="fixtures.book"><field type="CharField" name="name">Achieving self-awareness of Python programs</field><field to="fixtures.person" name="authors" rel="ManyToManyRel"></field></object></django-objects>""", format='xml', natural_foreign_keys=True)
class FixtureTransactionTests(DumpDataAssertMixin, TransactionTestCase): class FixtureTransactionTests(DumpDataAssertMixin, TransactionTestCase):

View File

@ -546,12 +546,13 @@ class NaturalKeyFixtureTests(TestCase):
'fixtures_regress.store', 'fixtures_regress.store',
verbosity=0, verbosity=0,
format='json', format='json',
use_natural_keys=True, use_natural_foreign_keys=True,
use_natural_primary_keys=True,
stdout=stdout, stdout=stdout,
) )
self.assertJSONEqual( self.assertJSONEqual(
stdout.getvalue(), 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): def test_dependency_sorting(self):

View File

@ -118,6 +118,7 @@ class NaturalKeyAnchor(models.Model):
objects = NaturalKeyAnchorManager() objects = NaturalKeyAnchorManager()
data = models.CharField(max_length=100, unique=True) data = models.CharField(max_length=100, unique=True)
title = models.CharField(max_length=100, null=True)
def natural_key(self): def natural_key(self):
return (self.data,) return (self.data,)

View File

@ -11,6 +11,7 @@ from __future__ import unicode_literals
import datetime import datetime
import decimal import decimal
from unittest import expectedFailure, skipUnless from unittest import expectedFailure, skipUnless
import warnings
try: try:
import yaml import yaml
@ -476,6 +477,9 @@ def naturalKeySerializerTest(format, self):
for klass in instance_count: for klass in instance_count:
instance_count[klass] = klass.objects.count() instance_count[klass] = klass.objects.count()
# 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 # Serialize the test database
serialized_data = serializers.serialize(format, objects, indent=2, serialized_data = serializers.serialize(format, objects, indent=2,
use_natural_keys=True) use_natural_keys=True)
@ -523,6 +527,35 @@ def streamTest(format, self):
else: else:
self.assertEqual(string_data, stream.content.decode('utf-8')) 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 [ for format in [
f for f in serializers.get_serializer_formats() f for f in serializers.get_serializer_formats()
if not isinstance(serializers.get_serializer(f), serializers.BadSerializer) 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 + '_serializer', curry(serializerTest, format))
setattr(SerializerTests, 'test_' + format + '_natural_key_serializer', curry(naturalKeySerializerTest, 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_fields', curry(fieldsTest, format))
setattr(SerializerTests, 'test_' + format + '_serializer_natural_keys', curry(naturalKeyTest, format))
if format != 'python': if format != 'python':
setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format)) setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))