From e7723683dc652613df369d5ca412e8b1217012d3 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sun, 30 Sep 2012 16:34:13 +0400 Subject: [PATCH] Fixed #9279 -- Added ignorenonexistent option to loaddata Thanks to Roman Gladkov for the initial patch and Simon Charette for review. --- django/core/management/commands/loaddata.py | 8 ++++- django/core/serializers/python.py | 12 +++++++- docs/ref/django-admin.txt | 5 ++++ docs/releases/1.5.txt | 3 ++ docs/topics/serialization.txt | 8 +++++ .../fixtures/sequence_extra.json | 13 +++++++++ .../regressiontests/fixtures_regress/tests.py | 29 +++++++++++++++++++ 7 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 30cf740cdf..a2e7f7d4c9 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -23,6 +23,7 @@ try: except ImportError: has_bz2 = False + class Command(BaseCommand): help = 'Installs the named fixture(s) in the database.' args = "fixture [fixture ...]" @@ -31,9 +32,14 @@ class Command(BaseCommand): make_option('--database', action='store', dest='database', default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load ' '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): + + ignore = options.get('ignore') using = options.get('database') connection = connections[using] @@ -175,7 +181,7 @@ class Command(BaseCommand): self.stdout.write("Installing %s fixture '%s' from %s." % \ (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: objects_in_fixture += 1 diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index a1fff6f9bb..37fa906280 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -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 import six + class Serializer(base.Serializer): """ Serializes a QuerySet to basic Python objects. @@ -72,6 +73,7 @@ class Serializer(base.Serializer): def getvalue(self): return self.objects + def Deserializer(object_list, **options): """ 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 """ db = options.pop('using', DEFAULT_DB_ALIAS) + ignore = options.pop('ignorenonexistent', False) + models.get_apps() 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 = {Model._meta.pk.attname: Model._meta.pk.to_python(d["pk"])} m2m_data = {} + model_fields = Model._meta.get_all_field_names() # Handle each field 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): field_value = smart_text(field_value, options.get("encoding", settings.DEFAULT_CHARSET), strings_only=True) diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 93e8fd9856..7fa7539985 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -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 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"? ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 546170b2a8..11b3488c11 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -195,6 +195,9 @@ Django 1.5 also includes several smaller improvements worth noting: whenever a user fails to login successfully. See :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 ===================================== diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt index ac1a77ed98..9b44166e42 100644 --- a/docs/topics/serialization.txt +++ b/docs/topics/serialization.txt @@ -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``. +.. 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 diff --git a/tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json b/tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json new file mode 100644 index 0000000000..03c0f36696 --- /dev/null +++ b/tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json @@ -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 + } + } +] diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py index d675372c7a..c9b9058dff 100644 --- a/tests/regressiontests/fixtures_regress/tests.py +++ b/tests/regressiontests/fixtures_regress/tests.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, unicode_literals import os import re +from django.core.serializers.base import DeserializationError from django.core import management from django.core.management.base import CommandError 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): + def animal_pre_save_check(self, signal, sender, instance, **kwargs): self.pre_save_checks.append( ( @@ -54,6 +56,33 @@ class TestFixtures(TestCase): animal.save() 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') def test_pretty_print_xml(self): """