Fixed #20468 -- Added loaddata --exclude option.

Thanks Alex Morozov for the initial patch.
This commit is contained in:
Berker Peksag 2016-05-17 09:52:01 +03:00 committed by Tim Graham
parent 21130ce1a9
commit ae2a7da86b
6 changed files with 75 additions and 19 deletions

View File

@ -4,6 +4,7 @@ from collections import OrderedDict
from django.apps import apps from django.apps import apps
from django.core import serializers from django.core import serializers
from django.core.management.base import BaseCommand, CommandError 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 from django.db import DEFAULT_DB_ALIAS, router
@ -81,21 +82,7 @@ class Command(BaseCommand):
else: else:
primary_keys = [] primary_keys = []
excluded_apps = set() excluded_models, excluded_apps = parse_apps_and_model_labels(excludes)
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)
if len(app_labels) == 0: if len(app_labels) == 0:
if primary_keys: if primary_keys:

View File

@ -13,6 +13,7 @@ from django.core import serializers
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.core.management.color import no_style from django.core.management.color import no_style
from django.core.management.utils import parse_apps_and_model_labels
from django.db import ( from django.db import (
DEFAULT_DB_ALIAS, DatabaseError, IntegrityError, connections, router, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError, connections, router,
transaction, transaction,
@ -52,13 +53,17 @@ class Command(BaseCommand):
help='Ignores entries in the serialized data for fields that do not ' help='Ignores entries in the serialized data for fields that do not '
'currently exist on the model.', '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): def handle(self, *fixture_labels, **options):
self.ignore = options['ignore'] self.ignore = options['ignore']
self.using = options['database'] self.using = options['database']
self.app_label = options['app_label'] self.app_label = options['app_label']
self.verbosity = options['verbosity'] self.verbosity = options['verbosity']
self.excluded_models, self.excluded_apps = parse_apps_and_model_labels(options['exclude'])
with transaction.atomic(using=self.using): with transaction.atomic(using=self.using):
self.loaddata(fixture_labels) self.loaddata(fixture_labels)
@ -160,6 +165,9 @@ class Command(BaseCommand):
for obj in objects: for obj in objects:
objects_in_fixture += 1 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__): if router.allow_migrate_model(self.using, obj.object.__class__):
loaded_objects_in_fixture += 1 loaded_objects_in_fixture += 1
self.models.add(obj.object.__class__) self.models.add(obj.object.__class__)

View File

@ -4,6 +4,7 @@ import os
import sys import sys
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
from django.apps import apps as installed_apps
from django.utils import six from django.utils import six
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.encoding import DEFAULT_LOCALE_ENCODING, force_text from django.utils.encoding import DEFAULT_LOCALE_ENCODING, force_text
@ -84,3 +85,30 @@ def get_random_secret_key():
""" """
chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
return get_random_string(50, chars) 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

View File

@ -416,6 +416,14 @@ originally generated.
Specifies a single app to look for fixtures in rather than looking in all apps. 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"? What's a "fixture"?
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~

View File

@ -169,7 +169,8 @@ Internationalization
Management Commands Management Commands
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
* ... * The new :option:`loaddata --exclude` option allows excluding models and apps
while loading data from fixtures.
Migrations Migrations
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -20,7 +20,7 @@ from django.test import (
from django.utils import six from django.utils import six
from django.utils.encoding import force_text 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): class TestCaseFixtureLoadingTests(TestCase):
@ -370,7 +370,7 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase):
self._dumpdata_assert(['fixtures', 'sites'], '', exclude_list=['foo_app']) self._dumpdata_assert(['fixtures', 'sites'], '', exclude_list=['foo_app'])
# Excluding a bogus model should throw an error # 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']) self._dumpdata_assert(['fixtures', 'sites'], '', exclude_list=['fixtures.FooModel'])
@unittest.skipIf(sys.platform.startswith('win'), "Windows doesn't support '?' in filenames.") @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 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(), ['<Site: example.com>'])
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(), ['<Category: News Stories>'])
self.assertQuerysetEqual(Site.objects.all(), ['<Site: example.com>'])
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): class NonExistentFixtureTests(TestCase):
""" """