Project

General

Profile

Statistics
| Branch: | Revision:

root / env / lib / python2.7 / site-packages / south / management / commands / schemamigration.py @ d1a4905f

History | View | Annotate | Download (9.76 KB)

1
"""
2
Startmigration command, version 2.
3
"""
4

    
5
import sys
6
import os
7
import re
8
import string
9
import random
10
import inspect
11
from optparse import make_option
12

    
13
try:
14
    set
15
except NameError:
16
    from sets import Set as set
17

    
18
from django.core.management.base import BaseCommand
19
from django.core.management.color import no_style
20
from django.db import models
21
from django.conf import settings
22

    
23
from south.migration import Migrations, migrate_app
24
from south.models import MigrationHistory
25
from south.exceptions import NoMigrations
26
from south.creator import changes, actions, freezer
27
from south.management.commands.datamigration import Command as DataCommand
28

    
29
class Command(DataCommand):
30
    option_list = DataCommand.option_list + (
31
        make_option('--add-model', action='append', dest='added_model_list', type='string',
32
            help='Generate a Create Table migration for the specified model.  Add multiple models to this migration with subsequent --model parameters.'),
33
        make_option('--add-field', action='append', dest='added_field_list', type='string',
34
            help='Generate an Add Column migration for the specified modelname.fieldname - you can use this multiple times to add more than one column.'),
35
        make_option('--add-index', action='append', dest='added_index_list', type='string',
36
            help='Generate an Add Index migration for the specified modelname.fieldname - you can use this multiple times to add more than one column.'),
37
        make_option('--initial', action='store_true', dest='initial', default=False,
38
            help='Generate the initial schema for the app.'),
39
        make_option('--auto', action='store_true', dest='auto', default=False,
40
            help='Attempt to automatically detect differences from the last migration.'),
41
        make_option('--empty', action='store_true', dest='empty', default=False,
42
            help='Make a blank migration.'),
43
        make_option('--update', action='store_true', dest='update', default=False,
44
                    help='Update the most recent migration instead of creating a new one. Rollback this migration if it is already applied.'),
45
    )
46
    help = "Creates a new template schema migration for the given app"
47
    usage_str = "Usage: ./manage.py schemamigration appname migrationname [--empty] [--initial] [--auto] [--add-model ModelName] [--add-field ModelName.field_name] [--stdout]"
48
    
49
    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, empty=False, update=False, **options):
50
        
51
        # Any supposed lists that are None become empty lists
52
        added_model_list = added_model_list or []
53
        added_field_list = added_field_list or []
54
        added_index_list = added_index_list or []
55
        freeze_list = freeze_list or []
56

    
57
        # --stdout means name = -
58
        if stdout:
59
            name = "-"
60
        
61
        # Only allow valid names
62
        if re.search('[^_\w]', name) and name != "-":
63
            self.error("Migration names should contain only alphanumeric characters and underscores.")
64
        
65
        # Make sure options are compatable
66
        if initial and (added_model_list or added_field_list or auto):
67
            self.error("You cannot use --initial and other options together\n" + self.usage_str)
68
        
69
        if auto and (added_model_list or added_field_list or initial):
70
            self.error("You cannot use --auto and other options together\n" + self.usage_str)
71
        
72
        if not app:
73
            self.error("You must provide an app to create a migration for.\n" + self.usage_str)
74
        
75
        # Get the Migrations for this app (creating the migrations dir if needed)
76
        migrations = Migrations(app, force_creation=True, verbose_creation=int(verbosity) > 0)
77
        
78
        # What actions do we need to do?
79
        if auto:
80
            # Get the old migration
81
            try:
82
                last_migration = migrations[-2 if update else -1]
83
            except IndexError:
84
                self.error("You cannot use --auto on an app with no migrations. Try --initial.")
85
            # Make sure it has stored models
86
            if migrations.app_label() not in getattr(last_migration.migration_class(), "complete_apps", []):
87
                self.error("You cannot use automatic detection, since the previous migration does not have this whole app frozen.\nEither make migrations using '--freeze %s' or set 'SOUTH_AUTO_FREEZE_APP = True' in your settings.py." % migrations.app_label())
88
            # Alright, construct two model dicts to run the differ on.
89
            old_defs = dict(
90
                (k, v) for k, v in last_migration.migration_class().models.items()
91
                if k.split(".")[0] == migrations.app_label()
92
            )
93
            new_defs = dict(
94
                (k, v) for k, v in freezer.freeze_apps([migrations.app_label()]).items()
95
                if k.split(".")[0] == migrations.app_label()
96
            )
