DeadWisdom / Migratory

Migratory (pronounced as a pythoner: 'MY GRAY ER EE') is a migration contrib app for Django. It both generates custom migration files for you and is a system for executing them. What's more, it works great in a vcs environment.

Clone this repository (size: 102.2 KB): HTTPS / SSH
$ hg clone http://bitbucket.org/DeadWisdom/migratory/

Changed (Δ5.9 KB):

raw changeset »

migratory/management/commands/migrate.py (164 lines added, 111 lines removed)

migratory/manager/base.py (111 lines added, 167 lines removed)

migratory/migrate.py (15 lines added, 57 lines removed)

migratory/models.py (2 lines added, 2 lines removed)

migratory/snapshot.py (4 lines added, 6 lines removed)

test_project/app/models.py (14 lines added, 7 lines removed)

test_project/test (3 lines added, 3 lines removed)

Up to file-list migratory/management/commands/migrate.py:

@@ -5,11 +5,12 @@ Automation of the creation of migration
5
5
import os, os.path
6
6
from datetime import date
7
7
8
from django.db.models.loading import get_models as get_new_models, get_app as get_new_app
8
from django.utils import simplejson
9
from django.db.models.loading import get_models, get_app
9
10
from django.core.management.base import BaseCommand, CommandError
10
11
from django.core.management.color import no_style
11
12
12
from django.contrib.migratory import manager, snapshot
13
from django.contrib.migratory.app import AppManager
13
14
from django.contrib.migratory.matchmaker import matchmaker, compare_strings
14
15
15
16
class Command(BaseCommand):
@@ -17,30 +18,28 @@ class Command(BaseCommand):
17
18
    args = '<app-name migration-name>'
18
19
    
19
20
    def handle(self, *args, **options):
21
        self.verbosity = options.get('verbosity', 1)
22
        
20
23
        try:
21
24
            app_name, mig_name = args
22
25
        except:
23
26
            raise CommandError("Specify the name of an app, and a slug-formatted name for the migration.")
24
27
        
25
        self.manager = manager.DatabaseManager(style=self.style, verbosity=options['verbosity'])
28
        self.app = get_app(app_name)
26
29
        
27
        try:
28
            if app_name not in self.manager:
29
                raise CommandError("Unable to find previous snapshot of app %r." % app_name)
30
        except:
31
            # No snapshots yet, so we kick out.
32
            return
30
        last = AppManager('last')
31
        old_models = last.get_models(app_name)
33
32
        
34
        app = get_new_app(app_name)
35
        new_models = get_new_models(app)
36
        old_models = self.manager.get_models(app_name)
33
        current = AppManager('current')
34
        new_models = current.get_models(app_name)
37
35
        
38
36
        # The order of old_models then new_models is important here, as 
39
37
        # compareModels weights so that all the models in old are also in new.  
40
38
        # If new has a bunch of new ones, it still makes a perfect match.
41
39
        matches, drop, add = matchmaker(old_models, new_models, compareModels, .4)
42
40
        
43
        changeset = Changeset(app)
41
        ### Build The Changeset ###
42
        changeset = Changeset(self.app)
44
43
        
45
44
        for model in drop:
46
45
            changeset.drop_model(model)
@@ -70,8 +69,8 @@ class Command(BaseCommand):
70
69
                changeset.add_field(field)
71
70
            
72
71
            for old, new in fields:
73
                old_snap = snapshot.field_snap(old, old_model)
74
                new_snap = snapshot.field_snap(new, new_model)
72
                old_snap = AppManager.field_snap(old_model, old)
73
                new_snap = AppManager.field_snap(new_model, new)
75
74
                
76
75
                # Strip the 'migration-' prefix for the app on the 
77
76
                # related-field.
@@ -95,14 +94,114 @@ class Command(BaseCommand):
95
94
            filename = "%d-%d-%d-%s.py" % (
96
95
                today.year, today.month, today.day, mig_name
97
96
            )
97
        
98
        if (self.verbosity > 0):
99
            print changeset.report()
100
        
101
        self.save(filename, changeset)
102
        
103
        if (self.verbosity > 0 and changeset):
104
            print "\nPlease look over the changes. When you feel confident they are correct, issue a 'syncdb':\n> python manage.py syncdb"
105
    
106
    def save(self, filename, changeset, comment = ''):
107
        """
108
        Saves the changeset to a new migration file, creates the snapshot 
109
        file, and appends the migration to the manifest (__manifest__.py).
110
        """
111
        path = os.path.join(self.get_migrations_dir(), filename)
112
        if os.path.exists(path):
113
            raise CommandError("Migration with that name (%r) already exists." % path)
114
        
115
        if (self.verbosity > 0):
116
            print "Writing migration to: %s" % path
