[snowy] Add in reversion and django_evolution ftw
- From: Brad Taylor <btaylor src gnome org>
- To: svn-commits-list gnome org
- Subject: [snowy] Add in reversion and django_evolution ftw
- Date: Wed, 13 May 2009 17:28:18 -0400 (EDT)
commit 1da67e2e7138a132262b6575c123f4f0e3fe86ad
Author: Brad Taylor <brad getcoded net>
Date: Wed May 13 17:27:44 2009 -0400
Add in reversion and django_evolution ftw
---
lib/django_evolution/__init__.py | 15 +
lib/django_evolution/admin.py | 5 +
lib/django_evolution/db/__init__.py | 9 +
lib/django_evolution/db/common.py | 166 +++++
lib/django_evolution/db/mysql.py | 89 +++
lib/django_evolution/db/mysql_old.py | 2 +
lib/django_evolution/db/postgresql.py | 14 +
lib/django_evolution/db/postgresql_psycopg2.py | 2 +
lib/django_evolution/db/sqlite3.py | 200 ++++++
lib/django_evolution/diff.py | 205 ++++++
lib/django_evolution/evolve.py | 60 ++
lib/django_evolution/management/__init__.py | 100 +++
.../management/commands}/__init__.py | 0
lib/django_evolution/management/commands/evolve.py | 225 +++++++
lib/django_evolution/models.py | 27 +
lib/django_evolution/mutations.py | 475 ++++++++++++++
lib/django_evolution/signature.py | 94 +++
lib/django_evolution/tests/__init__.py | 27 +
lib/django_evolution/tests/add_field.py | 551 ++++++++++++++++
lib/django_evolution/tests/change_field.py | 687 ++++++++++++++++++++
.../django_evolution/tests/db}/__init__.py | 0
lib/django_evolution/tests/db/mysql.py | 254 ++++++++
lib/django_evolution/tests/db/mysql_old.py | 2 +
lib/django_evolution/tests/db/postgresql.py | 236 +++++++
.../tests/db/postgresql_psycopg2.py | 2 +
lib/django_evolution/tests/db/sqlite3.py | 540 +++++++++++++++
lib/django_evolution/tests/delete_app.py | 76 +++
lib/django_evolution/tests/delete_field.py | 268 ++++++++
lib/django_evolution/tests/delete_model.py | 131 ++++
lib/django_evolution/tests/generics.py | 71 ++
lib/django_evolution/tests/inheritance.py | 82 +++
lib/django_evolution/tests/models.py | 3 +
lib/django_evolution/tests/ordering.py | 49 ++
lib/django_evolution/tests/rename_field.py | 399 ++++++++++++
lib/django_evolution/tests/signature.py | 248 +++++++
lib/django_evolution/tests/sql_mutation.py | 93 +++
lib/django_evolution/tests/utils.py | 185 ++++++
lib/django_evolution/utils.py | 22 +
.../commands => lib/registration}/__init__.py | 0
{registration => lib/registration}/admin.py | 0
{registration => lib/registration}/forms.py | 0
.../registration}/locale/ar/LC_MESSAGES/django.mo | Bin 2135 -> 2135 bytes
.../registration}/locale/ar/LC_MESSAGES/django.po | 0
.../registration}/locale/bg/LC_MESSAGES/django.mo | Bin 2302 -> 2302 bytes
.../registration}/locale/bg/LC_MESSAGES/django.po | 0
.../registration}/locale/de/LC_MESSAGES/django.mo | Bin 1909 -> 1909 bytes
.../registration}/locale/de/LC_MESSAGES/django.po | 0
.../registration}/locale/el/LC_MESSAGES/django.mo | Bin 2424 -> 2424 bytes
.../registration}/locale/el/LC_MESSAGES/django.po | 0
.../registration}/locale/en/LC_MESSAGES/django.mo | Bin 367 -> 367 bytes
.../registration}/locale/en/LC_MESSAGES/django.po | 0
.../registration}/locale/es/LC_MESSAGES/django.mo | Bin 1909 -> 1909 bytes
.../registration}/locale/es/LC_MESSAGES/django.po | 0
.../locale/es_AR/LC_MESSAGES/django.mo | Bin 1849 -> 1849 bytes
.../locale/es_AR/LC_MESSAGES/django.po | 0
.../registration}/locale/fr/LC_MESSAGES/django.mo | Bin 1883 -> 1883 bytes
.../registration}/locale/fr/LC_MESSAGES/django.po | 0
.../registration}/locale/he/LC_MESSAGES/django.mo | Bin 1896 -> 1896 bytes
.../registration}/locale/he/LC_MESSAGES/django.po | 0
.../registration}/locale/is/LC_MESSAGES/django.mo | Bin 1476 -> 1476 bytes
.../registration}/locale/is/LC_MESSAGES/django.po | 0
.../registration}/locale/it/LC_MESSAGES/django.mo | Bin 1864 -> 1864 bytes
.../registration}/locale/it/LC_MESSAGES/django.po | 0
.../registration}/locale/ja/LC_MESSAGES/django.mo | Bin 2035 -> 2035 bytes
.../registration}/locale/ja/LC_MESSAGES/django.po | 0
.../registration}/locale/nl/LC_MESSAGES/django.mo | Bin 1898 -> 1898 bytes
.../registration}/locale/nl/LC_MESSAGES/django.po | 0
.../registration}/locale/pl/LC_MESSAGES/django.mo | Bin 1769 -> 1769 bytes
.../registration}/locale/pl/LC_MESSAGES/django.po | 0
.../locale/pt_BR/LC_MESSAGES/django.mo | Bin 1796 -> 1796 bytes
.../locale/pt_BR/LC_MESSAGES/django.po | 0
.../registration}/locale/ru/LC_MESSAGES/django.mo | Bin 2360 -> 2360 bytes
.../registration}/locale/ru/LC_MESSAGES/django.po | 0
.../registration}/locale/sr/LC_MESSAGES/django.mo | Bin 1966 -> 1966 bytes
.../registration}/locale/sr/LC_MESSAGES/django.po | 0
.../registration}/locale/sv/LC_MESSAGES/django.mo | Bin 1687 -> 1687 bytes
.../registration}/locale/sv/LC_MESSAGES/django.po | 0
.../locale/zh_CN/LC_MESSAGES/django.mo | Bin 1669 -> 1669 bytes
.../locale/zh_CN/LC_MESSAGES/django.po | 0
.../locale/zh_TW/LC_MESSAGES/django.mo | Bin 1669 -> 1669 bytes
.../locale/zh_TW/LC_MESSAGES/django.po | 0
.../registration/management}/__init__.py | 0
.../registration/management/commands}/__init__.py | 0
.../management/commands/cleanupregistration.py | 0
{registration => lib/registration}/models.py | 0
{registration => lib/registration}/signals.py | 0
{registration => lib/registration}/tests.py | 0
{registration => lib/registration}/urls.py | 0
{registration => lib/registration}/views.py | 0
lib/reversion/__init__.py | 11 +
lib/reversion/admin.py | 264 ++++++++
lib/reversion/helpers.py | 141 ++++
lib/reversion/managers.py | 60 ++
lib/reversion/middleware.py | 27 +
lib/reversion/models.py | 111 ++++
lib/reversion/registration.py | 59 ++
lib/reversion/revisions.py | 188 ++++++
lib/reversion/storage.py | 18 +
lib/reversion/templates/reversion/change_list.html | 14 +
.../templates/reversion/object_history.html | 35 +
.../templates/reversion/recover_form.html | 25 +
.../templates/reversion/recover_list.html | 39 ++
.../templates/reversion/revision_form.html | 33 +
settings.py | 17 +-
104 files changed, 6654 insertions(+), 2 deletions(-)
diff --git a/lib/django_evolution/__init__.py b/lib/django_evolution/__init__.py
new file mode 100644
index 0000000..323db75
--- /dev/null
+++ b/lib/django_evolution/__init__.py
@@ -0,0 +1,15 @@
+class EvolutionException(Exception):
+ def __init__(self,msg):
+ self.msg = msg
+
+ def __str__(self):
+ return str(self.msg)
+
+class CannotSimulate(EvolutionException):
+ pass
+
+class SimulationFailure(EvolutionException):
+ pass
+
+class EvolutionNotImplementedError(EvolutionException, NotImplementedError):
+ pass
\ No newline at end of file
diff --git a/lib/django_evolution/admin.py b/lib/django_evolution/admin.py
new file mode 100644
index 0000000..c41d472
--- /dev/null
+++ b/lib/django_evolution/admin.py
@@ -0,0 +1,5 @@
+from django.contrib import admin
+from django_evolution.models import Version, Evolution
+
+admin.site.register(Version)
+admin.site.register(Evolution)
diff --git a/lib/django_evolution/db/__init__.py b/lib/django_evolution/db/__init__.py
new file mode 100644
index 0000000..fcaf3c2
--- /dev/null
+++ b/lib/django_evolution/db/__init__.py
@@ -0,0 +1,9 @@
+# Establish the common EvolutionOperations instance, called evolver.
+
+from django.conf import settings
+
+module_name = ['django_evolution.db',settings.DATABASE_ENGINE]
+module = __import__('.'.join(module_name),{},{},[''])
+
+evolver = module.EvolutionOperations()
+
diff --git a/lib/django_evolution/db/common.py b/lib/django_evolution/db/common.py
new file mode 100644
index 0000000..ae332bf
--- /dev/null
+++ b/lib/django_evolution/db/common.py
@@ -0,0 +1,166 @@
+from django.core.management import color
+from django.db import connection, models
+import copy
+
+class BaseEvolutionOperations(object):
+ def quote_sql_param(self, param):
+ "Add protective quoting around an SQL string parameter"
+ if isinstance(param, basestring):
+ return u"'%s'" % unicode(param).replace(u"'",ur"\'")
+ else:
+ return param
+
+ def rename_table(self, old_db_tablename, db_tablename):
+ if old_db_tablename == db_tablename:
+ # No Operation
+ return []
+
+ qn = connection.ops.quote_name
+ params = (qn(old_db_tablename), qn(db_tablename))
+ return ['ALTER TABLE %s RENAME TO %s;' % params]
+
+ def delete_column(self, model, f):
+ qn = connection.ops.quote_name
+ params = (qn(model._meta.db_table), qn(f.column))
+
+ return ['ALTER TABLE %s DROP COLUMN %s CASCADE;' % params]
+
+ def delete_table(self, table_name):
+ qn = connection.ops.quote_name
+ return ['DROP TABLE %s;' % qn(table_name)]
+
+ def add_m2m_table(self, model, f):
+ final_output = []
+ qn = connection.ops.quote_name
+ opts = model._meta
+ style = color.no_style()
+
+ return connection.creation.sql_for_many_to_many_field(model, f, style)
+
+ def add_column(self, model, f, initial):
+ qn = connection.ops.quote_name
+
+ if f.rel:
+ # it is a foreign key field
+ # NOT NULL REFERENCES "django_evolution_addbasemodel" ("id") DEFERRABLE INITIALLY DEFERRED
+ # ALTER TABLE <tablename> ADD COLUMN <column name> NULL REFERENCES <tablename1> ("<colname>") DEFERRABLE INITIALLY DEFERRED
+ related_model = f.rel.to
+ related_table = related_model._meta.db_table
+ related_pk_col = related_model._meta.pk.name
+ constraints = ['%sNULL' % (not f.null and 'NOT ' or '')]
+ if f.unique or f.primary_key:
+ constraints.append('UNIQUE')
+ params = (qn(model._meta.db_table), qn(f.column), f.db_type(), ' '.join(constraints),
+ qn(related_table), qn(related_pk_col), connection.ops.deferrable_sql())
+ output = ['ALTER TABLE %s ADD COLUMN %s %s %s REFERENCES %s (%s) %s;' % params]
+ else:
+ null_constraints = '%sNULL' % (not f.null and 'NOT ' or '')
+ if f.unique or f.primary_key:
+ unique_constraints = 'UNIQUE'
+ else:
+ unique_constraints = ''
+
+ # At this point, initial can only be None if null=True, otherwise it is
+ # a user callable or the default AddFieldInitialCallback which will shortly raise an exception.
+ if initial is not None:
+ params = (qn(model._meta.db_table), qn(f.column), f.db_type(), unique_constraints)
+ output = ['ALTER TABLE %s ADD COLUMN %s %s %s;' % params]
+
+ if callable(initial):
+ params = (qn(model._meta.db_table), qn(f.column), initial(), qn(f.column))
+ output.append('UPDATE %s SET %s = %s WHERE %s IS NULL;' % params)
+ else:
+ params = (qn(model._meta.db_table), qn(f.column), qn(f.column))
+ output.append(('UPDATE %s SET %s = %%s WHERE %s IS NULL;' % params, (initial,)))
+
+ if not f.null:
+ # Only put this sql statement if the column cannot be null.
+ output.append(self.set_field_null(model, f, f.null))
+ else:
+ params = (qn(model._meta.db_table), qn(f.column), f.db_type(),' '.join([null_constraints, unique_constraints]))
+ output = ['ALTER TABLE %s ADD COLUMN %s %s %s;' % params]
+ return output
+
+ def set_field_null(self, model, f, null):
+ qn = connection.ops.quote_name
+ params = (qn(model._meta.db_table), qn(f.column),)
+ if null:
+ return 'ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL;' % params
+ else:
+ return 'ALTER TABLE %s ALTER COLUMN %s SET NOT NULL;' % params
+
+ def create_index(self, model, f):
+ "Returns the CREATE INDEX SQL statements."
+ output = []
+ qn = connection.ops.quote_name
+ style = color.no_style()
+
+ return connection.creation.sql_indexes_for_field(model, f, style)
+
+ def drop_index(self, model, f):
+ qn = connection.ops.quote_name
+ return ['DROP INDEX %s;' % qn(self.get_index_name(model, f))]
+
+ def get_index_name(self, model, f):
+ return '%s_%s' % (model._meta.db_table, f.column)
+
+ def change_null(self, model, field_name, new_null_attr, initial=None):
+ qn = connection.ops.quote_name
+ opts = model._meta
+ f = opts.get_field(field_name)
+ output = []
+ if new_null_attr:
+ # Setting null to True
+ opts = model._meta
+ params = (qn(opts.db_table), qn(f.column),)
+ output.append(self.set_field_null(model, f, new_null_attr))
+ else:
+ if initial is not None:
+ output = []
+ if callable(initial):
+ params = (qn(opts.db_table), qn(f.column), initial(), qn(f.column))
+ output.append('UPDATE %s SET %s = %s WHERE %s IS NULL;' % params)
+ else:
+ params = (qn(opts.db_table), qn(f.column), qn(f.column))
+ output.append(('UPDATE %s SET %s = %%s WHERE %s IS NULL;' % params, (initial,)))
+ output.append(self.set_field_null(model, f, new_null_attr))
+
+ return output
+
+ def change_max_length(self, model, field_name, new_max_length, initial=None):
+ qn = connection.ops.quote_name
+ opts = model._meta
+ f = opts.get_field(field_name)
+ f.max_length = new_max_length
+ params = (qn(opts.db_table), qn(f.column), f.db_type(), qn(f.column), f.db_type())
+ return ['ALTER TABLE %s ALTER COLUMN %s TYPE %s USING CAST(%s as %s);' % params]
+
+ def change_db_column(self, model, field_name, new_db_column, initial=None):
+ opts = model._meta
+ old_field = opts.get_field(field_name)
+ new_field = copy.copy(old_field)
+ new_field.column = new_db_column
+ return self.rename_column(opts, old_field, new_field)
+
+ def change_db_table(self, old_db_tablename, new_db_tablename):
+ return self.rename_table(old_db_tablename, new_db_tablename)
+
+ def change_db_index(self, model, field_name, new_db_index, initial=None):
+ f = model._meta.get_field(field_name)
+ f.db_index = new_db_index
+ if new_db_index:
+ return self.create_index(model, f)
+ else:
+ return self.drop_index(model, f)
+
+ def change_unique(self, model, field_name, new_unique_value, initial=None):
+ qn = connection.ops.quote_name
+ opts = model._meta
+ f = opts.get_field(field_name)
+ constraint_name = '%s_%s_key' % (opts.db_table, f.column,)
+ if new_unique_value:
+ params = (qn(opts.db_table), constraint_name, qn(f.column),)
+ return ['ALTER TABLE %s ADD CONSTRAINT %s UNIQUE(%s);' % params]
+ else:
+ params = (qn(opts.db_table), constraint_name,)
+ return ['ALTER TABLE %s DROP CONSTRAINT %s;' % params]
diff --git a/lib/django_evolution/db/mysql.py b/lib/django_evolution/db/mysql.py
new file mode 100644
index 0000000..04d18c7
--- /dev/null
+++ b/lib/django_evolution/db/mysql.py
@@ -0,0 +1,89 @@
+from django.core.management import color
+from django.db import connection
+
+from common import BaseEvolutionOperations
+
+class EvolutionOperations(BaseEvolutionOperations):
+ def rename_column(self, opts, old_field, f):
+ if old_field.column == f.column:
+ # No Operation
+ return []
+
+ qn = connection.ops.quote_name
+ style = color.no_style()
+
+ ###
+ col_type = f.db_type()
+ tablespace = f.db_tablespace or opts.db_tablespace
+ if col_type is None:
+ # Skip ManyToManyFields, because they're not represented as
+ # database columns in this table.
+ return []
+ # Make the definition (e.g. 'foo VARCHAR(30)') for this field.
+ field_output = [style.SQL_FIELD(qn(f.column)),
+ style.SQL_COLTYPE(col_type)]
+ field_output.append(style.SQL_KEYWORD('%sNULL' % (not f.null and 'NOT ' or '')))
+ if f.primary_key:
+ field_output.append(style.SQL_KEYWORD('PRIMARY KEY'))
+ if f.unique:
+ field_output.append(style.SQL_KEYWORD('UNIQUE'))
+ if tablespace and connection.features.supports_tablespaces and (f.unique or f.primary_key) and connection.features.autoindexes_primary_keys:
+ # We must specify the index tablespace inline, because we
+ # won't be generating a CREATE INDEX statement for this field.
+ field_output.append(connection.ops.tablespace_sql(tablespace, inline=True))
+ if f.rel:
+ field_output.append(style.SQL_KEYWORD('REFERENCES') + ' ' + \
+ style.SQL_TABLE(qn(f.rel.to._meta.db_table)) + ' (' + \
+ style.SQL_FIELD(qn(f.rel.to._meta.get_field(f.rel.field_name).column)) + ')' +
+ connection.ops.deferrable_sql()
+ )
+
+ params = (qn(opts.db_table), qn(old_field.column), ' '.join(field_output))
+ return ['ALTER TABLE %s CHANGE COLUMN %s %s;' % params]
+
+ def set_field_null(self, model, f, null):
+ qn = connection.ops.quote_name
+ params = (qn(model._meta.db_table), qn(f.column), f.db_type())
+ if null:
+ return 'ALTER TABLE %s MODIFY COLUMN %s %s DEFAULT NULL;' % params
+ else:
+ return 'ALTER TABLE %s MODIFY COLUMN %s %s NOT NULL;' % params
+
+ def change_max_length(self, model, field_name, new_max_length, initial=None):
+ qn = connection.ops.quote_name
+ opts = model._meta
+ f = opts.get_field(field_name)
+ f.max_length = new_max_length
+ params = {
+ 'table': qn(opts.db_table),
+ 'column': qn(f.column),
+ 'length': f.max_length,
+ 'type': f.db_type()
+ }
+ return ['UPDATE %(table)s SET %(column)s=LEFT(%(column)s,%(length)d);' % params,
+ 'ALTER TABLE %(table)s MODIFY COLUMN %(column)s %(type)s;' % params]
+
+ def drop_index(self, model, f):
+ qn = connection.ops.quote_name
+ params = (qn(self.get_index_name(model, f)), qn(model._meta.db_table))
+ return ['DROP INDEX %s ON %s;' % params]
+
+ def change_unique(self, model, field_name, new_unique_value, initial=None):
+ qn = connection.ops.quote_name
+ opts = model._meta
+ f = opts.get_field(field_name)
+ constraint_name = '%s' % (f.column,)
+ if new_unique_value:
+ params = (constraint_name, qn(opts.db_table), qn(f.column),)
+ return ['CREATE UNIQUE INDEX %s ON %s(%s);' % params]
+ else:
+ params = (constraint_name, qn(opts.db_table))
+ return ['DROP INDEX %s ON %s;' % params]
+
+ def rename_table(self, old_db_tablename, db_tablename):
+ if old_db_tablename == db_tablename:
+ return []
+
+ qn = connection.ops.quote_name
+ params = (qn(old_db_tablename), qn(db_tablename))
+ return ['RENAME TABLE %s TO %s;' % params]
diff --git a/lib/django_evolution/db/mysql_old.py b/lib/django_evolution/db/mysql_old.py
new file mode 100644
index 0000000..505a6b8
--- /dev/null
+++ b/lib/django_evolution/db/mysql_old.py
@@ -0,0 +1,2 @@
+# MySQL_old behaviour is identical to mysql base
+from mysql import *
\ No newline at end of file
diff --git a/lib/django_evolution/db/postgresql.py b/lib/django_evolution/db/postgresql.py
new file mode 100644
index 0000000..95bbf11
--- /dev/null
+++ b/lib/django_evolution/db/postgresql.py
@@ -0,0 +1,14 @@
+from django.db import connection
+
+from common import BaseEvolutionOperations
+
+class EvolutionOperations(BaseEvolutionOperations):
+ def rename_column(self, opts, old_field, new_field):
+ if old_field.column == new_field.column:
+ # No Operation
+ return []
+
+ qn = connection.ops.quote_name
+ params = (qn(opts.db_table), qn(old_field.column), qn(new_field.column))
+ return ['ALTER TABLE %s RENAME COLUMN %s TO %s;' % params]
+
\ No newline at end of file
diff --git a/lib/django_evolution/db/postgresql_psycopg2.py b/lib/django_evolution/db/postgresql_psycopg2.py
new file mode 100644
index 0000000..7557d22
--- /dev/null
+++ b/lib/django_evolution/db/postgresql_psycopg2.py
@@ -0,0 +1,2 @@
+# Psycopg2 behaviour is identical to Psycopg1
+from postgresql import *
\ No newline at end of file
diff --git a/lib/django_evolution/db/sqlite3.py b/lib/django_evolution/db/sqlite3.py
new file mode 100644
index 0000000..5a93080
--- /dev/null
+++ b/lib/django_evolution/db/sqlite3.py
@@ -0,0 +1,200 @@
+from django.core.management import color
+from django.db import connection, models
+
+from common import BaseEvolutionOperations
+
+TEMP_TABLE_NAME = 'TEMP_TABLE'
+
+class EvolutionOperations(BaseEvolutionOperations):
+ def delete_column(self, model, f):
+ qn = connection.ops.quote_name
+ output = []
+
+ field_list = [field for field in model._meta.local_fields
+ if f.name != field.name # Remove the field to be deleted
+ and field.db_type() is not None] # and any Generic fields
+ table_name = model._meta.db_table
+
+ output.extend(self.create_temp_table(field_list))
+ output.extend(self.copy_to_temp_table(table_name, field_list))
+ output.extend(self.delete_table(table_name))
+ output.extend(self.create_table(table_name, field_list))
+ output.extend(self.copy_from_temp_table(table_name, field_list))
+ output.extend(self.delete_table(TEMP_TABLE_NAME))
+
+ return output
+
+ def create_mock_model(self, model, fields):
+ field_sig_dict = {}
+ for f in fields:
+ field_sig_dict[f.name] = create_field_sig(field)
+
+ proj_sig = create_project_sig()
+ model_sig = create_model_sig(model)
+ model_sig['fields'] = field_sig_dict
+ mock_model = MockModel(proj_sig, model.app_name, model.model_name, model_sig, stub=False)
+
+ def copy_to_temp_table(self, source_table_name, field_list):
+ qn = connection.ops.quote_name
+ output = [self.create_temp_table(field_list)]
+ columns = []
+ for field in field_list:
+ if not models.ManyToManyField == field.__class__:
+ columns.append(qn(field.column))
+ column_names = ', '.join(columns)
+ return ['INSERT INTO %s SELECT %s FROM %s;' % (qn(TEMP_TABLE_NAME), column_names, qn(source_table_name))]
+
+ def copy_from_temp_table(self, dest_table_name, field_list):
+ qn = connection.ops.quote_name
+ columns = []
+ for field in field_list:
+ if not models.ManyToManyField == field.__class__:
+ columns.append(qn(field.column))
+ column_names = ', '.join(columns)
+ params = {
+ 'dest_table_name': qn(dest_table_name),
+ 'temp_table': qn(TEMP_TABLE_NAME),
+ 'column_names': column_names,
+ }
+
+ return ['INSERT INTO %(dest_table_name)s (%(column_names)s) SELECT %(column_names)s FROM %(temp_table)s;' % params]
+
+ def insert_to_temp_table(self, field, initial):
+ qn = connection.ops.quote_name
+
+ # At this point, initial can only be None if null=True, otherwise it is
+ # a user callable or the default AddFieldInitialCallback which will shortly raise an exception.
+ if initial is None:
+ return []
+
+ params = {
+ 'table_name': qn(TEMP_TABLE_NAME),
+ 'column_name': qn(field.column),
+ }
+
+ if callable(initial):
+ params['value'] = initial()
+ return ["UPDATE %(table_name)s SET %(column_name)s = %(value)s;" % params]
+ else:
+ return [("UPDATE %(table_name)s SET %(column_name)s = %%s;" % params, (initial,))]
+
+
+ def create_temp_table(self, field_list):
+ return self.create_table(TEMP_TABLE_NAME, field_list, True, False)
+
+ def create_indexes_for_table(self, table_name, field_list):
+ class FakeMeta(object):
+ def __init__(self, table_name, field_list):
+ self.db_table = table_name
+ self.local_fields = field_list
+ self.fields = field_list # Required for Pre QS-RF support
+ self.db_tablespace = None
+ self.managed = True
+
+ class FakeModel(object):
+ def __init__(self, table_name, field_list):
+ self._meta = FakeMeta(table_name, field_list)
+
+ style = color.no_style()
+ return connection.creation.sql_indexes_for_model(FakeModel(table_name, field_list), style)
+
+ def create_table(self, table_name, field_list, temporary=False, create_index=True):
+ qn = connection.ops.quote_name
+ output = []
+
+ create = ['CREATE']
+ if temporary:
+ create.append('TEMPORARY')
+ create.append('TABLE %s' % qn(table_name))
+ output = [' '.join(create)]
+ output.append('(')
+ columns = []
+ for field in field_list:
+ if not models.ManyToManyField == field.__class__:
+ column_name = qn(field.column)
+ column_type = field.db_type()
+ params = [column_name, column_type]
+ if field.null:
+ params.append('NULL')
+ else:
+ params.append('NOT NULL')
+ if field.unique:
+ params.append('UNIQUE')
+ if field.primary_key:
+ params.append('PRIMARY KEY')
+ columns.append(' '.join(params))
+
+ output.append(', '.join(columns))
+ output.append(');')
+ output = [''.join(output)]
+
+ if create_index:
+ output.extend(self.create_indexes_for_table(table_name, field_list))
+
+ return output
+
+ def rename_column(self, opts, old_field, new_field):
+ if old_field.column == new_field.column:
+ # No Operation
+ return []
+
+ original_fields = opts.local_fields
+ new_fields = []
+ for f in original_fields:
+ if f.db_type() is not None: # Ignore Generic Fields
+ if f.name == old_field.name:
+ new_fields.append(new_field)
+ else:
+ new_fields.append(f)
+
+ table_name = opts.db_table
+ output = []
+ output.extend(self.create_temp_table(new_fields))
+ output.extend(self.copy_to_temp_table(table_name, original_fields))
+ output.extend(self.delete_table(table_name))
+ output.extend(self.create_table(table_name, new_fields))
+ output.extend(self.copy_from_temp_table(table_name, new_fields))
+ output.extend(self.delete_table(TEMP_TABLE_NAME))
+
+ return output
+
+ def add_column(self, model, f, initial):
+ output = []
+ table_name = model._meta.db_table
+ original_fields = [field for field in model._meta.local_fields if field.db_type() is not None]
+ new_fields = original_fields
+ new_fields.append(f)
+
+ output.extend(self.create_temp_table(new_fields))
+ output.extend(self.copy_to_temp_table(table_name, original_fields))
+ output.extend(self.insert_to_temp_table(f, initial))
+ output.extend(self.delete_table(table_name))
+ output.extend(self.create_table(table_name, new_fields, create_index=False))
+ output.extend(self.copy_from_temp_table(table_name, new_fields))
+ output.extend(self.delete_table(TEMP_TABLE_NAME))
+ return output
+
+ def change_null(self, model, field_name, new_null_attr, initial=None):
+ return self.change_attribute(model, field_name, 'null', new_null_attr, initial)
+
+ def change_max_length(self, model, field_name, new_max_length, initial=None):
+ return self.change_attribute(model, field_name, 'max_length', new_max_length, initial)
+
+ def change_unique(self, model, field_name, new_unique_value, initial=None):
+ return self.change_attribute(model, field_name, '_unique', new_unique_value, initial)
+
+ def change_attribute(self, model, field_name, attr_name, new_attr_value, initial=None):
+ output = []
+ opts = model._meta
+ table_name = opts.db_table
+ setattr(opts.get_field(field_name), attr_name, new_attr_value)
+ fields = [f for f in opts.local_fields if f.db_type() is not None]
+
+ output.extend(self.create_temp_table(fields))
+ output.extend(self.copy_to_temp_table(table_name, fields))
+ output.extend(self.insert_to_temp_table(opts.get_field(field_name), initial))
+ output.extend(self.delete_table(table_name))
+ output.extend(self.create_table(table_name, fields, create_index=False))
+ output.extend(self.copy_from_temp_table(table_name, fields))
+ output.extend(self.delete_table(TEMP_TABLE_NAME))
+ return output
diff --git a/lib/django_evolution/diff.py b/lib/django_evolution/diff.py
new file mode 100644
index 0000000..41d063b
--- /dev/null
+++ b/lib/django_evolution/diff.py
@@ -0,0 +1,205 @@
+from django.db import models
+from django.db.models.fields.related import *
+
+from django_evolution import EvolutionException
+from django_evolution.mutations import DeleteField, AddField, DeleteModel, ChangeField
+from django_evolution.signature import ATTRIBUTE_DEFAULTS
+
+try:
+ set
+except ImportError:
+ from sets import Set as set #Python 2.3 Fallback
+
+class NullFieldInitialCallback(object):
+ def __init__(self, app, model, field):
+ self.app = app
+ self.model = model
+ self.field = field
+
+ def __repr__(self):
+ return '<<USER VALUE REQUIRED>>'
+
+ def __call__(self):
+ raise EvolutionException("Cannot use hinted evolution: AddField or ChangeField mutation for '%s.%s' in '%s' requires user-specified initial value." % (
+ self.model, self.field, self.app))
+
+def get_initial_value(app_label, model_name, field_name):
+ """Derive an initial value for a field.
+
+ If a default has been provided on the field definition or the field allows
+ for an empty string, that value will be used. Otherwise, a placeholder
+ callable will be used. This callable cannot actually be used in an
+ evolution, but will indicate that user input is required.
+ """
+ model = models.get_model(app_label, model_name)
+ field = model._meta.get_field(field_name)
+ if field and (field.has_default() or (field.empty_strings_allowed and field.blank)):
+ return field.get_default()
+ return NullFieldInitialCallback(app_label, model_name, field_name)
+
+class Diff(object):
+ """
+ A diff between two model signatures.
+
+ The resulting diff is contained in two attributes:
+
+ self.changed = {
+ app_label: {
+ 'changed': {
+ model_name : {
+ 'added': [ list of added field names ]
+ 'deleted': [ list of deleted field names ]
+ 'changed': {
+ field: [ list of modified property names ]
+ }
+ }
+ 'deleted': [ list of deleted model names ]
+ }
+ }
+ self.deleted = {
+ app_label: [ list of models in deleted app ]
+ }
+ """
+ def __init__(self, original, current):
+ self.original_sig = original
+ self.current_sig = current
+
+ self.changed = {}
+ self.deleted = {}
+
+ if self.original_sig.get('__version__', 1) != 1:
+ raise EvolutionException("Unknown version identifier in original signature: %s",
+ self.original_sig['__version__'])
+ if self.current_sig.get('__version__', 1) != 1:
+ raise EvolutionException("Unknown version identifier in target signature: %s",
+ self.current_sig['__version__'])
+
+ for app_name, old_app_sig in original.items():
+ if app_name == '__version__':
+ # Ignore the __version__ tag
+ continue
+ new_app_sig = self.current_sig.get(app_name, None)
+ if new_app_sig is None:
+ # App has been deleted
+ self.deleted[app_name] = old_app_sig.keys()
+ continue
+ for model_name, old_model_sig in old_app_sig.items():
+ new_model_sig = new_app_sig.get(model_name, None)
+ if new_model_sig is None:
+ # Model has been deleted
+ self.changed.setdefault(app_name,
+ {}).setdefault('deleted',
+ []).append(model_name)
+ continue
+ # Look for deleted or modified fields
+ for field_name,old_field_data in old_model_sig['fields'].items():
+ new_field_data = new_model_sig['fields'].get(field_name,None)
+ if new_field_data is None:
+ # Field has been deleted
+ self.changed.setdefault(app_name,
+ {}).setdefault('changed',
+ {}).setdefault(model_name,
+ {}).setdefault('deleted',
+ []).append(field_name)
+ continue
+ properties = set(old_field_data.keys())
+ properties.update(new_field_data.keys())
+ for prop in properties:
+ old_value = old_field_data.get(prop,
+ ATTRIBUTE_DEFAULTS.get(prop, None))
+ new_value = new_field_data.get(prop,
+ ATTRIBUTE_DEFAULTS.get(prop, None))
+ if old_value != new_value:
+ try:
+ if (prop == 'field_type' and
+ (old_value().get_internal_type() ==
+ new_value().get_internal_type())):
+ continue
+ except TypeError:
+ pass
+
+ # Field has been changed
+ self.changed.setdefault(app_name,
+ {}).setdefault('changed',
+ {}).setdefault(model_name,
+ {}).setdefault('changed',
+ {}).setdefault(field_name,[]).append(prop)
+ # Look for added fields
+ for field_name,new_field_data in new_model_sig['fields'].items():
+ old_field_data = old_model_sig['fields'].get(field_name,None)
+ if old_field_data is None:
+ self.changed.setdefault(app_name,
+ {}).setdefault('changed',
+ {}).setdefault(model_name,
+ {}).setdefault('added',
+ []).append(field_name)
+
+ def is_empty(self, ignore_apps=True):
+ """Is this an empty diff? i.e., is the source and target the same?
+
+ Set 'ignore_apps=False' if you wish to ignore changes caused by
+ deleted applications. This is used when you don't purge deleted
+ applications during an evolve.
+ """
+ if ignore_apps:
+ return not self.changed
+ else:
+ return not self.deleted and not self.changed
+
+ def __str__(self):
+ "Output an application signature diff in a human-readable format"
+ lines = []
+ for app_label in self.deleted:
+ lines.append('The application %s has been deleted' % app_label)
+ for app_label, app_changes in self.changed.items():
+ for model_name in app_changes.get('deleted', {}):
+ lines.append('The model %s.%s has been deleted' % (app_label, model_name))
+ for model_name, change in app_changes.get('changed', {}).items():
+ lines.append('In model %s.%s:' % (app_label, model_name))
+ for field_name in change.get('added',[]):
+ lines.append(" Field '%s' has been added" % field_name)
+ for field_name in change.get('deleted',[]):
+ lines.append(" Field '%s' has been deleted" % field_name)
+ for field_name,field_change in change.get('changed',{}).items():
+ lines.append(" In field '%s':" % field_name)
+ for prop in field_change:
+ lines.append(" Property '%s' has changed" % prop)
+ return '\n'.join(lines)
+
+ def evolution(self):
+ "Generate an evolution that would neutralize the diff"
+ mutations = {}
+ for app_label, app_changes in self.changed.items():
+ for model_name, change in app_changes.get('changed',{}).items():
+ for field_name in change.get('added',{}):
+ field_sig = self.current_sig[app_label][model_name]['fields'][field_name]
+ add_params = [(key,field_sig[key])
+ for key in field_sig.keys()
+ if key in ATTRIBUTE_DEFAULTS.keys()]
+ add_params.append(('field_type', field_sig['field_type']))
+
+ if field_sig['field_type'] != models.ManyToManyField and not field_sig.get('null', ATTRIBUTE_DEFAULTS['null']):
+ add_params.append(('initial', get_initial_value(app_label, model_name, field_name)))
+ if 'related_model' in field_sig:
+ add_params.append(('related_model', '%s' % field_sig['related_model']))
+ mutations.setdefault(app_label,[]).append(
+ AddField(model_name, field_name, **dict(add_params)))
+ for field_name in change.get('deleted',[]):
+ mutations.setdefault(app_label,[]).append(
+ DeleteField(model_name, field_name))
+ for field_name,field_change in change.get('changed',{}).items():
+ changed_attrs = {}
+ current_field_sig = self.current_sig[app_label][model_name]['fields'][field_name]
+ for prop in field_change:
+ if prop == 'related_model':
+ changed_attrs[prop] = current_field_sig[prop]
+ else:
+ changed_attrs[prop] = current_field_sig.get(prop, ATTRIBUTE_DEFAULTS[prop])
+ if changed_attrs.has_key('null') and \
+ current_field_sig['field_type'] != models.ManyToManyField and \
+ not current_field_sig.get('null', ATTRIBUTE_DEFAULTS['null']):
+ changed_attrs['initial'] = get_initial_value(app_label, model_name, field_name)
+ mutations.setdefault(app_label,[]).append(ChangeField(model_name, field_name, **changed_attrs))
+ for model_name in app_changes.get('deleted',{}):
+ mutations.setdefault(app_label,[]).append(DeleteModel(model_name))
+ return mutations
diff --git a/lib/django_evolution/evolve.py b/lib/django_evolution/evolve.py
new file mode 100644
index 0000000..5bafe8d
--- /dev/null
+++ b/lib/django_evolution/evolve.py
@@ -0,0 +1,60 @@
+import os
+import sys
+import copy
+
+from django.core.management.color import color_style
+from django.db import transaction, connection
+from django.db.models import loading
+
+from django_evolution import EvolutionException, CannotSimulate, SimulationFailure
+from django_evolution.models import Evolution
+from django_evolution.diff import Diff
+from django_evolution.mutations import SQLMutation
+
+def get_evolution_sequence(app):
+ "Obtain the full evolution sequence for an application"
+ try:
+ app_name = '.'.join(app.__name__.split('.')[:-1])
+ evolution_module = __import__(app_name + '.evolutions',{},{},[''])
+ return evolution_module.SEQUENCE
+ except:
+ return []
+
+def get_unapplied_evolutions(app):
+ "Obtain the list of unapplied evolutions for an application"
+ sequence = get_evolution_sequence(app)
+ app_label = app.__name__.split('.')[-2]
+ applied = [evo.label for evo in Evolution.objects.filter(app_label=app_label)]
+ return [seq for seq in sequence if seq not in applied]
+
+def get_mutations(app, evolution_labels):
+ """
+ Obtain the list of mutations described by the named evolutions.
+ """
+ # For each item in the evolution sequence. Check each item to see if it is
+ # a python file or an sql file.
+ try:
+ app_name = '.'.join(app.__name__.split('.')[:-1])
+ evolution_module = __import__(app_name + '.evolutions',{},{},[''])
+ except ImportError:
+ return []
+
+ mutations = []
+ for label in evolution_labels:
+ directory_name = os.path.dirname(evolution_module.__file__)
+ sql_file_name = os.path.join(directory_name, label+'.sql')
+ if os.path.exists(sql_file_name):
+ sql = []
+ sql_file = open(sql_file_name)
+ for line in sql_file:
+ sql.append(line)
+ mutations.append(SQLMutation(label, sql))
+ else:
+ try:
+ module_name = [evolution_module.__name__,label]
+ module = __import__('.'.join(module_name),{},{},[module_name]);
+ mutations.extend(module.MUTATIONS)
+ except ImportError, e:
+ raise EvolutionException('Error: Failed to find an SQL or Python evolution named %s' % label)
+
+ return mutations
diff --git a/lib/django_evolution/management/__init__.py b/lib/django_evolution/management/__init__.py
new file mode 100644
index 0000000..5e09c71
--- /dev/null
+++ b/lib/django_evolution/management/__init__.py
@@ -0,0 +1,100 @@
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle as pickle
+
+from django.dispatch import dispatcher
+from django.core.management.color import color_style
+from django.db.models import signals, get_apps
+
+from django_evolution import models as django_evolution
+from django_evolution.evolve import get_evolution_sequence, get_unapplied_evolutions
+from django_evolution.signature import create_project_sig
+from django_evolution.diff import Diff
+
+style = color_style()
+
+def evolution(app, created_models, verbosity=1, **kwargs):
+ """
+ A hook into syncdb's post_syncdb signal, that is used to notify the user
+ if a model evolution is necessary.
+ """
+ proj_sig = create_project_sig()
+ signature = pickle.dumps(proj_sig)
+
+ try:
+ latest_version = django_evolution.Version.objects.latest('when')
+ except django_evolution.Version.DoesNotExist:
+ # We need to create a baseline version.
+ if verbosity > 0:
+ print "Installing baseline version"
+
+ latest_version = django_evolution.Version(signature=signature)
+ latest_version.save()
+
+ for a in get_apps():
+ app_label = a.__name__.split('.')[-2]
+ sequence = get_evolution_sequence(a)
+ if sequence:
+ if verbosity > 0:
+ print 'Evolutions in %s baseline:' % app_label,', '.join(sequence)
+ for evo_label in sequence:
+ evolution = django_evolution.Evolution(app_label=app_label,
+ label=evo_label,
+ version=latest_version)
+ evolution.save()
+
+ unapplied = get_unapplied_evolutions(app)
+ if unapplied:
+ print style.NOTICE('There are unapplied evolutions for %s.' % app.__name__.split('.')[-2])
+
+ # Evolutions are checked over the entire project, so we only need to
+ # check once. We do this check when Django Evolutions itself is synchronized.
+ if app == django_evolution:
+ old_proj_sig = pickle.loads(str(latest_version.signature))
+
+ # If any models have been added, a baseline must be set
+ # for those new models
+ changed = False
+ for app_name, new_app_sig in proj_sig.items():
+ if app_name == '__version__':
+ # Ignore the __version__ tag
+ continue
+ old_app_sig = old_proj_sig.get(app_name, None)
+ if old_app_sig is None:
+ # App has been added
+ old_proj_sig[app_name] = proj_sig[app_name]
+ changed = True
+ continue
+ for model_name, new_model_sig in new_app_sig.items():
+ old_model_sig = old_app_sig.get(model_name, None)
+ if old_model_sig is None:
+ # Model has been added
+ old_proj_sig[app_name][model_name] = proj_sig[app_name][model_name]
+ changed = True
+
+ if changed:
+ if verbosity > 0:
+ print "Adding baseline version for new models"
+ latest_version = django_evolution.Version(signature=pickle.dumps(old_proj_sig))
+ latest_version.save()
+
+ # TODO: Model introspection step goes here.
+ # # If the current database state doesn't match the last
+ # # saved signature (as reported by latest_version),
+ # # then we need to update the Evolution table.
+ # actual_sig = introspect_project_sig()
+ # acutal = pickle.dumps(actual_sig)
+ # if actual != latest_version.signature:
+ # nudge = Version(signature=actual)
+ # nudge.save()
+ # latest_version = nudge
+
+ diff = Diff(old_proj_sig, proj_sig)
+ if not diff.is_empty():
+ print style.NOTICE('Project signature has changed - an evolution is required')
+ if verbosity > 1:
+ old_proj_sig = pickle.loads(str(latest_version.signature))
+ print diff
+
+signals.post_syncdb.connect(evolution)
diff --git a/registration/__init__.py b/lib/django_evolution/management/commands/__init__.py
similarity index 100%
copy from registration/__init__.py
copy to lib/django_evolution/management/commands/__init__.py
diff --git a/lib/django_evolution/management/commands/evolve.py b/lib/django_evolution/management/commands/evolve.py
new file mode 100644
index 0000000..b2e4bd0
--- /dev/null
+++ b/lib/django_evolution/management/commands/evolve.py
@@ -0,0 +1,225 @@
+from optparse import make_option
+import sys
+import copy
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle as pickle
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.core.management.base import BaseCommand, CommandError
+from django.db.models import get_apps, get_app, signals
+from django.db import connection, transaction
+
+from django_evolution import CannotSimulate, SimulationFailure, EvolutionException
+from django_evolution.diff import Diff
+from django_evolution.evolve import get_unapplied_evolutions, get_mutations
+from django_evolution.models import Version, Evolution
+from django_evolution.mutations import DeleteApplication
+from django_evolution.signature import create_project_sig
+from django_evolution.utils import write_sql, execute_sql
+
+class Command(BaseCommand):
+ option_list = BaseCommand.option_list + (
+ make_option('--noinput', action='store_false', dest='interactive', default=True,
+ help='Tells Django to NOT prompt the user for input of any kind.'),
+ make_option('--hint', action='store_true', dest='hint', default=False,
+ help='Generate an evolution script that would update the app.'),
+ make_option('--purge', action='store_true', dest='purge', default=False,
+ help='Generate evolutions to delete stale applications.'),
+ make_option('--sql', action='store_true', dest='compile_sql', default=False,
+ help='Compile a Django evolution script into SQL.'),
+ make_option('-x','--execute', action='store_true', dest='execute', default=False,
+ help='Apply the evolution to the database.'),
+ )
+ if '--verbosity' not in [opt.get_opt_string() for opt in BaseCommand.option_list]:
+ option_list += make_option('-v','--verbosity', action='store', dest='verbosity', default='1',
+ type='choice', choices=['0', '1', '2'],
+ help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
+
+ help = 'Evolve the models in a Django project.'
+ args = '<appname appname ...>'
+
+ requires_model_validation = False
+
+ def handle(self, *app_labels, **options):
+ verbosity = int(options['verbosity'])
+ interactive = options['interactive']
+ execute = options['execute']
+ compile_sql = options['compile_sql']
+ hint = options['hint']
+ purge = options['purge']
+
+ # Use the list of all apps, unless app labels are specified.
+ if app_labels:
+ if execute:
+ raise CommandError('Cannot specify an application name when executing evolutions.')
+ try:
+ app_list = [get_app(app_label) for app_label in app_labels]
+ except (ImproperlyConfigured, ImportError), e:
+ raise CommandError("%s. Are you sure your INSTALLED_APPS setting is correct?" % e)
+ else:
+ app_list = get_apps()
+
+ # Iterate over all applications running the mutations
+ evolution_required = False
+ simulated = True
+ sql = []
+ new_evolutions = []
+
+ current_proj_sig = create_project_sig()
+ current_signature = pickle.dumps(current_proj_sig)
+
+ try:
+ latest_version = Version.objects.latest('when')
+ database_sig = pickle.loads(str(latest_version.signature))
+ diff = Diff(database_sig, current_proj_sig)
+ except Evolution.DoesNotExist:
+ print self.style.ERROR("Can't evolve yet. Need to set an evolution baseline.")
+ sys.exit(1)
+
+ try:
+ for app in app_list:
+ app_label = app.__name__.split('.')[-2]
+ if hint:
+ evolutions = []
+ hinted_evolution = diff.evolution()
+ mutations = hinted_evolution.get(app_label,[])
+ else:
+ evolutions = get_unapplied_evolutions(app)
+ mutations = get_mutations(app, evolutions)
+
+ if mutations:
+ app_sql = ['-- Evolve application %s' % app_label]
+ evolution_required = True
+ for mutation in mutations:
+ # Only compile SQL if we want to show it
+ if compile_sql or execute:
+ app_sql.extend(mutation.mutate(app_label, database_sig))
+
+ # Now run the simulation, which will modify the signatures
+ try:
+ mutation.simulate(app_label, database_sig)
+ except CannotSimulate:
+ simulated = False
+ new_evolutions.extend(Evolution(app_label=app_label, label=label)
+ for label in evolutions)
+
+ if not execute:
+ if compile_sql:
+ write_sql(app_sql)
+ else:
+ print '#----- Evolution for %s' % app_label
+ print 'from django_evolution.mutations import *'
+ print 'from django.db import models'
+ print
+ print 'MUTATIONS = ['
+ print ' ',
+ print ',\n '.join(unicode(m) for m in mutations)
+ print ']'
+ print '#----------------------'
+
+ sql.extend(app_sql)
+ else:
+ if verbosity > 1:
+ print 'Application %s is up to date' % app_label
+
+ # Process the purged applications if requested to do so.
+ if purge:
+ if diff.deleted:
+ evolution_required = True
+ delete_app = DeleteApplication()
+ purge_sql = []
+ for app_label in diff.deleted:
+ if compile_sql or execute:
+ purge_sql.append('-- Purge application %s' % app_label)
+ purge_sql.extend(delete_app.mutate(app_label, database_sig))
+ delete_app.simulate(app_label, database_sig)
+
+ if not execute:
+ if compile_sql:
+ write_sql(purge_sql)
+ else:
+ print 'The following application(s) can be purged:'
+ for app_label in diff.deleted:
+ print ' ', app_label
+ print
+ sql.extend(purge_sql)
+ else:
+ if verbosity > 1:
+ print 'No applications need to be purged.'
+
+ except EvolutionException, e:
+ print self.style.ERROR(e)
+ sys.exit(1)
+
+ if simulated:
+ diff = Diff(database_sig, current_proj_sig)
+ if not diff.is_empty(not purge):
+ if hint:
+ print self.style.ERROR('Your models contain changes that Django Evolution cannot resolve automatically.')
+ print 'This is probably due to a currently unimplemented mutation type.'
+ print 'You will need to manually construct a mutation to resolve the remaining changes.'
+ else:
+ print self.style.ERROR('The stored evolutions do not completely resolve all model changes.')
+ print 'Run `./manage.py evolve --hint` to see a suggestion for the changes required.'
+ print
+ print 'The following are the changes that could not be resolved:'
+ print diff
+ sys.exit(1)
+ else:
+ print self.style.NOTICE('Evolution could not be simulated, possibly due to raw SQL mutations')
+
+ if evolution_required:
+ if execute:
+ # Now that we've worked out the mutations required,
+ # and we know they simulate OK, run the evolutions
+ if interactive:
+ confirm = raw_input("""
+You have requested a database evolution. This will alter tables
+and data currently in the %r database, and may result in
+IRREVERSABLE DATA LOSS. Evolutions should be *thoroughly* reviewed
+prior to execution.
+
+Are you sure you want to execute the evolutions?
+
+Type 'yes' to continue, or 'no' to cancel: """ % settings.DATABASE_NAME)
+ else:
+ confirm = 'yes'
+
+ if confirm.lower() == 'yes':
+ # Begin Transaction
+ transaction.enter_transaction_management()
+ transaction.managed(True)
+ cursor = connection.cursor()
+ try:
+ # Perform the SQL
+ execute_sql(cursor, sql)
+
+ # Now update the evolution table
+ version = Version(signature=current_signature)
+ version.save()
+ for evolution in new_evolutions:
+ evolution.version = version
+ evolution.save()
+
+ transaction.commit()
+ except Exception, ex:
+ transaction.rollback()
+ print self.style.ERROR('Error applying evolution: %s' % str(ex))
+ sys.exit(1)
+ transaction.leave_transaction_management()
+
+ if verbosity > 0:
+ print 'Evolution successful.'
+ else:
+ print self.style.ERROR('Evolution cancelled.')
+ elif not compile_sql:
+ if verbosity > 0:
+ if simulated:
+ print "Trial evolution successful."
+ print "Run './manage.py evolve %s--execute' to apply evolution." % (hint and '--hint ' or '')
+ else:
+ if verbosity > 0:
+ print 'No evolution required.'
diff --git a/lib/django_evolution/models.py b/lib/django_evolution/models.py
new file mode 100644
index 0000000..f2d8337
--- /dev/null
+++ b/lib/django_evolution/models.py
@@ -0,0 +1,27 @@
+from datetime import datetime
+
+from django.db import models
+
+class Version(models.Model):
+ signature = models.TextField()
+ when = models.DateTimeField(default=datetime.now)
+
+ class Meta:
+ ordering = ('-when',)
+ db_table = 'django_project_version'
+
+ def __unicode__(self):
+ if not self.evolutions.count():
+ return u'Hinted version, updated on %s' % self.when
+ return u'Stored version, updated on %s' % self.when
+
+class Evolution(models.Model):
+ version = models.ForeignKey(Version, related_name='evolutions')
+ app_label = models.CharField(max_length=200)
+ label = models.CharField(max_length=100)
+
+ class Meta:
+ db_table = 'django_evolution'
+
+ def __unicode__(self):
+ return u"Evolution %s, applied to %s" % (self.label, self.app_label)
diff --git a/lib/django_evolution/mutations.py b/lib/django_evolution/mutations.py
new file mode 100644
index 0000000..7335abd
--- /dev/null
+++ b/lib/django_evolution/mutations.py
@@ -0,0 +1,475 @@
+import copy
+
+from django.contrib.contenttypes import generic
+from django.db.models.fields import *
+from django.db.models.fields.related import *
+from django.db import models
+from django.utils.functional import curry
+
+from django_evolution.signature import ATTRIBUTE_DEFAULTS, create_field_sig
+from django_evolution import CannotSimulate, SimulationFailure, EvolutionNotImplementedError
+from django_evolution.db import evolver
+
+FK_INTEGER_TYPES = ['AutoField', 'PositiveIntegerField', 'PositiveSmallIntegerField']
+
+def create_field(proj_sig, field_name, field_type, field_attrs):
+ """
+ Create an instance of a field from a field signature. This is useful for
+ accessing all the database property mechanisms built into fields.
+ """
+ # related_model isn't a valid field attribute, so it must be removed
+ # prior to instantiating the field, but it must be restored
+ # to keep the signature consistent.
+ related_model = field_attrs.pop('related_model', None)
+ if related_model:
+ related_app_name, related_model_name = related_model.split('.')
+ related_model_sig = proj_sig[related_app_name][related_model_name]
+ to = MockModel(proj_sig, related_app_name, related_model_name, related_model_sig, stub=True)
+ field = field_type(to, name=field_name, **field_attrs)
+ field_attrs['related_model'] = related_model
+ else:
+ field = field_type(name=field_name, **field_attrs)
+ field.set_attributes_from_name(field_name)
+
+ return field
+
+class MockMeta(object):
+ """
+ A mockup of a models Options object, based on the model signature.
+
+ The stub argument is used to circumvent recursive relationships. If
+ 'stub' is provided, the constructed model will only be a stub -
+ it will only have a primary key field.
+ """
+ def __init__(self, proj_sig, app_name, model_name, model_sig, stub=False):
+ self.object_name = model_name
+ self.app_label = app_name
+ self.meta = {
+ 'order_with_respect_to': None,
+ 'has_auto_field': None
+ }
+ self.meta.update(model_sig['meta'])
+ self._fields = {}
+ self._many_to_many = {}
+ self.abstract = False
+
+ for field_name,field_sig in model_sig['fields'].items():
+ if not stub or field_sig.get('primary_key', False):
+ field_type = field_sig.pop('field_type')
+ field = create_field(proj_sig, field_name, field_type, field_sig)
+
+ if AutoField == type(field):
+ self.meta['has_auto_field'] = True
+ self.meta['auto_field'] = field
+
+ field_sig['field_type'] = field_type
+
+ if ManyToManyField == type(field):
+ self._many_to_many[field.name] = field
+ else:
+ self._fields[field.name] = field
+
+ field.set_attributes_from_name(field_name)
+ if field_sig.get('primary_key', False):
+ self.pk = field
+
+ def __getattr__(self, name):
+ return self.meta[name]
+
+ def get_field(self, name):
+ try:
+ return self._fields[name]
+ except KeyError:
+ try:
+ return self._many_to_many[name]
+ except KeyError:
+ raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, name))
+
+ def get_field_by_name(self, name):
+ return (self.get_field(name), None, True, None)
+
+ def get_fields(self):
+ return self._fields.values()
+
+ def get_many_to_many_fields(self):
+ return self._many_to_many.values()
+
+ local_fields = property(fget=get_fields)
+ local_many_to_many = property(fget=get_many_to_many_fields)
+
+class MockModel(object):
+ """
+ A mockup of a model object, providing sufficient detail
+ to derive database column and table names using the standard
+ Django fields.
+ """
+ def __init__(self, proj_sig, app_name, model_name, model_sig, stub=False):
+ self.app_name = app_name
+ self.model_name = model_name
+ self._meta = MockMeta(proj_sig, app_name, model_name, model_sig, stub)
+
+ def __eq__(self, other):
+ return self.app_name == other.app_name and self.model_name == other.model_name
+
+class MockRelated(object):
+ """
+ A mockup of django.db.models.related.RelatedObject, providing
+ sufficient detail to derive database column and table names using
+ the standard Django fields.
+ """
+ def __init__(self, related_model, model, field):
+ self.parent_model = related_model
+ self.model = model
+ self.field = field
+
+class BaseMutation:
+ def __init__(self):
+ pass
+
+ def mutate(self, app_label, proj_sig):
+ """
+ Performs the mutation on the database. Database changes will occur
+ after this function is invoked.
+ """
+ raise NotImplementedError()
+
+ def simulate(self, app_label, proj_sig):
+ """
+ Performs a simulation of the mutation to be performed. The purpose of
+ the simulate function is to ensure that after all mutations have occured
+ the database will emerge in a state consistent with the currently loaded
+ models file.
+ """
+ raise NotImplementedError()
+
+class SQLMutation(BaseMutation):
+ def __init__(self, tag, sql, update_func=None):
+ self.tag = tag
+ self.sql = sql
+ self.update_func = update_func
+
+ def __str__(self):
+ return "SQLMutation('%s')" % self.tag
+
+ def simulate(self, app_label, proj_sig):
+ "SQL mutations cannot be simulated unless an update function is provided"
+ if callable(self.update_func):
+ self.update_func(app_label, proj_sig)
+ else:
+ raise CannotSimulate('Cannot simulate SQLMutations')
+
+ def mutate(self, app_label, proj_sig):
+ "The mutation of an SQL mutation returns the raw SQL"
+ return self.sql
+
+class DeleteField(BaseMutation):
+ def __init__(self, model_name, field_name):
+
+ self.model_name = model_name
+ self.field_name = field_name
+
+ def __str__(self):
+ return "DeleteField('%s', '%s')" % (self.model_name, self.field_name)
+
+ def simulate(self, app_label, proj_sig):
+ app_sig = proj_sig[app_label]
+ model_sig = app_sig[self.model_name]
+
+ # If the field was used in the unique_together attribute, update it.
+ unique_together = model_sig['meta']['unique_together']
+ unique_together_list = []
+ for ut_index in range(0, len(unique_together), 1):
+ ut = unique_together[ut_index]
+ unique_together_fields = []
+ for field_name_index in range(0, len(ut), 1):
+ field_name = ut[field_name_index]
+ if not field_name == self.field_name:
+ unique_together_fields.append(field_name)
+
+ unique_together_list.append(tuple(unique_together_fields))
+ model_sig['meta']['unique_together'] = tuple(unique_together_list)
+
+ if model_sig['fields'][self.field_name].get('primary_key',False):
+ raise SimulationFailure('Cannot delete a primary key.')
+
+ # Simulate the deletion of the field.
+ try:
+ field_sig = model_sig['fields'].pop(self.field_name)
+ except KeyError, ke:
+ raise SimulationFailure('Cannot find the field named "%s".' % self.field_name)
+
+ def mutate(self, app_label, proj_sig):
+ app_sig = proj_sig[app_label]
+ model_sig = app_sig[self.model_name]
+ field_sig = model_sig['fields'][self.field_name]
+
+ model = MockModel(proj_sig, app_label, self.model_name, model_sig)
+ # Temporarily remove field_type from the field signature
+ # so that we can create a field
+ field_type = field_sig.pop('field_type')
+ field = create_field(proj_sig, self.field_name, field_type, field_sig)
+ field_sig['field_type'] = field_type
+
+ if field_type == models.ManyToManyField:
+ sql_statements = evolver.delete_table(field._get_m2m_db_table(model._meta))
+ else:
+ sql_statements = evolver.delete_column(model, field)
+
+ return sql_statements
+
+class AddField(BaseMutation):
+ def __init__(self, model_name, field_name, field_type, initial=None, **kwargs):
+ self.model_name = model_name
+ self.field_name = field_name
+ self.field_type = field_type
+ self.field_attrs = kwargs
+ self.initial = initial
+
+ def __str__(self):
+ params = (self.model_name, self.field_name, self.field_type.__name__)
+ str_output = ["'%s', '%s', models.%s" % params]
+
+ if self.initial is not None:
+ str_output.append('initial=%s' % repr(self.initial))
+
+ for key,value in self.field_attrs.items():
+ str_output.append("%s=%s" % (key,repr(value)))
+ return 'AddField(' + ', '.join(str_output) + ')'
+
+ def simulate(self, app_label, proj_sig):
+ app_sig = proj_sig[app_label]
+ model_sig = app_sig[self.model_name]
+
+ if self.field_name in model_sig['fields']:
+ raise SimulationFailure("Model '%s.%s' already has a field named '%s'" % (
+ app_label, self.model_name, self.field_name))
+
+ if self.field_type != models.ManyToManyField and not self.field_attrs.get('null', ATTRIBUTE_DEFAULTS['null']):
+ if self.initial is None:
+ raise SimulationFailure("Cannot create new column '%s' on '%s.%s' without a non-null initial value." % (
+ self.field_name, app_label, self.model_name))
+
+ model_sig['fields'][self.field_name] = {
+ 'field_type': self.field_type,
+ }
+ model_sig['fields'][self.field_name].update(self.field_attrs)
+
+ def mutate(self, app_label, proj_sig):
+ if self.field_type == models.ManyToManyField:
+ return self.add_m2m_table(app_label, proj_sig)
+ else:
+ return self.add_column(app_label, proj_sig)
+
+ def add_column(self, app_label, proj_sig):
+ app_sig = proj_sig[app_label]
+ model_sig = app_sig[self.model_name]
+
+ model = MockModel(proj_sig, app_label, self.model_name, model_sig)
+ field = create_field(proj_sig, self.field_name, self.field_type, self.field_attrs)
+
+ sql_statements = evolver.add_column(model, field, self.initial)
+
+ # Create SQL index if necessary
+ sql_statements.extend(evolver.create_index(model, field))
+
+ return sql_statements
+
+ def add_m2m_table(self, app_label, proj_sig):
+ app_sig = proj_sig[app_label]
+ model_sig = app_sig[self.model_name]
+
+ model = MockModel(proj_sig, app_label, self.model_name, model_sig)
+ field = create_field(proj_sig, self.field_name, self.field_type, self.field_attrs)
+ field.m2m_db_table = curry(field._get_m2m_db_table, model._meta)
+
+ related_app_label, related_model_name = self.field_attrs['related_model'].split('.')
+ related_sig = proj_sig[related_app_label][related_model_name]
+ related_model = MockModel(proj_sig, related_app_label, related_model_name, related_sig)
+ related = MockRelated(related_model, model, field)
+
+ field.m2m_column_name = curry(field._get_m2m_column_name, related)
+ field.m2m_reverse_name = curry(field._get_m2m_reverse_name, related)
+
+ sql_statements = evolver.add_m2m_table(model, field)
+
+ return sql_statements
+
+class RenameField(BaseMutation):
+ def __init__(self, model_name, old_field_name, new_field_name,
+ db_column=None, db_table=None):
+ self.model_name = model_name
+ self.old_field_name = old_field_name
+ self.new_field_name = new_field_name
+ self.db_column = db_column
+ self.db_table = db_table
+
+ def __str__(self):
+ params = "'%s', '%s', '%s'" % (self.model_name, self.old_field_name, self.new_field_name)
+
+ if self.db_column:
+ params = params + ", db_column='%s'" % (self.db_column)
+ if self.db_table:
+ params = params + ", db_table='%s'" % (self.db_table)
+
+ return "RenameField(%s)" % params
+
+ def simulate(self, app_label, proj_sig):
+ app_sig = proj_sig[app_label]
+ model_sig = app_sig[self.model_name]
+ field_dict = model_sig['fields']
+ field_sig = field_dict[self.old_field_name]
+
+ if models.ManyToManyField == field_sig['field_type']:
+ if self.db_table:
+ field_sig['db_table'] = self.db_table
+ else:
+ field_sig.pop('db_table',None)
+ elif self.db_column:
+ field_sig['db_column'] = self.db_column
+ else:
+ # db_column and db_table were not specified (or not specified for the
+ # appropriate field types). Clear the old value if one was set. This
+ # amounts to resetting the column or table name to the Django default name
+ field_sig.pop('db_column',None)
+
+ field_dict[self.new_field_name] = field_dict.pop(self.old_field_name)
+
+ def mutate(self, app_label, proj_sig):
+ app_sig = proj_sig[app_label]
+ model_sig = app_sig[self.model_name]
+ old_field_sig = model_sig['fields'][self.old_field_name]
+
+ # Temporarily remove the field type so that we can create mock field instances
+ field_type = old_field_sig.pop('field_type')
+ # Duplicate the old field sig, and apply the table/column changes
+ new_field_sig = copy.copy(old_field_sig)
+ if models.ManyToManyField == field_type:
+ if self.db_table:
+ new_field_sig['db_table'] = self.db_table
+ else:
+ new_field_sig.pop('db_table', None)
+ elif self.db_column:
+ new_field_sig['db_column'] = self.db_column
+ else:
+ new_field_sig.pop('db_column', None)
+
+ # Create the mock field instances.
+ old_field = create_field(proj_sig, self.old_field_name, field_type, old_field_sig)
+ new_field = create_field(proj_sig, self.new_field_name, field_type, new_field_sig)
+
+ # Restore the field type to the signature
+ old_field_sig['field_type'] = field_type
+
+ opts = MockMeta(proj_sig, app_label, self.model_name, model_sig)
+ if models.ManyToManyField == field_type:
+ old_m2m_table = old_field._get_m2m_db_table(opts)
+ new_m2m_table = new_field._get_m2m_db_table(opts)
+
+ return evolver.rename_table(old_m2m_table, new_m2m_table)
+ else:
+ return evolver.rename_column(opts, old_field, new_field)
+
+class ChangeField(BaseMutation):
+ def __init__(self, model_name, field_name, initial=None, **kwargs):
+ self.model_name = model_name
+ self.field_name = field_name
+ self.field_attrs = kwargs
+ self.initial = initial
+
+ def __str__(self):
+ params = (self.model_name, self.field_name)
+ str_output = ["'%s', '%s'" % params]
+
+ str_output.append('initial=%s' % repr(self.initial))
+
+ for attr_name, attr_value in self.field_attrs.items():
+ if str == type(attr_value):
+ str_attr_value = "'%s'" % attr_value
+ else:
+ str_attr_value = str(attr_value)
+ str_output.append('%s=%s' % (attr_name, str_attr_value,))
+
+ return 'ChangeField(' + ', '.join(str_output) + ')'
+
+
+ def simulate(self, app_label, proj_sig):
+ app_sig = proj_sig[app_label]
+ model_sig = app_sig[self.model_name]
+ field_sig = model_sig['fields'][self.field_name]
+
+ # Catch for no-op changes.
+ for field_attr, attr_value in self.field_attrs.items():
+ field_sig[field_attr] = attr_value
+
+ if self.field_attrs.has_key('null'):
+ if field_sig['field_type'] != models.ManyToManyField and not self.field_attrs['null']:
+ if self.initial is None:
+ raise SimulationFailure("Cannot change column '%s' on '%s.%s' without a non-null initial value." % (
+ self.field_name, app_label, self.model_name))
+
+ def mutate(self, app_label, proj_sig):
+ app_sig = proj_sig[app_label]
+ model_sig = app_sig[self.model_name]
+ old_field_sig = model_sig['fields'][self.field_name]
+ model = MockModel(proj_sig, app_label, self.model_name, model_sig)
+
+ sql_statements = []
+ for field_attr, attr_value in self.field_attrs.items():
+ old_field_attr = old_field_sig.get(field_attr, ATTRIBUTE_DEFAULTS[field_attr])
+ # Avoid useless SQL commands if nothing has changed.
+ if not old_field_attr == attr_value:
+ try:
+ evolver_func = getattr(evolver, 'change_%s' % field_attr)
+ if field_attr == 'null':
+ sql_statements.extend(evolver_func(model, self.field_name, attr_value, self.initial))
+ elif field_attr == 'db_table':
+ sql_statements.extend(evolver_func(old_field_attr, attr_value))
+ else:
+ sql_statements.extend(evolver_func(model, self.field_name, attr_value))
+ except AttributeError, ae:
+ raise EvolutionNotImplementedError("ChangeField does not support modifying the '%s' attribute on '%s.%s'." % (field_attr, self.model_name, self.field_name))
+
+ return sql_statements
+
+class DeleteModel(BaseMutation):
+ def __init__(self, model_name):
+ self.model_name = model_name
+
+ def __str__(self):
+ return "DeleteModel(%r)" % self.model_name
+
+ def simulate(self, app_label, proj_sig):
+ app_sig = proj_sig[app_label]
+ # Simulate the deletion of the model.
+ del app_sig[self.model_name]
+
+ def mutate(self, app_label, proj_sig):
+ app_sig = proj_sig[app_label]
+ model_sig = app_sig[self.model_name]
+
+ sql_statements = []
+ model = MockModel(proj_sig, app_label, self.model_name, model_sig)
+ # Remove any many to many tables.
+ for field_name, field_sig in model_sig['fields'].items():
+ if field_sig['field_type'] == models.ManyToManyField:
+ field = model._meta.get_field(field_name)
+ m2m_table = field._get_m2m_db_table(model._meta)
+ sql_statements += evolver.delete_table(m2m_table)
+ # Remove the table itself.
+ sql_statements += evolver.delete_table(model._meta.db_table)
+
+ return sql_statements
+
+class DeleteApplication(BaseMutation):
+ def __str__(self):
+ return 'DeleteApplication()'
+
+ def simulate(self, app_label, proj_sig):
+ del proj_sig[app_label]
+
+ def mutate(self, app_label, proj_sig):
+ sql_statements = []
+ app_sig = proj_sig[app_label]
+ for model_name in app_sig.keys():
+ sql_statements.extend(DeleteModel(model_name).mutate(app_label, proj_sig))
+ return sql_statements
diff --git a/lib/django_evolution/signature.py b/lib/django_evolution/signature.py
new file mode 100644
index 0000000..2c275c9
--- /dev/null
+++ b/lib/django_evolution/signature.py
@@ -0,0 +1,94 @@
+from django.db.models import get_apps, get_models
+from django.db.models.fields.related import *
+from django.conf import global_settings
+from django.contrib.contenttypes import generic
+
+ATTRIBUTE_DEFAULTS = {
+ # Common to all fields
+ 'primary_key': False,
+ 'max_length' : None,
+ 'unique' : False,
+ 'null' : False,
+ 'db_index' : False,
+ 'db_column' : None,
+ 'db_tablespace' : global_settings.DEFAULT_TABLESPACE,
+ 'rel': None,
+ # Decimal Field
+ 'max_digits' : None,
+ 'decimal_places' : None,
+ # ManyToManyField
+ 'db_table': None
+}
+
+# r7790 modified the unique attribute of the meta model to be
+# a property that combined an underlying _unique attribute with
+# the primary key attribute. We need the underlying property,
+# but we don't want to affect old signatures (plus the
+# underscore is ugly :-).
+ATTRIBUTE_ALIASES = {
+ 'unique': '_unique'
+}
+
+def create_field_sig(field):
+ field_sig = {
+ 'field_type': field.__class__,
+ }
+
+ for attrib in ATTRIBUTE_DEFAULTS.keys():
+ alias = ATTRIBUTE_ALIASES.get(attrib, attrib)
+ if hasattr(field,alias):
+ value = getattr(field,alias)
+ if isinstance(field, ForeignKey):
+ if attrib == 'db_index':
+ default = True
+ else:
+ default = ATTRIBUTE_DEFAULTS[attrib]
+ else:
+ default = ATTRIBUTE_DEFAULTS[attrib]
+ # only store non-default values
+ if default != value:
+ field_sig[attrib] = value
+
+ rel = field_sig.pop('rel', None)
+ if rel:
+ field_sig['related_model'] = '.'.join([rel.to._meta.app_label, rel.to._meta.object_name])
+ return field_sig
+
+def create_model_sig(model):
+ model_sig = {
+ 'meta': {
+ 'unique_together': model._meta.unique_together,
+ 'db_tablespace': model._meta.db_tablespace,
+ 'db_table': model._meta.db_table,
+ 'pk_column': model._meta.pk.column,
+ },
+ 'fields': {},
+ }
+
+ for field in model._meta.local_fields + model._meta.local_many_to_many:
+ # Special case - don't generate a signature for generic relations
+ if not isinstance(field, generic.GenericRelation):
+ model_sig['fields'][field.name] = create_field_sig(field)
+ return model_sig
+
+def create_app_sig(app):
+ """
+ Creates a dictionary representation of the models in a given app.
+ Only those attributes that are interesting from a schema-evolution
+ perspective are included.
+ """
+ app_sig = {}
+ for model in get_models(app):
+ app_sig[model._meta.object_name] = create_model_sig(model)
+ return app_sig
+
+def create_project_sig():
+ """
+ Create a dictionary representation of the apps in a given project.
+ """
+ proj_sig = {
+ '__version__': 1,
+ }
+ for app in get_apps():
+ proj_sig[app.__name__.split('.')[-2]] = create_app_sig(app)
+ return proj_sig
diff --git a/lib/django_evolution/tests/__init__.py b/lib/django_evolution/tests/__init__.py
new file mode 100644
index 0000000..7d843b9
--- /dev/null
+++ b/lib/django_evolution/tests/__init__.py
@@ -0,0 +1,27 @@
+import unittest
+
+from signature import tests as signature_tests
+from add_field import tests as add_field_tests
+from delete_field import tests as delete_field_tests
+from delete_model import tests as delete_model_tests
+from delete_app import tests as delete_app_tests
+from rename_field import tests as rename_field_tests
+from change_field import tests as change_field_tests
+from sql_mutation import tests as sql_mutation_tests
+from ordering import tests as ordering_tests
+from generics import tests as generics_tests
+from inheritance import tests as inheritance_tests
+# Define doctests
+__test__ = {
+ 'signature': signature_tests,
+ 'add_field': add_field_tests,
+ 'delete_field': delete_field_tests,
+ 'delete_model': delete_model_tests,
+ 'delete_app': delete_app_tests,
+ 'rename_field': rename_field_tests,
+ 'change_field': change_field_tests,
+ 'sql_mutation': sql_mutation_tests,
+ 'ordering': ordering_tests,
+ 'generics': generics_tests,
+ 'inheritance': inheritance_tests
+}
diff --git a/lib/django_evolution/tests/add_field.py b/lib/django_evolution/tests/add_field.py
new file mode 100644
index 0000000..9988dd1
--- /dev/null
+++ b/lib/django_evolution/tests/add_field.py
@@ -0,0 +1,551 @@
+from django_evolution.tests.utils import test_sql_mapping
+
+tests = r"""
+# The AddField tests will aim to test the following usecases:
+# Field resulting in a new database column.
+# Field resulting in a new database column with a non-default name.
+# Field resulting in a new database column in a table with a non-default name.
+# Primary key field.
+# Indexed field
+# Unique field.
+# Null field
+#
+# Foreign Key field.
+# M2M field between models with default table names.
+# M2M field between models with non-default table names.
+# M2M field between self
+>>> from datetime import datetime
+
+>>> from django.db import models
+
+>>> from django_evolution.mutations import AddField, DeleteField
+>>> from django_evolution.tests.utils import test_proj_sig, execute_test_sql, register_models, deregister_models
+>>> from django_evolution.diff import Diff
+>>> from django_evolution import signature
+>>> from django_evolution import models as test_app
+
+>>> import copy
+
+>>> class AddSequenceFieldInitial(object):
+... def __init__(self, suffix):
+... self.suffix = suffix
+...
+... def __call__(self):
+... from django.db import connection
+... qn = connection.ops.quote_name
+... return qn('int_field')
+
+>>> class AddAnchor1(models.Model):
+... value = models.IntegerField()
+
+>>> class AddAnchor2(models.Model):
+... value = models.IntegerField()
+... class Meta:
+... db_table = 'custom_add_anchor_table'
+
+>>> class AddBaseModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+
+>>> class CustomTableModel(models.Model):
+... value = models.IntegerField()
+... alt_value = models.CharField(max_length=20)
+... class Meta:
+... db_table = 'custom_table_name'
+
+# Store the base signatures
+>>> anchors = (
+... ('AddAnchor1', AddAnchor1),
+... ('AddAnchor2', AddAnchor2)
+... )
+
+>>> custom_model = ('CustomTableModel', CustomTableModel)
+>>> custom = register_models(custom_model)
+>>> custom_table_sig = test_proj_sig(custom_model)
+
+>>> test_model = ('TestModel', AddBaseModel)
+>>> start = register_models(*anchors)
+>>> start.update(register_models(test_model))
+>>> start_sig = test_proj_sig(test_model, *anchors)
+
+# Add non-null field with non-callable initial value
+>>> class AddNonNullColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.IntegerField()
+
+>>> end = register_models(('TestModel', AddNonNullColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddNonNullColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']] #AddNonNullColumnModel
+["AddField('TestModel', 'added_field', models.IntegerField, initial=<<USER VALUE REQUIRED>>)"]
+
+# Evolution won't run as-is
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+Traceback (most recent call last):
+...
+EvolutionException: Cannot use hinted evolution: AddField or ChangeField mutation for 'TestModel.added_field' in 'tests' requires user-specified initial value.
+
+# First try without an initial value. This will fail
+>>> evolution = [AddField('TestModel', 'added_field', models.IntegerField)]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+Traceback (most recent call last):
+...
+SimulationFailure: Cannot create new column 'added_field' on 'tests.TestModel' without a non-null initial value.
+
+# Now try with an explicitly null initial value. This will also fail
+>>> evolution = [AddField('TestModel', 'added_field', models.IntegerField, initial=None)]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+Traceback (most recent call last):
+...
+SimulationFailure: Cannot create new column 'added_field' on 'tests.TestModel' without a non-null initial value.
+
+# Now try with a good initial value
+>>> evolution = [AddField('TestModel', 'added_field', models.IntegerField, initial=1)]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddNonNullNonCallableColumnModel
+%(AddNonNullNonCallableColumnModel)s
+
+# Now try with a good callable initial value
+>>> evolution = [AddField('TestModel', 'added_field', models.IntegerField, initial=AddSequenceFieldInitial('AddNonNullCallableColumnModel'))]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddNonNullCallableColumnModel
+%(AddNonNullCallableColumnModel)s
+
+# Add nullable column with initial data
+>>> class AddNullColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.IntegerField(null=True)
+
+>>> end = register_models(('TestModel',AddNullColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddNullColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']] #AddNullColumnModel
+["AddField('TestModel', 'added_field', models.IntegerField, null=True)"]
+
+>>> evolution = [AddField('TestModel', 'added_field', models.IntegerField, initial=1, null=True)]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddNullColumnWithInitialColumnModel
+%(AddNullColumnWithInitialColumnModel)s
+
+# Add a field that requires string-form initial data
+>>> class AddStringColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.CharField(max_length=10)
+
+>>> end = register_models(('TestModel',AddStringColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddStringColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']] # AddStringColumnModel
+["AddField('TestModel', 'added_field', models.CharField, initial=<<USER VALUE REQUIRED>>, max_length=10)"]
+
+>>> evolution = [AddField('TestModel', 'added_field', models.CharField, initial="abc's xyz", max_length=10)]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddStringColumnModel
+%(AddStringColumnModel)s
+
+# Add a string field that allows empty strings as initial values
+>>> class AddBlankStringColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.CharField(max_length=10, blank=True)
+
+>>> end = register_models(('TestModel',AddBlankStringColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddBlankStringColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']] # AddBlankStringColumnModel
+["AddField('TestModel', 'added_field', models.CharField, initial='', max_length=10)"]
+
+# Add a field that requires date-form initial data
+>>> class AddDateColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.DateTimeField()
+
+>>> end = register_models(('TestModel',AddDateColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddDateColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']] # AddDateColumnModel
+["AddField('TestModel', 'added_field', models.DateTimeField, initial=<<USER VALUE REQUIRED>>)"]
+
+>>> new_date = datetime(2007,12,13,16,42,0)
+>>> evolution = [AddField('TestModel', 'added_field', models.DateTimeField, initial=new_date)]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddDateColumnModel
+%(AddDateColumnModel)s
+
+# Add column with default value
+>>> class AddDefaultColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.IntegerField(default=42)
+
+>>> end = register_models(('TestModel',AddDefaultColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddDefaultColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']] #AddDefaultColumnModel
+["AddField('TestModel', 'added_field', models.IntegerField, initial=42)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddDefaultColumnModel
+%(AddDefaultColumnModel)s
+
+# Add column with an empty string as the default value
+>>> class AddEmptyStringDefaultColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.CharField(max_length=20, default='')
+
+>>> end = register_models(('TestModel',AddEmptyStringDefaultColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddEmptyStringDefaultColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']] #AddEmptyStringDefaultColumnModel
+["AddField('TestModel', 'added_field', models.CharField, initial=u'', max_length=20)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddEmptyStringDefaultColumnModel
+%(AddEmptyStringDefaultColumnModel)s
+
+
+# Null field
+>>> class AddNullColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.IntegerField(null=True)
+
+>>> end = register_models(('TestModel', AddNullColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', AddNullColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']] #AddNullColumnModel
+["AddField('TestModel', 'added_field', models.IntegerField, null=True)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddNullColumnModel
+%(AddNullColumnModel)s
+
+# Field resulting in a new database column with a non-default name.
+>>> class NonDefaultColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... add_field = models.IntegerField(db_column='non-default_column', null=True)
+
+>>> end = register_models(('TestModel',NonDefaultColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',NonDefaultColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'add_field', models.IntegerField, null=True, db_column='non-default_column')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #NonDefaultColumnModel
+%(NonDefaultColumnModel)s
+
+# Field resulting in a new database column in a table with a non-default name.
+>>> class AddColumnCustomTableModel(models.Model):
+... value = models.IntegerField()
+... alt_value = models.CharField(max_length=20)
+... added_field = models.IntegerField(null=True)
+... class Meta:
+... db_table = 'custom_table_name'
+
+>>> end = register_models(('CustomTableModel',AddColumnCustomTableModel))
+>>> end_sig = test_proj_sig(('CustomTableModel',AddColumnCustomTableModel))
+>>> d = Diff(custom_table_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('CustomTableModel', 'added_field', models.IntegerField, null=True)"]
+
+>>> test_sig = copy.deepcopy(custom_table_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(custom, end, test_sql) #AddColumnCustomTableModel
+%(AddColumnCustomTableModel)s
+
+# Add Primary key field.
+# Delete of old Primary Key is prohibited.
+>>> class AddPrimaryKeyModel(models.Model):
+... my_primary_key = models.AutoField(primary_key=True)
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+
+>>> end = register_models(('TestModel', AddPrimaryKeyModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddPrimaryKeyModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'my_primary_key', models.AutoField, initial=<<USER VALUE REQUIRED>>, primary_key=True)", "DeleteField('TestModel', 'id')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+
+>>> for mutation in [AddField('TestModel', 'my_primary_key', models.AutoField, initial=AddSequenceFieldInitial('AddPrimaryKeyModel'), primary_key=True), DeleteField('TestModel', 'id')]:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+Traceback (most recent call last):
+...
+SimulationFailure: Cannot delete a primary key.
+
+# Indexed field
+>>> class AddIndexedColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... add_field = models.IntegerField(db_index=True, null=True)
+
+>>> end = register_models(('TestModel',AddIndexedColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddIndexedColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'add_field', models.IntegerField, null=True, db_index=True)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql, debug=False) #AddIndexedColumnModel
+%(AddIndexedColumnModel)s
+
+# Unique field.
+>>> class AddUniqueColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.IntegerField(unique=True, null=True)
+
+>>> end = register_models(('TestModel',AddUniqueColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddUniqueColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'added_field', models.IntegerField, unique=True, null=True)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddUniqueColumnModel
+%(AddUniqueColumnModel)s
+
+# Unique indexed field.
+>>> class AddUniqueIndexedModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.IntegerField(unique=True, db_index=True, null=True)
+
+>>> end = register_models(('TestModel',AddUniqueIndexedModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddUniqueIndexedModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'added_field', models.IntegerField, unique=True, null=True, db_index=True)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddUniqueIndexedModel
+%(AddUniqueIndexedModel)s
+
+Foreign Key field.
+>>> class AddForeignKeyModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.ForeignKey(AddAnchor1, null=True)
+
+>>> end = register_models(('TestModel',AddForeignKeyModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddForeignKeyModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'added_field', models.ForeignKey, null=True, related_model='tests.AddAnchor1')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddForeignKeyModel
+%(AddForeignKeyModel)s
+
+# M2M field between models with default table names.
+>>> class AddM2MDatabaseTableModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.ManyToManyField(AddAnchor1)
+
+>>> end = register_models(('TestModel',AddM2MDatabaseTableModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',AddM2MDatabaseTableModel), *anchors)
+>>> end_sig['tests'][AddAnchor1.__name__] = signature.create_model_sig(AddAnchor1)
+>>> anchor_sig = copy.deepcopy(start_sig)
+>>> anchor_sig['tests'][AddAnchor1.__name__] = signature.create_model_sig(AddAnchor1)
+>>> d = Diff(anchor_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'added_field', models.ManyToManyField, related_model='tests.AddAnchor1')"]
+
+>>> test_sig = copy.deepcopy(anchor_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddManyToManyDatabaseTableModel
+%(AddManyToManyDatabaseTableModel)s
+
+# M2M field between models with non-default table names.
+>>> class AddM2MNonDefaultDatabaseTableModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.ManyToManyField(AddAnchor2)
+
+>>> end = register_models(('TestModel', AddM2MNonDefaultDatabaseTableModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', AddM2MNonDefaultDatabaseTableModel), *anchors)
+>>> end_sig['tests'][AddAnchor2.__name__] = signature.create_model_sig(AddAnchor2)
+>>> anchor_sig = copy.deepcopy(start_sig)
+>>> anchor_sig['tests'][AddAnchor2.__name__] = signature.create_model_sig(AddAnchor2)
+>>> d = Diff(anchor_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'added_field', models.ManyToManyField, related_model='tests.AddAnchor2')"]
+
+>>> test_sig = copy.deepcopy(anchor_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddManyToManyNonDefaultDatabaseTableModel
+%(AddManyToManyNonDefaultDatabaseTableModel)s
+
+# M2M field between self
+# Need to find a better way to do this.
+>>> end_sig = copy.deepcopy(start_sig)
+>>> end_sig['tests']['TestModel']['fields']['added_field'] = {'field_type': models.ManyToManyField,'related_model': 'tests.TestModel'}
+
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'added_field', models.ManyToManyField, related_model='tests.TestModel')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #AddManyToManySelf
+%(AddManyToManySelf)s
+
+# Clean up after the applications that were installed
+>>> deregister_models()
+
+""" % test_sql_mapping('add_field')
\ No newline at end of file
diff --git a/lib/django_evolution/tests/change_field.py b/lib/django_evolution/tests/change_field.py
new file mode 100644
index 0000000..68b9c20
--- /dev/null
+++ b/lib/django_evolution/tests/change_field.py
@@ -0,0 +1,687 @@
+from django_evolution.tests.utils import test_sql_mapping
+
+tests = r"""
+>>> from django.db import models
+
+>>> from django_evolution.mutations import ChangeField
+>>> from django_evolution.tests.utils import test_proj_sig, execute_test_sql, register_models, deregister_models
+>>> from django_evolution.diff import Diff
+
+>>> import copy
+
+# Use Cases:
+# Setting a null constraint
+# -- without an initial value
+# -- with a null initial value
+# -- with a good initial value (constant)
+# -- with a good initial value (callable)
+# Removing a null constraint
+# Invoking a no-op change field
+# Changing the max_length of a character field
+# -- increasing the max_length
+# -- decreasing the max_length
+# Renaming a column
+# Changing the db_table of a many to many relationship
+# Adding an index
+# Removing an index
+# Adding a unique constraint
+# Removing a unique constraint
+# Redundant attributes. (Some attribute have changed, while others haven't but are specified anyway.)
+# Changing more than one attribute at a time (on different fields)
+# Changing more than one attribute at a time (on one field)
+
+
+### This one is a bit dubious because changing the primary key of a model will mean
+### that all referenced foreign keys and M2M relationships need to be updated
+# Adding a primary key constraint
+# Removing a Primary Key (Changing the primary key column)
+
+
+
+# Options that apply to all fields:
+# DB related options
+# null
+# db_column
+# db_index
+# db_tablespace (Ignored)
+# primary_key
+# unique
+# db_table (only for many to many relationships)
+# -- CharField
+# max_length
+
+# Non-DB options
+# blank
+# core
+# default
+# editable
+# help_text
+# radio_admin
+# unique_for_date
+# unique_for_month
+# unique_for_year
+# validator_list
+
+# I don't know yet
+# choices
+
+>>> class ChangeSequenceFieldInitial(object):
+... def __init__(self, suffix):
+... self.suffix = suffix
+...
+... def __call__(self):
+... from django.db import connection
+... qn = connection.ops.quote_name
+... return qn('char_field')
+
+# Now, a useful test model we can use for evaluating diffs
+>>> class ChangeAnchor1(models.Model):
+... value = models.IntegerField()
+
+>>> class ChangeBaseModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+# Store the base signatures
+>>> anchors = [('ChangeAnchor1', ChangeAnchor1)]
+>>> test_model = ('TestModel', ChangeBaseModel)
+
+>>> start = register_models(*anchors)
+>>> start.update(register_models(test_model))
+>>> start_sig = test_proj_sig(test_model, *anchors)
+
+# Setting a null constraint without an initial value
+>>> class SetNotNullChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=False)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', SetNotNullChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', SetNotNullChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'char_field1':
+ Property 'null' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # SetNotNullChangeModel
+["ChangeField('TestModel', 'char_field1', initial=<<USER VALUE REQUIRED>>, null=False)"]
+
+# Without an initial value
+>>> evolution = [ChangeField('TestModel', 'char_field1', null=False)]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+Traceback (most recent call last):
+...
+SimulationFailure: Cannot change column 'char_field1' on 'tests.TestModel' without a non-null initial value.
+
+# With a null initial value
+>>> evolution = [ChangeField('TestModel', 'char_field1', null=False, initial=None)]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+Traceback (most recent call last):
+...
+SimulationFailure: Cannot change column 'char_field1' on 'tests.TestModel' without a non-null initial value.
+
+# With a good initial value (constant)
+>>> evolution = [ChangeField('TestModel', 'char_field1', null=False, initial="abc's xyz")]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql)
+%(SetNotNullChangeModelWithConstant)s
+
+# With a good initial value (callable)
+>>> evolution = [ChangeField('TestModel', 'char_field1', null=False, initial=ChangeSequenceFieldInitial('SetNotNullChangeModel'))]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql)
+%(SetNotNullChangeModelWithCallable)s
+
+# Removing a null constraint
+>>> class SetNullChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=True)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', SetNullChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', SetNullChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'char_field2':
+ Property 'null' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # SetNullChangeModel
+["ChangeField('TestModel', 'char_field2', initial=None, null=True)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # SetNullChangeModel
+%(SetNullChangeModel)s
+
+# Removing a null constraint
+>>> class NoOpChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', NoOpChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', NoOpChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+<BLANKLINE>
+
+>>> evolution = [ChangeField('TestModel', 'char_field1', null=True)]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # NoOpChangeModel
+%(NoOpChangeModel)s
+
+# Increasing the max_length of a character field
+>>> class IncreasingMaxLengthChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=45)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', IncreasingMaxLengthChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', IncreasingMaxLengthChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'char_field':
+ Property 'max_length' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # IncreasingMaxLengthChangeModel
+["ChangeField('TestModel', 'char_field', initial=None, max_length=45)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # IncreasingMaxLengthChangeModel
+%(IncreasingMaxLengthChangeModel)s
+
+# Decreasing the max_length of a character field
+>>> class DecreasingMaxLengthChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=1)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', DecreasingMaxLengthChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', DecreasingMaxLengthChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'char_field':
+ Property 'max_length' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # DecreasingMaxLengthChangeModel
+["ChangeField('TestModel', 'char_field', initial=None, max_length=1)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # DecreasingMaxLengthChangeModel
+%(DecreasingMaxLengthChangeModel)s
+
+# Renaming a column
+>>> class DBColumnChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='customised_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', DBColumnChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', DBColumnChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'int_field':
+ Property 'db_column' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # DBColumnChangeModel
+["ChangeField('TestModel', 'int_field', initial=None, db_column='customised_db_column')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # DBColumnChangeModel
+%(DBColumnChangeModel)s
+
+# Changing the db_table of a many to many relationship
+>>> class M2MDBTableChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='custom_m2m_db_table_name')
+
+>>> end = register_models(('TestModel', M2MDBTableChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', M2MDBTableChangeModel), *anchors)
+
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'm2m_field1':
+ Property 'db_table' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # M2MDBTableChangeModel
+["ChangeField('TestModel', 'm2m_field1', initial=None, db_table='custom_m2m_db_table_name')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # M2MDBTableChangeModel
+%(M2MDBTableChangeModel)s
+
+# Adding an index
+>>> class AddDBIndexChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=True)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', AddDBIndexChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', AddDBIndexChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'int_field2':
+ Property 'db_index' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # AddDBIndexChangeModel
+["ChangeField('TestModel', 'int_field2', initial=None, db_index=True)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # AddDBIndexChangeModel
+%(AddDBIndexChangeModel)s
+
+# Removing an index
+>>> class RemoveDBIndexChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=False)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', RemoveDBIndexChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', RemoveDBIndexChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'int_field1':
+ Property 'db_index' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # RemoveDBIndexChangeModel
+["ChangeField('TestModel', 'int_field1', initial=None, db_index=False)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # RemoveDBIndexChangeModel
+%(RemoveDBIndexChangeModel)s
+
+# Adding a unique constraint
+>>> class AddUniqueChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=True)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', AddUniqueChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', AddUniqueChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'int_field4':
+ Property 'unique' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # AddUniqueChangeModel
+["ChangeField('TestModel', 'int_field4', initial=None, unique=True)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # AddUniqueChangeModel
+%(AddUniqueChangeModel)s
+
+# Remove a unique constraint
+>>> class RemoveUniqueChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=False)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', RemoveUniqueChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', RemoveUniqueChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'int_field3':
+ Property 'unique' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # RemoveUniqueChangeModel
+["ChangeField('TestModel', 'int_field3', initial=None, unique=False)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # RemoveUniqueChangeModel
+%(RemoveUniqueChangeModel)s
+
+# Changing more than one attribute at a time (on different fields)
+>>> class MultiAttrChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column2')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=35)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=True)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', MultiAttrChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', MultiAttrChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'char_field2':
+ Property 'null' has changed
+ In field 'int_field':
+ Property 'db_column' has changed
+ In field 'char_field':
+ Property 'max_length' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # MultiAttrChangeModel
+["ChangeField('TestModel', 'char_field2', initial=None, null=True)", "ChangeField('TestModel', 'int_field', initial=None, db_column='custom_db_column2')", "ChangeField('TestModel', 'char_field', initial=None, max_length=35)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # MultiAttrChangeModel
+%(MultiAttrChangeModel)s
+
+# Changing more than one attribute at a time (on one fields)
+>>> class MultiAttrSingleFieldChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=35, null=True)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', MultiAttrSingleFieldChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', MultiAttrSingleFieldChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print d
+In model tests.TestModel:
+ In field 'char_field2':
+ Property 'max_length' has changed
+ Property 'null' has changed
+
+>>> print [str(e) for e in d.evolution()['tests']] # MultiAttrSingleFieldChangeModel
+["ChangeField('TestModel', 'char_field2', initial=None, max_length=35, null=True)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # MultiAttrSingleFieldChangeModel
+%(MultiAttrSingleFieldChangeModel)s
+
+# Redundant attributes. (Some attribute have changed, while others haven't but are specified anyway.)
+>>> class RedundantAttrsChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column3')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = models.IntegerField(unique=False)
+... char_field = models.CharField(max_length=35)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=True)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', RedundantAttrsChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', RedundantAttrsChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> evolutions = [
+... ChangeField("TestModel", "char_field2", initial=None, null=True, max_length=30),
+... ChangeField("TestModel", "int_field", initial=None, db_column="custom_db_column3", primary_key=False, unique=False, db_index=False),
+... ChangeField("TestModel", "char_field", initial=None, max_length=35),
+... ]
+
+>>> for mutation in evolutions:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # RedundantAttrsChangeModel
+%(RedundantAttrsChangeModel)s
+
+# Change field type to another type with same internal_type
+>>> class MyIntegerField(models.IntegerField):
+... def get_internal_type(self):
+... return 'IntegerField'
+
+>>> class MinorFieldTypeChangeModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... alt_pk = models.IntegerField()
+... int_field = models.IntegerField(db_column='custom_db_column')
+... int_field1 = models.IntegerField(db_index=True)
+... int_field2 = models.IntegerField(db_index=False)
+... int_field3 = models.IntegerField(unique=True)
+... int_field4 = MyIntegerField(unique=False)
+... char_field = models.CharField(max_length=20)
+... char_field1 = models.CharField(max_length=25, null=True)
+... char_field2 = models.CharField(max_length=30, null=False)
+... m2m_field1 = models.ManyToManyField(ChangeAnchor1, db_table='change_field_non-default_m2m_table')
+
+>>> end = register_models(('TestModel', MinorFieldTypeChangeModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', MinorFieldTypeChangeModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+
+>>> d.is_empty()
+True
+
+# Clean up after the applications that were installed
+>>> deregister_models()
+
+""" % test_sql_mapping('change_field')
diff --git a/registration/management/__init__.py b/lib/django_evolution/tests/db/__init__.py
similarity index 100%
rename from registration/management/__init__.py
rename to lib/django_evolution/tests/db/__init__.py
diff --git a/lib/django_evolution/tests/db/mysql.py b/lib/django_evolution/tests/db/mysql.py
new file mode 100644
index 0000000..8ae2069
--- /dev/null
+++ b/lib/django_evolution/tests/db/mysql.py
@@ -0,0 +1,254 @@
+add_field = {
+ 'AddNonNullNonCallableColumnModel':
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field` integer ;',
+ 'UPDATE `tests_testmodel` SET `added_field` = 1 WHERE `added_field` IS NULL;',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `added_field` integer NOT NULL;',
+ ]),
+ 'AddNonNullCallableColumnModel':
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field` integer ;',
+ 'UPDATE `tests_testmodel` SET `added_field` = `int_field` WHERE `added_field` IS NULL;',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `added_field` integer NOT NULL;',
+ ]),
+ 'AddNullColumnWithInitialColumnModel':
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field` integer ;',
+ 'UPDATE `tests_testmodel` SET `added_field` = 1 WHERE `added_field` IS NULL;',
+ ]),
+ 'AddStringColumnModel':
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field` varchar(10) ;',
+ 'UPDATE `tests_testmodel` SET `added_field` = \'abc\\\'s xyz\' WHERE `added_field` IS NULL;',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `added_field` varchar(10) NOT NULL;',
+ ]),
+ 'AddDateColumnModel':
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field` datetime ;',
+ 'UPDATE `tests_testmodel` SET `added_field` = 2007-12-13 16:42:00 WHERE `added_field` IS NULL;',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `added_field` datetime NOT NULL;',
+ ]),
+ 'AddDefaultColumnModel':
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field` integer ;',
+ 'UPDATE `tests_testmodel` SET `added_field` = 42 WHERE `added_field` IS NULL;',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `added_field` integer NOT NULL;',
+ ]),
+ 'AddEmptyStringDefaultColumnModel':
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field` varchar(20) ;',
+ 'UPDATE `tests_testmodel` SET `added_field` = \'\' WHERE `added_field` IS NULL;',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `added_field` varchar(20) NOT NULL;',
+ ]),
+ 'AddNullColumnModel':
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field` integer NULL ;',
+ 'NonDefaultColumnModel':
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `non-default_column` integer NULL ;',
+ 'AddColumnCustomTableModel':
+ 'ALTER TABLE `custom_table_name` ADD COLUMN `added_field` integer NULL ;',
+ 'AddIndexedColumnModel':
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `add_field` integer NULL ;',
+ 'CREATE INDEX `tests_testmodel_add_field` ON `tests_testmodel` (`add_field`);'
+ ]),
+ 'AddUniqueColumnModel':
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field` integer NULL UNIQUE;',
+ 'AddUniqueIndexedModel':
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field` integer NULL UNIQUE;',
+ 'AddForeignKeyModel':
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field_id` integer NULL REFERENCES `tests_addanchor1` (`id`) ;',
+ 'CREATE INDEX `tests_testmodel_added_field_id` ON `tests_testmodel` (`added_field_id`);'
+ ]),
+ 'AddManyToManyDatabaseTableModel':
+ '\n'.join([
+ 'CREATE TABLE `tests_testmodel_added_field` (',
+ ' `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,',
+ ' `testmodel_id` integer NOT NULL,',
+ ' `addanchor1_id` integer NOT NULL,',
+ ' UNIQUE (`testmodel_id`, `addanchor1_id`)',
+ ')',
+ ';',
+ 'ALTER TABLE `tests_testmodel_added_field` ADD CONSTRAINT testmodel_id_refs_id_12ea61cd FOREIGN KEY (`testmodel_id`) REFERENCES `tests_testmodel` (`id`);',
+ 'ALTER TABLE `tests_testmodel_added_field` ADD CONSTRAINT addanchor1_id_refs_id_7efbb240 FOREIGN KEY (`addanchor1_id`) REFERENCES `tests_addanchor1` (`id`);'
+ ]),
+ 'AddManyToManyNonDefaultDatabaseTableModel':
+ '\n'.join([
+ 'CREATE TABLE `tests_testmodel_added_field` (',
+ ' `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,',
+ ' `testmodel_id` integer NOT NULL,',
+ ' `addanchor2_id` integer NOT NULL,',
+ ' UNIQUE (`testmodel_id`, `addanchor2_id`)',
+ ')',
+ ';',
+ 'ALTER TABLE `tests_testmodel_added_field` ADD CONSTRAINT testmodel_id_refs_id_12ea61cd FOREIGN KEY (`testmodel_id`) REFERENCES `tests_testmodel` (`id`);',
+ 'ALTER TABLE `tests_testmodel_added_field` ADD CONSTRAINT addanchor2_id_refs_id_13c1da78 FOREIGN KEY (`addanchor2_id`) REFERENCES `custom_add_anchor_table` (`id`);'
+ ]),
+ 'AddManyToManySelf':
+ '\n'.join([
+ 'CREATE TABLE `tests_testmodel_added_field` (',
+ ' `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,',
+ ' `from_testmodel_id` integer NOT NULL,',
+ ' `to_testmodel_id` integer NOT NULL,',
+ ' UNIQUE (`from_testmodel_id`, `to_testmodel_id`)',
+ ')',
+ ';',
+ 'ALTER TABLE `tests_testmodel_added_field` ADD CONSTRAINT from_testmodel_id_refs_id_12ea61cd FOREIGN KEY (`from_testmodel_id`) REFERENCES `tests_testmodel` (`id`);',
+ 'ALTER TABLE `tests_testmodel_added_field` ADD CONSTRAINT to_testmodel_id_refs_id_12ea61cd FOREIGN KEY (`to_testmodel_id`) REFERENCES `tests_testmodel` (`id`);'
+ ]),
+}
+
+delete_field = {
+ 'DefaultNamedColumnModel':
+ 'ALTER TABLE `tests_testmodel` DROP COLUMN `int_field` CASCADE;',
+ 'NonDefaultNamedColumnModel':
+ 'ALTER TABLE `tests_testmodel` DROP COLUMN `non-default_db_column` CASCADE;',
+ 'ConstrainedColumnModel':
+ 'ALTER TABLE `tests_testmodel` DROP COLUMN `int_field3` CASCADE;',
+ 'DefaultManyToManyModel':
+ 'DROP TABLE `tests_testmodel_m2m_field1`;',
+ 'NonDefaultManyToManyModel':
+ 'DROP TABLE `non-default_m2m_table`;',
+ 'DeleteForeignKeyModel':
+ 'ALTER TABLE `tests_testmodel` DROP COLUMN `fk_field1_id` CASCADE;',
+ 'DeleteColumnCustomTableModel':
+ 'ALTER TABLE `custom_table_name` DROP COLUMN `value` CASCADE;',
+}
+
+change_field = {
+ "SetNotNullChangeModelWithConstant":
+ '\n'.join([
+ 'UPDATE `tests_testmodel` SET `char_field1` = \'abc\\\'s xyz\' WHERE `char_field1` IS NULL;',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `char_field1` varchar(25) NOT NULL;',
+ ]),
+ "SetNotNullChangeModelWithCallable":
+ '\n'.join([
+ 'UPDATE `tests_testmodel` SET `char_field1` = `char_field` WHERE `char_field1` IS NULL;',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `char_field1` varchar(25) NOT NULL;',
+ ]),
+ "SetNullChangeModel": 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `char_field2` varchar(30) DEFAULT NULL;',
+ "NoOpChangeModel": '',
+ 'IncreasingMaxLengthChangeModel':
+ '\n'.join([
+ 'UPDATE `tests_testmodel` SET `char_field`=LEFT(`char_field`,45);',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `char_field` varchar(45);',
+ ]),
+ 'DecreasingMaxLengthChangeModel':
+ '\n'.join([
+ 'UPDATE `tests_testmodel` SET `char_field`=LEFT(`char_field`,1);',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `char_field` varchar(1);',
+ ]),
+ "DBColumnChangeModel": 'ALTER TABLE `tests_testmodel` CHANGE COLUMN `custom_db_column` `customised_db_column` integer NOT NULL;',
+ "M2MDBTableChangeModel": 'RENAME TABLE `change_field_non-default_m2m_table` TO `custom_m2m_db_table_name`;',
+ "AddDBIndexChangeModel": 'CREATE INDEX `tests_testmodel_int_field2` ON `tests_testmodel` (`int_field2`);',
+ "RemoveDBIndexChangeModel": 'DROP INDEX `tests_testmodel_int_field1` ON `tests_testmodel`;',
+ "AddUniqueChangeModel": 'CREATE UNIQUE INDEX int_field4 ON `tests_testmodel`(`int_field4`);',
+ "RemoveUniqueChangeModel": 'DROP INDEX int_field3 ON `tests_testmodel`;',
+ "MultiAttrChangeModel":
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `char_field2` varchar(30) DEFAULT NULL;',
+ 'ALTER TABLE `tests_testmodel` CHANGE COLUMN `custom_db_column` `custom_db_column2` integer NOT NULL;',
+ 'UPDATE `tests_testmodel` SET `char_field`=LEFT(`char_field`,35);',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `char_field` varchar(35);',
+ ]),
+ "MultiAttrSingleFieldChangeModel":
+ '\n'.join([
+ 'UPDATE `tests_testmodel` SET `char_field2`=LEFT(`char_field2`,35);',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `char_field2` varchar(35);',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `char_field2` varchar(35) DEFAULT NULL;',
+ ]),
+ "RedundantAttrsChangeModel":
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `char_field2` varchar(30) DEFAULT NULL;',
+ 'ALTER TABLE `tests_testmodel` CHANGE COLUMN `custom_db_column` `custom_db_column3` integer NOT NULL;',
+ 'UPDATE `tests_testmodel` SET `char_field`=LEFT(`char_field`,35);',
+ 'ALTER TABLE `tests_testmodel` MODIFY COLUMN `char_field` varchar(35);',
+ ]),
+}
+
+delete_model = {
+ 'BasicModel':
+ 'DROP TABLE `tests_basicmodel`;',
+ 'BasicWithM2MModel':
+ '\n'.join([
+ 'DROP TABLE `tests_basicwithm2mmodel_m2m`;',
+ 'DROP TABLE `tests_basicwithm2mmodel`;'
+ ]),
+ 'CustomTableModel':
+ 'DROP TABLE `custom_table_name`;',
+ 'CustomTableWithM2MModel':
+ '\n'.join([
+ 'DROP TABLE `another_custom_table_name_m2m`;',
+ 'DROP TABLE `another_custom_table_name`;'
+ ]),
+}
+
+delete_application = {
+ 'DeleteApplication':
+ '\n'.join([
+ 'DROP TABLE `tests_appdeleteanchor1`;',
+ 'DROP TABLE `tests_testmodel_anchor_m2m`;',
+ 'DROP TABLE `tests_testmodel`;',
+ 'DROP TABLE `app_delete_custom_add_anchor_table`;',
+ 'DROP TABLE `app_delete_custom_table_name`;',
+ ]),
+}
+
+rename_field = {
+ 'RenameColumnModel':
+ 'ALTER TABLE `tests_testmodel` CHANGE COLUMN `int_field` `renamed_field` integer NOT NULL;',
+ 'RenameColumnWithTableNameModel':
+ 'ALTER TABLE `tests_testmodel` CHANGE COLUMN `int_field` `renamed_field` integer NOT NULL;',
+ 'RenamePrimaryKeyColumnModel':
+ 'ALTER TABLE `tests_testmodel` CHANGE COLUMN `id` `my_pk_id`;',
+ 'RenameForeignKeyColumnModel':
+ 'ALTER TABLE `tests_testmodel` CHANGE COLUMN `fk_field_id` `renamed_field_id` integer NOT NULL;',
+ 'RenameNonDefaultColumnNameModel':
+ 'ALTER TABLE `tests_testmodel` CHANGE COLUMN `custom_db_col_name` `renamed_field` integer NOT NULL;',
+ 'RenameNonDefaultColumnNameToNonDefaultNameModel':
+ 'ALTER TABLE `tests_testmodel` CHANGE COLUMN `custom_db_col_name` `non-default_column_name` integer NOT NULL;',
+ 'RenameNonDefaultColumnNameToNonDefaultNameAndTableModel':
+ 'ALTER TABLE `tests_testmodel` CHANGE COLUMN `custom_db_col_name` `non-default_column_name2` integer NOT NULL;',
+ 'RenameColumnCustomTableModel':
+ 'ALTER TABLE `custom_rename_table_name` CHANGE COLUMN `value` `renamed_field` integer NOT NULL;',
+ 'RenameManyToManyTableModel':
+ 'ALTER TABLE `tests_testmodel_m2m_field` RENAME TO `tests_testmodel_renamed_field`;',
+ 'RenameManyToManyTableWithColumnNameModel':
+ 'ALTER TABLE `tests_testmodel_m2m_field` RENAME TO `tests_testmodel_renamed_field`;',
+ 'RenameNonDefaultManyToManyTableModel':
+ 'ALTER TABLE `non-default_db_table` RENAME TO `tests_testmodel_renamed_field`;',
+}
+
+
+sql_mutation = {
+ 'SQLMutationSequence': """[
+... SQLMutation('first-two-fields', [
+... 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field1` integer NULL;',
+... 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field2` integer NULL;'
+... ], update_first_two),
+... SQLMutation('third-field', [
+... 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field3` integer NULL;',
+... ], update_third)]
+""",
+ 'SQLMutationOutput':
+ '\n'.join([
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field1` integer NULL;',
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field2` integer NULL;',
+ 'ALTER TABLE `tests_testmodel` ADD COLUMN `added_field3` integer NULL;',
+ ]),
+}
+
+generics = {
+ 'DeleteColumnModel': "ALTER TABLE `tests_testmodel` DROP COLUMN `char_field` CASCADE;"
+}
+
+inheritance = {
+ 'AddToChildModel':
+ '\n'.join([
+ 'ALTER TABLE `tests_childmodel` ADD COLUMN `added_field` integer ;',
+ 'UPDATE `tests_childmodel` SET `added_field` = 42 WHERE `added_field` IS NULL;',
+ 'ALTER TABLE `tests_childmodel` MODIFY COLUMN `added_field` integer NOT NULL;',
+ ]),
+ 'DeleteFromChildModel':
+ 'ALTER TABLE `tests_childmodel` DROP COLUMN `int_field` CASCADE;',
+}
diff --git a/lib/django_evolution/tests/db/mysql_old.py b/lib/django_evolution/tests/db/mysql_old.py
new file mode 100644
index 0000000..505a6b8
--- /dev/null
+++ b/lib/django_evolution/tests/db/mysql_old.py
@@ -0,0 +1,2 @@
+# MySQL_old behaviour is identical to mysql base
+from mysql import *
\ No newline at end of file
diff --git a/lib/django_evolution/tests/db/postgresql.py b/lib/django_evolution/tests/db/postgresql.py
new file mode 100644
index 0000000..4e6e30f
--- /dev/null
+++ b/lib/django_evolution/tests/db/postgresql.py
@@ -0,0 +1,236 @@
+add_field = {
+ 'AddNonNullNonCallableColumnModel':
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field" integer ;',
+ 'UPDATE "tests_testmodel" SET "added_field" = 1 WHERE "added_field" IS NULL;',
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "added_field" SET NOT NULL;',
+ ]),
+ 'AddNonNullCallableColumnModel':
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field" integer ;',
+ 'UPDATE "tests_testmodel" SET "added_field" = "int_field" WHERE "added_field" IS NULL;',
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "added_field" SET NOT NULL;',
+ ]),
+ 'AddNullColumnWithInitialColumnModel':
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field" integer ;',
+ 'UPDATE "tests_testmodel" SET "added_field" = 1 WHERE "added_field" IS NULL;',
+ ]),
+ 'AddStringColumnModel':
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field" varchar(10) ;',
+ 'UPDATE "tests_testmodel" SET "added_field" = \'abc\\\'s xyz\' WHERE "added_field" IS NULL;',
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "added_field" SET NOT NULL;',
+ ]),
+ 'AddDateColumnModel':
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field" timestamp with time zone ;',
+ 'UPDATE "tests_testmodel" SET "added_field" = 2007-12-13 16:42:00 WHERE "added_field" IS NULL;',
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "added_field" SET NOT NULL;',
+ ]),
+ 'AddDefaultColumnModel':
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field" integer ;',
+ 'UPDATE "tests_testmodel" SET "added_field" = 42 WHERE "added_field" IS NULL;',
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "added_field" SET NOT NULL;',
+ ]),
+ 'AddEmptyStringDefaultColumnModel':
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field" varchar(20) ;',
+ 'UPDATE "tests_testmodel" SET "added_field" = \'\' WHERE "added_field" IS NULL;',
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "added_field" SET NOT NULL;',
+ ]),
+ 'AddNullColumnModel':
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field" integer NULL ;',
+ 'NonDefaultColumnModel':
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "non-default_column" integer NULL ;',
+ 'AddColumnCustomTableModel':
+ 'ALTER TABLE "custom_table_name" ADD COLUMN "added_field" integer NULL ;',
+ 'AddIndexedColumnModel':
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "add_field" integer NULL ;',
+ 'CREATE INDEX "tests_testmodel_add_field" ON "tests_testmodel" ("add_field");'
+ ]),
+ 'AddUniqueColumnModel':
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field" integer NULL UNIQUE;',
+ 'AddUniqueIndexedModel':
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field" integer NULL UNIQUE;',
+ 'AddForeignKeyModel':
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field_id" integer NULL REFERENCES "tests_addanchor1" ("id") DEFERRABLE INITIALLY DEFERRED;',
+ 'CREATE INDEX "tests_testmodel_added_field_id" ON "tests_testmodel" ("added_field_id");'
+ ]),
+ 'AddManyToManyDatabaseTableModel':
+ '\n'.join([
+ 'CREATE TABLE "tests_testmodel_added_field" (',
+ ' "id" serial NOT NULL PRIMARY KEY,',
+ ' "testmodel_id" integer NOT NULL REFERENCES "tests_testmodel" ("id") DEFERRABLE INITIALLY DEFERRED,',
+ ' "addanchor1_id" integer NOT NULL REFERENCES "tests_addanchor1" ("id") DEFERRABLE INITIALLY DEFERRED,',
+ ' UNIQUE ("testmodel_id", "addanchor1_id")',
+ ')',
+ ';'
+ ]),
+ 'AddManyToManyNonDefaultDatabaseTableModel':
+ '\n'.join([
+ 'CREATE TABLE "tests_testmodel_added_field" (',
+ ' "id" serial NOT NULL PRIMARY KEY,',
+ ' "testmodel_id" integer NOT NULL REFERENCES "tests_testmodel" ("id") DEFERRABLE INITIALLY DEFERRED,',
+ ' "addanchor2_id" integer NOT NULL REFERENCES "custom_add_anchor_table" ("id") DEFERRABLE INITIALLY DEFERRED,',
+ ' UNIQUE ("testmodel_id", "addanchor2_id")',
+ ')',
+ ';'
+ ]),
+ 'AddManyToManySelf':
+ '\n'.join([
+ 'CREATE TABLE "tests_testmodel_added_field" (',
+ ' "id" serial NOT NULL PRIMARY KEY,',
+ ' "from_testmodel_id" integer NOT NULL REFERENCES "tests_testmodel" ("id") DEFERRABLE INITIALLY DEFERRED,',
+ ' "to_testmodel_id" integer NOT NULL REFERENCES "tests_testmodel" ("id") DEFERRABLE INITIALLY DEFERRED,',
+ ' UNIQUE ("from_testmodel_id", "to_testmodel_id")',
+ ')',
+ ';'
+ ]),
+}
+
+delete_field = {
+ 'DefaultNamedColumnModel':
+ 'ALTER TABLE "tests_testmodel" DROP COLUMN "int_field" CASCADE;',
+ 'NonDefaultNamedColumnModel':
+ 'ALTER TABLE "tests_testmodel" DROP COLUMN "non-default_db_column" CASCADE;',
+ 'ConstrainedColumnModel':
+ 'ALTER TABLE "tests_testmodel" DROP COLUMN "int_field3" CASCADE;',
+ 'DefaultManyToManyModel':
+ 'DROP TABLE "tests_testmodel_m2m_field1";',
+ 'NonDefaultManyToManyModel':
+ 'DROP TABLE "non-default_m2m_table";',
+ 'DeleteForeignKeyModel':
+ 'ALTER TABLE "tests_testmodel" DROP COLUMN "fk_field1_id" CASCADE;',
+ 'DeleteColumnCustomTableModel':
+ 'ALTER TABLE "custom_table_name" DROP COLUMN "value" CASCADE;',
+}
+
+change_field = {
+ "SetNotNullChangeModelWithConstant":
+ '\n'.join([
+ 'UPDATE "tests_testmodel" SET "char_field1" = \'abc\\\'s xyz\' WHERE "char_field1" IS NULL;',
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "char_field1" SET NOT NULL;',
+ ]),
+ "SetNotNullChangeModelWithCallable":
+ '\n'.join([
+ 'UPDATE "tests_testmodel" SET "char_field1" = "char_field" WHERE "char_field1" IS NULL;',
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "char_field1" SET NOT NULL;',
+ ]),
+ "SetNullChangeModel": 'ALTER TABLE "tests_testmodel" ALTER COLUMN "char_field2" DROP NOT NULL;',
+ "NoOpChangeModel": '',
+ "IncreasingMaxLengthChangeModel": 'ALTER TABLE "tests_testmodel" ALTER COLUMN "char_field" TYPE varchar(45) USING CAST("char_field" as varchar(45));',
+ "DecreasingMaxLengthChangeModel": 'ALTER TABLE "tests_testmodel" ALTER COLUMN "char_field" TYPE varchar(1) USING CAST("char_field" as varchar(1));',
+ "DBColumnChangeModel": 'ALTER TABLE "tests_testmodel" RENAME COLUMN "custom_db_column" TO "customised_db_column";',
+ "M2MDBTableChangeModel": 'ALTER TABLE "change_field_non-default_m2m_table" RENAME TO "custom_m2m_db_table_name";',
+ "AddDBIndexChangeModel": 'CREATE INDEX "tests_testmodel_int_field2" ON "tests_testmodel" ("int_field2");',
+ "RemoveDBIndexChangeModel": 'DROP INDEX "tests_testmodel_int_field1";',
+ "AddUniqueChangeModel": 'ALTER TABLE "tests_testmodel" ADD CONSTRAINT tests_testmodel_int_field4_key UNIQUE("int_field4");',
+ "RemoveUniqueChangeModel": 'ALTER TABLE "tests_testmodel" DROP CONSTRAINT tests_testmodel_int_field3_key;',
+ "MultiAttrChangeModel":
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "char_field2" DROP NOT NULL;',
+ 'ALTER TABLE "tests_testmodel" RENAME COLUMN "custom_db_column" TO "custom_db_column2";',
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "char_field" TYPE varchar(35) USING CAST("char_field" as varchar(35));',
+ ]),
+ "MultiAttrSingleFieldChangeModel":
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "char_field2" TYPE varchar(35) USING CAST("char_field2" as varchar(35));',
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "char_field2" DROP NOT NULL;',
+ ]),
+ "RedundantAttrsChangeModel":
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "char_field2" DROP NOT NULL;',
+ 'ALTER TABLE "tests_testmodel" RENAME COLUMN "custom_db_column" TO "custom_db_column3";',
+ 'ALTER TABLE "tests_testmodel" ALTER COLUMN "char_field" TYPE varchar(35) USING CAST("char_field" as varchar(35));',
+ ]),
+}
+
+delete_model = {
+ 'BasicModel':
+ 'DROP TABLE "tests_basicmodel";',
+ 'BasicWithM2MModel':
+ '\n'.join([
+ 'DROP TABLE "tests_basicwithm2mmodel_m2m";',
+ 'DROP TABLE "tests_basicwithm2mmodel";'
+ ]),
+ 'CustomTableModel':
+ 'DROP TABLE "custom_table_name";',
+ 'CustomTableWithM2MModel':
+ '\n'.join([
+ 'DROP TABLE "another_custom_table_name_m2m";',
+ 'DROP TABLE "another_custom_table_name";'
+ ]),
+}
+
+delete_application = {
+ 'DeleteApplication':
+ '\n'.join([
+ 'DROP TABLE "tests_appdeleteanchor1";',
+ 'DROP TABLE "tests_testmodel_anchor_m2m";',
+ 'DROP TABLE "tests_testmodel";',
+ 'DROP TABLE "app_delete_custom_add_anchor_table";',
+ 'DROP TABLE "app_delete_custom_table_name";',
+ ]),
+}
+
+rename_field = {
+ 'RenameColumnModel':
+ 'ALTER TABLE "tests_testmodel" RENAME COLUMN "int_field" TO "renamed_field";',
+ 'RenameColumnWithTableNameModel':
+ 'ALTER TABLE "tests_testmodel" RENAME COLUMN "int_field" TO "renamed_field";',
+ 'RenamePrimaryKeyColumnModel':
+ 'ALTER TABLE "tests_testmodel" RENAME COLUMN "id" TO "my_pk_id";',
+ 'RenameForeignKeyColumnModel':
+ 'ALTER TABLE "tests_testmodel" RENAME COLUMN "fk_field_id" TO "renamed_field_id";',
+ 'RenameNonDefaultColumnNameModel':
+ 'ALTER TABLE "tests_testmodel" RENAME COLUMN "custom_db_col_name" TO "renamed_field";',
+ 'RenameNonDefaultColumnNameToNonDefaultNameModel':
+ 'ALTER TABLE "tests_testmodel" RENAME COLUMN "custom_db_col_name" TO "non-default_column_name";',
+ 'RenameNonDefaultColumnNameToNonDefaultNameAndTableModel':
+ 'ALTER TABLE "tests_testmodel" RENAME COLUMN "custom_db_col_name" TO "non-default_column_name2";',
+ 'RenameColumnCustomTableModel':
+ 'ALTER TABLE "custom_rename_table_name" RENAME COLUMN "value" TO "renamed_field";',
+ 'RenameManyToManyTableModel':
+ 'ALTER TABLE "tests_testmodel_m2m_field" RENAME TO "tests_testmodel_renamed_field";',
+ 'RenameManyToManyTableWithColumnNameModel':
+ 'ALTER TABLE "tests_testmodel_m2m_field" RENAME TO "tests_testmodel_renamed_field";',
+ 'RenameNonDefaultManyToManyTableModel':
+ 'ALTER TABLE "non-default_db_table" RENAME TO "tests_testmodel_renamed_field";',
+}
+
+sql_mutation = {
+ 'SQLMutationSequence': """[
+... SQLMutation('first-two-fields', [
+... 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field1" integer NULL;',
+... 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field2" integer NULL;'
+... ], update_first_two),
+... SQLMutation('third-field', [
+... 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field3" integer NULL;',
+... ], update_third)]
+""",
+ 'SQLMutationOutput':
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field1" integer NULL;',
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field2" integer NULL;',
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field3" integer NULL;',
+ ]),
+}
+
+generics = {
+ 'DeleteColumnModel': 'ALTER TABLE "tests_testmodel" DROP COLUMN "char_field" CASCADE;'
+}
+
+inheritance = {
+ 'AddToChildModel':
+ '\n'.join([
+ 'ALTER TABLE "tests_childmodel" ADD COLUMN "added_field" integer ;',
+ 'UPDATE "tests_childmodel" SET "added_field" = 42 WHERE "added_field" IS NULL;',
+ 'ALTER TABLE "tests_childmodel" ALTER COLUMN "added_field" SET NOT NULL;',
+ ]),
+ 'DeleteFromChildModel':
+ 'ALTER TABLE "tests_childmodel" DROP COLUMN "int_field" CASCADE;',
+}
diff --git a/lib/django_evolution/tests/db/postgresql_psycopg2.py b/lib/django_evolution/tests/db/postgresql_psycopg2.py
new file mode 100644
index 0000000..7557d22
--- /dev/null
+++ b/lib/django_evolution/tests/db/postgresql_psycopg2.py
@@ -0,0 +1,2 @@
+# Psycopg2 behaviour is identical to Psycopg1
+from postgresql import *
\ No newline at end of file
diff --git a/lib/django_evolution/tests/db/sqlite3.py b/lib/django_evolution/tests/db/sqlite3.py
new file mode 100644
index 0000000..0edeeb1
--- /dev/null
+++ b/lib/django_evolution/tests/db/sqlite3.py
@@ -0,0 +1,540 @@
+add_field = {
+ 'AddNonNullNonCallableColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field" FROM "tests_testmodel";',
+ 'UPDATE "TEMP_TABLE" SET "added_field" = 1;',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "added_field") SELECT "int_field", "id", "char_field", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'AddNonNullCallableColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field" FROM "tests_testmodel";',
+ 'UPDATE "TEMP_TABLE" SET "added_field" = "int_field";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "added_field") SELECT "int_field", "id", "char_field", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'AddNullColumnWithInitialColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field" FROM "tests_testmodel";',
+ 'UPDATE "TEMP_TABLE" SET "added_field" = 1;',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "added_field") SELECT "int_field", "id", "char_field", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'AddStringColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" varchar(10) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field" FROM "tests_testmodel";',
+ 'UPDATE "TEMP_TABLE" SET "added_field" = \'abc\\\'s xyz\';',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" varchar(10) NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "added_field") SELECT "int_field", "id", "char_field", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'AddDateColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" datetime NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field" FROM "tests_testmodel";',
+ 'UPDATE "TEMP_TABLE" SET "added_field" = 2007-12-13 16:42:00;',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" datetime NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "added_field") SELECT "int_field", "id", "char_field", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'AddDefaultColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field" FROM "tests_testmodel";',
+ 'UPDATE "TEMP_TABLE" SET "added_field" = 42;',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "added_field") SELECT "int_field", "id", "char_field", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'AddEmptyStringDefaultColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" varchar(20) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field" FROM "tests_testmodel";',
+ 'UPDATE "TEMP_TABLE" SET "added_field" = \'\';',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" varchar(20) NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "added_field") SELECT "int_field", "id", "char_field", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'AddNullColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "added_field") SELECT "int_field", "id", "char_field", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'NonDefaultColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "non-default_column" integer NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "non-default_column" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "non-default_column" integer NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "non-default_column") SELECT "int_field", "id", "char_field", "non-default_column" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'AddColumnCustomTableModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("id" integer NOT NULL UNIQUE PRIMARY KEY, "value" integer NOT NULL, "alt_value" varchar(20) NOT NULL, "added_field" integer NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "id", "value", "alt_value", "added_field" FROM "custom_table_name";',
+ 'DROP TABLE "custom_table_name";',
+ 'CREATE TABLE "custom_table_name"("id" integer NOT NULL UNIQUE PRIMARY KEY, "value" integer NOT NULL, "alt_value" varchar(20) NOT NULL, "added_field" integer NULL);',
+ 'INSERT INTO "custom_table_name" ("id", "value", "alt_value", "added_field") SELECT "id", "value", "alt_value", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'AddIndexedColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "add_field" integer NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "add_field" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "add_field" integer NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "add_field") SELECT "int_field", "id", "char_field", "add_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ 'CREATE INDEX "tests_testmodel_add_field" ON "tests_testmodel" ("add_field");',
+ ]),
+ 'AddUniqueColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NULL UNIQUE);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NULL UNIQUE);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "added_field") SELECT "int_field", "id", "char_field", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'AddUniqueIndexedModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NULL UNIQUE);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NULL UNIQUE);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "added_field") SELECT "int_field", "id", "char_field", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'AddForeignKeyModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field_id" integer NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field_id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field_id" integer NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "id", "char_field", "added_field_id") SELECT "int_field", "id", "char_field", "added_field_id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ 'CREATE INDEX "tests_testmodel_added_field_id" ON "tests_testmodel" ("added_field_id");',
+ ]),
+ 'AddManyToManyDatabaseTableModel':
+ '\n'.join([
+ 'CREATE TABLE "tests_testmodel_added_field" (',
+ ' "id" integer NOT NULL PRIMARY KEY,',
+ ' "testmodel_id" integer NOT NULL REFERENCES "tests_testmodel" ("id"),',
+ ' "addanchor1_id" integer NOT NULL REFERENCES "tests_addanchor1" ("id"),',
+ ' UNIQUE ("testmodel_id", "addanchor1_id")',
+ ')',
+ ';',
+ ]),
+ 'AddManyToManyNonDefaultDatabaseTableModel':
+ '\n'.join([
+ 'CREATE TABLE "tests_testmodel_added_field" (',
+ ' "id" integer NOT NULL PRIMARY KEY,',
+ ' "testmodel_id" integer NOT NULL REFERENCES "tests_testmodel" ("id"),',
+ ' "addanchor2_id" integer NOT NULL REFERENCES "custom_add_anchor_table" ("id"),',
+ ' UNIQUE ("testmodel_id", "addanchor2_id")',
+ ')',
+ ';',
+ ]),
+ 'AddManyToManySelf':
+ '\n'.join([
+ 'CREATE TABLE "tests_testmodel_added_field" (',
+ ' "id" integer NOT NULL PRIMARY KEY,',
+ ' "from_testmodel_id" integer NOT NULL REFERENCES "tests_testmodel" ("id"),',
+ ' "to_testmodel_id" integer NOT NULL REFERENCES "tests_testmodel" ("id"),',
+ ' UNIQUE ("from_testmodel_id", "to_testmodel_id")',
+ ')',
+ ';',
+ ]),
+}
+
+delete_field = {
+ 'DefaultNamedColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("non-default_db_column" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "fk_field1_id" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "non-default_db_column", "int_field3", "fk_field1_id", "char_field", "my_id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("non-default_db_column" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "fk_field1_id" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'CREATE INDEX "tests_testmodel_fk_field1_id" ON "tests_testmodel" ("fk_field1_id");',
+ 'INSERT INTO "tests_testmodel" ("non-default_db_column", "int_field3", "fk_field1_id", "char_field", "my_id") SELECT "non-default_db_column", "int_field3", "fk_field1_id", "char_field", "my_id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'NonDefaultNamedColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "fk_field1_id" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "int_field3", "fk_field1_id", "char_field", "my_id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "fk_field1_id" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'CREATE INDEX "tests_testmodel_fk_field1_id" ON "tests_testmodel" ("fk_field1_id");',
+ 'INSERT INTO "tests_testmodel" ("int_field", "int_field3", "fk_field1_id", "char_field", "my_id") SELECT "int_field", "int_field3", "fk_field1_id", "char_field", "my_id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'ConstrainedColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "non-default_db_column" integer NOT NULL, "fk_field1_id" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "non-default_db_column", "fk_field1_id", "char_field", "my_id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "non-default_db_column" integer NOT NULL, "fk_field1_id" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'CREATE INDEX "tests_testmodel_fk_field1_id" ON "tests_testmodel" ("fk_field1_id");',
+ 'INSERT INTO "tests_testmodel" ("int_field", "non-default_db_column", "fk_field1_id", "char_field", "my_id") SELECT "int_field", "non-default_db_column", "fk_field1_id", "char_field", "my_id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'DefaultManyToManyModel':
+ 'DROP TABLE "tests_testmodel_m2m_field1";',
+ 'NonDefaultManyToManyModel':
+ 'DROP TABLE "non-default_m2m_table";',
+ 'DeleteForeignKeyModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "non-default_db_column" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "non-default_db_column", "int_field3", "char_field", "my_id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "non-default_db_column" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "tests_testmodel" ("int_field", "non-default_db_column", "int_field3", "char_field", "my_id") SELECT "int_field", "non-default_db_column", "int_field3", "char_field", "my_id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'DeleteColumnCustomTableModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("id" integer NOT NULL UNIQUE PRIMARY KEY, "alt_value" varchar(20) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "id", "alt_value" FROM "custom_table_name";',
+ 'DROP TABLE "custom_table_name";',
+ 'CREATE TABLE "custom_table_name"("id" integer NOT NULL UNIQUE PRIMARY KEY, "alt_value" varchar(20) NOT NULL);',
+ 'INSERT INTO "custom_table_name" ("id", "alt_value") SELECT "id", "alt_value" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+}
+
+change_field = {
+ "SetNotNullChangeModelWithConstant":
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NOT NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'UPDATE "TEMP_TABLE" SET "char_field1" = \'abc\\\'s xyz\';',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NOT NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ "SetNotNullChangeModelWithCallable":
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NOT NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'UPDATE "TEMP_TABLE" SET "char_field1" = "char_field";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NOT NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ "SetNullChangeModel":
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ "NoOpChangeModel": '',
+ "IncreasingMaxLengthChangeModel":
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(45) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(45) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ "DecreasingMaxLengthChangeModel":
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(1) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(1) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ "DBColumnChangeModel":
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "customised_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "customised_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NOT NULL);',
+ 'CREATE INDEX "tests_testmodel_int_field1" ON "tests_testmodel" ("int_field1");',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "customised_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "customised_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ "M2MDBTableChangeModel": 'ALTER TABLE "change_field_non-default_m2m_table" RENAME TO "custom_m2m_db_table_name";',
+ "AddDBIndexChangeModel": 'CREATE INDEX "tests_testmodel_int_field2" ON "tests_testmodel" ("int_field2");',
+ "RemoveDBIndexChangeModel": 'DROP INDEX "tests_testmodel_int_field1";',
+ "AddUniqueChangeModel":
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL UNIQUE, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL UNIQUE, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ "RemoveUniqueChangeModel":
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ "MultiAttrChangeModel":
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column2" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column2" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'CREATE INDEX "tests_testmodel_int_field1" ON "tests_testmodel" ("int_field1");',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column2", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column2", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column2" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(35) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column2", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column2" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(35) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column2", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column2", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ "MultiAttrSingleFieldChangeModel":
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(35) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(35) NOT NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(35) NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(35) NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ "RedundantAttrsChangeModel":
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column3" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column3" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(20) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'CREATE INDEX "tests_testmodel_int_field1" ON "tests_testmodel" ("int_field1");',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column3", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column3", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field4" integer NOT NULL, "custom_db_column3" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(35) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field4", "custom_db_column3", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field4" integer NOT NULL, "custom_db_column3" integer NOT NULL, "int_field1" integer NOT NULL, "int_field2" integer NOT NULL, "int_field3" integer NOT NULL UNIQUE, "alt_pk" integer NOT NULL, "char_field" varchar(35) NOT NULL, "my_id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field1" varchar(25) NULL, "char_field2" varchar(30) NULL);',
+ 'INSERT INTO "tests_testmodel" ("int_field4", "custom_db_column3", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2") SELECT "int_field4", "custom_db_column3", "int_field1", "int_field2", "int_field3", "alt_pk", "char_field", "my_id", "char_field1", "char_field2" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+}
+
+delete_model = {
+ 'BasicModel':
+ 'DROP TABLE "tests_basicmodel";',
+ 'BasicWithM2MModel':
+ '\n'.join([
+ 'DROP TABLE "tests_basicwithm2mmodel_m2m";',
+ 'DROP TABLE "tests_basicwithm2mmodel";'
+ ]),
+ 'CustomTableModel':
+ 'DROP TABLE "custom_table_name";',
+ 'CustomTableWithM2MModel':
+ '\n'.join([
+ 'DROP TABLE "another_custom_table_name_m2m";',
+ 'DROP TABLE "another_custom_table_name";'
+ ]),
+}
+
+delete_application = {
+ 'DeleteApplication':
+ '\n'.join([
+ 'DROP TABLE "tests_appdeleteanchor1";',
+ 'DROP TABLE "tests_testmodel_anchor_m2m";',
+ 'DROP TABLE "tests_testmodel";',
+ 'DROP TABLE "app_delete_custom_add_anchor_table";',
+ 'DROP TABLE "app_delete_custom_table_name";',
+ ]),
+}
+
+rename_field = {
+ 'RenameColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("custom_db_col_name" integer NOT NULL, "char_field" varchar(20) NOT NULL, "renamed_field" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "custom_db_col_name", "char_field", "int_field", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("custom_db_col_name" integer NOT NULL, "char_field" varchar(20) NOT NULL, "renamed_field" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'CREATE INDEX "tests_testmodel_custom_db_col_name_indexed" ON "tests_testmodel" ("custom_db_col_name_indexed");',
+ 'CREATE INDEX "tests_testmodel_fk_field_id" ON "tests_testmodel" ("fk_field_id");',
+ 'INSERT INTO "tests_testmodel" ("custom_db_col_name", "char_field", "renamed_field", "custom_db_col_name_indexed", "fk_field_id", "id") SELECT "custom_db_col_name", "char_field", "renamed_field", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'RenameColumnWithTableNameModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("custom_db_col_name" integer NOT NULL, "char_field" varchar(20) NOT NULL, "renamed_field" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "custom_db_col_name", "char_field", "int_field", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("custom_db_col_name" integer NOT NULL, "char_field" varchar(20) NOT NULL, "renamed_field" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'CREATE INDEX "tests_testmodel_custom_db_col_name_indexed" ON "tests_testmodel" ("custom_db_col_name_indexed");',
+ 'CREATE INDEX "tests_testmodel_fk_field_id" ON "tests_testmodel" ("fk_field_id");',
+ 'INSERT INTO "tests_testmodel" ("custom_db_col_name", "char_field", "renamed_field", "custom_db_col_name_indexed", "fk_field_id", "id") SELECT "custom_db_col_name", "char_field", "renamed_field", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'RenamePrimaryKeyColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("custom_db_col_name" integer NOT NULL, "char_field" varchar(20) NOT NULL, "int_field" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "my_pk_id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "custom_db_col_name", "char_field", "int_field", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("custom_db_col_name" integer NOT NULL, "char_field" varchar(20) NOT NULL, "int_field" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "my_pk_id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'CREATE INDEX "tests_testmodel_custom_db_col_name_indexed" ON "tests_testmodel" ("custom_db_col_name_indexed");',
+ 'CREATE INDEX "tests_testmodel_fk_field_id" ON "tests_testmodel" ("fk_field_id");',
+ 'INSERT INTO "tests_testmodel" ("custom_db_col_name", "char_field", "int_field", "custom_db_col_name_indexed", "fk_field_id", "my_pk_id") SELECT "custom_db_col_name", "char_field", "int_field", "custom_db_col_name_indexed", "fk_field_id", "my_pk_id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'RenameForeignKeyColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "char_field" varchar(20) NOT NULL, "custom_db_col_name" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "renamed_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "char_field", "custom_db_col_name", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "char_field" varchar(20) NOT NULL, "custom_db_col_name" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "renamed_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'CREATE INDEX "tests_testmodel_custom_db_col_name_indexed" ON "tests_testmodel" ("custom_db_col_name_indexed");',
+ 'CREATE INDEX "tests_testmodel_renamed_field_id" ON "tests_testmodel" ("renamed_field_id");',
+ 'INSERT INTO "tests_testmodel" ("int_field", "char_field", "custom_db_col_name", "custom_db_col_name_indexed", "renamed_field_id", "id") SELECT "int_field", "char_field", "custom_db_col_name", "custom_db_col_name_indexed", "renamed_field_id", "id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'RenameNonDefaultColumnNameModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "char_field" varchar(20) NOT NULL, "renamed_field" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "char_field", "custom_db_col_name", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "char_field" varchar(20) NOT NULL, "renamed_field" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'CREATE INDEX "tests_testmodel_custom_db_col_name_indexed" ON "tests_testmodel" ("custom_db_col_name_indexed");',
+ 'CREATE INDEX "tests_testmodel_fk_field_id" ON "tests_testmodel" ("fk_field_id");',
+ 'INSERT INTO "tests_testmodel" ("int_field", "char_field", "renamed_field", "custom_db_col_name_indexed", "fk_field_id", "id") SELECT "int_field", "char_field", "renamed_field", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'RenameNonDefaultColumnNameToNonDefaultNameModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "char_field" varchar(20) NOT NULL, "non-default_column_name" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "char_field", "custom_db_col_name", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "char_field" varchar(20) NOT NULL, "non-default_column_name" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'CREATE INDEX "tests_testmodel_custom_db_col_name_indexed" ON "tests_testmodel" ("custom_db_col_name_indexed");',
+ 'CREATE INDEX "tests_testmodel_fk_field_id" ON "tests_testmodel" ("fk_field_id");',
+ 'INSERT INTO "tests_testmodel" ("int_field", "char_field", "non-default_column_name", "custom_db_col_name_indexed", "fk_field_id", "id") SELECT "int_field", "char_field", "non-default_column_name", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'RenameNonDefaultColumnNameToNonDefaultNameAndTableModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "char_field" varchar(20) NOT NULL, "non-default_column_name2" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "char_field", "custom_db_col_name", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "char_field" varchar(20) NOT NULL, "non-default_column_name2" integer NOT NULL, "custom_db_col_name_indexed" integer NOT NULL, "fk_field_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY);',
+ 'CREATE INDEX "tests_testmodel_custom_db_col_name_indexed" ON "tests_testmodel" ("custom_db_col_name_indexed");',
+ 'CREATE INDEX "tests_testmodel_fk_field_id" ON "tests_testmodel" ("fk_field_id");',
+ 'INSERT INTO "tests_testmodel" ("int_field", "char_field", "non-default_column_name2", "custom_db_col_name_indexed", "fk_field_id", "id") SELECT "int_field", "char_field", "non-default_column_name2", "custom_db_col_name_indexed", "fk_field_id", "id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'RenameColumnCustomTableModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("id" integer NOT NULL UNIQUE PRIMARY KEY, "renamed_field" integer NOT NULL, "alt_value" varchar(20) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "id", "value", "alt_value" FROM "custom_rename_table_name";',
+ 'DROP TABLE "custom_rename_table_name";',
+ 'CREATE TABLE "custom_rename_table_name"("id" integer NOT NULL UNIQUE PRIMARY KEY, "renamed_field" integer NOT NULL, "alt_value" varchar(20) NOT NULL);',
+ 'INSERT INTO "custom_rename_table_name" ("id", "renamed_field", "alt_value") SELECT "id", "renamed_field", "alt_value" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'RenameManyToManyTableModel':
+ 'ALTER TABLE "tests_testmodel_m2m_field" RENAME TO "tests_testmodel_renamed_field";',
+ 'RenameManyToManyTableWithColumnNameModel':
+ 'ALTER TABLE "tests_testmodel_m2m_field" RENAME TO "tests_testmodel_renamed_field";',
+ 'RenameNonDefaultManyToManyTableModel':
+ 'ALTER TABLE "non-default_db_table" RENAME TO "tests_testmodel_renamed_field";',
+}
+
+sql_mutation = {
+ 'SQLMutationSequence': """[
+... SQLMutation('first-two-fields', [
+... 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field1" integer NULL;',
+... 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field2" integer NULL;'
+... ], update_first_two),
+... SQLMutation('third-field', [
+... 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field3" integer NULL;',
+... ], update_third)]
+""",
+ 'SQLMutationOutput':
+ '\n'.join([
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field1" integer NULL;',
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field2" integer NULL;',
+ 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field3" integer NULL;',
+ ]),
+}
+
+generics = {
+ 'DeleteColumnModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "content_type_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "object_id" integer unsigned NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "content_type_id", "id", "object_id" FROM "tests_testmodel";',
+ 'DROP TABLE "tests_testmodel";',
+ 'CREATE TABLE "tests_testmodel"("int_field" integer NOT NULL, "content_type_id" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "object_id" integer unsigned NOT NULL);',
+ 'CREATE INDEX "tests_testmodel_content_type_id" ON "tests_testmodel" ("content_type_id");',
+ 'CREATE INDEX "tests_testmodel_object_id" ON "tests_testmodel" ("object_id");',
+ 'INSERT INTO "tests_testmodel" ("int_field", "content_type_id", "id", "object_id") SELECT "int_field", "content_type_id", "id", "object_id" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ])
+}
+
+inheritance = {
+ 'AddToChildModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "int_field", "id", "char_field", "added_field" FROM "tests_childmodel";',
+ 'UPDATE "TEMP_TABLE" SET "added_field" = 42;',
+ 'DROP TABLE "tests_childmodel";',
+ 'CREATE TABLE "tests_childmodel"("int_field" integer NOT NULL, "id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL, "added_field" integer NOT NULL);',
+ 'INSERT INTO "tests_childmodel" ("int_field", "id", "char_field", "added_field") SELECT "int_field", "id", "char_field", "added_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";',
+ ]),
+ 'DeleteFromChildModel':
+ '\n'.join([
+ 'CREATE TEMPORARY TABLE "TEMP_TABLE"("id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL);',
+ 'INSERT INTO "TEMP_TABLE" SELECT "id", "char_field" FROM "tests_childmodel";',
+ 'DROP TABLE "tests_childmodel";',
+ 'CREATE TABLE "tests_childmodel"("id" integer NOT NULL UNIQUE PRIMARY KEY, "char_field" varchar(20) NOT NULL);',
+ 'INSERT INTO "tests_childmodel" ("id", "char_field") SELECT "id", "char_field" FROM "TEMP_TABLE";',
+ 'DROP TABLE "TEMP_TABLE";'
+ ])
+}
\ No newline at end of file
diff --git a/lib/django_evolution/tests/delete_app.py b/lib/django_evolution/tests/delete_app.py
new file mode 100644
index 0000000..dddbd70
--- /dev/null
+++ b/lib/django_evolution/tests/delete_app.py
@@ -0,0 +1,76 @@
+from django_evolution.tests.utils import test_sql_mapping
+
+tests = r"""
+>>> from datetime import datetime
+>>> from pprint import PrettyPrinter
+
+>>> from django.db import models
+
+>>> from django_evolution.mutations import AddField, DeleteField, DeleteApplication
+>>> from django_evolution.tests.utils import test_proj_sig, execute_test_sql, register_models, deregister_models
+>>> from django_evolution.diff import Diff
+>>> from django_evolution import signature
+>>> from django_evolution import models as test_app
+
+>>> import copy
+
+>>> class AppDeleteAnchor1(models.Model):
+... value = models.IntegerField()
+
+>>> class AppDeleteAnchor2(models.Model):
+... value = models.IntegerField()
+... class Meta:
+... db_table = 'app_delete_custom_add_anchor_table'
+
+>>> class AppDeleteBaseModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... anchor_fk = models.ForeignKey(AppDeleteAnchor1)
+... anchor_m2m = models.ManyToManyField(AppDeleteAnchor2)
+
+>>> class AppDeleteCustomTableModel(models.Model):
+... value = models.IntegerField()
+... alt_value = models.CharField(max_length=20)
+... class Meta:
+... db_table = 'app_delete_custom_table_name'
+
+# Store the base signatures, and populate the app cache
+
+>>> anchors = [('AppDeleteAnchor1', AppDeleteAnchor1), ('AppDeleteAnchor2',AppDeleteAnchor2)]
+>>> test_model = [('TestModel', AppDeleteBaseModel)]
+>>> custom_model = [('CustomTestModel', AppDeleteCustomTableModel)]
+>>> all_models = []
+>>> all_models.extend(anchors)
+>>> all_models.extend(test_model)
+>>> all_models.extend(custom_model)
+>>> start = register_models(*all_models)
+>>> start_sig = test_proj_sig(*all_models)
+
+# Copy the base signature, and delete the tests app.
+>>> deleted_app_sig = copy.deepcopy(start_sig)
+>>> deleted_app_sig = deleted_app_sig.pop('tests')
+
+>>> d = Diff(start_sig, deleted_app_sig)
+>>> print d.deleted
+{'tests': ['AppDeleteAnchor1', 'TestModel', 'AppDeleteAnchor2', 'CustomTestModel']}
+
+>>> test_sig = copy.deepcopy(start_sig)
+
+>>> test_sql = []
+>>> delete_app = DeleteApplication()
+>>> for app_label in d.deleted.keys():
+... test_sql.append(delete_app.mutate(app_label, test_sig))
+... delete_app.simulate(app_label, test_sig)
+
+>>> Diff(test_sig, deleted_app_sig).is_empty(ignore_apps=True)
+True
+
+>>> for sql_list in test_sql:
+... for sql in sql_list:
+... print sql
+%(DeleteApplication)s
+
+# Clean up after the applications that were installed
+>>> deregister_models()
+
+""" % test_sql_mapping('delete_application')
\ No newline at end of file
diff --git a/lib/django_evolution/tests/delete_field.py b/lib/django_evolution/tests/delete_field.py
new file mode 100644
index 0000000..ba80653
--- /dev/null
+++ b/lib/django_evolution/tests/delete_field.py
@@ -0,0 +1,268 @@
+from django_evolution.tests.utils import test_sql_mapping
+
+tests = r"""
+>>> from django.db import models
+
+>>> from django_evolution.mutations import DeleteField
+>>> from django_evolution.tests.utils import test_proj_sig, execute_test_sql, register_models, deregister_models
+>>> from django_evolution.diff import Diff
+
+>>> import copy
+
+# All Fields
+# db index (ignored for now)
+# db tablespace (ignored for now)
+# db column
+# primary key
+# unique
+
+# M2M Fields
+# to field
+# db table
+
+# Model Meta
+# db table
+# db tablespace (ignored for now)
+# unique together (ignored for now)
+
+# Now, a useful test model we can use for evaluating diffs
+>>> class DeleteAnchor1(models.Model):
+... value = models.IntegerField()
+>>> class DeleteAnchor2(models.Model):
+... value = models.IntegerField()
+>>> class DeleteAnchor3(models.Model):
+... value = models.IntegerField()
+>>> class DeleteAnchor4(models.Model):
+... value = models.IntegerField()
+
+>>> class DeleteBaseModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field2 = models.IntegerField(db_column='non-default_db_column')
+... int_field3 = models.IntegerField(unique=True)
+... fk_field1 = models.ForeignKey(DeleteAnchor1)
+... m2m_field1 = models.ManyToManyField(DeleteAnchor3)
+... m2m_field2 = models.ManyToManyField(DeleteAnchor4, db_table='non-default_m2m_table')
+
+>>> class CustomTableModel(models.Model):
+... value = models.IntegerField()
+... alt_value = models.CharField(max_length=20)
+... class Meta:
+... db_table = 'custom_table_name'
+
+# Store the base signatures
+>>> anchors = (
+... ('DeleteAnchor1', DeleteAnchor1),
+... ('DeleteAnchor2', DeleteAnchor2),
+... ('DeleteAnchor3', DeleteAnchor3),
+... ('DeleteAnchor4', DeleteAnchor4),
+... )
+
+>>> custom_model = ('CustomTableModel', CustomTableModel)
+>>> custom = register_models(custom_model)
+>>> custom_sig = test_proj_sig(custom_model)
+
+>>> test_model = ('TestModel', DeleteBaseModel)
+>>> start = register_models(*anchors)
+>>> start.update(register_models(test_model))
+>>> start_sig = test_proj_sig(test_model, *anchors)
+
+# Deleting a default named column
+>>> class DefaultNamedColumnModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... char_field = models.CharField(max_length=20)
+... int_field2 = models.IntegerField(db_column='non-default_db_column')
+... int_field3 = models.IntegerField(unique=True)
+... fk_field1 = models.ForeignKey(DeleteAnchor1)
+... m2m_field1 = models.ManyToManyField(DeleteAnchor3)
+... m2m_field2 = models.ManyToManyField(DeleteAnchor4, db_table='non-default_m2m_table')
+
+>>> end = register_models(('TestModel', DefaultNamedColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', DefaultNamedColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteField('TestModel', 'int_field')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #DefaultNamedColumnModel
+%(DefaultNamedColumnModel)s
+
+# Deleting a non-default named column
+>>> class NonDefaultNamedColumnModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field3 = models.IntegerField(unique=True)
+... fk_field1 = models.ForeignKey(DeleteAnchor1)
+... m2m_field1 = models.ManyToManyField(DeleteAnchor3)
+... m2m_field2 = models.ManyToManyField(DeleteAnchor4, db_table='non-default_m2m_table')
+
+>>> end = register_models(('TestModel', NonDefaultNamedColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', NonDefaultNamedColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteField('TestModel', 'int_field2')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #NonDefaultNamedColumnModel
+%(NonDefaultNamedColumnModel)s
+
+# Deleting a column with database constraints (unique)
+# TODO: Verify that the produced SQL is actually correct
+# -- BK
+>>> class ConstrainedColumnModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field2 = models.IntegerField(db_column='non-default_db_column')
+... fk_field1 = models.ForeignKey(DeleteAnchor1)
+... m2m_field1 = models.ManyToManyField(DeleteAnchor3)
+... m2m_field2 = models.ManyToManyField(DeleteAnchor4, db_table='non-default_m2m_table')
+
+>>> end = register_models(('TestModel', ConstrainedColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', ConstrainedColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteField('TestModel', 'int_field3')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #ConstrainedColumnModel
+%(ConstrainedColumnModel)s
+
+# Deleting a default m2m
+>>> class DefaultM2MModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field2 = models.IntegerField(db_column='non-default_db_column')
+... int_field3 = models.IntegerField(unique=True)
+... fk_field1 = models.ForeignKey(DeleteAnchor1)
+... m2m_field2 = models.ManyToManyField(DeleteAnchor4, db_table='non-default_m2m_table')
+
+>>> end = register_models(('TestModel', DefaultM2MModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', DefaultM2MModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteField('TestModel', 'm2m_field1')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #DefaultManyToManyModel
+%(DefaultManyToManyModel)s
+
+# Deleting a m2m stored in a non-default table
+>>> class NonDefaultM2MModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field2 = models.IntegerField(db_column='non-default_db_column')
+... int_field3 = models.IntegerField(unique=True)
+... fk_field1 = models.ForeignKey(DeleteAnchor1)
+... m2m_field1 = models.ManyToManyField(DeleteAnchor3)
+
+>>> end = register_models(('TestModel', NonDefaultM2MModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', NonDefaultM2MModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteField('TestModel', 'm2m_field2')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #NonDefaultManyToManyModel
+%(NonDefaultManyToManyModel)s
+
+# Delete a foreign key
+>>> class DeleteForeignKeyModel(models.Model):
+... my_id = models.AutoField(primary_key=True)
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field2 = models.IntegerField(db_column='non-default_db_column')
+... int_field3 = models.IntegerField(unique=True)
+... m2m_field1 = models.ManyToManyField(DeleteAnchor3)
+... m2m_field2 = models.ManyToManyField(DeleteAnchor4, db_table='non-default_m2m_table')
+
+>>> end = register_models(('TestModel', DeleteForeignKeyModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', DeleteForeignKeyModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteField('TestModel', 'fk_field1')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #DeleteForeignKeyModel
+%(DeleteForeignKeyModel)s
+
+# Deleting a column from a non-default table
+>>> class DeleteColumnCustomTableModel(models.Model):
+... alt_value = models.CharField(max_length=20)
+... class Meta:
+... db_table = 'custom_table_name'
+
+>>> end = register_models(('CustomTableModel', DeleteColumnCustomTableModel))
+>>> end_sig = test_proj_sig(('CustomTableModel', DeleteColumnCustomTableModel))
+>>> d = Diff(custom_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteField('CustomTableModel', 'value')"]
+
+>>> test_sig = copy.deepcopy(custom_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(custom, end, test_sql) #DeleteColumnCustomTableModel
+%(DeleteColumnCustomTableModel)s
+
+# Clean up after the applications that were installed
+>>> deregister_models()
+
+""" % test_sql_mapping('delete_field')
\ No newline at end of file
diff --git a/lib/django_evolution/tests/delete_model.py b/lib/django_evolution/tests/delete_model.py
new file mode 100644
index 0000000..73d1739
--- /dev/null
+++ b/lib/django_evolution/tests/delete_model.py
@@ -0,0 +1,131 @@
+from django_evolution.tests.utils import test_sql_mapping
+
+tests = r"""
+>>> from django.db import models
+
+>>> from django_evolution.mutations import DeleteModel
+>>> from django_evolution.tests.utils import test_proj_sig, execute_test_sql, register_models, deregister_models
+>>> from django_evolution.diff import Diff
+
+>>> import copy
+
+# Now, a useful test model we can use for evaluating diffs
+>>> class DeleteModelAnchor(models.Model):
+... value = models.IntegerField()
+>>> class BasicModel(models.Model):
+... value = models.IntegerField()
+>>> class BasicWithM2MModel(models.Model):
+... value = models.IntegerField()
+... m2m = models.ManyToManyField(DeleteModelAnchor)
+>>> class CustomTableModel(models.Model):
+... value = models.IntegerField()
+... class Meta:
+... db_table = 'custom_table_name'
+>>> class CustomTableWithM2MModel(models.Model):
+... value = models.IntegerField()
+... m2m = models.ManyToManyField(DeleteModelAnchor)
+... class Meta:
+... db_table = 'another_custom_table_name'
+
+# Store the base signature
+>>> base_models = (
+... ('DeleteModelAnchor', DeleteModelAnchor),
+... ('BasicModel', BasicModel),
+... ('BasicWithM2MModel', BasicWithM2MModel),
+... ('CustomTableModel', CustomTableModel),
+... ('CustomTableWithM2MModel', CustomTableWithM2MModel),
+... )
+
+>>> start = register_models(*base_models)
+>>> start_sig = test_proj_sig(*base_models)
+
+# Delete a Model
+>>> end_sig = copy.deepcopy(start_sig)
+>>> _ = end_sig['tests'].pop('BasicModel')
+>>> end = copy.deepcopy(start)
+>>> _ = end.pop('basicmodel')
+
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteModel('BasicModel')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #BasicModel
+%(BasicModel)s
+
+# Delete a model with an m2m field
+>>> end_sig = copy.deepcopy(start_sig)
+>>> _ = end_sig['tests'].pop('BasicWithM2MModel')
+>>> end = copy.deepcopy(start)
+>>> _ = end.pop('basicwithm2mmodel')
+
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteModel('BasicWithM2MModel')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # BasicWithM2MModels
+%(BasicWithM2MModel)s
+
+# Delete a model with a custom table name
+>>> end_sig = copy.deepcopy(start_sig)
+>>> _ = end_sig['tests'].pop('CustomTableModel')
+>>> end = copy.deepcopy(start)
+>>> _ = end.pop('customtablemodel')
+
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteModel('CustomTableModel')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #CustomTableModel
+%(CustomTableModel)s
+
+# Delete a model with a custom table name and an m2m field
+>>> end_sig = copy.deepcopy(start_sig)
+>>> _ = end_sig['tests'].pop('CustomTableWithM2MModel')
+
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteModel('CustomTableWithM2MModel')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #CustomTableWithM2MModel
+%(CustomTableWithM2MModel)s
+
+# Clean up after the applications that were installed
+>>> deregister_models()
+
+""" % test_sql_mapping('delete_model')
diff --git a/lib/django_evolution/tests/generics.py b/lib/django_evolution/tests/generics.py
new file mode 100644
index 0000000..dd0987b
--- /dev/null
+++ b/lib/django_evolution/tests/generics.py
@@ -0,0 +1,71 @@
+from django_evolution.tests.utils import test_sql_mapping
+
+tests = r"""
+>>> from django.db import models
+
+>>> from django_evolution.mutations import DeleteField
+>>> from django_evolution.tests.utils import test_proj_sig, execute_test_sql, register_models, deregister_models
+>>> from django_evolution.diff import Diff
+>>> from django.contrib.contenttypes import generic
+>>> from django.contrib.contenttypes.models import ContentType
+
+>>> import copy
+
+# Now, a useful test model we can use for evaluating diffs
+>>> class GenericAnchor(models.Model):
+... value = models.IntegerField()
+... # Host a generic key here, too
+... content_type = models.ForeignKey(ContentType)
+... object_id = models.PositiveIntegerField(db_index=True)
+... content_object = generic.GenericForeignKey('content_type','object_id')
+
+>>> class GenericBaseModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... # Plus a generic foreign key - the Generic itself should be ignored
+... content_type = models.ForeignKey(ContentType)
+... object_id = models.PositiveIntegerField(db_index=True)
+... content_object = generic.GenericForeignKey('content_type','object_id')
+... # Plus a generic relation, which should be ignored
+... generic = generic.GenericRelation(GenericAnchor)
+
+# Store the base signatures
+>>> anchor = ('Anchor', GenericAnchor)
+>>> content_type = ('contenttypes.ContentType', ContentType)
+>>> test_model = ('TestModel', GenericBaseModel)
+>>> start = register_models(anchor)
+>>> start.update(register_models(test_model))
+>>> start_sig = test_proj_sig(test_model, content_type, anchor)
+
+# Delete a column
+>>> class DeleteColumnModel(models.Model):
+... int_field = models.IntegerField()
+... # Plus a generic foreign key - the Generic itself should be ignored
+... content_type = models.ForeignKey(ContentType)
+... object_id = models.PositiveIntegerField(db_index=True)
+... content_object = generic.GenericForeignKey('content_type','object_id')
+... # Plus a generic relation, which should be ignored
+... generic = generic.GenericRelation(GenericAnchor)
+
+>>> end = register_models(('TestModel', DeleteColumnModel), anchor)
+>>> end_sig = test_proj_sig(('TestModel', DeleteColumnModel), content_type, anchor)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteField('TestModel', 'char_field')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #DeleteColumnModel
+%(DeleteColumnModel)s
+
+# Clean up after the applications that were installed
+>>> deregister_models()
+
+""" % test_sql_mapping('generics')
\ No newline at end of file
diff --git a/lib/django_evolution/tests/inheritance.py b/lib/django_evolution/tests/inheritance.py
new file mode 100644
index 0000000..55b140d
--- /dev/null
+++ b/lib/django_evolution/tests/inheritance.py
@@ -0,0 +1,82 @@
+from django_evolution.tests.utils import test_sql_mapping
+
+tests = r"""
+>>> from datetime import datetime
+
+>>> from django.db import models
+
+>>> from django_evolution.mutations import AddField, DeleteField
+>>> from django_evolution.tests.utils import test_proj_sig, execute_test_sql, register_models, deregister_models
+>>> from django_evolution.diff import Diff
+>>> from django_evolution import signature
+>>> from django_evolution import models as test_app
+
+>>> import copy
+
+>>> class ParentModel(models.Model):
+... parent_field = models.CharField(max_length=20)
+... other_field = models.IntegerField()
+
+>>> class ChildModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+
+# Store the base signatures
+>>> parent_model = ('ParentModel', ParentModel)
+>>> parent = register_models(parent_model)
+>>> parent_table_sig = test_proj_sig(parent_model)
+
+>>> test_model = ('ChildModel', ChildModel)
+>>> start = register_models(test_model)
+>>> start_sig = test_proj_sig(test_model, parent_model)
+
+# Add field to child model
+>>> class AddToChildModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field = models.IntegerField(default=42)
+
+>>> end = register_models(('ChildModel', AddToChildModel), parent_model)
+>>> end_sig = test_proj_sig(('ChildModel',AddToChildModel), parent_model)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']] # AddToChildModel
+["AddField('ChildModel', 'added_field', models.IntegerField, initial=42)"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # AddToChildModel
+%(AddToChildModel)s
+
+# Delete field from child model
+>>> class AddToChildModel(models.Model):
+... char_field = models.CharField(max_length=20)
+
+>>> end = register_models(('ChildModel', AddToChildModel), parent_model)
+>>> end_sig = test_proj_sig(('ChildModel',AddToChildModel), parent_model)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']] # DeleteFromChildModel
+["DeleteField('ChildModel', 'int_field')"]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in d.evolution()['tests']:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) # DeleteFromChildModel
+%(DeleteFromChildModel)s
+
+# Clean up after the applications that were installed
+>>> deregister_models()
+
+""" % test_sql_mapping('inheritance')
\ No newline at end of file
diff --git a/lib/django_evolution/tests/models.py b/lib/django_evolution/tests/models.py
new file mode 100644
index 0000000..8f0c2e7
--- /dev/null
+++ b/lib/django_evolution/tests/models.py
@@ -0,0 +1,3 @@
+# This module is used as a placeholder for the registration of test models.
+# It is intentionally empty; individual tests create and register models
+# that will appear to Django as if they are in this module.
\ No newline at end of file
diff --git a/lib/django_evolution/tests/ordering.py b/lib/django_evolution/tests/ordering.py
new file mode 100644
index 0000000..f7a91e9
--- /dev/null
+++ b/lib/django_evolution/tests/ordering.py
@@ -0,0 +1,49 @@
+from django_evolution.tests.utils import test_sql_mapping
+
+tests = r"""
+>>> from django.db import models
+
+>>> from django_evolution.tests.utils import test_proj_sig, register_models, deregister_models
+>>> from django_evolution.diff import Diff
+
+>>> import copy
+
+>>> class Case41Anchor(models.Model):
+... value = models.IntegerField()
+
+>>> class Case41Model(models.Model):
+... value = models.IntegerField()
+... ref = models.ForeignKey(Case41Anchor)
+
+# Store the base signatures
+>>> anchors = (
+... ('Case41Anchor', Case41Anchor),
+... )
+
+>>> test_model = ('TestModel', Case41Model)
+>>> start = register_models(*anchors)
+>>> start.update(register_models(test_model))
+>>> start_sig = test_proj_sig(test_model, *anchors)
+
+# Regression case 41: If deleteing a model and a foreign key to that model,
+# The key deletion needs to happen before the model deletion.
+
+# Delete the foreign key...
+>>> class UpdatedCase41Model(models.Model):
+... value = models.IntegerField()
+
+>>> end = register_models(('TestModel', UpdatedCase41Model), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',UpdatedCase41Model), *anchors)
+
+# ... And also delete the model that was being referenced
+>>> _ = end_sig['tests'].pop('Case41Anchor')
+
+# The evolution sequence needs
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["DeleteField('TestModel', 'ref')", "DeleteModel('Case41Anchor')"]
+
+# Clean up after the applications that were installed
+>>> deregister_models()
+
+"""
\ No newline at end of file
diff --git a/lib/django_evolution/tests/rename_field.py b/lib/django_evolution/tests/rename_field.py
new file mode 100644
index 0000000..b83f520
--- /dev/null
+++ b/lib/django_evolution/tests/rename_field.py
@@ -0,0 +1,399 @@
+from django_evolution.tests.utils import test_sql_mapping
+
+tests = r"""
+# Rename a database column (done)
+# RenameField with a specified db table for a field other than a M2MField is allowed (but will be ignored) (done)
+# Rename a primary key database column (done)
+# Rename a foreign key database column (done)
+
+# Rename a database column with a non-default name to a default name (done)
+# Rename a database column with a non-default name to a different non-default name (done)
+# RenameField with a specified db column and db table is allowed (but one will be ignored) (done)
+
+# Rename a database column in a non-default table (done)
+
+# Rename an indexed database column (Redundant, Not explicitly tested)
+# Rename a database column with null constraints (Redundant, Not explicitly tested)
+
+# Rename a M2M database table (done)
+# RenameField with a specified db column for a M2MField is allowed (but will be ignored) (done)
+# Rename a M2M non-default database table to a default name (done)
+
+>>> from django.db import models
+>>> from django_evolution.mutations import RenameField
+>>> from django_evolution.tests.utils import test_proj_sig, execute_test_sql, register_models, deregister_models
+>>> from django_evolution.diff import Diff
+>>> from django_evolution import signature
+>>> from django_evolution import models as test_app
+
+>>> import copy
+
+>>> class RenameAnchor1(models.Model):
+... value = models.IntegerField()
+
+>>> class RenameAnchor2(models.Model):
+... value = models.IntegerField()
+... class Meta:
+... db_table = 'custom_rename_anchor_table'
+
+>>> class RenameAnchor3(models.Model):
+... value = models.IntegerField()
+
+>>> class RenameBaseModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field_named = models.IntegerField(db_column='custom_db_col_name')
+... int_field_named_indexed = models.IntegerField(db_column='custom_db_col_name_indexed', db_index=True)
+... fk_field = models.ForeignKey(RenameAnchor1)
+... m2m_field = models.ManyToManyField(RenameAnchor2)
+... m2m_field_named = models.ManyToManyField(RenameAnchor3, db_table='non-default_db_table')
+
+>>> class CustomRenameTableModel(models.Model):
+... value = models.IntegerField()
+... alt_value = models.CharField(max_length=20)
+... class Meta:
+... db_table = 'custom_rename_table_name'
+
+# Store the base signatures
+>>> anchors = [
+... ('RenameAnchor1', RenameAnchor1),
+... ('RenameAnchor2', RenameAnchor2),
+... ('RenameAnchor3',RenameAnchor3)
+... ]
+>>> test_model = ('TestModel', RenameBaseModel)
+>>> custom_model = ('CustomTableModel', CustomRenameTableModel)
+
+>>> custom = register_models(custom_model)
+>>> custom_table_sig = test_proj_sig(custom_model)
+
+>>> start = register_models(*anchors)
+>>> start.update(register_models(test_model))
+>>> start_sig = test_proj_sig(test_model, *anchors)
+
+# Rename a database column
+>>> class RenameColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... renamed_field = models.IntegerField()
+... int_field_named = models.IntegerField(db_column='custom_db_col_name')
+... int_field_named_indexed = models.IntegerField(db_column='custom_db_col_name_indexed', db_index=True)
+... fk_field = models.ForeignKey(RenameAnchor1)
+... m2m_field = models.ManyToManyField(RenameAnchor2)
+... m2m_field_named = models.ManyToManyField(RenameAnchor3, db_table='non-default_db_table')
+
+>>> end = register_models(('TestModel', RenameColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel', RenameColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'renamed_field', models.IntegerField, initial=<<USER VALUE REQUIRED>>)", "DeleteField('TestModel', 'int_field')"]
+
+>>> evolution = [RenameField('TestModel', 'int_field', 'renamed_field')]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #RenameColumnModel
+%(RenameColumnModel)s
+
+# RenameField with a specified db table for a field other than a M2MField is allowed (but will be ignored) (done)
+>>> class RenameColumnWithTableNameModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... renamed_field = models.IntegerField()
+... int_field_named = models.IntegerField(db_column='custom_db_col_name')
+... int_field_named_indexed = models.IntegerField(db_column='custom_db_col_name_indexed', db_index=True)
+... fk_field = models.ForeignKey(RenameAnchor1)
+... m2m_field = models.ManyToManyField(RenameAnchor2)
+... m2m_field_named = models.ManyToManyField(RenameAnchor3, db_table='non-default_db_table')
+
+>>> end = register_models(('TestModel', RenameColumnWithTableNameModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',RenameColumnWithTableNameModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'renamed_field', models.IntegerField, initial=<<USER VALUE REQUIRED>>)", "DeleteField('TestModel', 'int_field')"]
+
+>>> evolution = [RenameField('TestModel', 'int_field', 'renamed_field', db_table='ignored_db-table')]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #RenameColumnWithTableNameModel
+%(RenameColumnWithTableNameModel)s
+
+# Rename a primary key database column
+>>> class RenamePrimaryKeyColumnModel(models.Model):
+... my_pk_id = models.AutoField(primary_key=True)
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field_named = models.IntegerField(db_column='custom_db_col_name')
+... int_field_named_indexed = models.IntegerField(db_column='custom_db_col_name_indexed', db_index=True)
+... fk_field = models.ForeignKey(RenameAnchor1)
+... m2m_field = models.ManyToManyField(RenameAnchor2)
+... m2m_field_named = models.ManyToManyField(RenameAnchor3, db_table='non-default_db_table')
+
+>>> end = register_models(('TestModel', RenamePrimaryKeyColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',RenamePrimaryKeyColumnModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'my_pk_id', models.AutoField, initial=<<USER VALUE REQUIRED>>, primary_key=True)", "DeleteField('TestModel', 'id')"]
+
+>>> evolution = [RenameField('TestModel', 'id', 'my_pk_id')]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #RenamePrimaryKeyColumnModel
+%(RenamePrimaryKeyColumnModel)s
+
+# Rename a foreign key database column
+>>> class RenameForeignKeyColumnModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field_named = models.IntegerField(db_column='custom_db_col_name')
+... int_field_named_indexed = models.IntegerField(db_column='custom_db_col_name_indexed', db_index=True)
+... renamed_field = models.ForeignKey(RenameAnchor1)
+... m2m_field = models.ManyToManyField(RenameAnchor2)
+... m2m_field_named = models.ManyToManyField(RenameAnchor3, db_table='non-default_db_table')
+
+>>> end = register_models(('TestModel', RenameForeignKeyColumnModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',RenameForeignKeyColumnModel), *anchors)
+>>> start_sig = copy.deepcopy(start_sig)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'renamed_field', models.ForeignKey, initial=<<USER VALUE REQUIRED>>, related_model='tests.RenameAnchor1')", "DeleteField('TestModel', 'fk_field')"]
+
+>>> evolution = [RenameField('TestModel', 'fk_field', 'renamed_field')]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+# FIXME!! This test doesn't work on Postgres
+#>>> execute_test_sql(start, end, test_sql) #RenameForeignKeyColumnModel
+#%(RenameForeignKeyColumnModel)s
+
+# Rename a database column with a non-default name
+>>> class RenameNonDefaultColumnNameModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... renamed_field = models.IntegerField()
+... int_field_named_indexed = models.IntegerField(db_column='custom_db_col_name_indexed', db_index=True)
+... fk_field = models.ForeignKey(RenameAnchor1)
+... m2m_field = models.ManyToManyField(RenameAnchor2)
+... m2m_field_named = models.ManyToManyField(RenameAnchor3, db_table='non-default_db_table')
+
+>>> end = register_models(('TestModel', RenameNonDefaultColumnNameModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',RenameNonDefaultColumnNameModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'renamed_field', models.IntegerField, initial=<<USER VALUE REQUIRED>>)", "DeleteField('TestModel', 'int_field_named')"]
+
+>>> evolution = [RenameField('TestModel', 'int_field_named', 'renamed_field')]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #RenameNonDefaultColumnNameModel
+%(RenameNonDefaultColumnNameModel)s
+
+# Rename a database column with a non-default name to a different non-default name
+>>> class RenameNonDefaultColumnNameToNonDefaultNameModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... renamed_field = models.IntegerField(db_column='non-default_column_name')
+... int_field_named_indexed = models.IntegerField(db_column='custom_db_col_name_indexed', db_index=True)
+... fk_field = models.ForeignKey(RenameAnchor1)
+... m2m_field = models.ManyToManyField(RenameAnchor2)
+... m2m_field_named = models.ManyToManyField(RenameAnchor3, db_table='non-default_db_table')
+
+>>> end = register_models(('TestModel', RenameNonDefaultColumnNameToNonDefaultNameModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',RenameNonDefaultColumnNameToNonDefaultNameModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'renamed_field', models.IntegerField, initial=<<USER VALUE REQUIRED>>, db_column='non-default_column_name')", "DeleteField('TestModel', 'int_field_named')"]
+
+>>> evolution = [RenameField('TestModel', 'int_field_named', 'renamed_field', db_column='non-default_column_name')]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #RenameNonDefaultColumnNameToNonDefaultNameModel
+%(RenameNonDefaultColumnNameToNonDefaultNameModel)s
+
+# RenameField with a specified db column and db table is allowed (but one will be ignored)
+>>> class RenameNonDefaultColumnNameToNonDefaultNameAndTableModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... renamed_field = models.IntegerField(db_column='non-default_column_name2')
+... int_field_named_indexed = models.IntegerField(db_column='custom_db_col_name_indexed', db_index=True)
+... fk_field = models.ForeignKey(RenameAnchor1)
+... m2m_field = models.ManyToManyField(RenameAnchor2)
+... m2m_field_named = models.ManyToManyField(RenameAnchor3, db_table='non-default_db_table')
+
+>>> end = register_models(('TestModel', RenameNonDefaultColumnNameToNonDefaultNameAndTableModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',RenameNonDefaultColumnNameToNonDefaultNameAndTableModel), *anchors)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'renamed_field', models.IntegerField, initial=<<USER VALUE REQUIRED>>, db_column='non-default_column_name2')", "DeleteField('TestModel', 'int_field_named')"]
+
+>>> evolution = [RenameField('TestModel', 'int_field_named', 'renamed_field', db_column='non-default_column_name2', db_table='custom_ignored_db-table')]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #RenameNonDefaultColumnNameToNonDefaultNameAndTableModel
+%(RenameNonDefaultColumnNameToNonDefaultNameAndTableModel)s
+
+# Rename a database column in a non-default table
+# Rename a database column
+>>> class RenameColumnCustomTableModel(models.Model):
+... renamed_field = models.IntegerField()
+... alt_value = models.CharField(max_length=20)
+... class Meta:
+... db_table = 'custom_rename_table_name'
+
+>>> end = register_models(('CustomTableModel', RenameColumnCustomTableModel))
+>>> end_sig = test_proj_sig(('CustomTableModel',RenameColumnCustomTableModel))
+>>> d = Diff(custom_table_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('CustomTableModel', 'renamed_field', models.IntegerField, initial=<<USER VALUE REQUIRED>>)", "DeleteField('CustomTableModel', 'value')"]
+
+>>> evolution = [RenameField('CustomTableModel', 'value', 'renamed_field')]
+>>> test_sig = copy.deepcopy(custom_table_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(custom, end, test_sql) #RenameColumnCustomTableModel
+%(RenameColumnCustomTableModel)s
+
+# Rename a M2M database table
+>>> class RenameM2MTableModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field_named = models.IntegerField(db_column='custom_db_col_name')
+... int_field_named_indexed = models.IntegerField(db_column='custom_db_col_name_indexed', db_index=True)
+... fk_field = models.ForeignKey(RenameAnchor1)
+... renamed_field = models.ManyToManyField(RenameAnchor2)
+... m2m_field_named = models.ManyToManyField(RenameAnchor3, db_table='non-default_db_table')
+
+>>> end = register_models(('TestModel', RenameM2MTableModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',RenameM2MTableModel), *anchors)
+>>> start_sig = copy.deepcopy(start_sig)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'renamed_field', models.ManyToManyField, related_model='tests.RenameAnchor2')", "DeleteField('TestModel', 'm2m_field')"]
+
+>>> evolution = [RenameField('TestModel', 'm2m_field', 'renamed_field')]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+>>> execute_test_sql(start, end, test_sql) #RenameManyToManyTableModel
+%(RenameManyToManyTableModel)s
+
+# RenameField with a specified db column for a M2MField is allowed (but will be ignored)
+>>> class RenameM2MTableWithColumnNameModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field_named = models.IntegerField(db_column='custom_db_col_name')
+... int_field_named_indexed = models.IntegerField(db_column='custom_db_col_name_indexed', db_index=True)
+... fk_field = models.ForeignKey(RenameAnchor1)
+... renamed_field = models.ManyToManyField(RenameAnchor2)
+... m2m_field_named = models.ManyToManyField(RenameAnchor3, db_table='non-default_db_table')
+
+>>> end = register_models(('TestModel', RenameM2MTableWithColumnNameModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',RenameM2MTableWithColumnNameModel), *anchors)
+>>> start_sig = copy.deepcopy(start_sig)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'renamed_field', models.ManyToManyField, related_model='tests.RenameAnchor2')", "DeleteField('TestModel', 'm2m_field')"]
+
+>>> evolution = [RenameField('TestModel', 'm2m_field', 'renamed_field', db_column='ignored_db-column')]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #RenameManyToManyTableWithColumnNameModel
+%(RenameManyToManyTableWithColumnNameModel)s
+
+# Rename a M2M non-default database table to a default name
+>>> class RenameNonDefaultM2MTableModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... int_field_named = models.IntegerField(db_column='custom_db_col_name')
+... int_field_named_indexed = models.IntegerField(db_column='custom_db_col_name_indexed', db_index=True)
+... fk_field = models.ForeignKey(RenameAnchor1)
+... m2m_field = models.ManyToManyField(RenameAnchor2)
+... renamed_field = models.ManyToManyField(RenameAnchor3, db_table='non-default_db_table')
+
+>>> end = register_models(('TestModel', RenameNonDefaultM2MTableModel), *anchors)
+>>> end_sig = test_proj_sig(('TestModel',RenameNonDefaultM2MTableModel), *anchors)
+>>> start_sig = copy.deepcopy(start_sig)
+>>> d = Diff(start_sig, end_sig)
+>>> print [str(e) for e in d.evolution()['tests']]
+["AddField('TestModel', 'renamed_field', models.ManyToManyField, db_table='non-default_db_table', related_model='tests.RenameAnchor3')", "DeleteField('TestModel', 'm2m_field_named')"]
+
+>>> evolution = [RenameField('TestModel', 'm2m_field_named', 'renamed_field')]
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in evolution:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+False
+
+# FIXME!! This test fails under Postgres
+#>>> execute_test_sql(start, end, test_sql) #RenameNonDefaultManyToManyTableModel
+#%(RenameNonDefaultManyToManyTableModel)s
+
+# Clean up after the applications that were installed
+>>> deregister_models()
+
+""" % test_sql_mapping('rename_field')
\ No newline at end of file
diff --git a/lib/django_evolution/tests/signature.py b/lib/django_evolution/tests/signature.py
new file mode 100644
index 0000000..9dde07a
--- /dev/null
+++ b/lib/django_evolution/tests/signature.py
@@ -0,0 +1,248 @@
+
+tests = r"""
+>>> from django.db import models
+>>> from django_evolution import signature
+>>> from django_evolution.diff import Diff
+>>> from django_evolution.tests.utils import test_proj_sig, register_models, deregister_models
+>>> from pprint import pprint
+>>> from django.contrib.contenttypes import generic
+>>> from django.contrib.contenttypes.models import ContentType
+
+# First, a model that has one of everything so we can validate all cases for a signature
+>>> class Anchor1(models.Model):
+... value = models.IntegerField()
+>>> class Anchor2(models.Model):
+... value = models.IntegerField()
+>>> class Anchor3(models.Model):
+... value = models.IntegerField()
+... # Host a generic key here, too
+... content_type = models.ForeignKey(ContentType)
+... object_id = models.PositiveIntegerField(db_index=True)
+... content_object = generic.GenericForeignKey('content_type','object_id')
+
+>>> anchors = [('Anchor1', Anchor1),('Anchor2', Anchor2),('Anchor3', Anchor3)]
+
+>>> class SigModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... null_field = models.IntegerField(null=True, db_column='size_column')
+... id_card = models.IntegerField(unique=True, db_index=True)
+... dec_field = models.DecimalField(max_digits=10, decimal_places=4)
+... ref1 = models.ForeignKey(Anchor1)
+... ref2 = models.ForeignKey(Anchor1, related_name='other_sigmodel')
+... ref3 = models.ForeignKey(Anchor2, db_column='value', db_index=True)
+... ref4 = models.ForeignKey('self')
+... ref5 = models.ManyToManyField(Anchor3)
+... ref6 = models.ManyToManyField(Anchor3, related_name='other_sigmodel')
+... ref7 = models.ManyToManyField('self')
+... # Plus a generic foreign key - the Generic itself should be ignored
+... content_type = models.ForeignKey(ContentType)
+... object_id = models.PositiveIntegerField(db_index=True)
+... content_object = generic.GenericForeignKey('content_type','object_id')
+... # Plus a generic relation, which should be ignored
+... generic = generic.GenericRelation(Anchor3)
+
+>>> class ParentModel(models.Model):
+... parent_field = models.CharField(max_length=20)
+
+>>> class ChildModel(ParentModel):
+... child_field = models.CharField(max_length=20)
+
+# Store the base signatures
+>>> base_cache = register_models(('Anchor1', Anchor1), ('Anchor2', Anchor2), ('Anchor3', Anchor3), ('TestModel', SigModel), ('ParentModel',ParentModel), ('ChildModel',ChildModel))
+
+# You can create a model signature for a model
+>>> pprint(signature.create_model_sig(SigModel))
+{'fields': {'char_field': {'field_type': <class 'django.db.models.fields.CharField'>,
+ 'max_length': 20},
+ 'content_type': {'field_type': <class 'django.db.models.fields.related.ForeignKey'>,
+ 'related_model': 'contenttypes.ContentType'},
+ 'dec_field': {'decimal_places': 4,
+ 'field_type': <class 'django.db.models.fields.DecimalField'>,
+ 'max_digits': 10},
+ 'id': {'field_type': <class 'django.db.models.fields.AutoField'>,
+ 'primary_key': True},
+ 'id_card': {'db_index': True,
+ 'field_type': <class 'django.db.models.fields.IntegerField'>,
+ 'unique': True},
+ 'int_field': {'field_type': <class 'django.db.models.fields.IntegerField'>},
+ 'null_field': {'db_column': 'size_column',
+ 'field_type': <class 'django.db.models.fields.IntegerField'>,
+ 'null': True},
+ 'object_id': {'db_index': True,
+ 'field_type': <class 'django.db.models.fields.PositiveIntegerField'>},
+ 'ref1': {'field_type': <class 'django.db.models.fields.related.ForeignKey'>,
+ 'related_model': 'tests.Anchor1'},
+ 'ref2': {'field_type': <class 'django.db.models.fields.related.ForeignKey'>,
+ 'related_model': 'tests.Anchor1'},
+ 'ref3': {'db_column': 'value',
+ 'field_type': <class 'django.db.models.fields.related.ForeignKey'>,
+ 'related_model': 'tests.Anchor2'},
+ 'ref4': {'field_type': <class 'django.db.models.fields.related.ForeignKey'>,
+ 'related_model': 'tests.TestModel'},
+ 'ref5': {'field_type': <class 'django.db.models.fields.related.ManyToManyField'>,
+ 'related_model': 'tests.Anchor3'},
+ 'ref6': {'field_type': <class 'django.db.models.fields.related.ManyToManyField'>,
+ 'related_model': 'tests.Anchor3'},
+ 'ref7': {'field_type': <class 'django.db.models.fields.related.ManyToManyField'>,
+ 'related_model': 'tests.TestModel'}},
+ 'meta': {'db_table': 'tests_testmodel',
+ 'db_tablespace': '',
+ 'pk_column': 'id',
+ 'unique_together': []}}
+
+>>> pprint(signature.create_model_sig(ChildModel))
+{'fields': {'child_field': {'field_type': <class 'django.db.models.fields.CharField'>,
+ 'max_length': 20},
+ 'parentmodel_ptr': {'field_type': <class 'django.db.models.fields.related.OneToOneField'>,
+ 'primary_key': True,
+ 'related_model': 'tests.ParentModel',
+ 'unique': True}},
+ 'meta': {'db_table': 'tests_childmodel',
+ 'db_tablespace': '',
+ 'pk_column': 'parentmodel_ptr_id',
+ 'unique_together': []}}
+
+# Now, a useful test model we can use for evaluating diffs
+>>> class BaseModel(models.Model):
+... name = models.CharField(max_length=20)
+... age = models.IntegerField()
+... ref = models.ForeignKey(Anchor1)
+>>> start = register_models(('TestModel', BaseModel), *anchors)
+
+>>> start_sig = test_proj_sig(('TestModel', BaseModel), *anchors)
+
+# An identical model gives an empty Diff
+>>> class TestModel(models.Model):
+... name = models.CharField(max_length=20)
+... age = models.IntegerField()
+... ref = models.ForeignKey(Anchor1)
+
+>>> end = register_models(('TestModel', TestModel), *anchors)
+>>> test_sig = test_proj_sig(('TestModel',TestModel), *anchors)
+>>> d = Diff(start_sig, test_sig)
+>>> d.is_empty()
+True
+>>> d.evolution()
+{}
+
+# Adding a field gives a non-empty diff
+>>> class AddFieldModel(models.Model):
+... name = models.CharField(max_length=20)
+... age = models.IntegerField()
+... ref = models.ForeignKey(Anchor1)
+... date_of_birth = models.DateField()
+
+>>> end = register_models(('TestModel', AddFieldModel), *anchors)
+>>> test_sig = test_proj_sig(('TestModel',AddFieldModel), *anchors)
+>>> d = Diff(start_sig, test_sig)
+>>> d.is_empty()
+False
+>>> print [str(e) for e in d.evolution()['tests']] # Add Field
+["AddField('TestModel', 'date_of_birth', models.DateField, initial=<<USER VALUE REQUIRED>>)"]
+
+# Deleting a field gives a non-empty diff
+>>> class DeleteFieldModel(models.Model):
+... name = models.CharField(max_length=20)
+... ref = models.ForeignKey(Anchor1)
+
+>>> end = register_models(('TestModel', DeleteFieldModel), *anchors)
+>>> test_sig = test_proj_sig(('TestModel',DeleteFieldModel), *anchors)
+>>> d = Diff(start_sig, test_sig)
+>>> d.is_empty()
+False
+>>> print [str(e) for e in d.evolution()['tests']] # Delete Field
+["DeleteField('TestModel', 'age')"]
+
+# Renaming a field is caught as 2 diffs
+# (For the moment - long term, this should hint as a Rename)
+>>> class RenameFieldModel(models.Model):
+... full_name = models.CharField(max_length=20)
+... age = models.IntegerField()
+... ref = models.ForeignKey(Anchor1)
+
+>>> end = register_models(('TestModel', RenameFieldModel), *anchors)
+>>> test_sig = test_proj_sig(('TestModel',RenameFieldModel), *anchors)
+>>> d = Diff(start_sig, test_sig)
+>>> d.is_empty()
+False
+>>> print [str(e) for e in d.evolution()['tests']] # Rename Field
+["AddField('TestModel', 'full_name', models.CharField, initial=<<USER VALUE REQUIRED>>, max_length=20)", "DeleteField('TestModel', 'name')"]
+
+# Adding a property to a field which was not present in the original Model
+>>> class AddPropertyModel(models.Model):
+... name = models.CharField(max_length=20)
+... age = models.IntegerField(null=True)
+... ref = models.ForeignKey(Anchor1)
+
+>>> end = register_models(('TestModel', AddPropertyModel), *anchors)
+>>> test_sig = test_proj_sig(('TestModel',AddPropertyModel), *anchors)
+>>> d = Diff(start_sig, test_sig)
+>>> d.is_empty()
+False
+
+>>> print [str(e) for e in d.evolution()['tests']] # Change Field - add property
+["ChangeField('TestModel', 'age', initial=None, null=True)"]
+
+# Since we can't check the evolutions, check the diff instead
+>>> print d
+In model tests.TestModel:
+ In field 'age':
+ Property 'null' has changed
+
+# Adding a property of a field which was not present in the original Model, but
+# is now set to the default for that property.
+>>> class AddDefaultPropertyModel(models.Model):
+... name = models.CharField(max_length=20)
+... age = models.IntegerField(null=False)
+... ref = models.ForeignKey(Anchor1)
+
+>>> end = register_models(('TestModel', AddDefaultPropertyModel), *anchors)
+>>> test_sig = test_proj_sig(('TestModel',AddDefaultPropertyModel), *anchors)
+>>> d = Diff(start_sig, test_sig)
+>>> d.is_empty()
+True
+>>> print d.evolution()
+{}
+
+# Changing a property of a field
+>>> class ChangePropertyModel(models.Model):
+... name = models.CharField(max_length=30)
+... age = models.IntegerField()
+... ref = models.ForeignKey(Anchor1)
+
+>>> end = register_models(('TestModel', ChangePropertyModel), *anchors)
+>>> test_sig = test_proj_sig(('TestModel',ChangePropertyModel), *anchors)
+>>> d = Diff(start_sig, test_sig)
+>>> d.is_empty()
+False
+
+>>> print [str(e) for e in d.evolution()['tests']] # Change Field - change property
+["ChangeField('TestModel', 'name', initial=None, max_length=30)"]
+
+# Since we can't check the evolutions, check the diff instead
+>>> print d
+In model tests.TestModel:
+ In field 'name':
+ Property 'max_length' has changed
+
+# Changing the model that a ForeignKey references
+>>> class ChangeFKModel(models.Model):
+... name = models.CharField(max_length=20)
+... age = models.IntegerField()
+... ref = models.ForeignKey(Anchor2)
+
+>>> end = register_models(('TestModel', ChangeFKModel), *anchors)
+>>> test_sig = test_proj_sig(('TestModel',ChangeFKModel), *anchors)
+>>> d = Diff(start_sig, test_sig)
+>>> d.is_empty()
+False
+
+>>> print [str(e) for e in d.evolution()['tests']] # Change Field - change property
+["ChangeField('TestModel', 'ref', initial=None, related_model='tests.Anchor2')"]
+
+# Clean up after the applications that were installed
+>>> deregister_models()
+
+"""
+
diff --git a/lib/django_evolution/tests/sql_mutation.py b/lib/django_evolution/tests/sql_mutation.py
new file mode 100644
index 0000000..71d6785
--- /dev/null
+++ b/lib/django_evolution/tests/sql_mutation.py
@@ -0,0 +1,93 @@
+from django_evolution.tests.utils import test_sql_mapping
+
+tests = r"""
+>>> from django.db import models
+>>> from django_evolution.mutations import SQLMutation
+
+>>> from django.db import models
+
+>>> from django_evolution.mutations import AddField
+>>> from django_evolution.tests.utils import test_proj_sig, execute_test_sql, register_models, deregister_models
+>>> from django_evolution.diff import Diff
+>>> from django_evolution import signature
+>>> from django_evolution import models as test_app
+
+>>> import copy
+
+>>> class SQLBaseModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+
+# Store the base signatures
+>>> start = register_models(('TestModel', SQLBaseModel))
+>>> start_sig = test_proj_sig(('TestModel', SQLBaseModel))
+
+# Add 3 Fields resulting in new database columns.
+>>> class SQLMutationModel(models.Model):
+... char_field = models.CharField(max_length=20)
+... int_field = models.IntegerField()
+... added_field1 = models.IntegerField(null=True)
+... added_field2 = models.IntegerField(null=True)
+... added_field3 = models.IntegerField(null=True)
+>>> end = register_models(('TestModel', SQLMutationModel))
+>>> end_sig = test_proj_sig(('TestModel',SQLMutationModel))
+>>> d = Diff(start_sig, end_sig)
+
+# Add the fields using SQLMutations
+>>> sequence = [
+... SQLMutation('first-two-fields', [
+... 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field1" integer NULL;',
+... 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field2" integer NULL;'
+... ]),
+... SQLMutation('third-field', [
+... 'ALTER TABLE "tests_testmodel" ADD COLUMN "added_field3" integer NULL;',
+... ])]
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in sequence:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+Traceback (most recent call last):
+...
+CannotSimulate: Cannot simulate SQLMutations
+
+# Redefine the sequence with update functions.
+>>> def update_first_two(app_label, proj_sig):
+... app_sig = proj_sig[app_label]
+... model_sig = app_sig['TestModel']
+... model_sig['fields']['added_field1'] = {
+... 'field_type': models.IntegerField,
+... 'null': True
+... }
+... model_sig['fields']['added_field2'] = {
+... 'field_type': models.IntegerField,
+... 'null': True
+... }
+
+>>> def update_third(app_label, proj_sig):
+... app_sig = proj_sig[app_label]
+... model_sig = app_sig['TestModel']
+... model_sig['fields']['added_field3'] = {
+... 'field_type': models.IntegerField,
+... 'null': True
+... }
+
+>>> sequence = %(SQLMutationSequence)s
+
+>>> test_sig = copy.deepcopy(start_sig)
+>>> test_sql = []
+>>> for mutation in sequence:
+... test_sql.extend(mutation.mutate('tests', test_sig))
+... mutation.simulate('tests', test_sig)
+
+>>> Diff(test_sig, end_sig).is_empty()
+True
+
+>>> execute_test_sql(start, end, test_sql) #SQLMutationOutput
+%(SQLMutationOutput)s
+
+# Clean up after the applications that were installed
+>>> deregister_models()
+
+""" % test_sql_mapping('sql_mutation')
\ No newline at end of file
diff --git a/lib/django_evolution/tests/utils.py b/lib/django_evolution/tests/utils.py
new file mode 100644
index 0000000..6a6f1e0
--- /dev/null
+++ b/lib/django_evolution/tests/utils.py
@@ -0,0 +1,185 @@
+import copy
+
+from datetime import datetime
+from django.core.management.color import no_style
+from django.core.management.sql import sql_create, sql_delete, sql_indexes
+from django.db import connection, transaction, settings, models
+from django.db.backends.util import truncate_name
+from django.db.models.loading import cache
+
+from django_evolution import signature
+from django_evolution.tests import models as evo_test
+from django_evolution.utils import write_sql, execute_sql
+
+from django.contrib.contenttypes import models as contenttypes
+
+DEFAULT_TEST_ATTRIBUTE_VALUES = {
+ models.CharField: 'TestCharField',
+ models.IntegerField: '123',
+ models.AutoField: None,
+ models.DateTimeField: datetime.now(),
+ models.PositiveIntegerField: '42'
+}
+
+def register_models(*models):
+ app_cache = {}
+ for name, model in models:
+ if model._meta.module_name in cache.app_models['django_evolution']:
+ del cache.app_models['django_evolution'][model._meta.module_name]
+
+ if model._meta.db_table.startswith("%s_%s" % (model._meta.app_label,
+ model._meta.module_name)):
+ model._meta.db_table = 'tests_%s' % name.lower()
+
+ model._meta.app_label = 'tests'
+ model._meta.object_name = name
+ model._meta.module_name = name.lower()
+
+ cache.app_models.setdefault('tests', {})[name.lower()] = model
+
+ app_cache[name.lower()] = model
+
+ return app_cache
+
+def test_proj_sig(*models, **kwargs):
+ "Generate a dummy project signature based around a single model"
+ version = kwargs.get('version',1)
+ proj_sig = {
+ 'tests': {},
+ '__version__': version,
+ }
+
+ # Compute the project siguature
+ for full_name,model in models:
+ parts = full_name.split('.')
+ if len(parts) == 1:
+ name = parts[0]
+ app = 'tests'
+ else:
+ app,name = parts
+ proj_sig.setdefault(app,{})[name] = signature.create_model_sig(model)
+
+ return proj_sig
+
+def execute_transaction(sql, output=False):
+ "A transaction wrapper for executing a list of SQL statements"
+ try:
+ # Begin Transaction
+ transaction.enter_transaction_management()
+ transaction.managed(True)
+ cursor = connection.cursor()
+
+ # Perform the SQL
+ if output:
+ write_sql(sql)
+ execute_sql(cursor, sql)
+
+ transaction.commit()
+ transaction.leave_transaction_management()
+ except Exception, ex:
+ transaction.rollback()
+ raise ex
+
+def execute_test_sql(start, end, sql, debug=False):
+ """
+ Execute a test SQL sequence. This method also creates and destroys the
+ database tables required by the models registered against the test application.
+
+ start and end are the start- and end-point states of the application cache.
+
+ sql is the list of sql statements to execute.
+
+ cleanup is a list of extra sql statements required to clean up. This is
+ primarily for any extra m2m tables that were added during a test that won't
+ be cleaned up by Django's sql_delete() implementation.
+
+ debug is a helper flag. It displays the ALL the SQL that would be executed,
+ (including setup and teardown SQL), and executes the Django-derived setup/teardown
+ SQL.
+ """
+ # Set up the initial state of the app cache
+ cache.app_models['tests'] = copy.deepcopy(start)
+
+ # Install the initial tables and indicies
+ style = no_style()
+ execute_transaction(sql_create(evo_test, style), output=debug)
+ execute_transaction(sql_indexes(evo_test, style), output=debug)
+ create_test_data(models.get_models(evo_test))
+
+ # Set the app cache to the end state
+ cache.app_models['tests'] = copy.deepcopy(end)
+
+ try:
+ # Execute the test sql
+ if debug:
+ write_sql(sql)
+ else:
+ execute_transaction(sql, output=True)
+ finally:
+ # Cleanup the apps.
+ if debug:
+ print sql_delete(evo_test, style)
+ else:
+ execute_transaction(sql_delete(evo_test, style), output=debug)
+
+def create_test_data(app_models):
+ deferred_models = []
+ deferred_fields = {}
+ for model in app_models:
+ params = {}
+ deferred = False
+ for field in model._meta.fields:
+ if not deferred:
+ if type(field) == models.ForeignKey or type(field) == models.ManyToManyField:
+ related_model = field.rel.to
+ if related_model.objects.count():
+ related_instance = related_model.objects.all()[0]
+ else:
+ if field.null == False:
+ # Field cannot be null yet the related object hasn't been created yet
+ # Defer the creation of this model
+ deferred = True
+ deferred_models.append(model)
+ else:
+ # Field cannot be set yet but null is acceptable for the moment
+ deferred_fields[type(model)] = deferred_fields.get(type(model), []).append(field)
+ related_instance = None
+ if not deferred:
+ if type(field) == models.ForeignKey:
+ params[field.name] = related_instance
+ else:
+ params[field.name] = [related_instance]
+ else:
+ params[field.name] = DEFAULT_TEST_ATTRIBUTE_VALUES[type(field)]
+
+ if not deferred:
+ model(**params).save()
+
+ # Create all deferred models.
+ if deferred_models:
+ create_test_data(deferred_models)
+
+ # All models should be created (Not all deferred fields have been populated yet)
+ # Populate deferred fields that we know about.
+ # Here lies untested code!
+ if deferred_fields:
+ for model, field_list in deferred_fields.items():
+ for field in field_list:
+ related_model = field.rel.to
+ related_instance = related_model.objects.all()[0]
+ if type(field) == models.ForeignKey:
+ setattr(model, field.name, related_instance)
+ else:
+ getattr(model, field.name).add(related_instance)
+ model.save()
+
+def test_sql_mapping(test_field_name):
+ engine = settings.DATABASE_ENGINE
+ sql_for_engine = __import__('django_evolution.tests.db.%s' % (settings.DATABASE_ENGINE), {}, {}, [''])
+ return getattr(sql_for_engine, test_field_name)
+
+
+def deregister_models():
+ "Clear the test section of the app cache"
+ del cache.app_models['tests']
+
\ No newline at end of file
diff --git a/lib/django_evolution/utils.py b/lib/django_evolution/utils.py
new file mode 100644
index 0000000..e210898
--- /dev/null
+++ b/lib/django_evolution/utils.py
@@ -0,0 +1,22 @@
+from django_evolution.db import evolver
+
+def write_sql(sql):
+ "Output a list of SQL statements, unrolling parameters as required"
+ for statement in sql:
+ if isinstance(statement, tuple):
+ print unicode(statement[0] % tuple(evolver.quote_sql_param(s) for s in statement[1]))
+ else:
+ print unicode(statement)
+
+def execute_sql(cursor, sql):
+ """
+ Execute a list of SQL statements on the provided cursor, unrolling
+ parameters as required
+ """
+ for statement in sql:
+ if isinstance(statement, tuple):
+ if not statement[0].startswith('--'):
+ cursor.execute(*statement)
+ else:
+ if not statement.startswith('--'):
+ cursor.execute(statement)
diff --git a/registration/management/commands/__init__.py b/lib/registration/__init__.py
similarity index 100%
rename from registration/management/commands/__init__.py
rename to lib/registration/__init__.py
diff --git a/registration/admin.py b/lib/registration/admin.py
similarity index 100%
rename from registration/admin.py
rename to lib/registration/admin.py
diff --git a/registration/forms.py b/lib/registration/forms.py
similarity index 100%
rename from registration/forms.py
rename to lib/registration/forms.py
diff --git a/registration/locale/ar/LC_MESSAGES/django.mo b/lib/registration/locale/ar/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/ar/LC_MESSAGES/django.mo
rename to lib/registration/locale/ar/LC_MESSAGES/django.mo
diff --git a/registration/locale/ar/LC_MESSAGES/django.po b/lib/registration/locale/ar/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/ar/LC_MESSAGES/django.po
rename to lib/registration/locale/ar/LC_MESSAGES/django.po
diff --git a/registration/locale/bg/LC_MESSAGES/django.mo b/lib/registration/locale/bg/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/bg/LC_MESSAGES/django.mo
rename to lib/registration/locale/bg/LC_MESSAGES/django.mo
diff --git a/registration/locale/bg/LC_MESSAGES/django.po b/lib/registration/locale/bg/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/bg/LC_MESSAGES/django.po
rename to lib/registration/locale/bg/LC_MESSAGES/django.po
diff --git a/registration/locale/de/LC_MESSAGES/django.mo b/lib/registration/locale/de/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/de/LC_MESSAGES/django.mo
rename to lib/registration/locale/de/LC_MESSAGES/django.mo
diff --git a/registration/locale/de/LC_MESSAGES/django.po b/lib/registration/locale/de/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/de/LC_MESSAGES/django.po
rename to lib/registration/locale/de/LC_MESSAGES/django.po
diff --git a/registration/locale/el/LC_MESSAGES/django.mo b/lib/registration/locale/el/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/el/LC_MESSAGES/django.mo
rename to lib/registration/locale/el/LC_MESSAGES/django.mo
diff --git a/registration/locale/el/LC_MESSAGES/django.po b/lib/registration/locale/el/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/el/LC_MESSAGES/django.po
rename to lib/registration/locale/el/LC_MESSAGES/django.po
diff --git a/registration/locale/en/LC_MESSAGES/django.mo b/lib/registration/locale/en/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/en/LC_MESSAGES/django.mo
rename to lib/registration/locale/en/LC_MESSAGES/django.mo
diff --git a/registration/locale/en/LC_MESSAGES/django.po b/lib/registration/locale/en/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/en/LC_MESSAGES/django.po
rename to lib/registration/locale/en/LC_MESSAGES/django.po
diff --git a/registration/locale/es/LC_MESSAGES/django.mo b/lib/registration/locale/es/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/es/LC_MESSAGES/django.mo
rename to lib/registration/locale/es/LC_MESSAGES/django.mo
diff --git a/registration/locale/es/LC_MESSAGES/django.po b/lib/registration/locale/es/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/es/LC_MESSAGES/django.po
rename to lib/registration/locale/es/LC_MESSAGES/django.po
diff --git a/registration/locale/es_AR/LC_MESSAGES/django.mo b/lib/registration/locale/es_AR/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/es_AR/LC_MESSAGES/django.mo
rename to lib/registration/locale/es_AR/LC_MESSAGES/django.mo
diff --git a/registration/locale/es_AR/LC_MESSAGES/django.po b/lib/registration/locale/es_AR/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/es_AR/LC_MESSAGES/django.po
rename to lib/registration/locale/es_AR/LC_MESSAGES/django.po
diff --git a/registration/locale/fr/LC_MESSAGES/django.mo b/lib/registration/locale/fr/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/fr/LC_MESSAGES/django.mo
rename to lib/registration/locale/fr/LC_MESSAGES/django.mo
diff --git a/registration/locale/fr/LC_MESSAGES/django.po b/lib/registration/locale/fr/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/fr/LC_MESSAGES/django.po
rename to lib/registration/locale/fr/LC_MESSAGES/django.po
diff --git a/registration/locale/he/LC_MESSAGES/django.mo b/lib/registration/locale/he/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/he/LC_MESSAGES/django.mo
rename to lib/registration/locale/he/LC_MESSAGES/django.mo
diff --git a/registration/locale/he/LC_MESSAGES/django.po b/lib/registration/locale/he/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/he/LC_MESSAGES/django.po
rename to lib/registration/locale/he/LC_MESSAGES/django.po
diff --git a/registration/locale/is/LC_MESSAGES/django.mo b/lib/registration/locale/is/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/is/LC_MESSAGES/django.mo
rename to lib/registration/locale/is/LC_MESSAGES/django.mo
diff --git a/registration/locale/is/LC_MESSAGES/django.po b/lib/registration/locale/is/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/is/LC_MESSAGES/django.po
rename to lib/registration/locale/is/LC_MESSAGES/django.po
diff --git a/registration/locale/it/LC_MESSAGES/django.mo b/lib/registration/locale/it/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/it/LC_MESSAGES/django.mo
rename to lib/registration/locale/it/LC_MESSAGES/django.mo
diff --git a/registration/locale/it/LC_MESSAGES/django.po b/lib/registration/locale/it/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/it/LC_MESSAGES/django.po
rename to lib/registration/locale/it/LC_MESSAGES/django.po
diff --git a/registration/locale/ja/LC_MESSAGES/django.mo b/lib/registration/locale/ja/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/ja/LC_MESSAGES/django.mo
rename to lib/registration/locale/ja/LC_MESSAGES/django.mo
diff --git a/registration/locale/ja/LC_MESSAGES/django.po b/lib/registration/locale/ja/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/ja/LC_MESSAGES/django.po
rename to lib/registration/locale/ja/LC_MESSAGES/django.po
diff --git a/registration/locale/nl/LC_MESSAGES/django.mo b/lib/registration/locale/nl/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/nl/LC_MESSAGES/django.mo
rename to lib/registration/locale/nl/LC_MESSAGES/django.mo
diff --git a/registration/locale/nl/LC_MESSAGES/django.po b/lib/registration/locale/nl/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/nl/LC_MESSAGES/django.po
rename to lib/registration/locale/nl/LC_MESSAGES/django.po
diff --git a/registration/locale/pl/LC_MESSAGES/django.mo b/lib/registration/locale/pl/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/pl/LC_MESSAGES/django.mo
rename to lib/registration/locale/pl/LC_MESSAGES/django.mo
diff --git a/registration/locale/pl/LC_MESSAGES/django.po b/lib/registration/locale/pl/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/pl/LC_MESSAGES/django.po
rename to lib/registration/locale/pl/LC_MESSAGES/django.po
diff --git a/registration/locale/pt_BR/LC_MESSAGES/django.mo b/lib/registration/locale/pt_BR/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/pt_BR/LC_MESSAGES/django.mo
rename to lib/registration/locale/pt_BR/LC_MESSAGES/django.mo
diff --git a/registration/locale/pt_BR/LC_MESSAGES/django.po b/lib/registration/locale/pt_BR/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/pt_BR/LC_MESSAGES/django.po
rename to lib/registration/locale/pt_BR/LC_MESSAGES/django.po
diff --git a/registration/locale/ru/LC_MESSAGES/django.mo b/lib/registration/locale/ru/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/ru/LC_MESSAGES/django.mo
rename to lib/registration/locale/ru/LC_MESSAGES/django.mo
diff --git a/registration/locale/ru/LC_MESSAGES/django.po b/lib/registration/locale/ru/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/ru/LC_MESSAGES/django.po
rename to lib/registration/locale/ru/LC_MESSAGES/django.po
diff --git a/registration/locale/sr/LC_MESSAGES/django.mo b/lib/registration/locale/sr/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/sr/LC_MESSAGES/django.mo
rename to lib/registration/locale/sr/LC_MESSAGES/django.mo
diff --git a/registration/locale/sr/LC_MESSAGES/django.po b/lib/registration/locale/sr/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/sr/LC_MESSAGES/django.po
rename to lib/registration/locale/sr/LC_MESSAGES/django.po
diff --git a/registration/locale/sv/LC_MESSAGES/django.mo b/lib/registration/locale/sv/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/sv/LC_MESSAGES/django.mo
rename to lib/registration/locale/sv/LC_MESSAGES/django.mo
diff --git a/registration/locale/sv/LC_MESSAGES/django.po b/lib/registration/locale/sv/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/sv/LC_MESSAGES/django.po
rename to lib/registration/locale/sv/LC_MESSAGES/django.po
diff --git a/registration/locale/zh_CN/LC_MESSAGES/django.mo b/lib/registration/locale/zh_CN/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/zh_CN/LC_MESSAGES/django.mo
rename to lib/registration/locale/zh_CN/LC_MESSAGES/django.mo
diff --git a/registration/locale/zh_CN/LC_MESSAGES/django.po b/lib/registration/locale/zh_CN/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/zh_CN/LC_MESSAGES/django.po
rename to lib/registration/locale/zh_CN/LC_MESSAGES/django.po
diff --git a/registration/locale/zh_TW/LC_MESSAGES/django.mo b/lib/registration/locale/zh_TW/LC_MESSAGES/django.mo
similarity index 100%
rename from registration/locale/zh_TW/LC_MESSAGES/django.mo
rename to lib/registration/locale/zh_TW/LC_MESSAGES/django.mo
diff --git a/registration/locale/zh_TW/LC_MESSAGES/django.po b/lib/registration/locale/zh_TW/LC_MESSAGES/django.po
similarity index 100%
rename from registration/locale/zh_TW/LC_MESSAGES/django.po
rename to lib/registration/locale/zh_TW/LC_MESSAGES/django.po
diff --git a/registration/__init__.py b/lib/registration/management/__init__.py
similarity index 100%
copy from registration/__init__.py
copy to lib/registration/management/__init__.py
diff --git a/registration/__init__.py b/lib/registration/management/commands/__init__.py
similarity index 100%
rename from registration/__init__.py
rename to lib/registration/management/commands/__init__.py
diff --git a/registration/management/commands/cleanupregistration.py b/lib/registration/management/commands/cleanupregistration.py
similarity index 100%
rename from registration/management/commands/cleanupregistration.py
rename to lib/registration/management/commands/cleanupregistration.py
diff --git a/registration/models.py b/lib/registration/models.py
similarity index 100%
rename from registration/models.py
rename to lib/registration/models.py
diff --git a/registration/signals.py b/lib/registration/signals.py
similarity index 100%
rename from registration/signals.py
rename to lib/registration/signals.py
diff --git a/registration/tests.py b/lib/registration/tests.py
similarity index 100%
rename from registration/tests.py
rename to lib/registration/tests.py
diff --git a/registration/urls.py b/lib/registration/urls.py
similarity index 100%
rename from registration/urls.py
rename to lib/registration/urls.py
diff --git a/registration/views.py b/lib/registration/views.py
similarity index 100%
rename from registration/views.py
rename to lib/registration/views.py
diff --git a/lib/reversion/__init__.py b/lib/reversion/__init__.py
new file mode 100644
index 0000000..bb6a02b
--- /dev/null
+++ b/lib/reversion/__init__.py
@@ -0,0 +1,11 @@
+"""
+Transactional version control for Django models.
+
+Project sponsored by Etianen.com
+
+<http://www.etianen.com/>
+"""
+
+
+from reversion.registration import register, unregister, is_registered
+from reversion.revisions import revision
\ No newline at end of file
diff --git a/lib/reversion/admin.py b/lib/reversion/admin.py
new file mode 100644
index 0000000..046a775
--- /dev/null
+++ b/lib/reversion/admin.py
@@ -0,0 +1,264 @@
+"""Admin extensions for Reversion."""
+
+
+from django.db import models, transaction
+from django.conf.urls.defaults import *
+from django.conf import settings
+from django.contrib import admin
+from django.contrib.admin.models import LogEntry, DELETION
+from django.contrib.contenttypes.generic import GenericInlineModelAdmin, GenericRelation
+from django.contrib.contenttypes.models import ContentType
+from django.forms.formsets import all_valid
+from django.http import Http404, HttpResponseRedirect
+from django.shortcuts import get_object_or_404, render_to_response
+from django.template import RequestContext
+from django.utils.dateformat import format
+from django.utils.encoding import force_unicode
+from django.utils.html import mark_safe
+from django.utils.text import capfirst
+from django.utils.translation import ugettext as _
+
+from reversion.helpers import deserialized_model_to_dict
+from reversion.registration import is_registered, register
+from reversion.revisions import revision
+from reversion.models import Version
+
+
+class VersionAdmin(admin.ModelAdmin):
+
+ """Abstract admin class for handling version controlled models."""
+
+ revision_form_template = "reversion/revision_form.html"
+ object_history_template = "reversion/object_history.html"
+ change_list_template = "reversion/change_list.html"
+ recover_list_template = "reversion/recover_list.html"
+ recover_form_template = "reversion/recover_form.html"
+
+ def _autoregister(self, model, follow=None):
+ """Registers a model with reversion, if required."""
+ if not is_registered(model):
+ follow = follow or []
+ for parent_cls, field in model._meta.parents.items():
+ follow.append(field.name)
+ self._autoregister(parent_cls)
+ register(model, follow=follow)
+
+ def __init__(self, *args, **kwargs):
+ """Initializes the VersionAdmin"""
+ super(VersionAdmin, self).__init__(*args, **kwargs)
+ # Automatically register models if required.
+ if not is_registered(self.model):
+ inline_fields = []
+ for inline in self.inlines:
+ inline_model = inline.model
+ self._autoregister(inline_model)
+ if issubclass(inline, (admin.TabularInline, admin.StackedInline)):
+ fk_name = inline.fk_name
+ if not fk_name:
+ for field in inline_model._meta.fields:
+ if isinstance(field, models.ForeignKey) and issubclass(self.model, field.rel.to):
+ fk_name = field.name
+ accessor = inline_model._meta.get_field(fk_name).rel.related_name or inline_model.__name__.lower() + "_set"
+ inline_fields.append(accessor)
+ elif issubclass(inline, GenericInlineModelAdmin):
+ ct_field = inline.ct_field
+ ct_fk_field = inline.ct_fk_field
+ for field in self.model._meta.many_to_many:
+ if isinstance(field, GenericRelation) and field.object_id_field_name == ct_fk_field and field.content_type_field_name == ct_field:
+ inline_fields.append(field.name)
+ self._autoregister(self.model, inline_fields)
+
+ # TODO: This is deprecated in Django 1.1
+ def __call__(self, request, url):
+ """Adds additional functionality to the admin class."""
+ path = url or ""
+ parts = path.strip("/").split("/")
+ if len(parts) == 3 and parts[1] == "history":
+ object_id = parts[0]
+ version_id = parts[2]
+ return self.revision_view(request, object_id, version_id)
+ elif len(parts) == 1 and parts[0] == "recover":
+ return self.recover_list_view(request)
+ elif len(parts) == 2 and parts[0] == "recover":
+ return self.recover_view(request, parts[1])
+ else:
+ return super(VersionAdmin, self).__call__(request, url)
+
+ def get_urls(self):
+ """Returns the additional urls used by the Reversion admin."""
+ urls = super(VersionAdmin, self).get_urls()
+ reversion_urls = patterns("",
+ url("^recover/$", self.recover_list_view),
+ url("^recover/([^/]+)/$", self.recover_view),
+ url("^([^/]+)/history/([^/]+)/$", self.revision_view),)
+ return reversion_urls + urls
+
+ def log_addition(self, request, object):
+ """Sets the version meta information."""
+ super(VersionAdmin, self).log_addition(request, object)
+ revision.user = request.user
+
+ def log_change(self, request, object, message):
+ """Sets the version meta information."""
+ super(VersionAdmin, self).log_change(request, object, message)
+ revision.user = request.user
+ revision.comment = message
+
+ def recover_list_view(self, request, extra_context=None):
+ """Displays a deleted model to allow recovery."""
+ model = self.model
+ opts = model._meta
+ app_label = opts.app_label
+ alive_ids = [unicode(id) for id, in model._default_manager.all().values_list("pk")]
+ deleted = Version.objects.get_deleted(self.model)
+ context = {"opts": opts,
+ "app_label": app_label,
+ "module_name": capfirst(opts.verbose_name),
+ "title": _("Recover deleted %(name)s") % {"name": opts.verbose_name_plural},
+ "deleted": deleted}
+ extra_context = extra_context or {}
+ context.update(extra_context)
+ return render_to_response(self.recover_list_template, context, RequestContext(request))
+
+ def render_revision_form(self, request, obj, version, revision, context, template, redirect_url):
+ """Renders the object revision form."""
+ model = self.model
+ opts = model._meta
+ object_id = obj.pk
+ ordered_objects = opts.get_ordered_objects()
+ app_label = opts.app_label
+ object_version = version.object_version
+ ModelForm = self.get_form(request, obj)
+ formsets = []
+ if request.method == "POST":
+ form = ModelForm(request.POST, request.FILES, instance=obj)
+ if form.is_valid():
+ form_validated = True
+ new_object = self.save_form(request, form, change=True)
+ else:
+ form_validated = False
+ new_object = obj
+ for FormSet in self.get_formsets(request, new_object):
+ formset = FormSet(request.POST, request.FILES,
+ instance=new_object)
+ formsets.append(formset)
+ if all_valid(formsets) and form_validated:
+ self.save_model(request, new_object, form, change=True)
+ form.save_m2m()
+ for formset in formsets:
+ self.save_formset(request, form, formset, change=True)
+ change_message = _(u"Reverted to previous version, saved on %(datetime)s") % {"datetime": format(version.revision.date_created, _(settings.DATETIME_FORMAT))}
+ self.log_change(request, new_object, change_message)
+ self.message_user(request, _(u'The %(model)s "%(name)s" was reverted successfully. You may edit it again below.') % {"model": opts.verbose_name, "name": unicode(obj)})
+ return HttpResponseRedirect(redirect_url)
+ else:
+ initial = deserialized_model_to_dict(object_version, revision)
+ form = ModelForm(instance=obj, initial=initial)
+ for FormSet in self.get_formsets(request, obj):
+ formset = FormSet(instance=obj)
+ try:
+ attname = FormSet.fk.attname
+ except AttributeError:
+ # This is a GenericInlineFormset, or similar.
+ attname = FormSet.ct_fk_field_name
+ pk_name = FormSet.model._meta.pk.name
+ initial_overrides = dict([(getattr(version.object, pk_name), version) for version in revision if version.object.__class__ == FormSet.model and unicode(getattr(version.object, attname)) == unicode(object_id)])
+ initial = formset.initial
+ for initial_row in initial:
+ pk = initial_row[pk_name]
+ if pk in initial_overrides:
+ initial_row.update(deserialized_model_to_dict(initial_overrides[pk], revision))
+ del initial_overrides[pk]
+ else:
+ initial_row["DELETE"] = True
+ initial.extend([deserialized_model_to_dict(override, revision) for override in initial_overrides.values()])
+ # HACK: no way to specify initial values.
+ formset._total_form_count = len(initial)
+ formset.initial = initial
+ formset._construct_forms()
+ formsets.append(formset)
+ # Generate the context.
+ adminForm = admin.helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
+ media = self.media + adminForm.media
+ inline_admin_formsets = []
+ for inline, formset in zip(self.inline_instances, formsets):
+ fieldsets = list(inline.get_fieldsets(request, obj))
+ inline_admin_formset = admin.helpers.InlineAdminFormSet(inline, formset, fieldsets)
+ inline_admin_formsets.append(inline_admin_formset)
+ media = media + inline_admin_formset.media
+ context.update({"adminform": adminForm,
+ "object_id": obj.pk,
+ "original": obj,
+ "is_popup": False,
+ "media": mark_safe(media),
+ "inline_admin_formsets": inline_admin_formsets,
+ "errors": admin.helpers.AdminErrorList(form, formsets),
+ "root_path": self.admin_site.root_path,
+ "app_label": app_label,
+ "add": False,
+ "change": True,
+ "has_add_permission": self.has_add_permission(request),
+ "has_change_permission": self.has_change_permission(request, obj),
+ "has_delete_permission": self.has_delete_permission(request, obj),
+ "has_file_field": True, # FIXME - this should check if form or formsets have a FileField,
+ "has_absolute_url": hasattr(self.model, "get_absolute_url"),
+ "ordered_objects": ordered_objects,
+ "form_url": mark_safe(request.path),
+ "opts": opts,
+ "content_type_id": ContentType.objects.get_for_model(self.model).id,
+ "save_as": self.save_as,
+ "save_on_top": self.save_on_top,
+ "root_path": self.admin_site.root_path,})
+ return render_to_response(template, context, RequestContext(request))
+
+ def recover_view(self, request, version_id, extra_context=None):
+ """Displays a form that can recover a deleted model."""
+ model = self.model
+ opts = model._meta
+ app_label = opts.app_label
+ version = get_object_or_404(Version, pk=version_id)
+ object_id = version.object_id
+ content_type = ContentType.objects.get_for_model(self.model)
+ obj = version.object_version.object
+ revision = [related_version.object_version for related_version in version.revision.version_set.all()]
+ context = {"title": _("Recover %s") % force_unicode(version.object_repr),}
+ extra_context = extra_context or {}
+ context.update(extra_context)
+ return self.render_revision_form(request, obj, version, revision, context, self.recover_form_template, "../../%s/" % object_id)
+ recover_view = transaction.commit_on_success(revision.create_on_success(recover_view))
+
+ def revision_view(self, request, object_id, version_id, extra_context=None):
+ """Displays the contents of the given revision."""
+ model = self.model
+ content_type = ContentType.objects.get_for_model(model)
+ opts = model._meta
+ app_label = opts.app_label
+ obj = get_object_or_404(self.model, pk=object_id)
+ version = get_object_or_404(Version, pk=version_id)
+ # Generate the form.
+ revision = [related_version.object_version for related_version in version.revision.version_set.all()]
+ context = {"title": _("Revert %(name)s") % {"name": opts.verbose_name},}
+ extra_context = extra_context or {}
+ context.update(extra_context)
+ return self.render_revision_form(request, obj, version, revision, context, self.revision_form_template, "../../")
+ revision_view = transaction.commit_on_success(revision.create_on_success(revision_view))
+
+ # Wrap the data-modifying views in revisions.
+ add_view = transaction.commit_on_success(revision.create_on_success(admin.ModelAdmin.add_view))
+ change_view = transaction.commit_on_success(revision.create_on_success(admin.ModelAdmin.change_view))
+ delete_view = transaction.commit_on_success(revision.create_on_success(admin.ModelAdmin.delete_view))
+
+ def changelist_view(self, request, extra_context=None):
+ """Renders the modified change list."""
+ extra_context = extra_context or {}
+ extra_context.update({"has_change_permission": self.has_change_permission(request)})
+ return super(VersionAdmin, self).changelist_view(request, extra_context)
+
+ def history_view(self, request, object_id, extra_context=None):
+ """Renders the history view."""
+ extra_context = extra_context or {}
+ content_type = ContentType.objects.get_for_model(self.model)
+ obj = content_type.get_object_for_this_type(pk=object_id)
+ action_list = Version.objects.get_for_object(obj)
+ extra_context.update({"action_list": action_list})
+ return super(VersionAdmin, self).history_view(request, object_id, extra_context)
\ No newline at end of file
diff --git a/lib/reversion/helpers.py b/lib/reversion/helpers.py
new file mode 100644
index 0000000..6dcb5dc
--- /dev/null
+++ b/lib/reversion/helpers.py
@@ -0,0 +1,141 @@
+"""A number of useful helper functions to automate common tasks."""
+
+
+from django.contrib import admin
+from django.contrib.admin.sites import NotRegistered
+from django.db import models
+from django.db.models.query import QuerySet
+from django.forms.models import model_to_dict
+
+from reversion.registration import get_registration_info
+
+
+def add_to_revision(instance, revision_set):
+ """
+ Calculates the objects that should be included in the same revision as the
+ given instance.
+
+ All calculated objects are added to the `revision_set`.
+ """
+ # Prevent recursion.
+ if instance in revision_set:
+ return
+ revision_set.add(instance)
+ # Follow relations.
+ fields, follow, format = get_registration_info(instance.__class__)
+ if follow:
+ for relationship in follow:
+ try:
+ # Clear foreign key cache.
+ related_field = instance._meta.get_field(relationship)
+ if isinstance(related_field, models.ForeignKey):
+ if hasattr(instance, related_field.get_cache_name()):
+ delattr(instance, related_field.get_cache_name())
+ except models.FieldDoesNotExist:
+ pass
+ related = getattr(instance, relationship, None)
+ if isinstance(related, models.Model):
+ add_to_revision(related, revision_set)
+ elif isinstance(related, (models.Manager, QuerySet)):
+ for related_obj in related.all():
+ add_to_revision(related_obj, revision_set)
+ elif related is not None:
+ raise TypeError, "Cannot follow the relationship '%s', unexpected type %s" % (relationship, type(related).__name__)
+
+
+def deserialized_model_to_dict(deserialized_model, revision_data):
+ """
+ Converts a deserialized model to a dictionary.
+
+ In order to properly follow any parent links, this method requires that the
+ full revision data is also specified.
+ """
+ model = deserialized_model.object
+ result = model_to_dict(model)
+ result.update(deserialized_model.m2m_data)
+ # Add parent data.
+ for parent_class, field in model._meta.parents.items():
+ attname = field.attname
+ attvalue = getattr(model, attname)
+ pk_name = parent_class._meta.pk.attname
+ for deserialized_model in revision_data:
+ parent = deserialized_model.object
+ if parent_class == parent.__class__ and unicode(getattr(parent, pk_name)) == unicode(getattr(model, attname)):
+ result.update(deserialized_model_to_dict(deserialized_model, revision_data))
+ return result
+
+
+def version_to_dict(version):
+ """
+ Returns the serialiazed model contained in the version as dictionary of
+ property names and values.
+ """
+ revision = version.revision
+ object_version = version.object_version
+ return deserialized_model_to_dict(object_version, revision)
+
+
+def patch_admin(model, admin_site=None):
+ """
+ Enables version control with full admin integration for a model that has
+ already been registered with the django admin site.
+
+ This is excellent for adding version control to existing Django contrib
+ applications.
+ """
+ from reversion.admin import VersionAdmin
+ admin_site = admin_site or admin.site
+ try:
+ ModelAdmin = admin_site._registry[model].__class__
+ except KeyError:
+ raise NotRegistered, "The model %s has not been registered with the admin site." % model
+ # Unregister existing admin class.
+ admin_site.unregister(model)
+ # Register patched admin class.
+ class PatchedModelAdmin(VersionAdmin, ModelAdmin):
+ pass
+ admin_site.register(model, PatchedModelAdmin)
+
+
+# Patch generation methods, only available if the google-diff-match-patch
+# library is installed.
+#
+# http://code.google.com/p/google-diff-match-patch/
+
+try:
+ from diff_match_patch import diff_match_patch
+except ImportError:
+ pass
+else:
+ dmp = diff_match_patch()
+
+ def generate_diffs(old_version, new_version, field_name):
+ """
+ Generates a diff array of the named field between the two versions.
+ """
+ # Extract the text from the versions.
+ old_dict = version_to_dict(old_version)
+ new_dict = version_to_dict(new_version)
+ old_text = old_dict[field_name]
+ new_text = new_dict[field_name]
+ # Generate the patch.
+ diffs = dmp.diff_main(old_text, new_text)
+ return diffs
+
+ def generate_patch(old_version, new_version, field_name):
+ """
+ Generates a text patch of the named field between the two versions.
+ """
+ diffs = generate_diffs(old_version, new_version, field_name)
+ patch = dmp.patch_make(diffs)
+ return dmp.patch_toText(patch)
+
+ def generate_patch_html(old_version, new_version, field_name):
+ """
+ Generates a pretty html version of the differences between the named
+ field in two versions.
+ """
+ diffs = generate_diffs(old_version, new_version, field_name)
+ return dmp.diff_prettyHtml(diffs)
+
+
\ No newline at end of file
diff --git a/lib/reversion/managers.py b/lib/reversion/managers.py
new file mode 100644
index 0000000..bede23a
--- /dev/null
+++ b/lib/reversion/managers.py
@@ -0,0 +1,60 @@
+"""Model managers for Reversion."""
+
+try:
+ set
+except NameError:
+ from sets import Set as set # Python 2.3 fallback.
+
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+
+
+class VersionManager(models.Manager):
+
+ """Manager for Version models."""
+
+ def get_for_object(self, object):
+ """Returns all the versions of the given Revision, ordered by date created."""
+ content_type = ContentType.objects.get_for_model(object)
+ return self.filter(content_type=content_type, object_id=unicode(object.pk)).order_by("pk").select_related().order_by("pk")
+
+ def get_unique_for_object(self,obj):
+ """Returns unique versions associated with the object."""
+ versions = self.get_for_object(obj)
+ changed_versions = []
+ known_serialized_data = set()
+ for version in versions:
+ serialized_data = version.serialized_data
+ if serialized_data in known_serialized_data:
+ continue
+ known_serialized_data.add(serialized_data)
+ changed_versions.append(version)
+ return changed_versions
+
+ def get_for_date(self, object, date):
+ """Returns the latest version of an object for the given date."""
+ try:
+ return self.get_for_object(object).filter(revision__date_created__lte=date).order_by("-pk")[0]
+ except IndexError:
+ raise self.model.DoesNotExist
+
+ def get_deleted(self, model_class):
+ """Returns all the deleted versions for the given model class."""
+ live_ids = [unicode(row[0]) for row in model_class._default_manager.all().values_list("pk")]
+ content_type = ContentType.objects.get_for_model(model_class)
+ deleted_ids = self.filter(content_type=content_type).exclude(object_id__in=live_ids).order_by().values_list("object_id").distinct()
+ deleted = []
+ for object_id, in deleted_ids:
+ deleted.append(self.get_deleted_object(model_class, object_id))
+ return deleted
+
+ def get_deleted_object(self, model_class, object_id):
+ """
+ Returns the version corresponding to the deletion of the object with
+ the given id.
+ """
+ try:
+ content_type = ContentType.objects.get_for_model(model_class)
+ return self.filter(content_type=content_type, object_id=unicode(object_id)).order_by("-pk").select_related()[0]
+ except IndexError:
+ raise self.model.DoesNotExist
\ No newline at end of file
diff --git a/lib/reversion/middleware.py b/lib/reversion/middleware.py
new file mode 100644
index 0000000..7d7a9a9
--- /dev/null
+++ b/lib/reversion/middleware.py
@@ -0,0 +1,27 @@
+"""Middleware used by Reversion."""
+
+
+import sys
+
+from reversion.revisions import revision
+
+
+class RevisionMiddleware(object):
+
+ """Wraps the entire request in a Revision."""
+
+ def process_request(self, request):
+ """Starts a new revision."""
+ revision.start()
+ if request.user.is_authenticated():
+ revision.user = request.user
+
+ def process_response(self, request, response):
+ """Closes the revision."""
+ if revision.is_active():
+ revision.end()
+ return response
+
+ def process_exception(self, request, exception):
+ """Closes the revision."""
+ revision.invalidate()
\ No newline at end of file
diff --git a/lib/reversion/models.py b/lib/reversion/models.py
new file mode 100644
index 0000000..1584976
--- /dev/null
+++ b/lib/reversion/models.py
@@ -0,0 +1,111 @@
+"""Database models used by Reversion."""
+
+
+try:
+ set
+except NameError:
+ from sets import Set as set # Python 2.3 fallback.
+
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.core import serializers
+from django.db import models
+
+from reversion.helpers import add_to_revision
+from reversion.managers import VersionManager
+from reversion.registration import get_registration_info
+
+
+class Revision(models.Model):
+
+ """A group of related object versions."""
+
+ date_created = models.DateTimeField(auto_now_add=True,
+ help_text="The date and time this revision was created.")
+
+ user = models.ForeignKey(User,
+ blank=True,
+ null=True,
+ help_text="The user who created this revision.")
+
+ comment = models.TextField(blank=True,
+ null=True,
+ help_text="A text comment on this revision.")
+
+ def revert(self, delete=False):
+ """Reverts all objects in this revision."""
+ versions = self.version_set.all()
+ for version in versions:
+ version.revert()
+ if delete:
+ # Get a set of all objects in this revision.
+ old_revision_set = set([version.latest_object_version for version in versions])
+ # Calculate the set of all objects that would be in the revision now.
+ current_revision_set = set()
+ for latest_object_version in old_revision_set:
+ add_to_revision(latest_object_version, current_revision_set)
+ for current_object in current_revision_set:
+ if not current_object in old_revision_set:
+ current_object.delete()
+
+ def __unicode__(self):
+ """Returns a unicode representation."""
+ return u", ".join([unicode(version)
+ for version in self.version_set.all()])
+
+
+class Version(models.Model):
+
+ """A saved version of a database model."""
+
+ objects = VersionManager()
+
+ revision = models.ForeignKey(Revision,
+ help_text="The revision that contains this version.")
+
+ object_id = models.TextField(help_text="Primary key of the model under version control.")
+
+ content_type = models.ForeignKey(ContentType,
+ help_text="Content type of the model under version control.")
+
+ format = models.CharField(max_length=255,
+ help_text="The serialization format used by this model.")
+
+ serialized_data = models.TextField(help_text="The serialized form of this version of the model.")
+
+ object_repr = models.TextField(help_text="A string representation of the object.")
+
+ def get_object_version(self):
+ """Returns the stored version of the model."""
+ data = self.serialized_data
+ if isinstance(data, unicode):
+ data = data.encode("utf8")
+ return list(serializers.deserialize(self.format, data))[0]
+
+ object_version = property(get_object_version,
+ doc="The stored version of the model.")
+
+ def get_latest_object_version(self):
+ """
+ Returns the latest version of the stored object.
+
+ If the object no longer exists, returns None.
+ """
+ model_class = self.content_type.model_class()
+ try:
+ return model_class._default_manager.get(pk=self.object_id)
+ except model_class.DoesNotExist:
+ return None
+
+ latest_object_version = property(get_latest_object_version,
+ doc="The latest version of the model.")
+
+ def revert(self):
+ """Recovers the model in this version."""
+ self.object_version.save()
+
+ def __unicode__(self):
+ """Returns a unicode representation."""
+ return self.object_repr
+
+
\ No newline at end of file
diff --git a/lib/reversion/registration.py b/lib/reversion/registration.py
new file mode 100644
index 0000000..ed0fc30
--- /dev/null
+++ b/lib/reversion/registration.py
@@ -0,0 +1,59 @@
+"""Functions for registering and unregistering models with Reversion."""
+
+
+from django.db import models
+from django.db.models.signals import post_save
+
+from reversion.storage import VersionFileStorageWrapper
+
+
+registered_models = {}
+
+
+class RegistrationError(Exception):
+
+ """Exception thrown when registration with Reversion goes wrong."""
+
+ pass
+
+
+def register(model_class, fields=None, follow=None, format="xml"):
+ """Registers a model for version control."""
+ from reversion.revisions import revision
+ if is_registered(model_class):
+ raise RegistrationError, "%s has already been registered with Reversion." % model_class.__name__
+ registered_models[model_class] = (fields, follow, format)
+ for field in model_class._meta.fields:
+ if (fields is None or field.name in fields) and isinstance(field, models.FileField):
+ field.storage = VersionFileStorageWrapper(field.storage)
+ post_save.connect(revision.post_save_receiver, model_class)
+
+
+def is_registered(model_class):
+ """Checks whether the given model has been registered."""
+ return model_class in registered_models
+
+
+def get_registration_info(model_class):
+ """Returns the registration information for the given model class."""
+ try:
+ return registered_models[model_class]
+ except KeyError:
+ raise RegistrationError, "%s has not been registered with Reversion." % model_class.__name__
+
+
+def unregister(model_class):
+ """Removes a model from version control."""
+ from reversion.revisions import revision
+ try:
+ fields, follow, format = registered_models.pop(model_class)
+ except KeyError:
+ raise RegistrationError, "%s has not been registered with Reversion." % model_class.__name__
+ else:
+ for field in model_class._meta.fields:
+ if (fields is None or field in fields) and isinstance(field, models.FileField):
+ field.storage = VersionFileStorageWrapper(field.storage)
+ post_save.disconnect(revision.post_save_receiver, model_class)
+
+
+
\ No newline at end of file
diff --git a/lib/reversion/revisions.py b/lib/reversion/revisions.py
new file mode 100644
index 0000000..3a1abf6
--- /dev/null
+++ b/lib/reversion/revisions.py
@@ -0,0 +1,188 @@
+"""Revision management for Reversion."""
+
+
+try:
+ set
+except NameError:
+ from sets import Set as set # Python 2.3 fallback.
+
+import sys
+
+try:
+ from threading import local
+except ImportError:
+ from django.utils._threading_local import local # Python 2.3 fallback.
+
+try:
+ from functools import wraps
+except ImportError:
+ from django.utils.functional import wraps # Python 2.3, 2.4 fallback.
+
+from django.contrib.contenttypes.models import ContentType
+from django.core import serializers
+from django.db import models
+from django.db.models.query import QuerySet
+
+from reversion.helpers import add_to_revision
+from reversion.models import Revision, Version
+from reversion.registration import get_registration_info
+
+
+class RevisionManagementError(Exception):
+
+ """
+ Exception that is thrown when something goes wrong with revision managment.
+ """
+
+ pass
+
+
+class RevisionManager(local):
+
+ """Manages the state of the current revision."""
+
+ def __init__(self):
+ """Initializes the RevisionManager."""
+ self._clear()
+
+ def _clear(self):
+ """Puts the revision manager back into its default state."""
+ self._versions = set()
+ self._user = None
+ self._comment = None
+ self._depth = 0
+ self._is_invalid = False
+ self._meta = []
+
+ def start(self):
+ """Begins a revision."""
+ self._depth += 1
+
+ def is_active(self):
+ """Returns whether there is an active revision for this thread."""
+ return self._depth > 0
+
+ def _assert_active(self):
+ """Checks for an active revision, throwning an exception if none."""
+ if not self.is_active():
+ raise RevisionManagementError, "There is no active revision for this thread."
+
+ def _add(self, obj):
+ """
+ Adds an object to the current revision.
+
+ If `fields` is specified, then only the named fields will be serialized.
+
+ If `follow` is specified, then the named foreign relationships will also
+ be included in the revision. `follow` can be specified as a list of
+ relationship names, or as a dictionary mapping relationship names to
+ a list of fields to be serialized.
+ """
+ self._assert_active()
+ self._versions.add(obj)
+
+ def set_user(self, user):
+ """Sets the user for the current revision"""
+ self._assert_active()
+ self._user = user
+
+ def get_user(self):
+ """Gets the user for the current revision."""
+ self._assert_active()
+ return self._user
+
+ user = property(get_user,
+ set_user,
+ doc="The user for the current revision.")
+
+ def set_comment(self, comment):
+ """Sets the comment for the current revision"""
+ self._assert_active()
+ self._comment = comment
+
+ def get_comment(self):
+ """Gets the comment for the current revision."""
+ self._assert_active()
+ return self._comment
+
+ comment = property(get_comment,
+ set_comment,
+ doc="The comment for the current revision.")
+
+ def add_meta(self, cls, **kwargs):
+ """Adds a class of mete information to the current revision."""
+ self._assert_active()
+ self._meta.append((cls, kwargs))
+
+ def invalidate(self):
+ """Marks this revision as broken, so should not be commited."""
+ self._assert_active()
+ self._is_invalid = True
+
+ def end(self):
+ """Ends a revision."""
+ self._assert_active()
+ self._depth -= 1
+ # Handle end of revision conditions here.
+ if self._depth == 0:
+ try:
+ if self._versions and not self._is_invalid:
+ # Save a new revision.
+ revision = Revision.objects.create(user=self._user,
+ comment=self._comment)
+ revision_set = set()
+ # Follow relationships.
+ for version in self._versions:
+ add_to_revision(version, revision_set)
+ # Save version models.
+ for obj in revision_set:
+ fields, follow, format = get_registration_info(obj.__class__)
+ object_id = unicode(obj.pk)
+ content_type = ContentType.objects.get_for_model(obj)
+ serialized_data = serializers.serialize(format, [obj], fields=fields)
+ Version.objects.create(revision=revision,
+ object_id=object_id,
+ content_type=content_type,
+ format=format,
+ serialized_data=serialized_data,
+ object_repr=unicode(obj))
+ for meta_cls, meta_kwargs in self._meta:
+ meta_cls._default_manager.create(revision=revision, **meta_kwargs)
+ finally:
+ self._clear()
+ return False
+
+ def post_save_receiver(self, instance, sender, **kwargs):
+ """Saves a new version of registered models."""
+ if self.is_active():
+ self._add(instance)
+
+ def __enter__(self):
+ """Enters a block of revision management."""
+ self.start()
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ """Leaves a block of revision management."""
+ if exc_type is not None:
+ self.invalidate()
+ self.end()
+ return False
+
+ def create_on_success(self, func):
+ """Creates a revision when the given function exist successfully."""
+ def _create_on_success(*args, **kwargs):
+ self.start()
+ try:
+ try:
+ result = func(*args, **kwargs)
+ except:
+ self.invalidate()
+ raise
+ finally:
+ self.end()
+ return result
+ return wraps(func)(_create_on_success)
+
+
+# A thread-safe shared revision manager.
+revision = RevisionManager()
\ No newline at end of file
diff --git a/lib/reversion/storage.py b/lib/reversion/storage.py
new file mode 100644
index 0000000..b44473d
--- /dev/null
+++ b/lib/reversion/storage.py
@@ -0,0 +1,18 @@
+"""File storage wrapper for version controlled file fields."""
+
+
+class VersionFileStorageWrapper(object):
+
+ """Wrapper for file storage implementations that blocks file deletions."""
+
+ def __init__(self, storage):
+ """Initializes the VersionFileStorageWrapper."""
+ self._storage = storage
+
+ def __getattr__(self, name):
+ """Proxies storage mechanism to the wrapped implementation."""
+ return getattr(self._storage, name)
+
+ def delete(self, name):
+ """File deletions are blocked for this storage class."""
+ pass
diff --git a/lib/reversion/templates/reversion/change_list.html b/lib/reversion/templates/reversion/change_list.html
new file mode 100644
index 0000000..b5c708b
--- /dev/null
+++ b/lib/reversion/templates/reversion/change_list.html
@@ -0,0 +1,14 @@
+{% extends "admin/change_list.html" %}
+{% load i18n %}
+
+
+{% block object-tools %}
+ <ul class="object-tools">
+ {% if has_change_permission %}
+ <li><a href="recover/" class="recoverlink">{% blocktrans with cl.opts.verbose_name_plural|escape as name %}Recover deleted {{name}}{% endblocktrans %}</a></li>
+ {% endif %}
+ {% if has_add_permission %}
+ <li><a href="add/{% if is_popup %}?_popup=1{% endif %}" class="addlink">{% blocktrans with cl.opts.verbose_name|escape as name %}Add {{name}}{% endblocktrans %}</a></li>
+ {% endif %}
+ </ul>
+{% endblock %}
\ No newline at end of file
diff --git a/lib/reversion/templates/reversion/object_history.html b/lib/reversion/templates/reversion/object_history.html
new file mode 100644
index 0000000..2550012
--- /dev/null
+++ b/lib/reversion/templates/reversion/object_history.html
@@ -0,0 +1,35 @@
+{% extends "admin/object_history.html" %}
+{% load i18n %}
+
+
+{% block content %}
+ <div id="content-main">
+
+ <p>{% blocktrans %}Choose a date from the list below to revert to a previous version of this object.{% endblocktrans %}</p>
+
+ <div class="module">
+ {% if action_list %}
+ <table id="change-history">
+ <thead>
+ <tr>
+ <th scope="col">{% trans 'Date/time' %}</th>
+ <th scope="col">{% trans 'User' %}</th>
+ <th scope="col">{% trans 'Comment' %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for action in action_list %}
+ <tr>
+ <th scope="row"><a href="./{{action.pk}}/">{{action.revision.date_created|date:_("DATETIME_FORMAT")}}</a></th>
+ <td>{{action.revision.user.username}}{% if action.revision.user.first_name %} ({{action.revision.user.first_name}} {{action.revision.user.last_name}}){% endif %}</td>
+ <td>{{action.revision.comment|default:""}}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% else %}
+ <p>{% trans "This object doesn't have a change history. It probably wasn't added via this admin site." %}</p>
+ {% endif %}
+ </div>
+ </div>
+{% endblock %}
\ No newline at end of file
diff --git a/lib/reversion/templates/reversion/recover_form.html b/lib/reversion/templates/reversion/recover_form.html
new file mode 100644
index 0000000..548ea30
--- /dev/null
+++ b/lib/reversion/templates/reversion/recover_form.html
@@ -0,0 +1,25 @@
+{% extends "reversion/revision_form.html" %}
+{% load i18n %}
+
+
+{% block extrahead %}
+ {{block.super.super.super}}
+ <script type="text/javascript" src="../../../../jsi18n/"></script>
+ {{media}}
+{% endblock %}
+
+
+{% block breadcrumbs %}
+ <div class="breadcrumbs">
+ <a href="../../../../">{% trans "Home" %}</a> ›
+ <a href="../../../">{{app_label|capfirst|escape}}</a> ›
+ <a href="../../">{{opts.verbose_name_plural|capfirst}}</a> ›
+ <a href="../">{% blocktrans with opts.verbose_name as verbose_name %}Recover deleted {{verbose_name}}{% endblocktrans %}</a> ›
+ {{title}}
+ </div>
+{% endblock %}
+
+
+{% block form_top %}
+ <p>{% blocktrans %}Press the save button below to recover this version of the object.{% endblocktrans %}</p>
+{% endblock %}
\ No newline at end of file
diff --git a/lib/reversion/templates/reversion/recover_list.html b/lib/reversion/templates/reversion/recover_list.html
new file mode 100644
index 0000000..640f620
--- /dev/null
+++ b/lib/reversion/templates/reversion/recover_list.html
@@ -0,0 +1,39 @@
+{% extends "admin/base_site.html" %}
+{% load i18n %}
+
+{% block breadcrumbs %}
+<div class="breadcrumbs">
+ <a href="../../../">{% trans 'Home' %}</a> ›
+ <a href="../../">{{app_label|capfirst|escape}}</a> ›
+ <a href="../">{{module_name}}</a> ›
+ {% blocktrans with opts.verbose_name_plural|escape as name %}Recover deleted {{name}}{% endblocktrans %}
+</div>
+{% endblock %}
+
+{% block content %}
+ <div id="content-main">
+ <p>{% blocktrans %}Choose a date from the list below to recover a deleted version of an object.{% endblocktrans %}</p>
+ <div class="module">
+ {% if deleted %}
+ <table id="change-history">
+ <thead>
+ <tr>
+ <th scope="col">{% trans 'Date/time' %}</th>
+ <th scope="col">{{opts.verbose_name|capfirst}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for deletion in deleted %}
+ <tr>
+ <th scope="row"><a href="{{deletion.pk}}/">{{deletion.revision.date_created|date:_("DATETIME_FORMAT")}}</a></th>
+ <td>{{deletion.object_repr}}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% else %}
+ <p>{% trans "There are no deleted objects to recover." %}</p>
+ {% endif %}
+ </div>
+ </div>
+{% endblock %}
\ No newline at end of file
diff --git a/lib/reversion/templates/reversion/revision_form.html b/lib/reversion/templates/reversion/revision_form.html
new file mode 100644
index 0000000..f6afca7
--- /dev/null
+++ b/lib/reversion/templates/reversion/revision_form.html
@@ -0,0 +1,33 @@
+{% extends "admin/change_form.html" %}
+{% load i18n %}
+
+
+{% block extrahead %}
+ {{block.super.super}}
+ <script type="text/javascript" src="../../../../../jsi18n/"></script>
+ {{media}}
+{% endblock %}
+
+
+{% block breadcrumbs %}
+ <div class="breadcrumbs">
+ <a href="../../../../../">{% trans "Home" %}</a> ›
+ <a href="../../../../">{{app_label|capfirst|escape}}</a> ›
+ <a href="../../../">{{opts.verbose_name_plural|capfirst}}</a> ›
+ <a href="../../">{{original|truncatewords:"18"}}</a> ›
+ <a href="../">{% trans "History" %}</a> ›
+ {% blocktrans with opts.verbose_name as verbose_name %}Revert {{verbose_name}}{% endblocktrans %}
+ </div>
+{% endblock %}
+
+
+{% block content %}
+ {% with 1 as is_popup %}
+ {{block.super}}
+ {% endwith %}
+{% endblock %}
+
+
+{% block form_top %}
+ <p>{% blocktrans %}Press the save button below to revert to this version of the object.{% endblocktrans %}</p>
+{% endblock %}
\ No newline at end of file
diff --git a/settings.py b/settings.py
index becbaea..4ab9d97 100644
--- a/settings.py
+++ b/settings.py
@@ -77,14 +77,27 @@ TEMPLATE_DIRS = (
os.path.join(PROJECT_ROOT, "templates"),
)
+# Add the lib/ directory to the path for external apps
+EXTERNAL_APPS_PATH = os.path.join(PROJECT_ROOT, "lib")
+
+import sys
+sys.path.append(EXTERNAL_APPS_PATH)
+
INSTALLED_APPS = (
- 'notes',
- 'registration',
+ # System apps
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
+
+ # External apps
+ 'registration',
+ 'django_evolution',
+ 'reversion',
+
+ # Local apps
+ 'notes',
)
ACCOUNT_ACTIVATION_DAYS = 30
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]