Commits

Andrew Godwin committed 825ad00

Schema migration mostly working for --initial and --auto (missing M2Ms), new datamigration command.

Comments (0)

Files changed (7)

south/creator/changes.py

 
 from django.db import models
 
-from south.creator.freezer import remove_useless_attributes, freeze_apps
+from south.creator.freezer import remove_useless_attributes, freeze_apps, model_key
 
 class AutoChanges(object):
     """
         """
         
         deleted_models = set()
-        added_fields = set()
-        deleted_fields = set()
-        changed_fields = []
-        added_uniques = set()
-        deleted_uniques = set()
         
         # See if anything's vanished
         for key in self.old_defs:
     
     def get_changes(self):
         # Get the frozen models for this app
-        model_defs = freeze_apps(self.migrations.app_label())
+        model_defs = freeze_apps([self.migrations.app_label()])
         
         for model in models.get_models(models.get_app(self.migrations.app_label())):
             
-            model_def = model_defs[model._meta.app_label + "." + model._meta.object_name]
-            
+            # Firstly, add all the models
+            model_def = model_defs[model_key(model)]
             yield ("AddModel", {
                 "model": model,
-                "model_def": dict((k, v) for k, v in model_defs.items() if k != "Meta"),
-            })
+                "model_def": dict((k, v) for k, v in model_def.items() if k != "Meta"),
+            })
+            
+            # Then, add any uniqueness that's around
+            meta = model_def.get("Meta", {})
+            if meta:
+                unique_together = eval(meta.get("unique_together", []))
+                if unique_together:
+                    # If it's only a single tuple, make it into the longer one
+                    if isinstance(unique_together[0], basestring):
+                        unique_together = [unique_together]
+                    # For each combination, make an action for it
+                    for fields in unique_together:
+                        yield ("AddUnique", {
+                            "model": model,
+                            "fields": list(fields),
+                        })

south/creator/freezer.py

     """
     Takes a list of app labels, and returns a string of their frozen form.
     """
+    if isinstance(apps, basestring):
+        apps = [apps]
     frozen_models = set()
     # For each app, add in all its models
     for app in apps:

south/management/commands/datamigration.py

+"""
+Data migration creation command
+"""
+
+import sys
+import os
+from optparse import make_option
+
+try:
+    set
+except NameError:
+    from sets import Set as set
+
+from django.core.management.base import BaseCommand
+from django.core.management.color import no_style
+from django.db import models
+from django.conf import settings
+
+from south.migration import Migrations
+from south.exceptions import NoMigrations
+from south.creator import freezer
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list + (
+        make_option('--freeze', action='append', dest='freeze_list', type='string',
+            help='Freeze the specified model(s). Pass in either an app name (to freeze the whole app) or a single model, as appname.modelname.'),
+        make_option('--stdout', action='store_true', dest='stdout', default=False,
+            help='Print the migration to stdout instead of writing it to a file.'),
+    )
+    help = "Creates a new template data migration for the given app"
+    usage_str = "Usage: ./manage.py datamigration appname migrationname [--stdout] [--freeze appname]"
+    
+    def handle(self, app=None, name="", freeze_list=None, stdout=False, verbosity=1, **options):
+        
+        # Any supposed lists that are None become empty lists
+        freeze_list = freeze_list or []
+
+        # --stdout means name = -
+        if stdout:
+            name = "-"
+        
+        # if not name, there's an error
+        if not name:
+            self.error("You must provide a name for this migration\n" + self.usage_str)
+        
+        if not app:
+            self.error("You must provide an app to create a migration for.\n" + self.usage_str)
+        
+        # Get the Migrations for this app (creating the migrations dir if needed)
+        try:
+            migrations = Migrations(app)
+        except NoMigrations:
+            Migrations.create_migrations_directory(app, verbose=verbosity > 0)
+            migrations = Migrations(app)
+        
+        # See what filename is next in line. We assume they use numbers.
+        new_filename = migrations.next_filename(name)
+        
+        # Work out which apps to freeze
+        apps_to_freeze = self.calc_frozen_apps(migrations, freeze_list)
+        
+        # So, what's in this file, then?
+        file_contents = MIGRATION_TEMPLATE % {
+            "frozen_models":  freezer.freeze_apps_to_string(apps_to_freeze),
+            "complete_apps": apps_to_freeze and "complete_apps = [%s]" % (", ".join(map(repr, apps_to_freeze))) or ""
+        }
+        
+        # - is a special name which means 'print to stdout'
+        if name == "-":
+            print file_contents
+        # Write the migration file if the name isn't -
+        else:
+            fp = open(os.path.join(migrations_dir, new_filename), "w")
+            fp.write(file_contents)
+            fp.close()
+            print >>sys.stderr, "Created %s." % new_filename
+    
+    def calc_frozen_apps(self, migrations, freeze_list):
+        """
+        Works out, from the current app, settings, and the command line options,
+        which apps should be frozen.
+        """
+        apps_to_freeze = []
+        for to_freeze in freeze_list:
+            if "." in to_freeze:
+                self.error("You cannot freeze %r; you must provide an app label, like 'auth' or 'books'." % to_freeze)
+            # Make sure it's a real app
+            if not models.get_app(to_freeze):
+                self.error("You cannot freeze %r; it's not an installed app." % to_freeze)
+            # OK, it's fine
+            apps_to_freeze.append(to_freeze)
+        if getattr(settings, 'SOUTH_AUTO_FREEZE_APP', True):
+            apps_to_freeze.append(migrations.app_label())
+        return apps_to_freeze
+    
+    def error(self, message, code=1):
+        """
+        Prints the error, and exits with the given code.
+        """
+        print >>sys.stderr(message)
+        sys.exit(code)
+
+
+MIGRATION_TEMPLATE = """# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+    
+    def forwards(self, orm):
+        "Write your forwards methods here."
+    
+    
+    def backwards(self, orm):
+        "Write your backwards methods here."
+    
+    models = %(frozen_models)s
+    
+    %(complete_apps)s
+"""