98
117
            
99
        changeset.save(filename)  
118
        o = open(path, 'w')
119
        if path.endswith('.py'):
120
            o.write("\"\"\"\nDjango Migration %s\n%s\n\"\"\"\n\n" % \
121
                (filename, comment))
122
            o.write(changeset.up())
123
            o.write('\n')
124
            o.write(changeset.down())
125
        o.close()
126
        
127
        self.update_manifest(filename, comment)
128
        
129
        if path.endswith('.py'):
130
            self.save_snapshot(filename)
100
131
    
101
        print changeset.report()
132
    def save_snapshot(self, filename):
133
        manager = AppManager('current')
134
        json = simplejson.dumps(manager.get_snapshot())
102
135
        
103
        if (changeset):
104
            print "\nPlease look over the changes. When you feel confident they are correct, issue a 'syncdb':\n> python manage.py syncdb"
136
        # Convert to '2009-6-16-added-thing.snap' for instance:
137
        filename = filename.rsplit('.', 1)[0] + '.snap' 
138
        path = os.path.join(self.get_migrations_dir(), filename)
139
        
140
        o = open(path, 'w')
141
        o.write(json)
142
        o.close()
143
    
144
    def get_migrations_dir(self):
145
        """
146
        Gets or creates the migration directory on the filesystem.
147
        """
148
        path = os.path.join(os.path.dirname(self.app.__file__), 'migrations')
149
        if not os.path.isdir(path):
150
            os.mkdir(path)
151
        return path
152
    
153
    def update_manifest(self, filename, comment=''):
154
        path, src = self.get_manifest()
155
        manifests = eval(src)
156
        if (filename in manifests):
157
            raise CommandError("A migration with that name (%r) is already present in the manifest." % filename)
158
        
159
        lines = src.split('\n')
160
        
161
        spaces = '    '
162
        if len(lines) > 2:
163
            # Get the padding of the previous line.
164
            prev = lines[-2]
165
            spaces = prev[: len( prev ) - len( prev.lstrip() ) ]    
166
        
167
        line = spaces + repr(filename) + ','
168
        
169
        if (comment):
170
            count = len(line)
171
            if '\t' in spaces:
172
                for c in spaces:
173
                    if c == '\t': count += 3
174
            line += (50 - count) * ' ' + '# ' + comment
175
        
176
        lines.insert(-1, line)
177
        
178
        if (self.verbosity > 1):
179
            print "Adding %r to manifest..." % filename
180
            
181
        o = open(path, 'w')
182
        o.write( "\n".join(lines) )
183
        o.close()
184
    
185
    def get_manifest(self):
186
        """
187
        get_manifest() -> (path, src)
188
        
189
        Returns the manifest (__manifest__.py) path and source.
190
        """
191
        migrations_dir = self.get_migrations_dir()
192
        path = os.path.join(migrations_dir, '__manifest__.py')
105
193
194
        if not os.path.exists(path):
195
            src = '[\n]'
196
            o = open(path, 'w')
197
            o.write(src)
198
            o.close()
199
        else:
200
            o = open(path, 'r')
201
            src = o.read()
202
            o.close()
203
            
204
        return path, src
106
205
107
206
### Helper Functions / Classes ###
108
207
def compareModels(a, b):
@@ -150,13 +249,15 @@ class Changeset(object):
150
249
    def drop_model(self, model):
151
250
        self.changes.append((
152
251
            "Drop the model %r." % model._meta.object_name,
153
            "database.drop_model(%r)" % model._meta.object_name
252
            "database.drop_model(%r)" % model._meta.object_name,
253
            "database.add_model(%r)" % model._meta.object_name
154
254
        ))
155
255
    
156
256
    def add_model(self, model):
157
257
        self.changes.append((
158
258
            "Add the new model %r." % model._meta.object_name,
159
            "database.add_model(%r)" % model._meta.object_name
259
            "database.add_model(%r)" % model._meta.object_name,
260
            "database.drop_model(%r)" % model._meta.object_name
160
261
        ))
161
262
    
162
263
    def change_model(self, model):
@@ -177,14 +278,17 @@ class Changeset(object):
177
278
        if (model_changes):
178
279
            self.changes.append((
179
280
                "Changes to the model %r:" % self.model._meta.object_name,
180
                model_changes
281
                model_changes,
282
                None
181
283
            ))
182
284
    
183
285
    def rename_model(self, new):
184
286
        self.changes.append((
185
287
             "Rename to %r." % new._meta.object_name,
186
288
             "database.rename_model(%r, %r)" % (
187
                self.model._meta.object_name, new._meta.object_name)
289
                self.model._meta.object_name, new._meta.object_name),
290
             "database.rename_model(%r, %r)" % (
291
                new._meta.object_name, self.model._meta.object_name),
188
292
        ))
