Commits

Anonymous committed 4eba2b0

startmigration is almost working again

this patch added support for proper foreign key generation using mock models that hold onto db data we're interested in at the time startmigration was run.

TODO:
M2Ms aren't output
db_index and unique_together values aren't used

KNOWN LMITATIONS:
startmigration doesn't play well with abstract model inheritance yet. we need to figure out the best way to get at the parent classes to scan their source for field definitions.

Comments (0)

Files changed (2)

 from django.db import connection, transaction, models
 from django.db.backends.util import truncate_name
 from django.dispatch import dispatcher
+from django.conf import settings
 
 class DatabaseOperations(object):
 
             dispatcher.send(signal=models.signals.post_syncdb, sender=app,
                 app=app, created_models=created_models,
                 verbosity=verbosity, interactive=interactive)
-            
+                
+    def mock_model(self, model_name, db_table, db_tablespace='', 
+                    pk_field_name='id', pk_field_type=models.AutoField,
+                    pk_field_kwargs={}):
+        """
+        Generates a MockModel class that provides enough information
+        to be used by a foreign key/many-to-many relationship.
+        
+        Migrations should prefer to use these rather than actual models
+        as models could get deleted over time, but these can remain in
+        migration files forever.
+        """
+        class MockOptions(object):
+            def __init__(self):
+                self.db_table = db_table
+                self.db_tablespace = db_tablespace or settings.DEFAULT_TABLESPACE
+                self.object_name = model_name
+                self.module_name = model_name.lower()
+
+                if pk_field_type == models.AutoField:
+                    pk_field_kwargs['primary_key'] = True
+
+                self.pk = pk_field_type(**pk_field_kwargs)
+                self.pk.set_attributes_from_name(pk_field_name)
+
+            def get_field_by_name(self, field_name):
+                # we only care about the pk field
+                return (self.pk, self.model, True, False)
+
+            def get_field(self, name):
+                # we only care about the pk field
+                return self.pk
+
+        class MockModel(object):
+            _meta = None
+
+        # We need to return an actual class object here, not an instance
+        MockModel._meta = MockOptions()
+        MockModel._meta.model = MockModel
+        return MockModel

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 optparse import make_option
 from south import migration
 import sys
             # Touch the init py file
             open(os.path.join(migrations_dir, "__init__.py"), "w").close()
         # See what filename is next in line. We assume they use numbers.
-        migrations = migration.get_migration_files(app)
+        migrations = migration.get_migration_files('.'.join(app_module_path))
         highest_number = 0
         for migration_name in migrations:
             try:
         if models_to_migrate:
             for model in models_to_migrate:
                 table_name = model._meta.db_table
+                mock_models = []
                 fields = []
                 for f in model._meta.local_fields:
-                    # We use a list of tuples to get nice ordering
+                    # look up the field definition to see how this was created
                     field_definition = generate_field_definition(model, f)
+                    
+                    if isinstance(f, models.ForeignKey):
+                        mock_models.append(create_mock_model(f.rel.to))
+                        field_definition = related_field_definition(f, field_definition)
+                        
+                    # give field subclasses a chance to do anything tricky
+                    # with the field definition
+                    if hasattr(f, 'south_field_definition'):
+                        field_definition = f.south_field_definition(field_definition)
+                        
                     fields.append((f.name, field_definition))
+                    
+                if mock_models:
+                    forwards += '''
+        # Mock Models
+        %s
+        ''' % "\n        ".join(mock_models)
+        
                 forwards += '''
         # Model '%s'
         db.create_table("%s", (
     # django.db.models.options::_prepare
     if field.name == 'id' and field.__class__ == models.AutoField:
         return "models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)"
+    
+    print "Warning: Could not generate field definition for %s.%s, manual editing of migration required." % \
+        (model._meta.object_name, field.name)
+        
+    return '<<< REPLACE THIS WITH FIELD DEFINITION FOR %s.%s >>>' % (model._meta.object_name, field.name)
+    
+def replace_model_string(field_definition, search_string, model_name):
+    # wrap 'search_string' in both ' and " chars when searching
+    quotes = ["'", '"']
+    for quote in quotes:
+        test = "%s%s%s" % (quote, search_string, quote)
+        if test in field_definition:
+            return field_definition.replace(test, model_name)
             
-    raise Exception("Couldn't find field definition for field: '%s' on model: '%s'" % (
-        field.name, model))
+    return None
+        
+def related_field_definition(field, field_definition):
+    # if the field definition contains any of the following strings,
+    # replace them with the model definition:
+    #   applabel.modelname
+    #   modelname
+    #   django.db.models.fields.related.RECURSIVE_RELATIONSHIP_CONSTANT
+    strings = [
+        '%s.%s' % (field.rel.to._meta.app_label, field.rel.to._meta.object_name),
+        '%s' % field.rel.to._meta.object_name,
+        RECURSIVE_RELATIONSHIP_CONSTANT
+    ]
+    
+    for test in strings:
+        fd = replace_model_string(field_definition, test, field.rel.to._meta.object_name)
+        if fd:
+            return fd
+    
+    return field_definition
 
+def create_mock_model(model):
+    # produce a string representing the python syntax necessary for creating
+    # a mock model using the supplied real model
+    if model._meta.pk.__class__.__module__ != 'django.db.models.fields':
+        # we can fix this with some clever imports, but it doesn't seem necessary to
+        # spend time on just yet
+        print "Can't generate a mock model for %s because it's primary key isn't a default django field" % model
+        sys.exit()
+    
+    return "%s = db.mock_model(model_name='%s', db_table='%s', db_tablespace='%s', pk_field_name='%s', pk_field_type=models.%s)" % \
+        (
+        model._meta.object_name,
+        model._meta.object_name,
+        model._meta.db_table,
+        model._meta.db_tablespace,
+        model._meta.pk.name,
+        model._meta.pk.__class__.__name__
+        )
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.