From ae2a7da86bf841d42be86dea3effb0261187c950 Mon Sep 17 00:00:00 2001 From: Berker Peksag Date: Tue, 17 May 2016 09:52:01 +0300 Subject: [PATCH] Fixed #20468 -- Added loaddata --exclude option. Thanks Alex Morozov for the initial patch. --- django/core/management/commands/dumpdata.py | 17 ++----------- django/core/management/commands/loaddata.py | 10 +++++++- django/core/management/utils.py | 28 +++++++++++++++++++++ docs/ref/django-admin.txt | 8 ++++++ docs/releases/1.11.txt | 3 ++- tests/fixtures/tests.py | 28 +++++++++++++++++++-- 6 files changed, 75 insertions(+), 19 deletions(-) diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index b8547d684f..e16c8f1f17 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -4,6 +4,7 @@ from collections import OrderedDict from django.apps import apps from django.core import serializers from django.core.management.base import BaseCommand, CommandError +from django.core.management.utils import parse_apps_and_model_labels from django.db import DEFAULT_DB_ALIAS, router @@ -81,21 +82,7 @@ class Command(BaseCommand): else: primary_keys = [] - excluded_apps = set() - excluded_models = set() - for exclude in excludes: - if '.' in exclude: - try: - model = apps.get_model(exclude) - except LookupError: - raise CommandError('Unknown model in excludes: %s' % exclude) - excluded_models.add(model) - else: - try: - app_config = apps.get_app_config(exclude) - except LookupError as e: - raise CommandError(str(e)) - excluded_apps.add(app_config) + excluded_models, excluded_apps = parse_apps_and_model_labels(excludes) if len(app_labels) == 0: if primary_keys: diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 1b0cee0d2b..d824011a03 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -13,6 +13,7 @@ from django.core import serializers from django.core.exceptions import ImproperlyConfigured from django.core.management.base import BaseCommand, CommandError from django.core.management.color import no_style +from django.core.management.utils import parse_apps_and_model_labels from django.db import ( DEFAULT_DB_ALIAS, DatabaseError, IntegrityError, connections, router, transaction, @@ -52,13 +53,17 @@ class Command(BaseCommand): help='Ignores entries in the serialized data for fields that do not ' 'currently exist on the model.', ) + parser.add_argument( + '-e', '--exclude', dest='exclude', action='append', default=[], + help='An app_label or app_label.ModelName to exclude. Can be used multiple times.', + ) def handle(self, *fixture_labels, **options): - self.ignore = options['ignore'] self.using = options['database'] self.app_label = options['app_label'] self.verbosity = options['verbosity'] + self.excluded_models, self.excluded_apps = parse_apps_and_model_labels(options['exclude']) with transaction.atomic(using=self.using): self.loaddata(fixture_labels) @@ -160,6 +165,9 @@ class Command(BaseCommand): for obj in objects: objects_in_fixture += 1 + if (obj.object._meta.app_config in self.excluded_apps or + type(obj.object) in self.excluded_models): + continue if router.allow_migrate_model(self.using, obj.object.__class__): loaded_objects_in_fixture += 1 self.models.add(obj.object.__class__) diff --git a/django/core/management/utils.py b/django/core/management/utils.py index 637091e68a..7ea005a6f0 100644 --- a/django/core/management/utils.py +++ b/django/core/management/utils.py @@ -4,6 +4,7 @@ import os import sys from subprocess import PIPE, Popen +from django.apps import apps as installed_apps from django.utils import six from django.utils.crypto import get_random_string from django.utils.encoding import DEFAULT_LOCALE_ENCODING, force_text @@ -84,3 +85,30 @@ def get_random_secret_key(): """ chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' return get_random_string(50, chars) + + +def parse_apps_and_model_labels(labels): + """ + Parse a list of "app_label.ModelName" or "app_label" strings into actual + objects and return a two-element tuple: + (set of model classes, set of app_configs). + Raise a CommandError if some specified models or apps don't exist. + """ + apps = set() + models = set() + + for label in labels: + if '.' in label: + try: + model = installed_apps.get_model(label) + except LookupError: + raise CommandError('Unknown model: %s' % label) + models.add(model) + else: + try: + app_config = installed_apps.get_app_config(label) + except LookupError as e: + raise CommandError(str(e)) + apps.add(app_config) + + return models, apps diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 12c43f3dab..2ecf082575 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -416,6 +416,14 @@ originally generated. Specifies a single app to look for fixtures in rather than looking in all apps. +.. django-admin-option:: --exclude EXCLUDE, -e EXCLUDE + +.. versionadded:: 1.11 + +Excludes loading the fixtures from the given applications and/or models (in the +form of ``app_label`` or ``app_label.ModelName``). Use the option multiple +times to exclude more than one app or model. + What's a "fixture"? ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index e8cf5ae0f3..ed4620ec44 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -169,7 +169,8 @@ Internationalization Management Commands ~~~~~~~~~~~~~~~~~~~ -* ... +* The new :option:`loaddata --exclude` option allows excluding models and apps + while loading data from fixtures. Migrations ~~~~~~~~~~ diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py index b021d927f0..95240652f3 100644 --- a/tests/fixtures/tests.py +++ b/tests/fixtures/tests.py @@ -20,7 +20,7 @@ from django.test import ( from django.utils import six from django.utils.encoding import force_text -from .models import Article, ProxySpy, Spy, Tag, Visa +from .models import Article, Category, ProxySpy, Spy, Tag, Visa class TestCaseFixtureLoadingTests(TestCase): @@ -370,7 +370,7 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): self._dumpdata_assert(['fixtures', 'sites'], '', exclude_list=['foo_app']) # Excluding a bogus model should throw an error - with self.assertRaisesMessage(management.CommandError, "Unknown model in excludes: fixtures.FooModel"): + with self.assertRaisesMessage(management.CommandError, "Unknown model: fixtures.FooModel"): self._dumpdata_assert(['fixtures', 'sites'], '', exclude_list=['fixtures.FooModel']) @unittest.skipIf(sys.platform.startswith('win'), "Windows doesn't support '?' in filenames.") @@ -650,6 +650,30 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): format='xml', natural_foreign_keys=True ) + def test_loading_with_exclude_app(self): + Site.objects.all().delete() + management.call_command('loaddata', 'fixture1', exclude=['fixtures'], verbosity=0) + self.assertFalse(Article.objects.exists()) + self.assertFalse(Category.objects.exists()) + self.assertQuerysetEqual(Site.objects.all(), ['']) + + def test_loading_with_exclude_model(self): + Site.objects.all().delete() + management.call_command('loaddata', 'fixture1', exclude=['fixtures.Article'], verbosity=0) + self.assertFalse(Article.objects.exists()) + self.assertQuerysetEqual(Category.objects.all(), ['']) + self.assertQuerysetEqual(Site.objects.all(), ['']) + + def test_exclude_option_errors(self): + """Excluding a bogus app or model should raise an error.""" + msg = "No installed app with label 'foo_app'." + with self.assertRaisesMessage(management.CommandError, msg): + management.call_command('loaddata', 'fixture1', exclude=['foo_app'], verbosity=0) + + msg = "Unknown model: fixtures.FooModel" + with self.assertRaisesMessage(management.CommandError, msg): + management.call_command('loaddata', 'fixture1', exclude=['fixtures.FooModel'], verbosity=0) + class NonExistentFixtureTests(TestCase): """