189
293
        self.model._meta.object_name = new._meta.object_name
190
294
    
@@ -192,40 +296,48 @@ class Changeset(object):
192
296
        self.changes.append((
193
297
            "Drop the field %r." % field.name,
194
298
            "database.drop_field(%r, %r)" % (
195
                self.model._meta.object_name, field.name)
299
                self.model._meta.object_name, field.name),
300
            "database.add_field(%r, %r)" % (
301
                self.model._meta.object_name, field.name),
196
302
        ))
197
303
        
198
304
    def add_field(self, field):
199
305
        self.changes.append((
200
306
            "Add the new field %r." % field.name,
201
307
            "database.add_field(%r, %r)" % (
202
                self.model._meta.object_name, field.name)
308
                self.model._meta.object_name, field.name),
309
            "database.drop_field(%r, %r)" % (
310
                self.model._meta.object_name, field.name),
203
311
        ))
204
        
312
    
205
313
    def change_field(self, old, new=None):
206
314
        if (new):
207
315
            self.changes.append((
208
316
                "Change to %r, renaming it %r." % (old.name, new.name),
209
317
                "database.change_field(%r, %r, %r)" % (
210
                    self.model._meta.object_name, old.name, new.name)
318
                    self.model._meta.object_name, old.name, new.name),
319
                "database.change_field(%r, %r, %r)" % (
320
                    self.model._meta.object_name, new.name, old.name)
211
321
            ))
212
322
        else:
213
323
            self.changes.append((
214
324
                "Change the field %r." % old.name,
215
325
                "database.change_field(%r, %r)" % (
326
                    self.model._meta.object_name, old.name),
327
                "database.change_field(%r, %r)" % (
216
328
                    self.model._meta.object_name, old.name)
217
329
            ))
218
        
330
    
219
331
    def report(self):
220
332
        """
221
333
        Returns a natural language report of the changes as a big string.
222
334
        """
223
335
        gather = []
224
336
        
225
        for report, sub in self.changes:
337
        for report, up, _ in self.changes:
226
338
            gather.append(report)
227
            if not isinstance(sub, basestring):
228
                for report, _ in sub:
339
            if not isinstance(up, basestring):
340
                for report, _, _ in up:
229
341
                    gather.append('    ' + report)
230
342
        
231
343
        if not self.changes:
@@ -240,91 +352,32 @@ class Changeset(object):
240
352
        """
241
353
        gather = []
242
354
        
243
        for _, code in self.changes:
244
            if isinstance(code, basestring):
245
                gather.append(code)
355
        for _, up, down in self.changes:
356
            if isinstance(up, basestring):
357
                gather.append(up)
246
358
            else:
247
                for _, sub in code:
248
                    gather.append(sub)
359
                for _, up, down in up:
360
                    gather.append(up)
249
361
        
250
362
        if not self.changes:
251
363
            gather.append('pass')
252
364
        
253
        return "def up(database):\n    %s\n" % '\n    '.join(gather) 
365
        return "def up(database):\n    %s\n" % '\n    '.join(gather)
366
    
367
    def down(self):
368
        """
369
        Produces the python source-code for the down() function.
370
        """
371
        gather = []
254
372
        
255
    def get_migrations_dir(self):
256
        """
257
        Gets or creates the migration directory on the filesystem.
258
        """
259
        path = os.path.join(os.path.dirname(self.app.__file__), 'migrations')
260
        if not os.path.isdir(path):
261
            os.mkdir(path)
262
        return path
373
        for _, up, down in self.changes:
374
            if isinstance(up, basestring):
375
                gather.append(down)
376
            else:
377
                for _, up, down in up:
378
                    gather.append(down)
263
379
        
264
    def get_manifest(self):
265
        """
266
        get_manifest() -> (path, src)
380
        if not self.changes:
381
            gather.append('pass')
267
382
        
268
        Returns the manifest (__manifest__.py) path and source.
269
        """
270
        migrations_dir = self.get_migrations_dir()
271
        path = os.path.join(migrations_dir, '__manifest__.py')
272
273
        if not os.path.exists(path):
274
            src = '[\n]'
275
            o = open(path, 'w')
276
            o.write(src)
277
            o.close()
278
        else:
279
            o = open(path, 'r')
280
            src = o.read()
281
            o.close()
282
            
283
        return path, src
284
        
285
    def save(self, filename, comment = ''):
286
        """
287
        Saves the changeset to a new migration file, and appends to the 
288
        manifest (__manifest__.py).
