def test_get_migration_names(self): app = self.create_test_app() self.assertEqual( ["0001_spam", "0002_eggs", "0003_alter_spam"], migration.get_migration_names(app), )
def list_migrations(apps): from south.models import MigrationHistory apps = list(apps) names = [migration.get_app_name(app) for app in apps] applied_migrations = MigrationHistory.objects.filter(app_name__in=names) applied_migrations = ['%s.%s' % (mi.app_name,mi.migration) for mi in applied_migrations] print for app in apps: print migration.get_app_name(app) all_migrations = migration.get_migration_names(app) for migration_name in all_migrations: long_form = '%s.%s' % (migration.get_app_name(app),migration_name) if long_form in applied_migrations: print format_migration_list_item(migration_name) else: print format_migration_list_item(migration_name, applied=False) print
def handle(self, app=None, name="", added_model_list=None, added_field_list=None, initial=False, freeze_list=None, auto=False, stdout=False, **options): # Any supposed lists that are None become empty lists added_model_list = added_model_list or [] added_field_list = added_field_list or [] # --stdout means name = - if stdout: name = "-" # Make sure options are compatable if initial and (added_model_list or added_field_list or auto): print "You cannot use --initial and other options together" print self.usage_str return if auto and (added_model_list or added_field_list or initial): print "You cannot use --auto and other options together" print self.usage_str return # specify the default name 'initial' if a name wasn't specified and we're # doing a migration for an entire app if not name and initial: name = 'initial' # if not name, there's an error if not name: print "You must name this migration" print self.usage_str return if not app: print "Please provide an app in which to create the migration." print self.usage_str return # Make sure the app is short form app = app.split(".")[-1] # See if the app exists app_models_module = models.get_app(app) if not app_models_module: print "App '%s' doesn't seem to exist, isn't in INSTALLED_APPS, or has no models." % app print self.usage_str return # If they've set SOUTH_AUTO_FREEZE_APP = True (or not set it - defaults to True) if not hasattr(settings, 'SOUTH_AUTO_FREEZE_APP') or settings.SOUTH_AUTO_FREEZE_APP: if freeze_list and app not in freeze_list: freeze_list += [app] else: freeze_list = [app] # Make the migrations directory if it's not there app_module_path = app_models_module.__name__.split('.')[0:-1] try: app_module = __import__('.'.join(app_module_path), {}, {}, ['']) except ImportError: print "Couldn't find path to App '%s'." % app print self.usage_str return migrations_dir = os.path.join( os.path.dirname(app_module.__file__), "migrations", ) # Make sure there's a migrations directory and __init__.py if not os.path.isdir(migrations_dir): print "Creating migrations directory at '%s'..." % migrations_dir os.mkdir(migrations_dir) init_path = os.path.join(migrations_dir, "__init__.py") if not os.path.isfile(init_path): # Touch the init py file print "Creating __init__.py in '%s'..." % migrations_dir open(init_path, "w").close() # See what filename is next in line. We assume they use numbers. migrations = migration.get_migration_names(migration.get_app(app)) highest_number = 0 for migration_name in migrations: try: number = int(migration_name.split("_")[0]) highest_number = max(highest_number, number) except ValueError: pass # Make the new filename new_filename = "%04i%s_%s.py" % ( highest_number + 1, "".join([random.choice(string.letters.lower()) for i in range(0)]), # Possible random stuff insertion name, ) # Find the source file encoding, using PEP 0263's method encoding = None first_two_lines = inspect.getsourcelines(app_models_module)[0][:2] for line in first_two_lines: if re.search("coding[:=]\s*([-\w.]+)", line): encoding = line # Initialise forwards, backwards and models to blank things forwards = "" backwards = "" frozen_models = {} # Frozen models, used by the Fake ORM complete_apps = set() # Apps that are completely frozen - useable for diffing. # Sets of actions added_models = set() deleted_models = [] # Special: contains instances _not_ string keys added_fields = set() deleted_fields = [] # Similar to deleted_models changed_fields = [] # (mkey, fname, old_def, new_def) added_uniques = set() # (mkey, field_names) deleted_uniques = set() # (mkey, field_names) # --initial means 'add all models in this app'. if initial: for model in models.get_models(app_models_module): added_models.add("%s.%s" % (app, model._meta.object_name)) # Added models might be 'model' or 'app.model'. for modelname in added_model_list: if "." in modelname: added_models.add(modelname) else: added_models.add("%s.%s" % (app, modelname)) # Fields need translating from "model.field" to (app.model, field) for fielddef in added_field_list: try: modelname, fieldname = fielddef.split(".", 1) except ValueError: print "The field specification '%s' is not in modelname.fieldname format." % fielddef else: added_fields.add(("%s.%s" % (app, modelname), fieldname)) # Add anything frozen (I almost called the dict Iceland...) if freeze_list: for item in freeze_list: if "." in item: # It's a specific model app_name, model_name = item.split(".", 1) model = models.get_model(app_name, model_name) if model is None: print "Cannot find the model '%s' to freeze it." % item print self.usage_str return frozen_models[model] = None else: # Get everything in an app! frozen_models.update(dict([(x, None) for x in models.get_models(models.get_app(item))])) complete_apps.add(item.split(".")[-1]) # For every model in the freeze list, add in frozen dependencies for model in list(frozen_models): frozen_models.update(model_dependencies(model)) ### Automatic Detection ### if auto: # Get the last migration for this app last_models = None app_module = migration.get_app(app) if app_module is None: print "You cannot use automatic detection on the first migration of an app. Try --initial instead." else: migrations = list(migration.get_migration_classes(app_module)) if not migrations: print "You cannot use automatic detection on the first migration of an app. Try --initial instead." else: if hasattr(migrations[-1], "complete_apps") and \ app in migrations[-1].complete_apps: last_models = migrations[-1].models last_orm = migrations[-1].orm else: print "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." % app # Right, did we manage to get the last set of models? if last_models is None: print self.usage_str return new = dict([ (model_key(model), prep_for_freeze(model)) for model in models.get_models(app_models_module) if ( not getattr(model._meta, "proxy", False) and \ getattr(model._meta, "managed", True) and \ not getattr(model._meta, "abstract", False) ) ]) # And filter other apps out of the old old = dict([ (key, fields) for key, fields in last_models.items() if key.split(".", 1)[0] == app ]) am, dm, cm, af, df, cf, afu, dfu = models_diff(old, new) # For models that were there before and after, do a meta diff was_meta_change = False for mkey in cm: au, du = meta_diff(old[mkey].get("Meta", {}), new[mkey].get("Meta", {})) for entry in au: added_uniques.add((mkey, entry)) was_meta_change = True for entry in du: deleted_uniques.add((mkey, entry, last_orm[mkey])) was_meta_change = True if not (am or dm or af or df or cf or afu or dfu or was_meta_change): print "Nothing seems to have changed." return # Add items to the todo lists added_models.update(am) added_fields.update(af) changed_fields.extend(cf) # Deleted models are from the past, and so we use instances instead. for mkey in dm: model = last_orm[mkey] fields = last_models[mkey] if "Meta" in fields: del fields['Meta'] deleted_models.append((model, fields, last_models)) # For deleted fields, we tag the instance on the end too for mkey, fname in df: deleted_fields.append(( mkey, fname, last_orm[mkey]._meta.get_field_by_name(fname)[0], last_models[mkey][fname], last_models, )) # Uniques need merging added_uniques = added_uniques.union(afu) for mkey, entry in dfu: deleted_uniques.add((mkey, entry, last_orm[mkey])) ### Added model ### for mkey in added_models: print " + Added model '%s'" % (mkey,) model = model_unkey(mkey) # Add the model's dependencies to the frozens frozen_models.update(model_dependencies(model)) # Get the field definitions fields = modelsinspector.get_model_fields(model) # Turn the (class, args, kwargs) format into a string fields = triples_to_defs(app, model, fields) # Make the code forwards += CREATE_TABLE_SNIPPET % ( model._meta.object_name, model._meta.db_table, "\n ".join(["('%s', orm[%r])," % (fname, mkey + ":" + fname) for fname, fdef in fields.items()]), model._meta.app_label, model._meta.object_name, ) # And the backwards code backwards += DELETE_TABLE_SNIPPET % ( model._meta.object_name, model._meta.db_table ) # Now add M2M fields to be done for field in model._meta.local_many_to_many: added_fields.add((mkey, field.attname)) # And unique_togethers to be added for ut in model._meta.unique_together: added_uniques.add((mkey, tuple(ut))) ### Added fields ### for mkey, field_name in added_fields: # Get the model model = model_unkey(mkey) # Get the field try: field = model._meta.get_field(field_name) except FieldDoesNotExist: print "Model '%s' doesn't have a field '%s'" % (mkey, field_name) return # ManyToMany fields need special attention. if isinstance(field, models.ManyToManyField): if not field.rel.through: # Bug #120 # Add a frozen model for each side frozen_models[model] = None frozen_models[field.rel.to] = None # And a field defn, that's actually a table creation forwards += CREATE_M2MFIELD_SNIPPET % ( model._meta.object_name, field.name, field.m2m_db_table(), field.m2m_column_name()[:-3], # strip off the '_id' at the end poss_ormise(app, model, model._meta.object_name), field.m2m_reverse_name()[:-3], # strip off the '_id' at the ned poss_ormise(app, field.rel.to, field.rel.to._meta.object_name) ) backwards += DELETE_M2MFIELD_SNIPPET % ( model._meta.object_name, field.name, field.m2m_db_table() ) print " + Added M2M '%s.%s'" % (mkey, field_name) continue # GenericRelations need ignoring if isinstance(field, GenericRelation): continue print " + Added field '%s.%s'" % (mkey, field_name) # Add any dependencies frozen_models.update(field_dependencies(field)) # Work out the definition triple = remove_useless_attributes( modelsinspector.get_model_fields(model)[field_name]) field_definition = make_field_constructor(app, field, triple) forwards += CREATE_FIELD_SNIPPET % ( model._meta.object_name, field.name, model._meta.db_table, field.name, "orm[%r]" % (mkey + ":" + field.name), ) backwards += DELETE_FIELD_SNIPPET % ( model._meta.object_name, field.name, model._meta.db_table, field.column, ) ### Deleted fields ### for mkey, field_name, field, triple, last_models in deleted_fields: print " - Deleted field '%s.%s'" % (mkey, field_name) # Get the model model = model_unkey(mkey) # ManyToMany fields need special attention. if isinstance(field, models.ManyToManyField): # And a field defn, that's actually a table deletion forwards += DELETE_M2MFIELD_SNIPPET % ( model._meta.object_name, field.name, field.m2m_db_table() ) backwards += CREATE_M2MFIELD_SNIPPET % ( model._meta.object_name, field.name, field.m2m_db_table(), field.m2m_column_name()[:-3], # strip off the '_id' at the end poss_ormise(app, model, model._meta.object_name), field.m2m_reverse_name()[:-3], # strip off the '_id' at the ned poss_ormise(app, field.rel.to, field.rel.to._meta.object_name) ) continue # Work out the definition triple = remove_useless_attributes(triple) field_definition = make_field_constructor(app, field, triple) forwards += DELETE_FIELD_SNIPPET % ( model._meta.object_name, field.name, model._meta.db_table, field.column, ) backwards += CREATE_FIELD_SNIPPET % ( model._meta.object_name, field.name, model._meta.db_table, field.name, "orm[%r]" % (mkey + ":" + field.name), ) ### Deleted model ### for model, fields, last_models in deleted_models: print " - Deleted model '%s.%s'" % (model._meta.app_label,model._meta.object_name) # Turn the (class, args, kwargs) format into a string fields = triples_to_defs(app, model, fields) # Make the code forwards += DELETE_TABLE_SNIPPET % ( model._meta.object_name, model._meta.db_table ) # And the backwards code backwards += CREATE_TABLE_SNIPPET % ( model._meta.object_name, model._meta.db_table, "\n ".join(["('%s', orm[%r])," % (fname, mkey + ":" + fname) for fname, fdef in fields.items()]), model._meta.app_label, model._meta.object_name, ) ### Changed fields ### for mkey, field_name, old_triple, new_triple in changed_fields: model = model_unkey(mkey) old_def = triples_to_defs(app, model, { field_name: old_triple, })[field_name] new_def = triples_to_defs(app, model, { field_name: new_triple, })[field_name] # We need to create the field, to see if it needs _id, or if it's an M2M field = model._meta.get_field_by_name(field_name)[0] if hasattr(field, "m2m_db_table"): # See if anything has ACTUALLY changed if old_triple[1] != new_triple[1]: print " ! Detected change to the target model of M2M field '%s.%s'. South can't handle this; leaving this change out." % (mkey, field_name) continue print " ~ Changed field '%s.%s'." % (mkey, field_name) forwards += CHANGE_FIELD_SNIPPET % ( model._meta.object_name, field_name, new_def, model._meta.db_table, field.get_attname(), "orm[%r]" % (mkey + ":" + field.name), ) backwards += CHANGE_FIELD_SNIPPET % ( model._meta.object_name, field_name, old_def, model._meta.db_table, field.get_attname(), "orm[%r]" % (mkey + ":" + field.name), ) ### Added unique_togethers ### for mkey, ut in added_uniques: model = model_unkey(mkey) if len(ut) == 1: print " + Added unique for %s on %s." % (", ".join(ut), model._meta.object_name) else: print " + Added unique_together for [%s] on %s." % (", ".join(ut), model._meta.object_name) cols = [get_field_column(model, f) for f in ut] forwards += CREATE_UNIQUE_SNIPPET % ( ", ".join(ut), model._meta.object_name, model._meta.db_table, cols, ) backwards = DELETE_UNIQUE_SNIPPET % ( ", ".join(ut), model._meta.object_name, model._meta.db_table, cols, ) + backwards ### Deleted unique_togethers ### for mkey, ut, model in deleted_uniques: if len(ut) == 1: print " - Deleted unique for %s on %s." % (", ".join(ut), model._meta.object_name) else: print " - Deleted unique_together for [%s] on %s." % (", ".join(ut), model._meta.object_name) cols = [get_field_column(model, f) for f in ut] forwards = DELETE_UNIQUE_SNIPPET % ( ", ".join(ut), model._meta.object_name, model._meta.db_table, cols, ) + forwards backwards += CREATE_UNIQUE_SNIPPET % ( ", ".join(ut), model._meta.object_name, model._meta.db_table, cols, ) # Default values for forwards/backwards if (not forwards) and (not backwards): forwards = '"Write your forwards migration here"' backwards = '"Write your backwards migration here"' all_models = {} # Fill out frozen model definitions for model, last_models in frozen_models.items(): all_models[model_key(model)] = prep_for_freeze(model, last_models) # Do some model cleanup, and warnings for modelname, model in all_models.items(): for fieldname, fielddef in model.items(): # Remove empty-after-cleaning Metas. if fieldname == "Meta" and not fielddef: del model['Meta'] # Warn about undefined fields elif fielddef is None: print "WARNING: Cannot get definition for '%s' on '%s'. Please edit the migration manually to define it, or add the south_field_triple method to it." % ( fieldname, modelname, ) model[fieldname] = FIELD_NEEDS_DEF_SNIPPET # So, what's in this file, then? file_contents = MIGRATION_SNIPPET % ( encoding or "", '.'.join(app_module_path), forwards, backwards, pprint_frozen_models(all_models), complete_apps and "complete_apps = [%s]" % (", ".join(map(repr, complete_apps))) 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 "Created %s." % new_filename
def handle(self, app=None, name="", added_model_list=None, added_field_list=None, initial=False, freeze_list=None, auto=False, **options): # Any supposed lists that are None become empty lists added_model_list = added_model_list or [] added_field_list = added_field_list or [] # Make sure options are compatable if initial and (added_model_list or added_field_list or auto): print "You cannot use --initial and other options together" return if auto and (added_model_list or added_field_list or initial): print "You cannot use --auto and other options together" return # specify the default name 'initial' if a name wasn't specified and we're # doing a migration for an entire app if not name and initial: name = 'initial' # if not name, there's an error if not name: print "You must name this migration" return if not app: print "Please provide an app in which to create the migration." return # Make sure the app is short form app = app.split(".")[-1] # See if the app exists app_models_module = models.get_app(app) if not app_models_module: print "App '%s' doesn't seem to exist, isn't in INSTALLED_APPS, or has no models." % app return # If they've set SOUTH_AUTO_FREEZE_APP = True (or not set it - defaults to True) if not hasattr( settings, 'SOUTH_AUTO_FREEZE_APP') or settings.SOUTH_AUTO_FREEZE_APP: if freeze_list and app not in freeze_list: freeze_list += [app] else: freeze_list = [app] # Make the migrations directory if it's not there app_module_path = app_models_module.__name__.split('.')[0:-1] try: app_module = __import__('.'.join(app_module_path), {}, {}, ['']) except ImportError: print "Couldn't find path to App '%s'." % app return migrations_dir = os.path.join( os.path.dirname(app_module.__file__), "migrations", ) # Make sure there's a migrations directory and __init__.py if not os.path.isdir(migrations_dir): print "Creating migrations directory at '%s'..." % migrations_dir os.mkdir(migrations_dir) init_path = os.path.join(migrations_dir, "__init__.py") if not os.path.isfile(init_path): # Touch the init py file print "Creating __init__.py in '%s'..." % migrations_dir open(init_path, "w").close() # See what filename is next in line. We assume they use numbers. migrations = migration.get_migration_names(migration.get_app(app)) highest_number = 0 for migration_name in migrations: try: number = int(migration_name.split("_")[0]) highest_number = max(highest_number, number) except ValueError: pass # Make the new filename new_filename = "%04i%s_%s.py" % ( highest_number + 1, "".join([random.choice(string.letters.lower()) for i in range(0)]), # Possible random stuff insertion name, ) # Find the source file encoding, using PEP 0263's method encoding = None first_two_lines = inspect.getsourcelines(app_models_module)[0][:2] for line in first_two_lines: if re.search("coding[:=]\s*([-\w.]+)", line): encoding = line # Initialise forwards, backwards and models to blank things forwards = "" backwards = "" frozen_models = {} # Frozen models, used by the Fake ORM stub_models = { } # Frozen models, but only enough for relation ends (old mock models) complete_apps = set( ) # Apps that are completely frozen - useable for diffing. # Sets of actions added_models = set() deleted_models = [] # Special: contains instances _not_ string keys added_fields = set() deleted_fields = [] # Similar to deleted_models changed_fields = [] # (mkey, fname, old_def, new_def) added_uniques = set() # (mkey, field_names) deleted_uniques = set() # (mkey, field_names) # --initial means 'add all models in this app'. if initial: for model in models.get_models(app_models_module): added_models.add("%s.%s" % (app, model._meta.object_name)) # Added models might be 'model' or 'app.model'. for modelname in added_model_list: if "." in modelname: added_models.add(modelname) else: added_models.add("%s.%s" % (app, modelname)) # Fields need translating from "model.field" to (app.model, field) for fielddef in added_field_list: try: modelname, fieldname = fielddef.split(".", 1) except ValueError: print "The field specification '%s' is not in modelname.fieldname format." % fielddef else: added_fields.add(("%s.%s" % (app, modelname), fieldname)) # Add anything frozen (I almost called the dict Iceland...) if freeze_list: for item in freeze_list: if "." in item: # It's a specific model app_name, model_name = item.split(".", 1) model = models.get_model(app_name, model_name) if model is None: print "Cannot find the model '%s' to freeze it." % item return frozen_models[model] = None else: # Get everything in an app! frozen_models.update( dict([(x, None) for x in models.get_models(models.get_app(item)) ])) complete_apps.add(item.split(".")[-1]) # For every model in the freeze list, add in dependency stubs for model in frozen_models: stub_models.update(model_dependencies(model)) ### Automatic Detection ### if auto: # Get the last migration for this app last_models = None app_module = migration.get_app(app) if app_module is None: print "You cannot use automatic detection on the first migration of an app. Try --initial instead." else: migrations = list(migration.get_migration_classes(app_module)) if not migrations: print "You cannot use automatic detection on the first migration of an app. Try --initial instead." else: if hasattr(migrations[-1], "complete_apps") and \ app in migrations[-1].complete_apps: last_models = migrations[-1].models last_orm = migrations[-1].orm else: print "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." % app # Right, did we manage to get the last set of models? if last_models is None: return # Good! Get new things. new = dict([(model_key(model), prep_for_freeze(model)) for model in models.get_models(app_models_module)]) # And filter other apps out of the old old = dict([(key, fields) for key, fields in last_models.items() if key.split(".", 1)[0] == app]) am, dm, cm, af, df, cf = models_diff(old, new) # For models that were there before and after, do a meta diff was_meta_change = False for mkey in cm: au, du = meta_diff(old[mkey].get("Meta", {}), new[mkey].get("Meta", {})) for entry in au: added_uniques.add((mkey, entry)) was_meta_change = True for entry in du: deleted_uniques.add((mkey, entry)) was_meta_change = True if not (am or dm or af or df or cf or was_meta_change): print "Nothing seems to have changed." return # Add items to the todo lists added_models.update(am) added_fields.update(af) changed_fields.extend(cf) # Deleted models are from the past, and so we use instances instead. for mkey in dm: model = last_orm[mkey] fields = last_models[mkey] if "Meta" in fields: del fields['Meta'] deleted_models.append((model, fields, last_models)) # For deleted fields, we tag the instance on the end too for mkey, fname in df: deleted_fields.append(( mkey, fname, last_orm[mkey]._meta.get_field_by_name(fname)[0], last_models[mkey][fname], last_models, )) ### Added model ### for mkey in added_models: print " + Added model '%s'" % (mkey, ) model = model_unkey(mkey) # Add the model's dependencies to the stubs stub_models.update(model_dependencies(model)) # Get the field definitions fields = modelsparser.get_model_fields(model) # Turn the (class, args, kwargs) format into a string fields = triples_to_defs(app, model, fields) # Make the code forwards += CREATE_TABLE_SNIPPET % ( model._meta.object_name, model._meta.db_table, "\n ".join([ "('%s', %s)," % (fname, fdef) for fname, fdef in fields.items() ]), model._meta.app_label, model._meta.object_name, ) # And the backwards code backwards += DELETE_TABLE_SNIPPET % (model._meta.object_name, model._meta.db_table) # Now add M2M fields to be done for field in model._meta.local_many_to_many: added_fields.add((mkey, field.attname)) # And unique_togethers to be added for ut in model._meta.unique_together: added_uniques.add((mkey, tuple(ut))) ### Added fields ### for mkey, field_name in added_fields: print " + Added field '%s.%s'" % (mkey, field_name) # Get the model model = model_unkey(mkey) # Get the field try: field = model._meta.get_field(field_name) except FieldDoesNotExist: print "Model '%s' doesn't have a field '%s'" % (mkey, field_name) return # ManyToMany fields need special attention. if isinstance(field, models.ManyToManyField): if not field.rel.through: # Bug #120 # Add a stub model for each side stub_models[model] = None stub_models[field.rel.to] = None # And a field defn, that's actually a table creation forwards += CREATE_M2MFIELD_SNIPPET % ( model._meta.object_name, field.name, field.m2m_db_table(), field.m2m_column_name() [:-3], # strip off the '_id' at the end model._meta.object_name, field.m2m_reverse_name() [:-3], # strip off the '_id' at the ned field.rel.to._meta.object_name) backwards += DELETE_M2MFIELD_SNIPPET % ( model._meta.object_name, field.name, field.m2m_db_table()) continue # GenericRelations need ignoring if isinstance(field, GenericRelation): continue # Add any dependencies stub_models.update(field_dependencies(field)) # Work out the definition triple = remove_useless_attributes( modelsparser.get_model_fields(model)[field_name]) field_definition = make_field_constructor(app, field, triple) forwards += CREATE_FIELD_SNIPPET % ( model._meta.object_name, field.name, model._meta.db_table, field.name, field_definition, ) backwards += DELETE_FIELD_SNIPPET % ( model._meta.object_name, field.name, model._meta.db_table, field.column, ) ### Deleted fields ### for mkey, field_name, field, triple, last_models in deleted_fields: print " - Deleted field '%s.%s'" % (mkey, field_name) # Get the model model = model_unkey(mkey) # ManyToMany fields need special attention. if isinstance(field, models.ManyToManyField): # Add a stub model for each side, if they're not already there # (if we just added old versions, we might override new ones) if model not in stub_models: stub_models[model] = last_models if field.rel.to not in last_models: stub_models[field.rel.to] = last_models # And a field defn, that's actually a table deletion forwards += DELETE_M2MFIELD_SNIPPET % ( model._meta.object_name, field.name, field.m2m_db_table()) backwards += CREATE_M2MFIELD_SNIPPET % ( model._meta.object_name, field.name, field.m2m_db_table(), field.m2m_column_name() [:-3], # strip off the '_id' at the end model._meta.object_name, field.m2m_reverse_name() [:-3], # strip off the '_id' at the ned field.rel.to._meta.object_name) continue # Add any dependencies deps = field_dependencies(field, last_models) deps.update(stub_models) stub_models = deps # Work out the definition triple = remove_useless_attributes(triple) field_definition = make_field_constructor(app, field, triple) forwards += DELETE_FIELD_SNIPPET % ( model._meta.object_name, field.name, model._meta.db_table, field.column, ) backwards += CREATE_FIELD_SNIPPET % ( model._meta.object_name, field.name, model._meta.db_table, field.name, field_definition, ) ### Deleted model ### for model, fields, last_models in deleted_models: print " - Deleted model '%s.%s'" % (model._meta.app_label, model._meta.object_name) # Add the model's dependencies to the stubs deps = model_dependencies(model, last_models) deps.update(stub_models) stub_models = deps # Turn the (class, args, kwargs) format into a string fields = triples_to_defs(app, model, fields) # Make the code forwards += DELETE_TABLE_SNIPPET % (model._meta.object_name, model._meta.db_table) # And the backwards code backwards += CREATE_TABLE_SNIPPET % ( model._meta.object_name, model._meta.db_table, "\n ".join([ "('%s', %s)," % (fname, fdef) for fname, fdef in fields.items() ]), model._meta.app_label, model._meta.object_name, ) ### Changed fields ### for mkey, field_name, old_triple, new_triple in changed_fields: model = model_unkey(mkey) old_def = triples_to_defs(app, model, { field_name: old_triple, })[field_name] new_def = triples_to_defs(app, model, { field_name: new_triple, })[field_name] # We need to create the field, to see if it needs _id, or if it's an M2M field = model._meta.get_field_by_name(field_name)[0] if hasattr(field, "m2m_db_table"): # See if anything has ACTUALLY changed if old_triple[1] != new_triple[1]: print " ! Detected change to the target model of M2M field '%s.%s'. South can't handle this; leaving this change out." % ( mkey, field_name) continue print " ~ Changed field '%s.%s'." % (mkey, field_name) forwards += CHANGE_FIELD_SNIPPET % ( model._meta.object_name, field_name, model._meta.db_table, field.get_attname(), new_def, ) backwards += CHANGE_FIELD_SNIPPET % ( model._meta.object_name, field_name, model._meta.db_table, field.get_attname(), old_def, ) ### Added unique_togethers ### for mkey, ut in added_uniques: model = model_unkey(mkey) print " + Added unique_together for [%s] on %s." % ( ", ".join(ut), model._meta.object_name) cols = [get_field_column(model, f) for f in ut] forwards += CREATE_UNIQUE_SNIPPET % ( ", ".join(ut), model._meta.object_name, model._meta.db_table, cols, ) backwards += DELETE_UNIQUE_SNIPPET % ( ", ".join(ut), model._meta.object_name, model._meta.db_table, cols, ) ### Deleted unique_togethers ### for mkey, ut in deleted_uniques: model = model_unkey(mkey) print " - Deleted unique_together for [%s] on %s." % ( ", ".join(ut), model._meta.object_name) forwards += DELETE_UNIQUE_SNIPPET % ( ", ".join(ut), model._meta.object_name, model._meta.db_table, ut, ) backwards += CREATE_UNIQUE_SNIPPET % ( ", ".join(ut), model._meta.object_name, model._meta.db_table, ut, ) # Default values for forwards/backwards if (not forwards) and (not backwards): forwards = '"Write your forwards migration here"' backwards = '"Write your backwards migration here"' all_models = {} # Fill out frozen model definitions for model, last_models in frozen_models.items(): all_models[model_key(model)] = prep_for_freeze(model, last_models) # Fill out stub model definitions for model, last_models in stub_models.items(): key = model_key(model) if key in all_models: continue # We'd rather use full models than stubs. all_models[key] = prep_for_stub(model, last_models) # Do some model cleanup, and warnings for modelname, model in all_models.items(): for fieldname, fielddef in model.items(): # Remove empty-after-cleaning Metas. if fieldname == "Meta" and not fielddef: del model['Meta'] # Warn about undefined fields elif fielddef is None: print "WARNING: Cannot get definition for '%s' on '%s'. Please edit the migration manually." % ( fieldname, modelname, ) model[fieldname] = FIELD_NEEDS_DEF_SNIPPET # Write the migration file fp = open(os.path.join(migrations_dir, new_filename), "w") fp.write(MIGRATION_SNIPPET % (encoding or "", '.'.join(app_module_path), forwards, backwards, pprint_frozen_models(all_models), complete_apps and "complete_apps = [%s]" % (", ".join(map(repr, complete_apps))) or "")) fp.close() print "Created %s." % new_filename
def handle(self, app=None, name="", model_list=None, initial=False, **options): # If model_list is None, then it's an empty list model_list = model_list or [] # make sure --model and --all aren't both specified if initial and model_list: print "You cannot use --initial and other options together" return # specify the default name 'initial' if a name wasn't specified and we're # doing a migration for an entire app if not name and initial: name = 'initial' # if not name, there's an error if not name: print "You must name this migration" return if not app: print "Please provide an app in which to create the migration." return # See if the app exists app_models_module = models.get_app(app) if not app_models_module: print "App '%s' doesn't seem to exist, isn't in INSTALLED_APPS, or has no models." % app return # Determine what models should be included in this migration. models_to_migrate = [] if initial: models_to_migrate = models.get_models(app_models_module) if not models_to_migrate: print "No models found in app '%s'" % (app) return else: for model_name in model_list: model = models.get_model(app, model_name) if not model: print "Couldn't find model '%s' in app '%s'" % (model_name, app) return models_to_migrate.append(model) # Make the migrations directory if it's not there app_module_path = app_models_module.__name__.split('.')[0:-1] try: app_module = __import__('.'.join(app_module_path), {}, {}, ['']) except ImportError: print "Couldn't find path to App '%s'." % app return migrations_dir = os.path.join( os.path.dirname(app_module.__file__), "migrations", ) if not os.path.isdir(migrations_dir): print "Creating migrations directory at '%s'..." % migrations_dir os.mkdir(migrations_dir) # 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_names(migration.get_app(app)) highest_number = 0 for migration_name in migrations: try: number = int(migration_name.split("_")[0]) highest_number = max(highest_number, number) except ValueError: pass # Make the new filename new_filename = "%04i%s_%s.py" % ( highest_number + 1, "".join([random.choice(string.letters.lower()) for i in range(0)]), # Possible random stuff insertion name, ) # If there's a model, make the migration skeleton, else leave it bare forwards, backwards = '', '' 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: # look up the field definition to see how this was created field_definition = generate_field_definition(model, f) if field_definition: if isinstance(f, models.ForeignKey): mock_models.append(create_mock_model(f.rel.to)) field_definition = related_field_definition(f, field_definition) else: print "Warning: Could not generate field definition for %s.%s, manual editing of migration required." % \ (model._meta.object_name, f.name) field_definition = '<<< REPLACE THIS WITH FIELD DEFINITION FOR %s.%s >>>' % (model._meta.object_name, f.name) fields.append((f.name, field_definition)) if mock_models: forwards += ''' # Mock Models %s ''' % "\n ".join(mock_models) forwards += ''' # Model '%s' db.create_table('%s', ( %s ))''' % ( model._meta.object_name, table_name, "\n ".join(["('%s', %s)," % (f[0], f[1]) for f in fields]), ) backwards = ('''db.delete_table('%s') ''' % table_name) + backwards # Now go through local M2Ms and add extra stuff for them for m in model._meta.local_many_to_many: # ignore generic relations if isinstance(m, GenericRelation): continue # if the 'through' option is specified, the table will # be created through the normal model creation above. if m.rel.through: continue mock_models = [create_mock_model(model), create_mock_model(m.rel.to)] forwards += ''' # Mock Models %s # M2M field '%s.%s' db.create_table('%s', ( ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), ('%s', models.ForeignKey(%s, null=False)), ('%s', models.ForeignKey(%s, null=False)) )) ''' % ( "\n ".join(mock_models), model._meta.object_name, m.name, m.m2m_db_table(), m.m2m_column_name()[:-3], # strip off the '_id' at the end model._meta.object_name, m.m2m_reverse_name()[:-3], # strip off the '_id' at the ned m.rel.to._meta.object_name ) backwards = '''db.delete_table('%s') ''' % m.m2m_db_table() + backwards if model._meta.unique_together: ut = model._meta.unique_together if not isinstance(ut[0], (list, tuple)): ut = (ut,) for unique in ut: columns = ["'%s'" % model._meta.get_field(f).column for f in unique] forwards += ''' db.create_index('%s', [%s], unique=True, db_tablespace='%s') ''' % ( table_name, ','.join(columns), model._meta.db_tablespace ) forwards += ''' db.send_create_signal('%s', ['%s'])''' % ( app, "','".join(model._meta.object_name for model in models_to_migrate) ) else: forwards = '"Write your forwards migration here"' backwards = '"Write your backwards migration here"' fp = open(os.path.join(migrations_dir, new_filename), "w") fp.write(""" from south.db import db from %s.models import * class Migration: def forwards(self): %s def backwards(self): %s """ % ('.'.join(app_module_path), forwards, backwards)) fp.close() print "Created %s." % new_filename
def handle(self, app=None, name="", model_list=None, field_list=None, initial=False, **options): # If model_list is None, then it's an empty list model_list = model_list or [] # If field_list is None, then it's an empty list field_list = field_list or [] # make sure --model and --all aren't both specified if initial and (model_list or field_list): print "You cannot use --initial and other options together" return # specify the default name 'initial' if a name wasn't specified and we're # doing a migration for an entire app if not name and initial: name = 'initial' # if not name, there's an error if not name: print "You must name this migration" return if not app: print "Please provide an app in which to create the migration." return # See if the app exists app_models_module = models.get_app(app) if not app_models_module: print "App '%s' doesn't seem to exist, isn't in INSTALLED_APPS, or has no models." % app return # Determine what models should be included in this migration. models_to_migrate = [] if initial: models_to_migrate = models.get_models(app_models_module) if not models_to_migrate: print "No models found in app '%s'" % (app) return else: for model_name in model_list: model = models.get_model(app, model_name) if not model: print "Couldn't find model '%s' in app '%s'" % (model_name, app) return models_to_migrate.append(model) # See what fields need to be included fields_to_add = [] for field_spec in field_list: model_name, field_name = field_spec.split(".", 1) model = models.get_model(app, model_name) if not model: print "Couldn't find model '%s' in app '%s'" % (model_name, app) return try: field = model._meta.get_field(field_name) except FieldDoesNotExist: print "Model '%s' doesn't have a field '%s'" % (model_name, field_name) return fields_to_add.append((model, field_name, field)) # Make the migrations directory if it's not there app_module_path = app_models_module.__name__.split('.')[0:-1] try: app_module = __import__('.'.join(app_module_path), {}, {}, ['']) except ImportError: print "Couldn't find path to App '%s'." % app return migrations_dir = os.path.join( os.path.dirname(app_module.__file__), "migrations", ) # Make sure there's a migrations directory and __init__.py if not os.path.isdir(migrations_dir): print "Creating migrations directory at '%s'..." % migrations_dir os.mkdir(migrations_dir) init_path = os.path.join(migrations_dir, "__init__.py") if not os.path.isfile(init_path): # Touch the init py file print "Creating __init__.py in '%s'..." % migrations_dir open(init_path, "w").close() # See what filename is next in line. We assume they use numbers. migrations = migration.get_migration_names(migration.get_app(app)) highest_number = 0 for migration_name in migrations: try: number = int(migration_name.split("_")[0]) highest_number = max(highest_number, number) except ValueError: pass # Make the new filename new_filename = "%04i%s_%s.py" % ( highest_number + 1, "".join([random.choice(string.letters.lower()) for i in range(0)]), # Possible random stuff insertion name, ) # If there's a model, make the migration skeleton, else leave it bare forwards, backwards = '', '' if fields_to_add: # First, do the added fields for model, field_name, field in fields_to_add: field_definition = generate_field_definition(model, field) if isinstance(field, models.ManyToManyField): # Make a mock model for each side mock_model = "\n".join([ create_mock_model(model, " "), create_mock_model(field.rel.to, " ") ]) # And a field defn, that's actually a table creation forwards += ''' # Mock Model %s # Adding ManyToManyField '%s.%s' db.create_table('%s', ( ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), ('%s', models.ForeignKey(%s, null=False)), ('%s', models.ForeignKey(%s, null=False)) )) ''' % ( mock_model, model._meta.object_name, field.name, field.m2m_db_table(), field.m2m_column_name()[:-3], # strip off the '_id' at the end model._meta.object_name, field.m2m_reverse_name()[:-3], # strip off the '_id' at the ned field.rel.to._meta.object_name ) backwards += ''' # Dropping ManyToManyField '%s.%s' db.drop_table('%s')''' % ( model._meta.object_name, field.name, field.m2m_db_table() ) continue elif field.rel: # ForeignKey, etc. mock_model = create_mock_model(field.rel.to, " ") field_definition = related_field_definition(field, field_definition) else: mock_model = None # If we can't get it (inspect madness?) then insert placeholder if not field_definition: print "Warning: Could not generate field definition for %s.%s, manual editing of migration required." % \ (model._meta.object_name, field.name) field_definition = '<<< REPLACE THIS WITH FIELD DEFINITION FOR %s.%s >>>' % (model._meta.object_name, f.name) if mock_model: forwards += ''' # Mock model %s ''' % (mock_model) forwards += ''' # Adding field '%s.%s' db.add_column(%r, %r, %s) ''' % ( model._meta.object_name, field.name, model._meta.db_table, field.name, field_definition, ) backwards += ''' # Deleting field '%s.%s' db.delete_column(%r, %r) ''' % ( model._meta.object_name, field.name, model._meta.db_table, field.column, ) if models_to_migrate: # Now, do the added models for model in models_to_migrate: table_name = model._meta.db_table mock_models = [] fields = [] for f in model._meta.local_fields: # Look up the field definition to see how this was created field_definition = generate_field_definition(model, f) # If it's a OneToOneField, and ends in _ptr, just use it if isinstance(f, models.OneToOneField) and f.name.endswith("_ptr"): mock_models.append(create_mock_model(f.rel.to, " ")) field_definition = "models.OneToOneField(%s)" % f.rel.to.__name__ # It's probably normal then elif field_definition: if isinstance(f, models.ForeignKey): mock_models.append(create_mock_model(f.rel.to, " ")) field_definition = related_field_definition(f, field_definition) # Oh noes, no defn found else: print "Warning: Could not generate field definition for %s.%s, manual editing of migration required." % \ (model._meta.object_name, f.name) print f, type(f) field_definition = '<<< REPLACE THIS WITH FIELD DEFINITION FOR %s.%s >>>' % (model._meta.object_name, f.name) fields.append((f.name, field_definition)) if mock_models: forwards += ''' # Mock Models %s ''' % "\n".join(mock_models) forwards += ''' # Model '%s' db.create_table(%r, ( %s ))''' % ( model._meta.object_name, table_name, "\n ".join(["('%s', %s)," % (f[0], f[1]) for f in fields]), ) backwards = ('''db.delete_table('%s') ''' % table_name) + backwards # Now go through local M2Ms and add extra stuff for them for m in model._meta.local_many_to_many: # ignore generic relations if isinstance(m, GenericRelation): continue # if the 'through' option is specified, the table will # be created through the normal model creation above. if m.rel.through: continue mock_models = [create_mock_model(model, " "), create_mock_model(m.rel.to, " ")] forwards += ''' # Mock Models %s # M2M field '%s.%s' db.create_table('%s', ( ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), ('%s', models.ForeignKey(%s, null=False)), ('%s', models.ForeignKey(%s, null=False)) )) ''' % ( "\n".join(mock_models), model._meta.object_name, m.name, m.m2m_db_table(), m.m2m_column_name()[:-3], # strip off the '_id' at the end model._meta.object_name, m.m2m_reverse_name()[:-3], # strip off the '_id' at the ned m.rel.to._meta.object_name ) backwards = '''db.delete_table('%s') ''' % m.m2m_db_table() + backwards if model._meta.unique_together: ut = model._meta.unique_together if not isinstance(ut[0], (list, tuple)): ut = (ut,) for unique in ut: columns = ["'%s'" % model._meta.get_field(f).column for f in unique] forwards += ''' db.create_index('%s', [%s], unique=True, db_tablespace='%s') ''' % ( table_name, ','.join(columns), model._meta.db_tablespace ) forwards += ''' db.send_create_signal('%s', ['%s'])''' % ( app, "','".join(model._meta.object_name for model in models_to_migrate) ) # Try sniffing the encoding using PEP 0263's method encoding = None first_two_lines = inspect.getsourcelines(app_models_module)[0][:2] for line in first_two_lines: if re.search("coding[:=]\s*([-\w.]+)", line): encoding = line if (not forwards) and (not backwards): forwards = '"Write your forwards migration here"' backwards = '"Write your backwards migration here"' fp = open(os.path.join(migrations_dir, new_filename), "w") fp.write("""%s from south.db import db from django.db import models from %s.models import * class Migration: def forwards(self): %s def backwards(self): %s """ % (encoding or "", '.'.join(app_module_path), forwards, backwards)) fp.close() print "Created %s." % new_filename