south/management/commands/schemamigration.py

 from django.core.management.base import BaseCommand
 from django.core.management.color import no_style
 from django.db import models
-from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
-from django.contrib.contenttypes.generic import GenericRelation
-from django.db.models.fields import FieldDoesNotExist
 from django.conf import settings
 
 from south.migration import Migrations
 from south.exceptions import NoMigrations
 from south.creator import changes, actions, freezer
+from south.management.commands.datamigration import Command as DataCommand
 
-class Command(BaseCommand):
-    option_list = BaseCommand.option_list + (
+class Command(DataCommand):
+    option_list = DataCommand.option_list + (
         make_option('--add-model', action='append', dest='added_model_list', type='string',
             help='Generate a Create Table migration for the specified model.  Add multiple models to this migration with subsequent --model parameters.'),
         make_option('--add-field', action='append', dest='added_field_list', type='string',
             help='Generate an Add Column migration for the specified modelname.fieldname - you can use this multiple times to add more than one column.'),
         make_option('--add-index', action='append', dest='added_index_list', type='string',
             help='Generate an Add Index migration for the specified modelname.fieldname - you can use this multiple times to add more than one column.'),
-        make_option('--freeze', action='append', dest='freeze_list', type='string',
-            help='Freeze the specified model(s). Pass in either an app name (to freeze the whole app) or a single model, as appname.modelname.'),
         make_option('--initial', action='store_true', dest='initial', default=False,
             help='Generate the initial schema for the app.'),
         make_option('--auto', action='store_true', dest='auto', default=False,
             help='Attempt to automatically detect differences from the last migration.'),
-        make_option('--stdout', action='store_true', dest='stdout', default=False,
-            help='Print the migration to stdout instead of writing it to a file.'),
     )
-    help = "Creates a new template migration for the given app"
+    help = "Creates a new template schema migration for the given app"
     usage_str = "Usage: ./manage.py schemamigration appname migrationname [--initial] [--auto] [--add-model ModelName] [--add-field ModelName.field_name] [--stdout]"
     
     def handle(self, app=None, name="", added_model_list=None, added_field_list=None, freeze_list=None, initial=False, auto=False, stdout=False, added_index_list=None, verbosity=1, **options):
             migrations = Migrations(app)
         
         # See what filename is next in line. We assume they use numbers.
-        highest_number = 0
-        for migration in migrations:
-            try:
-                number = int(migration.name().split("_")[0])
-                highest_number = max(highest_number, number)
-            except ValueError:
-                pass
-        
-        # Work out the new filename
-        new_filename = "%04i_%s.py" % (
-            highest_number + 1,
-            name,
-        )
+        new_filename = migrations.next_filename(name)
         
         # What actions do we need to do?
         if auto:
                 action.add_backwards(backwards_actions)
         
         # Work out which apps to freeze
-        apps_to_freeze = []
-        for to_freeze in freeze_list:
-            if "." in to_freeze:
-                self.error("You cannot freeze %r; you must provide an app label, like 'auth' or 'books'." % to_freeze)
-            # Make sure it's a real app
-            if not models.get_app(to_freeze):
-                self.error("You cannot freeze %r; it's not an installed app." % to_freeze)
-            # OK, it's fine
-            apps_to_freeze.append(to_freeze)
-        if getattr(settings, 'SOUTH_AUTO_FREEZE_APP', True):
-            apps_to_freeze.append(migrations.app_label())
+        apps_to_freeze = self.calc_frozen_apps(migrations, freeze_list)
         
         # So, what's in this file, then?
         file_contents = MIGRATION_TEMPLATE % {
             fp = open(os.path.join(migrations_dir, new_filename), "w")
             fp.write(file_contents)
             fp.close()
-            print "Created %s." % new_filename
-    
-    def error(self, message, code=1):
-        """
-        Prints the error, and exits with the given code.
-        """
-        print >>sys.stderr(message)
-        sys.exit(code)
+            print >>sys.stderr, "Created %s." % new_filename
 
 
 MIGRATION_TEMPLATE = """# encoding: utf-8

south/management/commands/startmigration.py

 from django.core.management.base import BaseCommand
 from django.core.management.color import no_style
 from django.db import models
-from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
-from django.contrib.contenttypes.generic import GenericRelation
-from django.db.models.fields import FieldDoesNotExist
 from django.conf import settings
 
 try:

south/migration/base.py

                 migration.add_dependent(None)
                 for dependency in migration.dependencies():
                     dependency.add_dependent(migration)
+    
+    def next_filename(self, name):
+        "Returns the fully-formatted filename of what a new migration 'name' would be"
+        highest_number = 0
+        for migration in self:
+            try:
+                number = int(migration.name().split("_")[0])
+                highest_number = max(highest_number, number)
+            except ValueError:
+                pass
+        # Work out the new filename
+        return "%04i_%s.py" % (
+            highest_number + 1,
+            name,
+        )
 
 
 class Migration(object):
     pass
 
 class DataMigration(BaseMigration):
-    pass
+    # Data migrations shouldn't be dry-run
+    no_dry_run = True