289
        """
290
        path, src = self.get_manifest()
291
        manifests = eval(src)
292
        if (filename in manifests):
293
            raise CommandError("A migration with that name (%r) is already present in the manifest." % filename)
294
        
295
        lines = src.split('\n')
296
        
297
        spaces = '    '
298
        if len(lines) > 2:
299
            # Get the padding of the previous line.
300
            prev = lines[-2]
301
            spaces = prev[: len( prev ) - len( prev.lstrip() ) ]    
302
        
303
        line = spaces + repr(filename) + ','
304
        
305
        if (comment):
306
            count = len(line)
307
            if '\t' in spaces:
308
                for c in spaces:
309
                    if c == '\t': count += 3
310
            line += (50 - count) * ' ' + '# ' + comment
311
        
312
        lines.insert(-1, line)
313
        
314
        print "Adding %r to manifest..." % filename
315
        o = open(path, 'w')
316
        o.write( "\n".join(lines) )
317
        o.close()
318
        
319
        path = os.path.join(os.path.dirname(path), filename)
320
        if os.path.exists(path):
321
            print "Migration already exists: %s" % path
322
            return
323
            
324
        print "Writing migration to: %s" % path
325
        o = open(path, 'w')
326
        if path.endswith('.py'):
327
            o.write("\"\"\"\nDjango Migration %s\n%s\n\"\"\"\n\n" % \
328
                (filename, comment))
329
            o.write(self.up())
330
        o.close()
383
        return "def down(database):\n    %s\n" % '\n    '.join(gather)

Up to file-list migratory/manager/base.py:

@@ -6,7 +6,9 @@ specified in settings.DATABASE_ENGINE.
6
6
DatabaseManagerBase.
7
7
8
8
"""
9
import os
9
10
11
from django.core.management.base import CommandError
10
12
from django.core.management.color import no_style
11
13
from django.utils import simplejson
12
14
from django.db import models
@@ -14,6 +16,7 @@ from django.db import connection
14
16
from django.core.exceptions import ImproperlyConfigured
15
17
16
18
from django.contrib.migratory.models import Migration, Snapshot
19
from django.contrib.migratory.app import AppManager
17
20
18
21
# Are we allowed to do this as a module level?  I am for now.
19
22
qn = connection.ops.quote_name  
@@ -34,6 +37,7 @@ class DatabaseManagerBase(object):
34
37
                       verbosity=0, 
35
38
                       mode='execute', 
36
39
                       style=None,
40
                       new_snap=[],
37
41
                       created_models=[]):
38
42
        """
39
43
        app_name: 
@@ -64,151 +68,77 @@ class DatabaseManagerBase(object):
64
68
        self.style = style or no_style()
65
69
        self.created_models = created_models
66
70
        
71
        self.now = None
72
        self.ideal = None
73
        
67
74
        self._apps = {}
68
75
        self._required = set()
69
76
        self._rel_fields = []
70
                
71
    def parse_sig(self, sig):
72
        """Turns 'app.Model' to ('app', 'model')."""
73
        if '.' not in sig:
74
            if self.app_name:
75
                return self.app_name, sig
76
            else:
77
                raise KeyError("Must specify the app_name, like: app.model, unless you set it on the DatabaseManager constructor.")
77
    
78
    def run_migration(self, path):
79
        if not os.path.exists(path):
80
            raise CommandError(
81
                "Unable to find migration at the given path: %r" % path)
82
    
83
        if (self.verbosity >= 1 and self.mode != 'sql'): 
84
            print "Executing migration %r..." % name
85
        
86
        ### Build Snapshots ###
87
        snap_path = path.rsplit('.', 1)[0] + '.snap'
88
        
89
        if os.path.exists(snap_path):
90
            o = open(snap_path)
91
            src = o.read()
92
            o.close()
93
        
94
            self.ideal = AppManager(simplejson.loads(src), prefix='ideal-')
78
95
        else:
79
            return sig.split('.')
96
            if (self.verbosity >= 1 and self.mode != 'sql'): 
97
                print "No snapshot found for migration %r, defaulting to current models..." % name
98
            self.ideal = AppManager('current', prefix='ideal-')
99
        
100
        self.now = AppManager('last', prefix='now-')
101
        
102
        ### Get Migration Up Function ###
103
        o = open(path)
104
        src = o.read()
105
        o.close()
106
        
107
        namespace = {}
108
        exec src in namespace
109
        
110
        up = namespace.get('up', None)
111
        if (not up):
112
            raise CommandError("No up() function found in migration: %s\nWe really need that." % path)
113
        
114
        ### Run Migration ###
115
        try:
116
            up(self)
117
        except Exception, e:
118
            import traceback, sys
80
119
    
81
    def __getitem__(self, sig):
82
        """Return the model matching the sig from the latest snapshot."""
83
        return self.get_model(*self.parse_sig(sig))
120
            lines = ["\nError in migration:\n"]
121
            lines.extend(traceback.format_tb(sys.exc_traceback)[1:])
122
            lines.append("%s: %s" % (e.__class__.__name__, str(e)))
84
123
85
    def get_model(self, app_name, model_name):
86
        """Return the request model from the latest snapshot."""
87
        if app_name not in self._apps:
88
            self.build_app(app_name)
89
            self.build_rel_fields()
90
        try:
91
            return self._apps[app_name][model_name]
92
        except:
93
            models.get_model(app_name, model_name)
124
            lines.append("\n\nLocals:\n")
125
            tb = sys.exc_info()[2]
126
            while tb.tb_next:
127
                tb = tb.tb_next
128
            frame = tb.tb_frame
129
            for key, value in frame.f_locals.items():
130
                try:
131
                    value = "%r" % value
132
                except:
133
                    value = '<not representable>'
134
                lines.append("\t%s: %s\n" % (key,value))
135
136
            raise CommandError("".join(lines))
94
137
    
95
    def get_models(self, app_name=None):
96
        """Return an iterator of models in the latest snapshot."""
97
        if app_name not in self._apps:
98
            self.build_app(app_name)
99
            self.build_rel_fields()
100
        if (app_name):
101
            return iter( self._apps[app_name].values() )
102
        else:
103
            for app, models in self._apps.items():
104
                return iter( models.values() )
105
    
106
    def __contains__(self, app_name):
107
        """Does the latest snapshot include this app?"""
108
        return Snapshot.objects.filter(app=app_name).count() > 0
109
    
110
    def build_app(self, app_name):
111
        """
