Fixed #9279 -- Added ignorenonexistent option to loaddata

Thanks to Roman Gladkov for the initial patch and Simon Charette for review.
This commit is contained in:
Preston Holmes 2012-09-30 16:34:13 +04:00
parent 7cc4068c44
commit e7723683dc
7 changed files with 76 additions and 2 deletions

View File

@ -23,6 +23,7 @@ try:
except ImportError: except ImportError:
has_bz2 = False has_bz2 = False
class Command(BaseCommand): class Command(BaseCommand):
help = 'Installs the named fixture(s) in the database.' help = 'Installs the named fixture(s) in the database.'
args = "fixture [fixture ...]" args = "fixture [fixture ...]"
@ -31,9 +32,14 @@ class Command(BaseCommand):
make_option('--database', action='store', dest='database', make_option('--database', action='store', dest='database',
default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load ' default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load '
'fixtures into. Defaults to the "default" database.'), 'fixtures into. Defaults to the "default" database.'),
make_option('--ignorenonexistent', '-i', action='store_true', dest='ignore',
default=False, help='Ignores entries in the serialised data for fields'
' that have been removed from the database'),
) )
def handle(self, *fixture_labels, **options): def handle(self, *fixture_labels, **options):
ignore = options.get('ignore')
using = options.get('database') using = options.get('database')
connection = connections[using] connection = connections[using]
@ -175,7 +181,7 @@ class Command(BaseCommand):
self.stdout.write("Installing %s fixture '%s' from %s." % \ self.stdout.write("Installing %s fixture '%s' from %s." % \
(format, fixture_name, humanize(fixture_dir))) (format, fixture_name, humanize(fixture_dir)))
objects = serializers.deserialize(format, fixture, using=using) objects = serializers.deserialize(format, fixture, using=using, ignorenonexistent=ignore)
for obj in objects: for obj in objects:
objects_in_fixture += 1 objects_in_fixture += 1

View File

@ -11,6 +11,7 @@ from django.db import models, DEFAULT_DB_ALIAS
from django.utils.encoding import smart_text, is_protected_type from django.utils.encoding import smart_text, is_protected_type
from django.utils import six from django.utils import six
class Serializer(base.Serializer): class Serializer(base.Serializer):
""" """
Serializes a QuerySet to basic Python objects. Serializes a QuerySet to basic Python objects.
@ -72,6 +73,7 @@ class Serializer(base.Serializer):
def getvalue(self): def getvalue(self):
return self.objects return self.objects
def Deserializer(object_list, **options): def Deserializer(object_list, **options):
""" """
Deserialize simple Python objects back into Django ORM instances. Deserialize simple Python objects back into Django ORM instances.
@ -80,15 +82,23 @@ def Deserializer(object_list, **options):
stream or a string) to the constructor stream or a string) to the constructor
""" """
db = options.pop('using', DEFAULT_DB_ALIAS) db = options.pop('using', DEFAULT_DB_ALIAS)
ignore = options.pop('ignorenonexistent', False)
models.get_apps() models.get_apps()
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["pk"])} data = {Model._meta.pk.attname: Model._meta.pk.to_python(d["pk"])}
m2m_data = {} m2m_data = {}
model_fields = Model._meta.get_all_field_names()
# Handle each field # Handle each field
for (field_name, field_value) in six.iteritems(d["fields"]): for (field_name, field_value) in six.iteritems(d["fields"]):
if ignore and field_name not in model_fields:
# skip fields no longer on model
continue
if isinstance(field_value, str): if isinstance(field_value, str):
field_value = smart_text(field_value, options.get("encoding", settings.DEFAULT_CHARSET), strings_only=True) field_value = smart_text(field_value, options.get("encoding", settings.DEFAULT_CHARSET), strings_only=True)

View File

@ -289,6 +289,11 @@ Searches for and loads the contents of the named fixture into the database.
The :djadminopt:`--database` option can be used to specify the database The :djadminopt:`--database` option can be used to specify the database
onto which the data will be loaded. onto which the data will be loaded.
.. versionadded:: 1.5
The :djadminopt:`--ignorenonexistent` option can be used to ignore fields that
may have been removed from models since the fixture was originally generated.
What's a "fixture"? What's a "fixture"?
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~

View File

@ -195,6 +195,9 @@ Django 1.5 also includes several smaller improvements worth noting:
whenever a user fails to login successfully. See whenever a user fails to login successfully. See
:data:`~django.contrib.auth.signals.user_login_failed` :data:`~django.contrib.auth.signals.user_login_failed`
* The loaddata management command now supports an `ignorenonexistent` option to
ignore data for fields that no longer exist.
Backwards incompatible changes in 1.5 Backwards incompatible changes in 1.5
===================================== =====================================

View File

@ -130,6 +130,14 @@ trust your data source you could just save the object and move on.
The Django object itself can be inspected as ``deserialized_object.object``. The Django object itself can be inspected as ``deserialized_object.object``.
.. versionadded:: 1.5
If fields in the serialized data do not exist on a model,
a ``DeserializationError`` will be raised unless the ``ignorenonexistent``
argument is passed in as True::
serializers.deserialize("xml", data, ignorenonexistent=True)
.. _serialization-formats: .. _serialization-formats:
Serialization formats Serialization formats

View File

@ -0,0 +1,13 @@
[
{
"pk": "1",
"model": "fixtures_regress.animal",
"fields": {
"name": "Lion",
"extra_name": "Super Lion",
"latin_name": "Panthera leo",
"count": 3,
"weight": 1.2
}
}
]

View File

@ -5,6 +5,7 @@ from __future__ import absolute_import, unicode_literals
import os import os
import re import re
from django.core.serializers.base import DeserializationError
from django.core import management from django.core import management
from django.core.management.base import CommandError from django.core.management.base import CommandError
from django.core.management.commands.dumpdata import sort_dependencies from django.core.management.commands.dumpdata import sort_dependencies
@ -22,6 +23,7 @@ from .models import (Animal, Stuff, Absolute, Parent, Child, Article, Widget,
class TestFixtures(TestCase): class TestFixtures(TestCase):
def animal_pre_save_check(self, signal, sender, instance, **kwargs): def animal_pre_save_check(self, signal, sender, instance, **kwargs):
self.pre_save_checks.append( self.pre_save_checks.append(
( (
@ -54,6 +56,33 @@ class TestFixtures(TestCase):
animal.save() animal.save()
self.assertGreater(animal.id, 1) self.assertGreater(animal.id, 1)
def test_loaddata_not_found_fields_not_ignore(self):
"""
Test for ticket #9279 -- Error is raised for entries in
the serialised data for fields that have been removed
from the database when not ignored.
"""
with self.assertRaises(DeserializationError):
management.call_command(
'loaddata',
'sequence_extra',
verbosity=0
)
def test_loaddata_not_found_fields_ignore(self):
"""
Test for ticket #9279 -- Ignores entries in
the serialised data for fields that have been removed
from the database.
"""
management.call_command(
'loaddata',
'sequence_extra',
ignore=True,
verbosity=0
)
self.assertEqual(Animal.specimens.all()[0].name, 'Lion')
@skipIfDBFeature('interprets_empty_strings_as_nulls') @skipIfDBFeature('interprets_empty_strings_as_nulls')
def test_pretty_print_xml(self): def test_pretty_print_xml(self):
""" """