diff --git a/django/core/management/commands/makemigration.py b/django/core/management/commands/makemigration.py index fd12652036..baf3f075ca 100644 --- a/django/core/management/commands/makemigration.py +++ b/django/core/management/commands/makemigration.py @@ -1,4 +1,5 @@ import sys +import os from optparse import make_option from django.core.management.base import BaseCommand @@ -8,6 +9,7 @@ from django.db import connections from django.db.migrations.loader import MigrationLoader from django.db.migrations.autodetector import MigrationAutodetector, InteractiveMigrationQuestioner from django.db.migrations.state import ProjectState +from django.db.migrations.writer import MigrationWriter from django.db.models.loading import cache @@ -49,4 +51,30 @@ class Command(BaseCommand): if app_labels: changes = autodetector.trim_to_apps(changes, app_labels) - print changes + # No changes? Tell them. + if not changes: + if len(app_labels) == 1: + self.stdout.write("No changes detected in app '%s'" % app_labels.pop()) + elif len(app_labels) > 1: + self.stdout.write("No changes detected in apps '%s'" % ("', '".join(app_labels))) + else: + self.stdout.write("No changes detected") + return + + for app_label, migrations in changes.items(): + self.stdout.write(self.style.MIGRATE_HEADING("Migrations for '%s':" % app_label) + "\n") + for migration in migrations: + # Describe the migration + writer = MigrationWriter(migration) + self.stdout.write(" %s:\n" % (self.style.MIGRATE_LABEL(writer.filename),)) + for operation in migration.operations: + self.stdout.write(" - %s\n" % operation.describe()) + # Write it + migrations_directory = os.path.dirname(writer.path) + if not os.path.isdir(migrations_directory): + os.mkdir(migrations_directory) + init_path = os.path.join(migrations_directory, "__init__.py") + if not os.path.isfile(init_path): + open(init_path, "w").close() + with open(writer.path, "w") as fh: + fh.write(writer.as_string()) diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index f89634f57b..79f710482d 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -41,10 +41,10 @@ class MigrationAutodetector(object): self.add_to_migration( app_label, operations.CreateModel( - model_state.name, - model_state.fields, - model_state.options, - model_state.bases, + name = model_state.name, + fields = model_state.fields, + options = model_state.options, + bases = model_state.bases, ) ) # Removing models diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py index 76d3fe8329..9658793094 100644 --- a/django/db/migrations/loader.py +++ b/django/db/migrations/loader.py @@ -37,7 +37,8 @@ class MigrationLoader(object): self.disk_migrations = None self.applied_migrations = None - def migration_module(self, app_label): + @classmethod + def migrations_module(cls, app_label): if app_label in settings.MIGRATION_MODULES: return settings.MIGRATION_MODULES[app_label] app = cache.get_app(app_label) @@ -52,7 +53,7 @@ class MigrationLoader(object): for app in cache.get_apps(): # Get the migrations module directory app_label = app.__name__.split(".")[-2] - module_name = self.migration_module(app_label) + module_name = self.migrations_module(app_label) try: module = import_module(module_name) except ImportError as e: diff --git a/django/db/migrations/operations/base.py b/django/db/migrations/operations/base.py index 084ce14959..dcdb1ad30b 100644 --- a/django/db/migrations/operations/base.py +++ b/django/db/migrations/operations/base.py @@ -54,3 +54,9 @@ class Operation(object): drop the model's table. """ raise NotImplementedError() + + def describe(self): + """ + Outputs a brief summary of what the action does. + """ + return "%s: %s" % (self.__class__.__name__, self._constructor_args) diff --git a/django/db/migrations/operations/fields.py b/django/db/migrations/operations/fields.py index 862716a347..8fd8c9151d 100644 --- a/django/db/migrations/operations/fields.py +++ b/django/db/migrations/operations/fields.py @@ -23,6 +23,9 @@ class AddField(Operation): from_model = from_state.render().get_model(app_label, self.model_name) schema_editor.remove_field(from_model, from_model._meta.get_field_by_name(self.name)[0]) + def describe(self): + return "Add field %s to %s" % (self.name, self.model_name) + class RemoveField(Operation): """ @@ -48,3 +51,6 @@ class RemoveField(Operation): from_model = from_state.render().get_model(app_label, self.model_name) to_model = to_state.render().get_model(app_label, self.model_name) schema_editor.add_field(from_model, to_model._meta.get_field_by_name(self.name)[0]) + + def describe(self): + return "Remove field %s from %s" % (self.name, self.model_name) diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py index 22d24f1eed..ef7dafab90 100644 --- a/django/db/migrations/operations/models.py +++ b/django/db/migrations/operations/models.py @@ -25,6 +25,9 @@ class CreateModel(Operation): app_cache = from_state.render() schema_editor.delete_model(app_cache.get_model(app, self.name)) + def describe(self): + return "Create model %s" % (self.name, ) + class DeleteModel(Operation): """ @@ -44,3 +47,6 @@ class DeleteModel(Operation): def database_backwards(self, app_label, schema_editor, from_state, to_state): app_cache = to_state.render() schema_editor.create_model(app_cache.get_model(app_label, self.name)) + + def describe(self): + return "Delete model %s" % (self.name, ) diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py index f386cd847c..00e83681cd 100644 --- a/django/db/migrations/writer.py +++ b/django/db/migrations/writer.py @@ -1,8 +1,12 @@ from __future__ import unicode_literals import datetime import types +import os from django.utils import six +from django.utils.importlib import import_module from django.db import models +from django.db.models.loading import cache +from django.db.migrations.loader import MigrationLoader class MigrationWriter(object): @@ -49,6 +53,24 @@ class MigrationWriter(object): def filename(self): return "%s.py" % self.migration.name + @property + def path(self): + migrations_module_name = MigrationLoader.migrations_module(self.migration.app_label) + app_module = cache.get_app(self.migration.app_label) + # See if we can import the migrations module directly + try: + migrations_module = import_module(migrations_module_name) + basedir = os.path.dirname(migrations_module.__file__) + except ImportError: + # Alright, see if it's a direct submodule of the app + oneup = ".".join(migrations_module_name.split(".")[:-1]) + app_oneup = ".".join(app_module.__name__.split(".")[:-1]) + if oneup == app_oneup: + basedir = os.path.join(os.path.dirname(app_module.__file__), migrations_module_name.split(".")[-1]) + else: + raise ImportError("Cannot open migrations module %s for app %s" % (migrations_module_name, self.migration.app_label)) + return os.path.join(basedir, self.filename) + @classmethod def serialize(cls, value): """ @@ -130,6 +152,17 @@ class MigrationWriter(object): if module is None: raise ValueError("Cannot serialize function %r: No module" % value) return "%s.%s" % (module, value.__name__), set(["import %s" % module]) + # Classes + elif isinstance(value, type): + special_cases = [ + (models.Model, "models.Model", []), + ] + for case, string, imports in special_cases: + if case is value: + return string, set(imports) + if hasattr(value, "__module__"): + module = value.__module__ + return "%s.%s" % (module, value.__name__), set(["import %s" % module]) # Uh oh. else: raise ValueError("Cannot serialize: %r" % value)