112
        Build a dict of models for the given application name from the latest 
113
        snapshot
114
        """
115
        if (app_name in self._apps):
116
            return
117
        try:
118
            snap = Snapshot.objects.get(app=app_name)
119
        except Snapshot.DoesNotExist:
120
            try:
121
                models.get_app(app)
122
                self._dicts[app_name] = {} # New app, allow it.
123
                return
124
            except ImproperlyConfigured:
125
                raise RuntimeError("Cannot find snapshot of an app named %r." % app_name)
126
        data = simplejson.loads(snap.json)
127
        self._apps[app_name] = {}
128
        for dct in data:
129
            self.build_model(app_name, dct)
130
    
131
    def build_model(self, app_name, dct):
132
        """
133
        Builds a model with the given app_name and snapshot-dict.
134
        """
135
        attrs = {}
136
        app_name = app_name.encode()
138
    def set_app_managers(self, now, ideal):
139
        self.now = now
140
        self.ideal = ideal
137
141
        
138
        attrs.update( self.build_fields(app_name, dct['name'], dct['meta'], dct['fields']) )
139
        attrs['Meta'] = type('Meta', (), dct['meta'])
140
        attrs['Meta'].app_label = 'migration-' + app_name
141
        attrs['__module__'] = dct['module'].encode()
142
        
143
        model = type(str(dct['name'].encode()), (models.Model,), attrs)
144
        self._apps[app_name][model._meta.object_name] = model
145
    
146
    def build_fields(self, app_name, model_name, model_meta, dicts):
147
        """
148
        Builds a field for each dict given.
149
        
150
        This will also populate self._rel_fields with any related fields, and
151
        self._required with required apps.  Only after all the required apps 
152
        and models are built will the relative fields be added with 
153
        self.build_rel_fields() below.
154
        """
155
        gather = {}
156
        
157
        for dct in dicts:
158
            name = dct['name'].encode()
159
            cls = getattr(models, dct['__class__'])
160
            attrs = {}
161
            for k, v in dct.items():
162
                if not k.startswith('_'): attrs[str(k)] = v
163
            
164
            if ('__rel__' in dct):
165
                to_app_name, to_model_name = dct['__rel__']
166
                if (to_app_name not in self._apps):
167
                    self._required.add( to_app_name )
168
                if model_meta.get('order_with_respect_to') == name:
169
                    del model_meta['order_with_respect_to']
170
                    attrs['__order_with_respect_to'] = True
171
                self._rel_fields.append( (app_name,
172
                                          model_name, 
173
                                          cls, 
174
                                          name, 
175
                                          to_app_name, 
176
                                          to_model_name, 
177
                                          attrs) )
178
            else:
179
                gather[name] = cls(**attrs)
180
        
181
        return gather
182
    
183
    def build_rel_fields(self):
184
        """
185
        Only once all the required apps and models are built, do we
186
        actually add the relative fields.
