Created
December 4, 2017 16:37
-
-
Save dcwatson/a1379acd910cabdaf8c5e157d5ba1190 to your computer and use it in GitHub Desktop.
A hacky script to rewrite old Django migrations to include current on_delete values
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# IMPORTANT! USAGE INSTRUCTIONS! | |
# Drop this file in the top level of your project *after* you've added on_delete to all your ForeignKey and | |
# OneToOneFields. Running it will rewrite your migration files to add whatever the current on_delete for each field is | |
# to the past migrations. Make sure you have a clean working copy so you can easily review the differences before | |
# committing. Assuming there were no pending migrations, running "makemigrations" should yield no new changes. | |
import os | |
import re | |
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") | |
import django | |
django.setup() | |
from django.apps import apps | |
from django.db import models | |
MIGRATION_TYPE_RE = re.compile(r'\s*migrations.([^(]+)\(') | |
MODEL_NAME_RE = re.compile(r"\s*model_name='([^']+)',") | |
NAME_RE = re.compile(r"\s*name='([^']+)',") | |
FK_NAME_RE = re.compile(r"\s*\('([^']+)', models\.(ForeignKey|OneToOneField)") | |
FIELD_TYPE_RE = re.compile(r"models\.(ForeignKey|OneToOneField)\(") | |
top_level = os.path.dirname(os.path.abspath(__file__)) | |
current_on_delete = {} | |
# Go through all the fields and figure out what the current on_delete values are. | |
for model_class in apps.get_models(): | |
for field in model_class._meta.fields: | |
if isinstance(field, (models.ForeignKey, models.OneToOneField)): | |
current_on_delete['%s.%s' % (model_class.__name__.lower(), field.name)] = field.remote_field.on_delete.__name__ | |
for app_config in apps.get_app_configs(): | |
if app_config.path.startswith(top_level): | |
migrations_path = os.path.join(app_config.path, 'migrations') | |
if os.path.isdir(migrations_path): | |
for filename in os.listdir(migrations_path): | |
if filename.endswith('.py'): | |
file_path = os.path.join(migrations_path, filename) | |
file_lines = [] | |
print(file_path) | |
# Keep track of the last known migration type, model_name, and name attributes. | |
migration_type = None | |
model_name = None | |
name = None | |
for lineno, line in enumerate(open(file_path)): | |
match = FIELD_TYPE_RE.search(line) | |
if match and 'on_delete' not in line: | |
field_type = match.group(1) | |
match = FK_NAME_RE.match(line) | |
if match: | |
# For CreateModel, each FK is on it's own line, prefixed by the field name - use the last known "name" as the model name. | |
key = '%s.%s' % (name.lower(), match.group(1)) | |
else: | |
# Otherwise, use the last name/model_name attributes to look up the FK. | |
key = '%s.%s' % (model_name, name) | |
new_line = line.replace('models.%s(' % field_type, 'models.%s(on_delete=models.%s, ' % (field_type, current_on_delete[key])) | |
print(' %s: %s\n %s\n %s' % (lineno + 1, key, line.strip(), new_line.strip())) | |
# TODO: add dry-run flag instead of always replacing | |
line = new_line | |
else: | |
match = MIGRATION_TYPE_RE.match(line) | |
if match: | |
migration_type = match.group(1) | |
match = MODEL_NAME_RE.match(line) | |
if match: | |
model_name = match.group(1) | |
match = NAME_RE.match(line) | |
if match: | |
name = match.group(1) | |
file_lines.append(line) | |
# Write out the new file with any fixed lines. | |
new_contents = ''.join(file_lines) | |
with open(file_path, 'w') as f: | |
f.write(new_contents) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment