Fixed #31700 -- Made makemigrations command display meaningful symbols for each operation.

This commit is contained in:
Amir Karimi 2023-09-16 05:41:22 +03:30 committed by Mariusz Felisiak
parent c7e986fc9f
commit 27a3eee721
14 changed files with 214 additions and 19 deletions

View File

@ -5,12 +5,13 @@ from django.contrib.postgres.signals import (
) )
from django.db import NotSupportedError, router from django.db import NotSupportedError, router
from django.db.migrations import AddConstraint, AddIndex, RemoveIndex from django.db.migrations import AddConstraint, AddIndex, RemoveIndex
from django.db.migrations.operations.base import Operation from django.db.migrations.operations.base import Operation, OperationCategory
from django.db.models.constraints import CheckConstraint from django.db.models.constraints import CheckConstraint
class CreateExtension(Operation): class CreateExtension(Operation):
reversible = True reversible = True
category = OperationCategory.ADDITION
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
@ -120,6 +121,7 @@ class AddIndexConcurrently(NotInTransactionMixin, AddIndex):
"""Create an index using PostgreSQL's CREATE INDEX CONCURRENTLY syntax.""" """Create an index using PostgreSQL's CREATE INDEX CONCURRENTLY syntax."""
atomic = False atomic = False
category = OperationCategory.ADDITION
def describe(self): def describe(self):
return "Concurrently create index %s on field(s) %s of model %s" % ( return "Concurrently create index %s on field(s) %s of model %s" % (
@ -145,6 +147,7 @@ class RemoveIndexConcurrently(NotInTransactionMixin, RemoveIndex):
"""Remove an index using PostgreSQL's DROP INDEX CONCURRENTLY syntax.""" """Remove an index using PostgreSQL's DROP INDEX CONCURRENTLY syntax."""
atomic = False atomic = False
category = OperationCategory.REMOVAL
def describe(self): def describe(self):
return "Concurrently remove index %s from %s" % (self.name, self.model_name) return "Concurrently remove index %s from %s" % (self.name, self.model_name)
@ -213,6 +216,8 @@ class CollationOperation(Operation):
class CreateCollation(CollationOperation): class CreateCollation(CollationOperation):
"""Create a collation.""" """Create a collation."""
category = OperationCategory.ADDITION
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
if schema_editor.connection.vendor != "postgresql" or not router.allow_migrate( if schema_editor.connection.vendor != "postgresql" or not router.allow_migrate(
schema_editor.connection.alias, app_label schema_editor.connection.alias, app_label
@ -236,6 +241,8 @@ class CreateCollation(CollationOperation):
class RemoveCollation(CollationOperation): class RemoveCollation(CollationOperation):
"""Remove a collation.""" """Remove a collation."""
category = OperationCategory.REMOVAL
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
if schema_editor.connection.vendor != "postgresql" or not router.allow_migrate( if schema_editor.connection.vendor != "postgresql" or not router.allow_migrate(
schema_editor.connection.alias, app_label schema_editor.connection.alias, app_label
@ -262,6 +269,8 @@ class AddConstraintNotValid(AddConstraint):
NOT VALID syntax. NOT VALID syntax.
""" """
category = OperationCategory.ADDITION
def __init__(self, model_name, constraint): def __init__(self, model_name, constraint):
if not isinstance(constraint, CheckConstraint): if not isinstance(constraint, CheckConstraint):
raise TypeError( raise TypeError(
@ -293,6 +302,8 @@ class AddConstraintNotValid(AddConstraint):
class ValidateConstraint(Operation): class ValidateConstraint(Operation):
"""Validate a table NOT VALID constraint.""" """Validate a table NOT VALID constraint."""
category = OperationCategory.ALTERATION
def __init__(self, model_name, name): def __init__(self, model_name, name):
self.model_name = model_name self.model_name = model_name
self.name = name self.name = name

View File

@ -348,7 +348,7 @@ class Command(BaseCommand):
migration_string = self.get_relative_path(writer.path) migration_string = self.get_relative_path(writer.path)
self.log(" %s\n" % self.style.MIGRATE_LABEL(migration_string)) self.log(" %s\n" % self.style.MIGRATE_LABEL(migration_string))
for operation in migration.operations: for operation in migration.operations:
self.log(" - %s" % operation.describe()) self.log(" %s" % operation.formatted_description())
if self.scriptable: if self.scriptable:
self.stdout.write(migration_string) self.stdout.write(migration_string)
if not self.dry_run: if not self.dry_run:
@ -456,7 +456,7 @@ class Command(BaseCommand):
for migration in merge_migrations: for migration in merge_migrations:
self.log(self.style.MIGRATE_LABEL(" Branch %s" % migration.name)) self.log(self.style.MIGRATE_LABEL(" Branch %s" % migration.name))
for operation in migration.merged_operations: for operation in migration.merged_operations:
self.log(" - %s" % operation.describe()) self.log(" %s" % operation.formatted_description())
if questioner.ask_merge(app_label): if questioner.ask_merge(app_label):
# If they still want to merge it, then write out an empty # If they still want to merge it, then write out an empty
# file depending on the migrations needing merging. # file depending on the migrations needing merging.

View File

@ -1,6 +1,17 @@
import enum
from django.db import router from django.db import router
class OperationCategory(str, enum.Enum):
ADDITION = "+"
REMOVAL = "-"
ALTERATION = "~"
PYTHON = "p"
SQL = "s"
MIXED = "?"
class Operation: class Operation:
""" """
Base class for migration operations. Base class for migration operations.
@ -33,6 +44,8 @@ class Operation:
serialization_expand_args = [] serialization_expand_args = []
category = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
# We capture the arguments to make returning them trivial # We capture the arguments to make returning them trivial
self = object.__new__(cls) self = object.__new__(cls)
@ -85,6 +98,13 @@ class Operation:
""" """
return "%s: %s" % (self.__class__.__name__, self._constructor_args) return "%s: %s" % (self.__class__.__name__, self._constructor_args)
def formatted_description(self):
"""Output a description prefixed by a category symbol."""
description = self.describe()
if self.category is None:
return f"{OperationCategory.MIXED.value} {description}"
return f"{self.category.value} {description}"
@property @property
def migration_name_fragment(self): def migration_name_fragment(self):
""" """

View File

@ -2,7 +2,7 @@ from django.db.migrations.utils import field_references
from django.db.models import NOT_PROVIDED from django.db.models import NOT_PROVIDED
from django.utils.functional import cached_property from django.utils.functional import cached_property
from .base import Operation from .base import Operation, OperationCategory
class FieldOperation(Operation): class FieldOperation(Operation):
@ -75,6 +75,8 @@ class FieldOperation(Operation):
class AddField(FieldOperation): class AddField(FieldOperation):
"""Add a field to a model.""" """Add a field to a model."""
category = OperationCategory.ADDITION
def __init__(self, model_name, name, field, preserve_default=True): def __init__(self, model_name, name, field, preserve_default=True):
self.preserve_default = preserve_default self.preserve_default = preserve_default
super().__init__(model_name, name, field) super().__init__(model_name, name, field)
@ -154,6 +156,8 @@ class AddField(FieldOperation):
class RemoveField(FieldOperation): class RemoveField(FieldOperation):
"""Remove a field from a model.""" """Remove a field from a model."""
category = OperationCategory.REMOVAL
def deconstruct(self): def deconstruct(self):
kwargs = { kwargs = {
"model_name": self.model_name, "model_name": self.model_name,
@ -201,6 +205,8 @@ class AlterField(FieldOperation):
new field. new field.
""" """
category = OperationCategory.ALTERATION
def __init__(self, model_name, name, field, preserve_default=True): def __init__(self, model_name, name, field, preserve_default=True):
self.preserve_default = preserve_default self.preserve_default = preserve_default
super().__init__(model_name, name, field) super().__init__(model_name, name, field)
@ -270,6 +276,8 @@ class AlterField(FieldOperation):
class RenameField(FieldOperation): class RenameField(FieldOperation):
"""Rename a field on the model. Might affect db_column too.""" """Rename a field on the model. Might affect db_column too."""
category = OperationCategory.ALTERATION
def __init__(self, model_name, old_name, new_name): def __init__(self, model_name, old_name, new_name):
self.old_name = old_name self.old_name = old_name
self.new_name = new_name self.new_name = new_name

View File

@ -1,5 +1,5 @@
from django.db import models from django.db import models
from django.db.migrations.operations.base import Operation from django.db.migrations.operations.base import Operation, OperationCategory
from django.db.migrations.state import ModelState from django.db.migrations.state import ModelState
from django.db.migrations.utils import field_references, resolve_relation from django.db.migrations.utils import field_references, resolve_relation
from django.db.models.options import normalize_together from django.db.models.options import normalize_together
@ -41,6 +41,7 @@ class ModelOperation(Operation):
class CreateModel(ModelOperation): class CreateModel(ModelOperation):
"""Create a model's table.""" """Create a model's table."""
category = OperationCategory.ADDITION
serialization_expand_args = ["fields", "options", "managers"] serialization_expand_args = ["fields", "options", "managers"]
def __init__(self, name, fields, options=None, bases=None, managers=None): def __init__(self, name, fields, options=None, bases=None, managers=None):
@ -347,6 +348,8 @@ class CreateModel(ModelOperation):
class DeleteModel(ModelOperation): class DeleteModel(ModelOperation):
"""Drop a model's table.""" """Drop a model's table."""
category = OperationCategory.REMOVAL
def deconstruct(self): def deconstruct(self):
kwargs = { kwargs = {
"name": self.name, "name": self.name,
@ -382,6 +385,8 @@ class DeleteModel(ModelOperation):
class RenameModel(ModelOperation): class RenameModel(ModelOperation):
"""Rename a model.""" """Rename a model."""
category = OperationCategory.ALTERATION
def __init__(self, old_name, new_name): def __init__(self, old_name, new_name):
self.old_name = old_name self.old_name = old_name
self.new_name = new_name self.new_name = new_name
@ -499,6 +504,8 @@ class RenameModel(ModelOperation):
class ModelOptionOperation(ModelOperation): class ModelOptionOperation(ModelOperation):
category = OperationCategory.ALTERATION
def reduce(self, operation, app_label): def reduce(self, operation, app_label):
if ( if (
isinstance(operation, (self.__class__, DeleteModel)) isinstance(operation, (self.__class__, DeleteModel))
@ -849,6 +856,8 @@ class IndexOperation(Operation):
class AddIndex(IndexOperation): class AddIndex(IndexOperation):
"""Add an index on a model.""" """Add an index on a model."""
category = OperationCategory.ADDITION
def __init__(self, model_name, index): def __init__(self, model_name, index):
self.model_name = model_name self.model_name = model_name
if not index.name: if not index.name:
@ -911,6 +920,8 @@ class AddIndex(IndexOperation):
class RemoveIndex(IndexOperation): class RemoveIndex(IndexOperation):
"""Remove an index from a model.""" """Remove an index from a model."""
category = OperationCategory.REMOVAL
def __init__(self, model_name, name): def __init__(self, model_name, name):
self.model_name = model_name self.model_name = model_name
self.name = name self.name = name
@ -954,6 +965,8 @@ class RemoveIndex(IndexOperation):
class RenameIndex(IndexOperation): class RenameIndex(IndexOperation):
"""Rename an index.""" """Rename an index."""
category = OperationCategory.ALTERATION
def __init__(self, model_name, new_name, old_name=None, old_fields=None): def __init__(self, model_name, new_name, old_name=None, old_fields=None):
if not old_name and not old_fields: if not old_name and not old_fields:
raise ValueError( raise ValueError(
@ -1104,6 +1117,7 @@ class RenameIndex(IndexOperation):
class AddConstraint(IndexOperation): class AddConstraint(IndexOperation):
category = OperationCategory.ADDITION
option_name = "constraints" option_name = "constraints"
def __init__(self, model_name, constraint): def __init__(self, model_name, constraint):
@ -1154,6 +1168,7 @@ class AddConstraint(IndexOperation):
class RemoveConstraint(IndexOperation): class RemoveConstraint(IndexOperation):
category = OperationCategory.REMOVAL
option_name = "constraints" option_name = "constraints"
def __init__(self, model_name, name): def __init__(self, model_name, name):

View File

@ -1,6 +1,6 @@
from django.db import router from django.db import router
from .base import Operation from .base import Operation, OperationCategory
class SeparateDatabaseAndState(Operation): class SeparateDatabaseAndState(Operation):
@ -11,6 +11,7 @@ class SeparateDatabaseAndState(Operation):
that affect the state or not the database, or so on. that affect the state or not the database, or so on.
""" """
category = OperationCategory.MIXED
serialization_expand_args = ["database_operations", "state_operations"] serialization_expand_args = ["database_operations", "state_operations"]
def __init__(self, database_operations=None, state_operations=None): def __init__(self, database_operations=None, state_operations=None):
@ -68,6 +69,7 @@ class RunSQL(Operation):
by this SQL change, in case it's custom column/table creation/deletion. by this SQL change, in case it's custom column/table creation/deletion.
""" """
category = OperationCategory.SQL
noop = "" noop = ""
def __init__( def __init__(
@ -138,6 +140,7 @@ class RunPython(Operation):
Run Python code in a context suitable for doing versioned ORM operations. Run Python code in a context suitable for doing versioned ORM operations.
""" """
category = OperationCategory.PYTHON
reduces_to_sql = False reduces_to_sql = False
def __init__( def __init__(

View File

@ -241,8 +241,8 @@ You should see something similar to the following:
Migrations for 'polls': Migrations for 'polls':
polls/migrations/0001_initial.py polls/migrations/0001_initial.py
- Create model Question + Create model Question
- Create model Choice + Create model Choice
By running ``makemigrations``, you're telling Django that you've made By running ``makemigrations``, you're telling Django that you've made
some changes to your models (in this case, you've made new ones) and that some changes to your models (in this case, you've made new ones) and that

View File

@ -241,7 +241,7 @@ create a database migration:
$ python manage.py makemigrations $ python manage.py makemigrations
Migrations for 'world': Migrations for 'world':
world/migrations/0001_initial.py: world/migrations/0001_initial.py:
- Create model WorldBorder + Create model WorldBorder
Let's look at the SQL that will generate the table for the ``WorldBorder`` Let's look at the SQL that will generate the table for the ``WorldBorder``
model: model:

View File

@ -475,6 +475,42 @@ operations.
For an example using ``SeparateDatabaseAndState``, see For an example using ``SeparateDatabaseAndState``, see
:ref:`changing-a-manytomanyfield-to-use-a-through-model`. :ref:`changing-a-manytomanyfield-to-use-a-through-model`.
Operation category
==================
.. versionadded:: 5.1
.. currentmodule:: django.db.migrations.operations.base
.. class:: OperationCategory
Categories of migration operation used by the :djadmin:`makemigrations`
command to display meaningful symbols.
.. attribute:: ADDITION
*Symbol*: ``+``
.. attribute:: REMOVAL
*Symbol*: ``-``
.. attribute:: ALTERATION
*Symbol*: ``~``
.. attribute:: PYTHON
*Symbol*: ``p``
.. attribute:: SQL
*Symbol*: ``s``
.. attribute:: MIXED
*Symbol*: ``?``
.. _writing-your-own-migration-operation: .. _writing-your-own-migration-operation:
Writing your own Writing your own
@ -495,6 +531,10 @@ structure of an ``Operation`` looks like this::
# If this is False, Django will refuse to reverse past this operation. # If this is False, Django will refuse to reverse past this operation.
reversible = False reversible = False
# This categorizes the operation. The corresponding symbol will be
# display by the makemigrations command.
category = OperationCategory.ADDITION
def __init__(self, arg1, arg2): def __init__(self, arg1, arg2):
# Operations are usually instantiated with arguments in migration # Operations are usually instantiated with arguments in migration
# files. Store the values of them on self for later use. # files. Store the values of them on self for later use.
@ -516,7 +556,7 @@ structure of an ``Operation`` looks like this::
pass pass
def describe(self): def describe(self):
# This is used to describe what the operation does in console output. # This is used to describe what the operation does.
return "Custom Operation" return "Custom Operation"
@property @property

View File

@ -178,12 +178,17 @@ Logging
Management Commands Management Commands
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
* ... * :djadmin:`makemigrations` command now displays meaningful symbols for each
operation to highlight :class:`operation categories
<django.db.migrations.operations.base.OperationCategory>`.
Migrations Migrations
~~~~~~~~~~ ~~~~~~~~~~
* ... * The new ``Operation.category`` attribute allows specifying an
:class:`operation category
<django.db.migrations.operations.base.OperationCategory>` used by the
:djadmin:`makemigrations` to display a meaningful symbol for the operation.
Models Models
~~~~~~ ~~~~~~

View File

@ -118,7 +118,7 @@ field and remove a model - and then run :djadmin:`makemigrations`:
$ python manage.py makemigrations $ python manage.py makemigrations
Migrations for 'books': Migrations for 'books':
books/migrations/0003_auto.py: books/migrations/0003_auto.py:
- Alter field author on book ~ Alter field author on book
Your models will be scanned and compared to the versions currently Your models will be scanned and compared to the versions currently
contained in your migration files, and then a new set of migrations contained in your migration files, and then a new set of migrations

View File

@ -2141,7 +2141,7 @@ class MakeMigrationsTests(MigrationTestBase):
) )
# Normal --dry-run output # Normal --dry-run output
self.assertIn("- Add field silly_char to sillymodel", out.getvalue()) self.assertIn("+ Add field silly_char to sillymodel", out.getvalue())
# Additional output caused by verbosity 3 # Additional output caused by verbosity 3
# The complete migrations file that would be written # The complete migrations file that would be written
@ -2171,7 +2171,7 @@ class MakeMigrationsTests(MigrationTestBase):
) )
initial_file = os.path.join(migration_dir, "0001_initial.py") initial_file = os.path.join(migration_dir, "0001_initial.py")
self.assertEqual(out.getvalue(), f"{initial_file}\n") self.assertEqual(out.getvalue(), f"{initial_file}\n")
self.assertIn(" - Create model ModelWithCustomBase\n", err.getvalue()) self.assertIn(" + Create model ModelWithCustomBase\n", err.getvalue())
@mock.patch("builtins.input", return_value="Y") @mock.patch("builtins.input", return_value="Y")
def test_makemigrations_scriptable_merge(self, mock_input): def test_makemigrations_scriptable_merge(self, mock_input):
@ -2216,7 +2216,7 @@ class MakeMigrationsTests(MigrationTestBase):
self.assertTrue(os.path.exists(initial_file)) self.assertTrue(os.path.exists(initial_file))
# Command output indicates the migration is created. # Command output indicates the migration is created.
self.assertIn(" - Create model SillyModel", out.getvalue()) self.assertIn(" + Create model SillyModel", out.getvalue())
@override_settings(MIGRATION_MODULES={"migrations": "some.nonexistent.path"}) @override_settings(MIGRATION_MODULES={"migrations": "some.nonexistent.path"})
def test_makemigrations_migrations_modules_nonexistent_toplevel_package(self): def test_makemigrations_migrations_modules_nonexistent_toplevel_package(self):
@ -2321,12 +2321,12 @@ class MakeMigrationsTests(MigrationTestBase):
out.getvalue().lower(), out.getvalue().lower(),
"merging conflicting_app_with_dependencies\n" "merging conflicting_app_with_dependencies\n"
" branch 0002_conflicting_second\n" " branch 0002_conflicting_second\n"
" - create model something\n" " + create model something\n"
" branch 0002_second\n" " branch 0002_second\n"
" - delete model tribble\n" " - delete model tribble\n"
" - remove field silly_field from author\n" " - remove field silly_field from author\n"
" - add field rating to author\n" " + add field rating to author\n"
" - create model book\n" " + create model book\n"
"\n" "\n"
"merging will only work if the operations printed above do not " "merging will only work if the operations printed above do not "
"conflict\n" "conflict\n"

View File

@ -4,6 +4,7 @@ from decimal import Decimal
from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
from django.db import IntegrityError, connection, migrations, models, transaction from django.db import IntegrityError, connection, migrations, models, transaction
from django.db.migrations.migration import Migration from django.db.migrations.migration import Migration
from django.db.migrations.operations.base import Operation
from django.db.migrations.operations.fields import FieldOperation from django.db.migrations.operations.fields import FieldOperation
from django.db.migrations.state import ModelState, ProjectState from django.db.migrations.state import ModelState, ProjectState
from django.db.models import F from django.db.models import F
@ -47,6 +48,7 @@ class OperationTests(OperationTestBase):
], ],
) )
self.assertEqual(operation.describe(), "Create model Pony") self.assertEqual(operation.describe(), "Create model Pony")
self.assertEqual(operation.formatted_description(), "+ Create model Pony")
self.assertEqual(operation.migration_name_fragment, "pony") self.assertEqual(operation.migration_name_fragment, "pony")
# Test the state alteration # Test the state alteration
project_state = ProjectState() project_state = ProjectState()
@ -710,6 +712,7 @@ class OperationTests(OperationTestBase):
# Test the state alteration # Test the state alteration
operation = migrations.DeleteModel("Pony") operation = migrations.DeleteModel("Pony")
self.assertEqual(operation.describe(), "Delete model Pony") self.assertEqual(operation.describe(), "Delete model Pony")
self.assertEqual(operation.formatted_description(), "- Delete model Pony")
self.assertEqual(operation.migration_name_fragment, "delete_pony") self.assertEqual(operation.migration_name_fragment, "delete_pony")
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards("test_dlmo", new_state) operation.state_forwards("test_dlmo", new_state)
@ -790,6 +793,9 @@ class OperationTests(OperationTestBase):
# Test the state alteration # Test the state alteration
operation = migrations.RenameModel("Pony", "Horse") operation = migrations.RenameModel("Pony", "Horse")
self.assertEqual(operation.describe(), "Rename model Pony to Horse") self.assertEqual(operation.describe(), "Rename model Pony to Horse")
self.assertEqual(
operation.formatted_description(), "~ Rename model Pony to Horse"
)
self.assertEqual(operation.migration_name_fragment, "rename_pony_horse") self.assertEqual(operation.migration_name_fragment, "rename_pony_horse")
# Test initial state and database # Test initial state and database
self.assertIn(("test_rnmo", "pony"), project_state.models) self.assertIn(("test_rnmo", "pony"), project_state.models)
@ -1350,6 +1356,9 @@ class OperationTests(OperationTestBase):
models.FloatField(null=True, default=5), models.FloatField(null=True, default=5),
) )
self.assertEqual(operation.describe(), "Add field height to Pony") self.assertEqual(operation.describe(), "Add field height to Pony")
self.assertEqual(
operation.formatted_description(), "+ Add field height to Pony"
)
self.assertEqual(operation.migration_name_fragment, "pony_height") self.assertEqual(operation.migration_name_fragment, "pony_height")
project_state, new_state = self.make_test_state("test_adfl", operation) project_state, new_state = self.make_test_state("test_adfl", operation)
self.assertEqual(len(new_state.models["test_adfl", "pony"].fields), 6) self.assertEqual(len(new_state.models["test_adfl", "pony"].fields), 6)
@ -1906,6 +1915,9 @@ class OperationTests(OperationTestBase):
# Test the state alteration # Test the state alteration
operation = migrations.RemoveField("Pony", "pink") operation = migrations.RemoveField("Pony", "pink")
self.assertEqual(operation.describe(), "Remove field pink from Pony") self.assertEqual(operation.describe(), "Remove field pink from Pony")
self.assertEqual(
operation.formatted_description(), "- Remove field pink from Pony"
)
self.assertEqual(operation.migration_name_fragment, "remove_pony_pink") self.assertEqual(operation.migration_name_fragment, "remove_pony_pink")
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards("test_rmfl", new_state) operation.state_forwards("test_rmfl", new_state)
@ -1952,6 +1964,10 @@ class OperationTests(OperationTestBase):
self.assertEqual( self.assertEqual(
operation.describe(), "Rename table for Pony to test_almota_pony_2" operation.describe(), "Rename table for Pony to test_almota_pony_2"
) )
self.assertEqual(
operation.formatted_description(),
"~ Rename table for Pony to test_almota_pony_2",
)
self.assertEqual(operation.migration_name_fragment, "alter_pony_table") self.assertEqual(operation.migration_name_fragment, "alter_pony_table")
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards("test_almota", new_state) operation.state_forwards("test_almota", new_state)
@ -2093,6 +2109,9 @@ class OperationTests(OperationTestBase):
"Pony", "pink", models.IntegerField(null=True) "Pony", "pink", models.IntegerField(null=True)
) )
self.assertEqual(operation.describe(), "Alter field pink on Pony") self.assertEqual(operation.describe(), "Alter field pink on Pony")
self.assertEqual(
operation.formatted_description(), "~ Alter field pink on Pony"
)
self.assertEqual(operation.migration_name_fragment, "alter_pony_pink") self.assertEqual(operation.migration_name_fragment, "alter_pony_pink")
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards("test_alfl", new_state) operation.state_forwards("test_alfl", new_state)
@ -2403,6 +2422,9 @@ class OperationTests(OperationTestBase):
# Add table comment. # Add table comment.
operation = migrations.AlterModelTableComment("Pony", "Custom pony comment") operation = migrations.AlterModelTableComment("Pony", "Custom pony comment")
self.assertEqual(operation.describe(), "Alter Pony table comment") self.assertEqual(operation.describe(), "Alter Pony table comment")
self.assertEqual(
operation.formatted_description(), "~ Alter Pony table comment"
)
self.assertEqual(operation.migration_name_fragment, "alter_pony_table_comment") self.assertEqual(operation.migration_name_fragment, "alter_pony_table_comment")
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards(app_label, new_state) operation.state_forwards(app_label, new_state)
@ -3073,6 +3095,9 @@ class OperationTests(OperationTestBase):
project_state = self.set_up_test_model("test_rnfl") project_state = self.set_up_test_model("test_rnfl")
operation = migrations.RenameField("Pony", "pink", "blue") operation = migrations.RenameField("Pony", "pink", "blue")
self.assertEqual(operation.describe(), "Rename field pink on Pony to blue") self.assertEqual(operation.describe(), "Rename field pink on Pony to blue")
self.assertEqual(
operation.formatted_description(), "~ Rename field pink on Pony to blue"
)
self.assertEqual(operation.migration_name_fragment, "rename_pink_pony_blue") self.assertEqual(operation.migration_name_fragment, "rename_pink_pony_blue")
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards("test_rnfl", new_state) operation.state_forwards("test_rnfl", new_state)
@ -3326,6 +3351,10 @@ class OperationTests(OperationTestBase):
self.assertEqual( self.assertEqual(
operation.describe(), "Alter unique_together for Pony (1 constraint(s))" operation.describe(), "Alter unique_together for Pony (1 constraint(s))"
) )
self.assertEqual(
operation.formatted_description(),
"~ Alter unique_together for Pony (1 constraint(s))",
)
self.assertEqual( self.assertEqual(
operation.migration_name_fragment, operation.migration_name_fragment,
"alter_pony_unique_together", "alter_pony_unique_together",
@ -3478,6 +3507,10 @@ class OperationTests(OperationTestBase):
operation.describe(), operation.describe(),
"Create index test_adin_pony_pink_idx on field(s) pink of model Pony", "Create index test_adin_pony_pink_idx on field(s) pink of model Pony",
) )
self.assertEqual(
operation.formatted_description(),
"+ Create index test_adin_pony_pink_idx on field(s) pink of model Pony",
)
self.assertEqual( self.assertEqual(
operation.migration_name_fragment, operation.migration_name_fragment,
"pony_test_adin_pony_pink_idx", "pony_test_adin_pony_pink_idx",
@ -3511,6 +3544,9 @@ class OperationTests(OperationTestBase):
self.assertIndexExists("test_rmin_pony", ["pink", "weight"]) self.assertIndexExists("test_rmin_pony", ["pink", "weight"])
operation = migrations.RemoveIndex("Pony", "pony_test_idx") operation = migrations.RemoveIndex("Pony", "pony_test_idx")
self.assertEqual(operation.describe(), "Remove index pony_test_idx from Pony") self.assertEqual(operation.describe(), "Remove index pony_test_idx from Pony")
self.assertEqual(
operation.formatted_description(), "- Remove index pony_test_idx from Pony"
)
self.assertEqual( self.assertEqual(
operation.migration_name_fragment, operation.migration_name_fragment,
"remove_pony_pony_test_idx", "remove_pony_pony_test_idx",
@ -3565,6 +3601,10 @@ class OperationTests(OperationTestBase):
operation.describe(), operation.describe(),
"Rename index pony_pink_idx on Pony to new_pony_test_idx", "Rename index pony_pink_idx on Pony to new_pony_test_idx",
) )
self.assertEqual(
operation.formatted_description(),
"~ Rename index pony_pink_idx on Pony to new_pony_test_idx",
)
self.assertEqual( self.assertEqual(
operation.migration_name_fragment, operation.migration_name_fragment,
"rename_pony_pink_idx_new_pony_test_idx", "rename_pony_pink_idx_new_pony_test_idx",
@ -3807,6 +3847,10 @@ class OperationTests(OperationTestBase):
self.assertEqual( self.assertEqual(
operation.describe(), "Alter index_together for Pony (0 constraint(s))" operation.describe(), "Alter index_together for Pony (0 constraint(s))"
) )
self.assertEqual(
operation.formatted_description(),
"~ Alter index_together for Pony (0 constraint(s))",
)
def test_add_constraint(self): def test_add_constraint(self):
project_state = self.set_up_test_model("test_addconstraint") project_state = self.set_up_test_model("test_addconstraint")
@ -3819,6 +3863,10 @@ class OperationTests(OperationTestBase):
gt_operation.describe(), gt_operation.describe(),
"Create constraint test_add_constraint_pony_pink_gt_2 on model Pony", "Create constraint test_add_constraint_pony_pink_gt_2 on model Pony",
) )
self.assertEqual(
gt_operation.formatted_description(),
"+ Create constraint test_add_constraint_pony_pink_gt_2 on model Pony",
)
self.assertEqual( self.assertEqual(
gt_operation.migration_name_fragment, gt_operation.migration_name_fragment,
"pony_test_add_constraint_pony_pink_gt_2", "pony_test_add_constraint_pony_pink_gt_2",
@ -4024,6 +4072,10 @@ class OperationTests(OperationTestBase):
gt_operation.describe(), gt_operation.describe(),
"Remove constraint test_remove_constraint_pony_pink_gt_2 from model Pony", "Remove constraint test_remove_constraint_pony_pink_gt_2 from model Pony",
) )
self.assertEqual(
gt_operation.formatted_description(),
"- Remove constraint test_remove_constraint_pony_pink_gt_2 from model Pony",
)
self.assertEqual( self.assertEqual(
gt_operation.migration_name_fragment, gt_operation.migration_name_fragment,
"remove_pony_test_remove_constraint_pony_pink_gt_2", "remove_pony_test_remove_constraint_pony_pink_gt_2",
@ -4564,6 +4616,9 @@ class OperationTests(OperationTestBase):
"Pony", {"permissions": [("can_groom", "Can groom")]} "Pony", {"permissions": [("can_groom", "Can groom")]}
) )
self.assertEqual(operation.describe(), "Change Meta options on Pony") self.assertEqual(operation.describe(), "Change Meta options on Pony")
self.assertEqual(
operation.formatted_description(), "~ Change Meta options on Pony"
)
self.assertEqual(operation.migration_name_fragment, "alter_pony_options") self.assertEqual(operation.migration_name_fragment, "alter_pony_options")
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards("test_almoop", new_state) operation.state_forwards("test_almoop", new_state)
@ -4630,6 +4685,10 @@ class OperationTests(OperationTestBase):
self.assertEqual( self.assertEqual(
operation.describe(), "Set order_with_respect_to on Rider to pony" operation.describe(), "Set order_with_respect_to on Rider to pony"
) )
self.assertEqual(
operation.formatted_description(),
"~ Set order_with_respect_to on Rider to pony",
)
self.assertEqual( self.assertEqual(
operation.migration_name_fragment, operation.migration_name_fragment,
"alter_rider_order_with_respect_to", "alter_rider_order_with_respect_to",
@ -4705,6 +4764,7 @@ class OperationTests(OperationTestBase):
], ],
) )
self.assertEqual(operation.describe(), "Change managers on Pony") self.assertEqual(operation.describe(), "Change managers on Pony")
self.assertEqual(operation.formatted_description(), "~ Change managers on Pony")
self.assertEqual(operation.migration_name_fragment, "alter_pony_managers") self.assertEqual(operation.migration_name_fragment, "alter_pony_managers")
managers = project_state.models["test_almoma", "pony"].managers managers = project_state.models["test_almoma", "pony"].managers
self.assertEqual(managers, []) self.assertEqual(managers, [])
@ -4840,6 +4900,7 @@ class OperationTests(OperationTestBase):
], ],
) )
self.assertEqual(operation.describe(), "Raw SQL operation") self.assertEqual(operation.describe(), "Raw SQL operation")
self.assertEqual(operation.formatted_description(), "s Raw SQL operation")
# Test the state alteration # Test the state alteration
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards("test_runsql", new_state) operation.state_forwards("test_runsql", new_state)
@ -5034,6 +5095,7 @@ class OperationTests(OperationTestBase):
inner_method, reverse_code=inner_method_reverse inner_method, reverse_code=inner_method_reverse
) )
self.assertEqual(operation.describe(), "Raw Python operation") self.assertEqual(operation.describe(), "Raw Python operation")
self.assertEqual(operation.formatted_description(), "p Raw Python operation")
# Test the state alteration does nothing # Test the state alteration does nothing
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards("test_runpython", new_state) operation.state_forwards("test_runpython", new_state)
@ -5565,6 +5627,10 @@ class OperationTests(OperationTestBase):
self.assertEqual( self.assertEqual(
operation.describe(), "Custom state/database change combination" operation.describe(), "Custom state/database change combination"
) )
self.assertEqual(
operation.formatted_description(),
"? Custom state/database change combination",
)
# Test the state alteration # Test the state alteration
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards("test_separatedatabaseandstate", new_state) operation.state_forwards("test_separatedatabaseandstate", new_state)
@ -6073,3 +6139,9 @@ class FieldOperationTests(SimpleTestCase):
self.assertIs( self.assertIs(
operation.references_field("Through", "second", "migrations"), True operation.references_field("Through", "second", "migrations"), True
) )
class BaseOperationTests(SimpleTestCase):
def test_formatted_description_no_category(self):
operation = Operation()
self.assertEqual(operation.formatted_description(), "? Operation: ((), {})")

View File

@ -59,6 +59,10 @@ class AddIndexConcurrentlyTests(OperationTestBase):
operation.describe(), operation.describe(),
"Concurrently create index pony_pink_idx on field(s) pink of model Pony", "Concurrently create index pony_pink_idx on field(s) pink of model Pony",
) )
self.assertEqual(
operation.formatted_description(),
"+ Concurrently create index pony_pink_idx on field(s) pink of model Pony",
)
operation.state_forwards(self.app_label, new_state) operation.state_forwards(self.app_label, new_state)
self.assertEqual( self.assertEqual(
len(new_state.models[self.app_label, "pony"].options["indexes"]), 1 len(new_state.models[self.app_label, "pony"].options["indexes"]), 1
@ -154,6 +158,10 @@ class RemoveIndexConcurrentlyTests(OperationTestBase):
operation.describe(), operation.describe(),
"Concurrently remove index pony_pink_idx from Pony", "Concurrently remove index pony_pink_idx from Pony",
) )
self.assertEqual(
operation.formatted_description(),
"- Concurrently remove index pony_pink_idx from Pony",
)
operation.state_forwards(self.app_label, new_state) operation.state_forwards(self.app_label, new_state)
self.assertEqual( self.assertEqual(
len(new_state.models[self.app_label, "pony"].options["indexes"]), 0 len(new_state.models[self.app_label, "pony"].options["indexes"]), 0
@ -190,6 +198,9 @@ class CreateExtensionTests(PostgreSQLTestCase):
@override_settings(DATABASE_ROUTERS=[NoMigrationRouter()]) @override_settings(DATABASE_ROUTERS=[NoMigrationRouter()])
def test_no_allow_migrate(self): def test_no_allow_migrate(self):
operation = CreateExtension("tablefunc") operation = CreateExtension("tablefunc")
self.assertEqual(
operation.formatted_description(), "+ Creates extension tablefunc"
)
project_state = ProjectState() project_state = ProjectState()
new_state = project_state.clone() new_state = project_state.clone()
# Don't create an extension. # Don't create an extension.
@ -287,6 +298,7 @@ class CreateCollationTests(PostgreSQLTestCase):
operation = CreateCollation("C_test", locale="C") operation = CreateCollation("C_test", locale="C")
self.assertEqual(operation.migration_name_fragment, "create_collation_c_test") self.assertEqual(operation.migration_name_fragment, "create_collation_c_test")
self.assertEqual(operation.describe(), "Create collation C_test") self.assertEqual(operation.describe(), "Create collation C_test")
self.assertEqual(operation.formatted_description(), "+ Create collation C_test")
project_state = ProjectState() project_state = ProjectState()
new_state = project_state.clone() new_state = project_state.clone()
# Create a collation. # Create a collation.
@ -418,6 +430,7 @@ class RemoveCollationTests(PostgreSQLTestCase):
operation = RemoveCollation("C_test", locale="C") operation = RemoveCollation("C_test", locale="C")
self.assertEqual(operation.migration_name_fragment, "remove_collation_c_test") self.assertEqual(operation.migration_name_fragment, "remove_collation_c_test")
self.assertEqual(operation.describe(), "Remove collation C_test") self.assertEqual(operation.describe(), "Remove collation C_test")
self.assertEqual(operation.formatted_description(), "- Remove collation C_test")
project_state = ProjectState() project_state = ProjectState()
new_state = project_state.clone() new_state = project_state.clone()
# Remove a collation. # Remove a collation.
@ -470,6 +483,10 @@ class AddConstraintNotValidTests(OperationTestBase):
operation.describe(), operation.describe(),
f"Create not valid constraint {constraint_name} on model Pony", f"Create not valid constraint {constraint_name} on model Pony",
) )
self.assertEqual(
operation.formatted_description(),
f"+ Create not valid constraint {constraint_name} on model Pony",
)
self.assertEqual( self.assertEqual(
operation.migration_name_fragment, operation.migration_name_fragment,
f"pony_{constraint_name}_not_valid", f"pony_{constraint_name}_not_valid",
@ -530,6 +547,10 @@ class ValidateConstraintTests(OperationTestBase):
operation.describe(), operation.describe(),
f"Validate constraint {constraint_name} on model Pony", f"Validate constraint {constraint_name} on model Pony",
) )
self.assertEqual(
operation.formatted_description(),
f"~ Validate constraint {constraint_name} on model Pony",
)
self.assertEqual( self.assertEqual(
operation.migration_name_fragment, operation.migration_name_fragment,
f"pony_validate_{constraint_name}", f"pony_validate_{constraint_name}",