187
        """
188
        while self._required:
189
            self.build_app( self._required.pop() )
190
        
191
        for (app_name,
192
             model_name,
193
             cls,
194
             name,
195
             to_app_name,
196
             to_model_name,
197
             attrs) in self._rel_fields:
198
            
199
            order = attrs.pop('__order_with_respect_to', False)
200
            
201
            model = self.get_model(app_name, model_name)
202
            to_model = self.get_model(to_app_name, to_model_name)
203
            field = cls(to_model, **attrs)
204
            model.add_to_class(name, field)
205
            
206
            if order:
207
                model._meta.order_with_respect_to = field.name
208
                model._meta._prepare(model)
209
        
210
        self._rel_fields = []
211
    
212
142
    def _field_sql(self, model, field):
213
143
        """
214
144
        Generate the sql needed to create a field.
@@ -448,6 +378,19 @@ class DatabaseManagerBase(object):
448
378
        
449
379
        return " ".join(sql)
450
380
    
381
    def __getitem__(self, sig):
382
        return self.new.get_model(*self.parse_sig(sig))
383
    
384
    def parse_sig(self, sig):
385
        """Turns 'app.Model' to ('app', 'model')."""
386
        if '.' not in sig:
387
            if self.app_name:
388
                return self.app_name, sig
389
            else:
390
                raise KeyError("Must specify the app_name, like: app.model.")
391
        else:
392
            return sig.split('.')
393
    
451
394
    def execute(self, description, *sql_statements):
452
395
        """
453
396
        Executes the sql statements and prints the description to the user, 
@@ -496,21 +439,15 @@ class DatabaseManagerBase(object):
496
439
    def add_field(self, model_sig, name):
497
440
        """
498
441
        Add the new field named 'name' to the model.
499
        
500
        The field should be defined in the current models.py
501
        
502
        TODO: This won't work, because we might be in a long-ago migration 
503
        refering to models.py that doesn't have the field, or model for that 
504
        matter. Big problem.
505
442
        """
506
443
        style = self.style
507
444
        
508
445
        app_name, model_name = self.parse_sig(model_sig)
509
        model = self.get_model(app_name, model_name)
510
        real_model = models.get_model(app_name, model_name)
446
        
447
        model = self.now.get_model(app_name, model_name)
511
448
        opts = model._meta
512
449
        
513
        field = real_model._meta.get_field(name)
450
        field = self.ideal.get_field(app_name, model_name, name)
514
451
        
515
452
        if field.db_type() is None:
516
453
            self.execute("Adding many-to-many table for field %s.%s" % (model_sig, field.name), self._m2m_sql(model, field))
@@ -559,15 +496,11 @@ class DatabaseManagerBase(object):
559
496
            new_field_name = old_field_name
560
497
561
498
        app_name, model_name = self.parse_sig(model_sig)
562
        model = self.get_model(app_name, model_name)
499
        model = self.now.get_model(app_name, model_name)
563
500
        opts = model._meta
564
501
        
565
502
        old = opts.get_field(old_field_name, True)
566
567
        real_model = models.get_model(app_name, model_name)
568
        real_opts = model._meta
569
        
570
        field = opts.get_field(new_field_name)
503
        field = self.ideal.get_field(app_name, model_name, new_field_name)
571
504
        
572
505
        if (field.db_type() is None):       # Many to Many
573
506
            opts.local_many_to_many = [f for f in opts.local_many_to_many if f != old]
@@ -645,7 +578,7 @@ class DatabaseManagerBase(object):
645
578
        
646
579
        Unless it shows up in self.created_models (see __init__).
647
580
        """
648
        model = models.get_model(*self.parse_sig(model_sig))
581
        model = self.ideal.get_model(*self.parse_sig(model_sig))
649
582
        
650
583
        if (model in self.created_models):
651
584
            return
@@ -655,42 +588,53 @@ class DatabaseManagerBase(object):
655
588
        for field in model._meta.many_to_many:
656
589
            self.execute("Adding many-to-many table for field %s.%s" % (model_sig, field.name), self._m2m_sql(model, field))
657
590
    
658
    def rename_model(self, model_sig, new_sig):
591
    def rename_model(self, old_sig, new_sig):
659
592
        """
660
593
        Rename a model.
661
594
        """
662
        old_model = self[model_sig]
663
595
        
664
        app_name, model_name = self.parse_sig(new_sig)
665
        new_model = models.get_model(app_name, model_name)
666
        self._apps[app_name][model_name] = new_model
596
        old_app_name, old_model_name = self.parse_sig(old_sig)
597
        model = self.now.get_model(old_app_name, old_model_name)
598
        
599
        new_app_name, new_model_name = self.parse_sig(new_sig)
600
        ideal = self.ideal.get_model(new_app_name, new_model_name)
601
        
602
        self.now.apps.setdefault(new_app_name, {})[new_model_name] = model
