From 1435cfbe8dbd85c17c9a25af99f910c407dfa9bd Mon Sep 17 00:00:00 2001 From: Raffaele Salmaso Date: Tue, 19 Aug 2014 15:24:31 +0200 Subject: [PATCH] Fixed #23302 -- Added --name/-n option to makemigrations command --- .../management/commands/makemigrations.py | 10 +++++- django/db/migrations/autodetector.py | 10 +++--- docs/ref/django-admin.txt | 7 +++++ docs/releases/1.8.txt | 3 ++ docs/topics/migrations.txt | 7 +++++ tests/migrations/test_autodetector.py | 25 +++++++++++++++ tests/migrations/test_commands.py | 31 +++++++++++++++++++ 7 files changed, 87 insertions(+), 6 deletions(-) mode change 100755 => 100644 docs/topics/migrations.txt diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index b1a9e0c017..96a0189ebb 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -28,6 +28,8 @@ class Command(BaseCommand): help="Create an empty migration.") parser.add_argument('--noinput', action='store_false', dest='interactive', default=True, help='Tells Django to NOT prompt the user for input of any kind.') + parser.add_argument('-n', '--name', action='store', dest='name', default=None, + help="Use this name for migration file(s).") def handle(self, *app_labels, **options): @@ -36,6 +38,7 @@ class Command(BaseCommand): self.dry_run = options.get('dry_run', False) self.merge = options.get('merge', False) self.empty = options.get('empty', False) + self.migration_name = options.get('name', None) # Make sure the app they asked for exists app_labels = set(app_labels) @@ -98,7 +101,11 @@ class Command(BaseCommand): (app, [Migration("custom", app)]) for app in app_labels ) - changes = autodetector.arrange_for_graph(changes, loader.graph) + changes = autodetector.arrange_for_graph( + changes=changes, + graph=loader.graph, + migration_name=self.migration_name, + ) self.write_migration_files(changes) return @@ -107,6 +114,7 @@ class Command(BaseCommand): graph=loader.graph, trim_to_apps=app_labels or None, convert_apps=app_labels or None, + migration_name=self.migration_name, ) # No changes? Tell them. diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index 4a837fbba1..41884058b7 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -31,14 +31,14 @@ class MigrationAutodetector(object): self.to_state = to_state self.questioner = questioner or MigrationQuestioner() - def changes(self, graph, trim_to_apps=None, convert_apps=None): + def changes(self, graph, trim_to_apps=None, convert_apps=None, migration_name=None): """ Main entry point to produce a list of appliable changes. Takes a graph to base names on and an optional set of apps to try and restrict to (restriction is not guaranteed) """ changes = self._detect_changes(convert_apps, graph) - changes = self.arrange_for_graph(changes, graph) + changes = self.arrange_for_graph(changes, graph, migration_name) if trim_to_apps: changes = self._trim_to_apps(changes, trim_to_apps) return changes @@ -951,7 +951,7 @@ class MigrationAutodetector(object): dependencies=dependencies, ) - def arrange_for_graph(self, changes, graph): + def arrange_for_graph(self, changes, graph, migration_name=None): """ Takes in a result from changes() and a MigrationGraph, and fixes the names and dependencies of the changes so they @@ -985,11 +985,11 @@ class MigrationAutodetector(object): if i == 0 and app_leaf: migration.dependencies.append(app_leaf) if i == 0 and not app_leaf: - new_name = "0001_initial" + new_name = "0001_%s" % migration_name if migration_name else "0001_initial" else: new_name = "%04i_%s" % ( next_number, - self.suggest_name(migration.operations)[:100], + migration_name or self.suggest_name(migration.operations)[:100], ) name_map[(app_label, migration.name)] = (app_label, new_name) next_number += 1 diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index fd80080f64..e91423ff6b 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -683,6 +683,13 @@ written. The ``--merge`` option enables fixing of migration conflicts. +.. django-admin-option:: --name, -n + +.. versionadded:: 1.8 + +The ``--name`` option allows you to give the migration(s) a custom name instead +of a generated one. + migrate [ []] --------------------------------------- diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 8b1df14418..a95024420c 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -228,6 +228,9 @@ Management Commands * The :djadmin:`dbshell` command now supports MySQL's optional SSL certificate authority setting (``--ssl-ca``). +* The :djadminopt:`--name` option for :djadmin:`makemigrations` allows you to + to give the migration(s) a custom name instead of a generated one. + Models ^^^^^^ diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt old mode 100755 new mode 100644 index cd03a2f840..626790183b --- a/docs/topics/migrations.txt +++ b/docs/topics/migrations.txt @@ -309,6 +309,13 @@ Note that this only works given two things: that your database doesn't match your models, you'll just get errors when migrations try to modify those tables. +.. versionadded:: 1.8 + +If you want to give the migration(s) a meaningful name instead of a generated one, +you can use the :djadminopt:`--name` option:: + + $ python manage.py makemigrations --name changed_my_model your_app_label + .. _historical-models: Historical models diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index c3bec6ba84..42569d354d 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -188,6 +188,31 @@ class AutodetectorTests(TestCase): self.assertEqual(changes["otherapp"][0].name, "0001_initial") self.assertNotIn("thirdapp", changes) + def test_custom_migration_name(self): + "Tests custom naming of migrations for graph matching." + # Make a fake graph + graph = MigrationGraph() + graph.add_node(("testapp", "0001_initial"), None) + graph.add_node(("testapp", "0002_foobar"), None) + graph.add_node(("otherapp", "0001_initial"), None) + graph.add_dependency("testapp.0002_foobar", ("testapp", "0002_foobar"), ("testapp", "0001_initial")) + + # Use project state to make a new migration change set + before = self.make_project_state([]) + after = self.make_project_state([self.author_empty, self.other_pony, self.other_stable]) + autodetector = MigrationAutodetector(before, after) + changes = autodetector._detect_changes() + + # Run through arrange_for_graph + migration_name = 'custom_name' + changes = autodetector.arrange_for_graph(changes, graph, migration_name) + + # Make sure there's a new name, deps match, etc. + self.assertEqual(changes["testapp"][0].name, "0003_%s" % migration_name) + self.assertEqual(changes["testapp"][0].dependencies, [("testapp", "0002_foobar")]) + self.assertEqual(changes["otherapp"][0].name, "0002_%s" % migration_name) + self.assertEqual(changes["otherapp"][0].dependencies, [("otherapp", "0001_initial")]) + def test_new_model(self): "Tests autodetection of new models" # Make state diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 00b7f65ab8..798dd86fe5 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -546,3 +546,34 @@ class MakeMigrationsTests(MigrationTestBase): questioner.input = old_input if os.path.exists(merge_file): os.remove(merge_file) + + @override_system_checks([]) + def test_makemigrations_with_custom_name(self): + """ + Makes sure that makemigrations generate a custom migration. + """ + def cmd(migration_count, migration_name, *args): + with override_settings(MIGRATION_MODULES={"migrations": self.migration_pkg}): + try: + call_command("makemigrations", "migrations", "--verbosity", "0", "--name", migration_name, *args) + except CommandError: + self.fail("Makemigrations errored in creating empty migration with custom name for a proper app.") + migration_file = os.path.join(self.migration_dir, "%s_%s.py" % (migration_count, migration_name)) + # Check for existing migration file in migration folder + self.assertTrue(os.path.exists(migration_file)) + with codecs.open(migration_file, "r", encoding="utf-8") as fp: + content = fp.read() + self.assertTrue("# -*- coding: utf-8 -*-" in content) + content = content.replace(" ", "") + return content + + # generate an initial migration + migration_name_0001 = "my_initial_migration" + content = cmd("0001", migration_name_0001) + self.assertIn("dependencies=[\n]", content) + + # generate an empty migration + migration_name_0002 = "my_custom_migration" + content = cmd("0002", migration_name_0002, "--empty") + self.assertIn("dependencies=[\n('migrations','0001_%s'),\n]" % migration_name_0001, content) + self.assertIn("operations=[\n]", content)