97
            change_source = changes.AutoChanges(
98
                migrations = migrations,
99
                old_defs = old_defs,
100
                old_orm = last_migration.orm(),
101
                new_defs = new_defs,
102
            )
103
        
104
        elif initial:
105
            # Do an initial migration
106
            change_source = changes.InitialChanges(migrations)
107
        
108
        else:
109
            # Read the commands manually off of the arguments
110
            if (added_model_list or added_field_list or added_index_list):
111
                change_source = changes.ManualChanges(
112
                    migrations,
113
                    added_model_list,
114
                    added_field_list,
115
                    added_index_list,
116
                )
117
            elif empty:
118
                change_source = None
119
            else:
120
                print >>sys.stderr, "You have not passed any of --initial, --auto, --empty, --add-model, --add-field or --add-index."
121
                sys.exit(1)
122

    
123
        # Validate this so we can access the last migration without worrying
124
        if update and not migrations:
125
            self.error("You cannot use --update on an app with no migrations.")
126
        
127
        # if not name, there's an error
128
        if not name:
129
            if change_source:
130
                name = change_source.suggest_name()
131
            if update:
132
                name = re.sub(r'^\d{4}_', '', migrations[-1].name())
133
            if not name:
134
                self.error("You must provide a name for this migration\n" + self.usage_str)
135
        
136
        # Get the actions, and then insert them into the actions lists
137
        forwards_actions = []
138
        backwards_actions = []
139
        if change_source:
140
            for action_name, params in change_source.get_changes():
141
                # Run the correct Action class
142
                try:
143
                    action_class = getattr(actions, action_name)
144
                except AttributeError:
145
                    raise ValueError("Invalid action name from source: %s" % action_name)
146
                else:
147
                    action = action_class(**params)
148
                    action.add_forwards(forwards_actions)
149
                    action.add_backwards(backwards_actions)
150
                    print >>sys.stderr, action.console_line()
151
        
152
        # Nowt happen? That's not good for --auto.
153
        if auto and not forwards_actions:
154
            self.error("Nothing seems to have changed.")
155
        
156
        # Work out which apps to freeze
157
        apps_to_freeze = self.calc_frozen_apps(migrations, freeze_list)
158
        
159
        # So, what's in this file, then?
160
        file_contents = MIGRATION_TEMPLATE % {
161
            "forwards": "\n".join(forwards_actions or ["        pass"]),
162
            "backwards": "\n".join(backwards_actions or ["        pass"]),
163
            "frozen_models":  freezer.freeze_apps_to_string(apps_to_freeze),
164
            "complete_apps": apps_to_freeze and "complete_apps = [%s]" % (", ".join(map(repr, apps_to_freeze))) or ""
165
        }
166

    
167
        # Deal with update mode as late as possible, avoid a rollback as long
168
        # as something else can go wrong.
169
        if update:
170
            last_migration = migrations[-1]
171
            if MigrationHistory.objects.filter(applied__isnull=False, app_name=app, migration=last_migration.name()):
172
                print >>sys.stderr, "Migration to be updated, %s, is already applied, rolling it back now..." % last_migration.name()
173
                migrate_app(migrations, 'current-1', verbosity=verbosity)
174
            for ext in ('py', 'pyc'):
175
                old_filename = "%s.%s" % (os.path.join(migrations.migrations_dir(), last_migration.filename), ext)
176
                if os.path.isfile(old_filename):
177
                    os.unlink(old_filename)
178
            migrations.remove(last_migration)
179

    
180
        # See what filename is next in line. We assume they use numbers.
181
        new_filename = migrations.next_filename(name)
182

    
183
        # - is a special name which means 'print to stdout'
184
        if name == "-":
185
            print file_contents
186
        # Write the migration file if the name isn't -
187
        else:
188
            fp = open(os.path.join(migrations.migrations_dir(), new_filename), "w")
189
            fp.write(file_contents)
190
            fp.close()
191
            verb = 'Updated' if update else 'Created'
192
            if empty:
193
                print >>sys.stderr, "%s %s. You must now edit this migration and add the code for each direction." % (verb, new_filename)
194
            else:
195
                print >>sys.stderr, "%s %s. You can now apply this migration with: ./manage.py migrate %s" % (verb, new_filename, app)
196

    
197

    
198
MIGRATION_TEMPLATE = """# -*- coding: utf-8 -*-
199
import datetime
200
from south.db import db
201
from south.v2 import SchemaMigration
202
from django.db import models
203

204

205
class Migration(SchemaMigration):
206

207
    def forwards(self, orm):
208
%(forwards)s
209

210
    def backwards(self, orm):
211
%(backwards)s
212

213
    models = %(frozen_models)s
214

215
    %(complete_apps)s"""