603
        del self.now.apps[old_app_name][old_model_name]
667
604
        
668
605
        style = self.style
669
606
        
670
607
        # Have to drop the one created by the sync db.  
671
608
        # TODO: Add ManyToMany support.
672
        # TODO: Check through self.created_models before doing this.
673
        sql = [
674
            style.SQL_KEYWORD('DROP TABLE'),
675
            style.SQL_TABLE(qn(new_model._meta.db_table))
676
        ]
609
        if (ideal in self.created_models):
610
            sql = [
611
                style.SQL_KEYWORD('DROP TABLE'),
612
                style.SQL_TABLE(qn(ideal._meta.db_table))
613
            ]
677
614
        
678
        self.execute('Dropping eroneously created table for %s.' % new_sig, sql)
615
            self.execute('Dropping eroneously created table for %s.' % new_sig, sql)
679
616
        
680
617
        sql = [
681
618
            style.SQL_KEYWORD('ALTER TABLE'),
682
            style.SQL_TABLE(qn(old_model._meta.db_table)),
619
            style.SQL_TABLE(qn(model._meta.db_table)),
683
620
            style.SQL_KEYWORD('RENAME TO'),
684
            style.SQL_TABLE(qn(new_model._meta.db_table))
621
            style.SQL_TABLE(qn(ideal._meta.db_table))
685
622
        ]
686
623
        
624
        # Execute the renaming.
625
        model._meta.object_name = ideal._meta.object_name
626
        model._meta.module_name = ideal._meta.module_name
627
        model._meta.verbose_name = ideal._meta.verbose_name
628
        model._meta.verbose_name_plural = ideal._meta.verbose_name_plural
629
        model._meta.db_table = ideal._meta.db_table
630
        
687
631
        self.execute("Renaming model %s to %s." % (model_sig, new_sig), sql)
688
632
    
689
633
    def drop_model(self, model_sig):
690
634
        """
691
635
        Drops a model.
692
636
        """
693
        model = self[model_sig]
637
        model = self.now.get_model(*self.parse_sig(model_sig))
694
638
        style = self.style
695
639
        
