mirror of
				https://github.com/django/django.git
				synced 2025-10-30 17:16:10 +00:00 
			
		
		
		
	Make migrate command recognise prefixes and 'zero'.
This commit is contained in:
		| @@ -4,17 +4,18 @@ import traceback | |||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.core.management import call_command | from django.core.management import call_command | ||||||
| from django.core.management.base import NoArgsCommand | from django.core.management.base import BaseCommand, CommandError | ||||||
| from django.core.management.color import color_style, no_style | from django.core.management.color import color_style, no_style | ||||||
| from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal, emit_pre_sync_signal | from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal, emit_pre_sync_signal | ||||||
| from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS | from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS | ||||||
| from django.db.migrations.executor import MigrationExecutor | from django.db.migrations.executor import MigrationExecutor | ||||||
|  | from django.db.migrations.loader import AmbiguityError | ||||||
| from django.utils.datastructures import SortedDict | from django.utils.datastructures import SortedDict | ||||||
| from django.utils.importlib import import_module | from django.utils.importlib import import_module | ||||||
|  |  | ||||||
|  |  | ||||||
| class Command(NoArgsCommand): | class Command(BaseCommand): | ||||||
|     option_list = NoArgsCommand.option_list + ( |     option_list = BaseCommand.option_list + ( | ||||||
|         make_option('--noinput', action='store_false', dest='interactive', default=True, |         make_option('--noinput', action='store_false', dest='interactive', default=True, | ||||||
|             help='Tells Django to NOT prompt the user for input of any kind.'), |             help='Tells Django to NOT prompt the user for input of any kind.'), | ||||||
|         make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True, |         make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True, | ||||||
| @@ -26,7 +27,7 @@ class Command(NoArgsCommand): | |||||||
|  |  | ||||||
|     help = "Updates database schema. Manages both apps with migrations and those without." |     help = "Updates database schema. Manages both apps with migrations and those without." | ||||||
|  |  | ||||||
|     def handle_noargs(self, **options): |     def handle(self, *args, **options): | ||||||
|  |  | ||||||
|         self.verbosity = int(options.get('verbosity')) |         self.verbosity = int(options.get('verbosity')) | ||||||
|         self.interactive = options.get('interactive') |         self.interactive = options.get('interactive') | ||||||
| @@ -60,24 +61,57 @@ class Command(NoArgsCommand): | |||||||
|         connection = connections[db] |         connection = connections[db] | ||||||
|  |  | ||||||
|         # Work out which apps have migrations and which do not |         # Work out which apps have migrations and which do not | ||||||
|         if self.verbosity >= 1: |  | ||||||
|             self.stdout.write(self.style.MIGRATE_HEADING("Calculating migration plan:")) |  | ||||||
|         executor = MigrationExecutor(connection, self.migration_progress_callback) |         executor = MigrationExecutor(connection, self.migration_progress_callback) | ||||||
|         if self.verbosity >= 1: |  | ||||||
|             self.stdout.write(self.style.MIGRATE_LABEL("  Apps without migrations: ") + (", ".join(executor.loader.unmigrated_apps) or "(none)")) |  | ||||||
|  |  | ||||||
|         # Work out what targets they want, and then make a migration plan |         # If they supplied command line arguments, work out what they mean. | ||||||
|         # TODO: Let users select targets |         run_syncdb = False | ||||||
|         targets = executor.loader.graph.leaf_nodes() |         target_app_labels_only = True | ||||||
|  |         if len(args) > 2: | ||||||
|  |             raise CommandError("Too many command-line arguments (expecting 'appname' or 'appname migrationname')") | ||||||
|  |         elif len(args) == 2: | ||||||
|  |             app_label, migration_name = args | ||||||
|  |             if app_label not in executor.loader.migrated_apps: | ||||||
|  |                 raise CommandError("App '%s' does not have migrations (you cannot selectively sync unmigrated apps)" % app_label) | ||||||
|  |             if migration_name == "zero": | ||||||
|  |                 migration_name = None | ||||||
|  |             else: | ||||||
|  |                 try: | ||||||
|  |                     migration = executor.loader.get_migration_by_prefix(app_label, migration_name) | ||||||
|  |                 except AmbiguityError: | ||||||
|  |                     raise CommandError("More than one migration matches '%s' in app '%s'. Please be more specific." % (app_label, migration_name)) | ||||||
|  |                 except KeyError: | ||||||
|  |                     raise CommandError("Cannot find a migration matching '%s' from app '%s'. Is it in INSTALLED_APPS?" % (app_label, migration_name)) | ||||||
|  |             targets = [(app_label, migration.name)] | ||||||
|  |             target_app_labels_only = False | ||||||
|  |         elif len(args) == 1: | ||||||
|  |             app_label = args[0] | ||||||
|  |             if app_label not in executor.loader.migrated_apps: | ||||||
|  |                 raise CommandError("App '%s' does not have migrations (you cannot selectively sync unmigrated apps)" % app_label) | ||||||
|  |             targets = [key for key in executor.loader.graph.leaf_nodes() if key[0] == app_label] | ||||||
|  |         else: | ||||||
|  |             targets = executor.loader.graph.leaf_nodes() | ||||||
|  |             run_syncdb = True | ||||||
|  |  | ||||||
|         plan = executor.migration_plan(targets) |         plan = executor.migration_plan(targets) | ||||||
|  |  | ||||||
|  |         # Print some useful info | ||||||
|         if self.verbosity >= 1: |         if self.verbosity >= 1: | ||||||
|             self.stdout.write(self.style.MIGRATE_LABEL("  Apps with migrations:    ") + (", ".join(executor.loader.migrated_apps) or "(none)")) |             self.stdout.write(self.style.MIGRATE_HEADING("Operations to perform:")) | ||||||
|  |             if run_syncdb: | ||||||
|  |                 self.stdout.write(self.style.MIGRATE_LABEL("  Synchronize unmigrated apps: ") + (", ".join(executor.loader.unmigrated_apps) or "(none)")) | ||||||
|  |             if target_app_labels_only: | ||||||
|  |                 self.stdout.write(self.style.MIGRATE_LABEL("  Apply all migrations: ") + (", ".join(set(a for a, n in targets)) or "(none)")) | ||||||
|  |             else: | ||||||
|  |                 if targets[0][1] is None: | ||||||
|  |                     self.stdout.write(self.style.MIGRATE_LABEL("  Unapply all migrations: ") + "%s" % (targets[0][0], )) | ||||||
|  |                 else: | ||||||
|  |                     self.stdout.write(self.style.MIGRATE_LABEL("  Target specific migration: ") + "%s, from %s" % (targets[0][1], targets[0][0])) | ||||||
|  |  | ||||||
|         # Run the syncdb phase. |         # Run the syncdb phase. | ||||||
|         # If you ever manage to get rid of this, I owe you many, many drinks. |         # If you ever manage to get rid of this, I owe you many, many drinks. | ||||||
|         self.stdout.write(self.style.MIGRATE_HEADING("Synchronizing apps without migrations:")) |         if run_syncdb: | ||||||
|         self.sync_apps(connection, executor.loader.unmigrated_apps) |             self.stdout.write(self.style.MIGRATE_HEADING("Synchronizing apps without migrations:")) | ||||||
|  |             self.sync_apps(connection, executor.loader.unmigrated_apps) | ||||||
|  |  | ||||||
|         # Migrate! |         # Migrate! | ||||||
|         if self.verbosity >= 1: |         if self.verbosity >= 1: | ||||||
|   | |||||||
| @@ -22,9 +22,17 @@ class MigrationExecutor(object): | |||||||
|         plan = [] |         plan = [] | ||||||
|         applied = self.recorder.applied_migrations() |         applied = self.recorder.applied_migrations() | ||||||
|         for target in targets: |         for target in targets: | ||||||
|  |             # If the target is (appname, None), that means unmigrate everything | ||||||
|  |             if target[1] is None: | ||||||
|  |                 for root in self.loader.graph.root_nodes(): | ||||||
|  |                     if root[0] == target[0]: | ||||||
|  |                         for migration in self.loader.graph.backwards_plan(root): | ||||||
|  |                             if migration in applied: | ||||||
|  |                                 plan.append((self.loader.graph.nodes[migration], True)) | ||||||
|  |                                 applied.remove(migration) | ||||||
|             # If the migration is already applied, do backwards mode, |             # If the migration is already applied, do backwards mode, | ||||||
|             # otherwise do forwards mode. |             # otherwise do forwards mode. | ||||||
|             if target in applied: |             elif target in applied: | ||||||
|                 for migration in self.loader.graph.backwards_plan(target)[:-1]: |                 for migration in self.loader.graph.backwards_plan(target)[:-1]: | ||||||
|                     if migration in applied: |                     if migration in applied: | ||||||
|                         plan.append((self.loader.graph.nodes[migration], True)) |                         plan.append((self.loader.graph.nodes[migration], True)) | ||||||
|   | |||||||
| @@ -79,6 +79,23 @@ class MigrationLoader(object): | |||||||
|                     raise BadMigrationError("Migration %s in app %s has no Migration class" % (migration_name, app_label)) |                     raise BadMigrationError("Migration %s in app %s has no Migration class" % (migration_name, app_label)) | ||||||
|                 self.disk_migrations[app_label, migration_name] = migration_module.Migration(migration_name, app_label) |                 self.disk_migrations[app_label, migration_name] = migration_module.Migration(migration_name, app_label) | ||||||
|  |  | ||||||
|  |     def get_migration_by_prefix(self, app_label, name_prefix): | ||||||
|  |         "Returns the migration(s) which match the given app label and name _prefix_" | ||||||
|  |         # Make sure we have the disk data | ||||||
|  |         if self.disk_migrations is None: | ||||||
|  |             self.load_disk() | ||||||
|  |         # Do the search | ||||||
|  |         results = [] | ||||||
|  |         for l, n in self.disk_migrations: | ||||||
|  |             if l == app_label and n.startswith(name_prefix): | ||||||
|  |                 results.append((l, n)) | ||||||
|  |         if len(results) > 1: | ||||||
|  |             raise AmbiguityError("There is more than one migration for '%s' with the prefix '%s'" % (app_label, name_prefix)) | ||||||
|  |         elif len(results) == 0: | ||||||
|  |             raise KeyError("There no migrations for '%s' with the prefix '%s'" % (app_label, name_prefix)) | ||||||
|  |         else: | ||||||
|  |             return self.disk_migrations[results[0]] | ||||||
|  |  | ||||||
|     @cached_property |     @cached_property | ||||||
|     def graph(self): |     def graph(self): | ||||||
|         """ |         """ | ||||||
| @@ -141,3 +158,10 @@ class BadMigrationError(Exception): | |||||||
|     Raised when there's a bad migration (unreadable/bad format/etc.) |     Raised when there's a bad migration (unreadable/bad format/etc.) | ||||||
|     """ |     """ | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AmbiguityError(Exception): | ||||||
|  |     """ | ||||||
|  |     Raised when more than one migration matches a name prefix | ||||||
|  |     """ | ||||||
|  |     pass | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.test.utils import override_settings | from django.test.utils import override_settings | ||||||
| from django.db import connection | from django.db import connection | ||||||
| from django.db.migrations.loader import MigrationLoader | from django.db.migrations.loader import MigrationLoader, AmbiguityError | ||||||
| from django.db.migrations.recorder import MigrationRecorder | from django.db.migrations.recorder import MigrationRecorder | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -64,3 +64,16 @@ class LoaderTests(TestCase): | |||||||
|             [x for x, y in book_state.fields], |             [x for x, y in book_state.fields], | ||||||
|             ["id", "author"] |             ["id", "author"] | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) | ||||||
|  |     def test_name_match(self): | ||||||
|  |         "Tests prefix name matching" | ||||||
|  |         migration_loader = MigrationLoader(connection) | ||||||
|  |         self.assertEqual( | ||||||
|  |             migration_loader.get_migration_by_prefix("migrations", "0001").name, | ||||||
|  |             "0001_initial", | ||||||
|  |         ) | ||||||
|  |         with self.assertRaises(AmbiguityError): | ||||||
|  |             migration_loader.get_migration_by_prefix("migrations", "0") | ||||||
|  |         with self.assertRaises(KeyError): | ||||||
|  |             migration_loader.get_migration_by_prefix("migrations", "blarg") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user