696
640
        drop_sql = [

Up to file-list migratory/migrate.py:

1
1
"""
2
2
Performs any new migrations.
3
3
4
This is not to be confused with the ./manage.py command which creates 
5
migration files rather than actually performing them.
4
This is not to be confused with the ./manage.py command "migrate" which 
5
creates migration files rather than actually performing them.
6
6
"""
7
7
import os
8
8
@@ -29,60 +29,18 @@ def do_migrations(sender=None,
29
29
    app_name = app.__name__.split('.')[-2]
30
30
    
31
31
    migrations = get_migrations(app, verbosity)
32
    if (verbosity > 1 and mode != 'sql'): print "%s found." % len(migrations.keys())
33
    
34
    if migrations:
35
        database_manager = DatabaseManager(app_name, 
36
                                           verbosity=verbosity,     
37
                                           created_models=created_models or [],
38
                                           style=style,
39
                                           mode=mode)
32
    if not migrations:
33
        return
40
34
        
41
        for name, path in migrations.items():
42
            if (verbosity >= 1 and mode != 'sql'): print "Executing migration %r..." % name
43
            if run_migration(path, database_manager, verbosity):
44
                Migration.objects.get_or_create(name=name)
45
46
def run_migration(path, database_manager, verbosity, sql_only=False):
47
    if not os.path.exists(path):
48
        raise RuntimeError(
49
            "Unable to find migration at the given path: %r" % path)
50
    
51
    o = open(path)
52
    src = o.read()
53
    o.close()
54
55
    namespace = {}
56
    exec src in namespace
57
    
58
    up = namespace.get('up', None)
59
    
60
    if (up):
61
        try:
62
            up(database_manager)
63
            return True
64
        except Exception, e:
65
            import traceback, sys
66
            
67
            lines = ["\nError in migration:\n"]
68
            lines.extend(traceback.format_tb(sys.exc_traceback)[1:])
69
            lines.append("%s: %s" % (e.__class__.__name__, str(e)))
70
71
            lines.append("\n\nLocals:\n")
72
            tb = sys.exc_info()[2]
73
            while tb.tb_next:
74
                tb = tb.tb_next
75
            frame = tb.tb_frame
76
            for key, value in frame.f_locals.items():
77
                try:
78
                    value = "%r" % value
79
                except:
80
                    value = '<not representable>'
81
                lines.append("\t%s: %s\n" % (key,value))
82
83
            raise RuntimeError("".join(lines))
84
    else:
85
        print "Nevermind, no up() defined."
35
    manager = DatabaseManager(app_name, 
36
                              verbosity=verbosity,     
37
                              created_models=created_models or [],
38
                              style=style,
39
                              mode=mode)
40
                                       
41
    for name, path in migrations:
42
        manager.run_migration(path)
43
        Migration.objects.get_or_create(name=name)
86
44
87
45
def get_migrations(app, verbosity):
88
46
    from django.contrib.migratory.models import Snapshot, Migration
@@ -102,13 +60,13 @@ def get_migrations(app, verbosity):
102
60
            Migration.objects.get_or_create(name=name)
103
61
        return {}
104
62
    
105
    gather = {}
63
    gather = []
106
64
    for migration in manifest:
107
65
        path = os.path.join(folder, migration)
108
66
        if Migration.objects.filter(name=migration).count() > 0:
109
67
            continue
110
68
        if not os.path.exists(path):
111
69
            raise RuntimeError( "Cannot find a migration present in the __manifest__.py file.")
112
        gather[migration] = path
70
        gather.append((migration, path))
113
71
    
114
72
    return gather

Up to file-list migratory/models.py:

@@ -20,7 +20,7 @@ class Migration(models.Model):
20
20
21
21
from django.db.models.signals import post_syncdb
22
22
from django.contrib.migratory.migrate import do_migrations
23
from django.contrib.migratory.snapshot import create_snapshots
23
from django.contrib.migratory.app import create_snapshot
24
24
25
25
post_syncdb.connect(do_migrations)
26
post_syncdb.connect(create_snapshots)
26
post_syncdb.connect(create_snapshot)

Up to file-list migratory/snapshot.py:

@@ -7,7 +7,6 @@ from django.utils import simplejson
7
7
from django.db import models
8
8
from django.db.models.fields import NOT_PROVIDED
9
9
10
11
10
def create_snapshots(sender=None, 
12
11
                     app=None, 
13
12
                     created_models=None,
@@ -20,16 +19,15 @@ def create_snapshots(sender=None,
20
19
    """
21
20
    
22
21
    from django.contrib.migratory.models import Snapshot
23
                     
22
    from django.contrib.migratory.app import AppManager
23
    
24
24
    app_name = app.__name__.split('.')[-2]
25
25
    
26
26
    if (verbosity > 1):
27
27
        print "\tCreating snapshot..."
28
        
29
    model_list = models.get_models(app)
30
    model_list = [model_snap(model) for model in model_list]
31
28
    
32
    json = simplejson.dumps(model_list)
29
    manager = AppManager(app = app)
30
    json = simplejson.dumps( manager.get_snapshot()) 
33
31
    
34
32
    if Snapshot.objects.filter(app=app_name).count() > 0:
35
33
        Snapshot.objects.filter(app=app_name).delete()

Up to file-list test_project/app/models.py:

1
1
from django.db import models
2
from django.contrib.auth.models import User
2
3
3
class Poll(models.Model):
4
    question = models.CharField(max_length=255)
5
    pub_date = models.DateField()
4
class Blorg(models.Model):
5
    name = models.CharField(max_length=255)
6
    description = models.TextField(blank=True)
7
    created = models.DateTimeField(auto_now_add=True)
8
    modified = models.DateTimeField(auto_now=True)
6
9
7
class Choice(models.Model):
8
    poll = models.ForeignKey(Poll)
9
    choice = models.CharField(max_length=200)
10
    votes = models.IntegerField(null=True)
10
class Post(models.Model):
11
    blog = models.ForeignKey(Blorg)
12
    slug = models.SlugField()
13
    title = models.CharField(max_length=255)
14
    body = models.TextField(blank=True)
15
    
16
    created = models.DateTimeField(auto_now_add=True)
17
    modified = models.DateTimeField(auto_now=True)

Up to file-list test_project/test:

@@ -27,18 +27,18 @@ fi
27
27
28
28
rm -rf app/migrations/ 2>/dev/null
29
29
30
for i in $( ls app/models-??.py | sed -e "s/[^0-9]*//g" ); do
30
for i in $( ls app/models-??.py | sed -n "s/.*\([0-9][0-9]\).py/\1/p" ); do
31
31
    echo ""
32
32
    echo --- $i ---
33
33
34
34
    rm app/models.py app/models.pyc 2>/dev/null
35
35
    ln app/models-$i.py app/models.py
36
    if [ "$i" != 0 ]; then
36
    if [ "$i" != "00" ]; then
37
37
        echo "> python manage.py migrate app test-migration-$i --settings=settings_$1"
38
38
        python manage.py migrate app test-migration-$i --settings=settings_$1 || exit;
39
        echo ""
39
40
    fi
40
41
    
41
    echo ""
42
42
    echo "> python manage.py migratesql app --settings=settings_$1"
43
43
    python manage.py migratesql app --settings=settings_$1 || exit;
44
44