Ver Fonte

HUE-1586 [core] Upgrade south to 0.82

Update migration files to use introspection
and check for the existence of tables.
Try-Catch was throwing errors.
Updated tests to use new test db patch method.
Abraham Elmahrek há 12 anos atrás
pai
commit
8a9946c3c3
100 ficheiros alterados com 5265 adições e 590 exclusões
  1. 6 8
      apps/jobsub/src/jobsub/migrations/0002_auto__add_ooziestreamingaction__add_oozieaction__add_oozieworkflow__ad.py
  2. 11 20
      apps/useradmin/src/useradmin/migrations/0001_permissions_and_profiles.py
  3. 3 7
      apps/useradmin/src/useradmin/migrations/0002_add_ldap_support.py
  4. 22 0
      desktop/core/ext-py/South-0.8.2/PKG-INFO
  5. 6 0
      desktop/core/ext-py/South-0.8.2/README
  6. 5 0
      desktop/core/ext-py/South-0.8.2/setup.cfg
  7. 9 3
      desktop/core/ext-py/South-0.8.2/setup.py
  8. 1 1
      desktop/core/ext-py/South-0.8.2/south/__init__.py
  9. 0 0
      desktop/core/ext-py/South-0.8.2/south/creator/__init__.py
  10. 159 63
      desktop/core/ext-py/South-0.8.2/south/creator/actions.py
  11. 87 43
      desktop/core/ext-py/South-0.8.2/south/creator/changes.py
  12. 51 29
      desktop/core/ext-py/South-0.8.2/south/creator/freezer.py
  13. 10 2
      desktop/core/ext-py/South-0.8.2/south/db/__init__.py
  14. 353 0
      desktop/core/ext-py/South-0.8.2/south/db/firebird.py
  15. 418 163
      desktop/core/ext-py/South-0.8.2/south/db/generic.py
  16. 283 0
      desktop/core/ext-py/South-0.8.2/south/db/mysql.py
  17. 326 0
      desktop/core/ext-py/South-0.8.2/south/db/oracle.py
  18. 96 0
      desktop/core/ext-py/South-0.8.2/south/db/postgresql_psycopg2.py
  19. 0 0
      desktop/core/ext-py/South-0.8.2/south/db/sql_server/__init__.py
  20. 444 0
      desktop/core/ext-py/South-0.8.2/south/db/sql_server/pyodbc.py
  21. 265 0
      desktop/core/ext-py/South-0.8.2/south/db/sqlite3.py
  22. 23 5
      desktop/core/ext-py/South-0.8.2/south/exceptions.py
  23. 0 0
      desktop/core/ext-py/South-0.8.2/south/hacks/__init__.py
  24. 110 0
      desktop/core/ext-py/South-0.8.2/south/hacks/django_1_0.py
  25. 4 0
      desktop/core/ext-py/South-0.8.2/south/introspection_plugins/__init__.py
  26. 11 0
      desktop/core/ext-py/South-0.8.2/south/introspection_plugins/annoying_autoonetoone.py
  27. 30 0
      desktop/core/ext-py/South-0.8.2/south/introspection_plugins/django_audit_log.py
  28. 16 0
      desktop/core/ext-py/South-0.8.2/south/introspection_plugins/django_objectpermissions.py
  29. 24 0
      desktop/core/ext-py/South-0.8.2/south/introspection_plugins/django_tagging.py
  30. 14 0
      desktop/core/ext-py/South-0.8.2/south/introspection_plugins/django_taggit.py
  31. 21 0
      desktop/core/ext-py/South-0.8.2/south/introspection_plugins/django_timezones.py
  32. 1 0
      desktop/core/ext-py/South-0.8.2/south/introspection_plugins/geodjango.py
  33. 10 6
      desktop/core/ext-py/South-0.8.2/south/logger.py
  34. 0 0
      desktop/core/ext-py/South-0.8.2/south/management/__init__.py
  35. 40 0
      desktop/core/ext-py/South-0.8.2/south/management/commands/__init__.py
  36. 28 12
      desktop/core/ext-py/South-0.8.2/south/management/commands/convert_to_south.py
  37. 31 19
      desktop/core/ext-py/South-0.8.2/south/management/commands/datamigration.py
  38. 63 0
      desktop/core/ext-py/South-0.8.2/south/management/commands/graphmigrations.py
  39. 264 0
      desktop/core/ext-py/South-0.8.2/south/management/commands/migrate.py
  40. 67 0
      desktop/core/ext-py/South-0.8.2/south/management/commands/migrationcheck.py
  41. 64 30
      desktop/core/ext-py/South-0.8.2/south/management/commands/schemamigration.py
  42. 5 3
      desktop/core/ext-py/South-0.8.2/south/management/commands/startmigration.py
  43. 27 23
      desktop/core/ext-py/South-0.8.2/south/management/commands/syncdb.py
  44. 3 2
      desktop/core/ext-py/South-0.8.2/south/management/commands/test.py
  45. 3 2
      desktop/core/ext-py/South-0.8.2/south/management/commands/testserver.py
  46. 63 36
      desktop/core/ext-py/South-0.8.2/south/migration/__init__.py
  47. 36 22
      desktop/core/ext-py/South-0.8.2/south/migration/base.py
  48. 74 53
      desktop/core/ext-py/South-0.8.2/south/migration/migrators.py
  49. 94 0
      desktop/core/ext-py/South-0.8.2/south/migration/utils.py
  50. 1 1
      desktop/core/ext-py/South-0.8.2/south/models.py
  51. 112 19
      desktop/core/ext-py/South-0.8.2/south/modelsinspector.py
  52. 21 15
      desktop/core/ext-py/South-0.8.2/south/orm.py
  53. 24 0
      desktop/core/ext-py/South-0.8.2/south/signals.py
  54. 108 0
      desktop/core/ext-py/South-0.8.2/south/tests/__init__.py
  55. 123 3
      desktop/core/ext-py/South-0.8.2/south/tests/autodetection.py
  56. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/__init__.py
  57. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/migrations/0001_depends_on_unmigrated.py
  58. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/migrations/0002_depends_on_unknown.py
  59. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/migrations/0003_depends_on_higher.py
  60. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/migrations/0004_higher.py
  61. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/migrations/__init__.py
  62. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/models.py
  63. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/circular_a/__init__.py
  64. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/circular_a/migrations/0001_first.py
  65. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/circular_a/migrations/__init__.py
  66. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/circular_a/models.py
  67. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/circular_b/__init__.py
  68. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/circular_b/migrations/0001_first.py
  69. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/circular_b/migrations/__init__.py
  70. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/circular_b/models.py
  71. 1031 0
      desktop/core/ext-py/South-0.8.2/south/tests/db.py
  72. 164 0
      desktop/core/ext-py/South-0.8.2/south/tests/db_mysql.py
  73. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_a/__init__.py
  74. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/0001_a.py
  75. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/0002_a.py
  76. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/0003_a.py
  77. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/0004_a.py
  78. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/0005_a.py
  79. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/__init__.py
  80. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_a/models.py
  81. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_b/__init__.py
  82. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/0001_b.py
  83. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/0002_b.py
  84. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/0003_b.py
  85. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/0004_b.py
  86. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/0005_b.py
  87. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/__init__.py
  88. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_b/models.py
  89. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_c/__init__.py
  90. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/0001_c.py
  91. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/0002_c.py
  92. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/0003_c.py
  93. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/0004_c.py
  94. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/0005_c.py
  95. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/__init__.py
  96. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/deps_c/models.py
  97. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/emptyapp/__init__.py
  98. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/emptyapp/migrations/__init__.py
  99. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/emptyapp/models.py
  100. 0 0
      desktop/core/ext-py/South-0.8.2/south/tests/fakeapp/__init__.py

+ 6 - 8
apps/jobsub/src/jobsub/migrations/0002_auto__add_ooziestreamingaction__add_oozieaction__add_oozieworkflow__ad.py

@@ -25,6 +25,7 @@ except ImportError:
 
 from south.db import db
 from south.v2 import SchemaMigration
+from django.db import connection
 from django.db import models
 
 from desktop.lib.django_db_util import remove_content_type
@@ -109,21 +110,18 @@ class Migration(SchemaMigration):
         db.commit_transaction()
 
         # Delete legacy tables. Note that this only applies to Hue 1.x installations
-        db.start_transaction()
-        try:
+        
+        if 'jobsub_submission' in connection.introspection.table_names():
+            db.start_transaction()
             db.delete_table('jobsub_submission')
             remove_content_type('jobsub', 'submission')
             db.commit_transaction()
-        except Exception, ex:
-            db.rollback_transaction()
 
-        db.start_transaction()
-        try:
+        if 'jobsub_serversubmissionstate' in connection.introspection.table_names():
+            db.start_transaction()
             db.delete_table('jobsub_serversubmissionstate')
             remove_content_type('jobsub', 'serversubmissionstate')
             db.commit_transaction()
-        except Exception, ex:
-            db.rollback_transaction()
 
         # South commits transaction at end of forward migration.
         db.start_transaction()

+ 11 - 20
apps/useradmin/src/useradmin/migrations/0001_permissions_and_profiles.py

@@ -1,6 +1,7 @@
 # encoding: utf-8
 from south.db import db
 from south.v2 import DataMigration
+from django.db import connection
 from django.db import models
 
 from useradmin.models import UserProfile
@@ -13,15 +14,14 @@ class Migration(DataMigration):
         This migration has been customized to support upgrades from Cloudera
         Enterprise 3.5, as well as Hue 1.2
         """
-        try:
-          # These will be removed if upgrading from a previous version of
-          # Cloudera Enterprise
+        # These will be removed if upgrading from a previous version of
+        # Cloudera Enterprise
+        if 'userman_groupadministrator' in connection.introspection.table_names():
           db.delete_table('userman_groupadministrator')
+        if 'userman_grouprelations' in connection.introspection.table_names():
           db.delete_table('userman_grouprelations')
-        except Exception:
-         pass
 
-        try:
+        if 'userman_userprofile' in connection.introspection.table_names():
           db.rename_table('userman_userprofile', 'useradmin_userprofile')
           db.delete_column('useradmin_userprofile', 'primary_group_id')
           db.create_index('useradmin_userprofile', ['user_id'])
@@ -36,10 +36,7 @@ class Migration(DataMigration):
             elif up.creation_method == '0':
               up.creation_method = UserProfile.CreationMethod.HUE
             up.save()
-        except Exception:
-          db.rollback_transaction()
-          db.start_transaction()
-
+        else:
           # Adding model 'UserProfile'
           db.create_table('useradmin_userprofile', (
               ('home_directory', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True)),
@@ -50,15 +47,12 @@ class Migration(DataMigration):
           db.start_transaction()
           db.send_create_signal('useradmin', ['UserProfile'])
 
-        try:
+        if 'userman_grouppermission' in connection.introspection.table_names():
           db.rename_table('userman_grouppermission', 'useradmin_grouppermission')
           db.rename_column('useradmin_grouppermission', 'desktop_permission_id', 'hue_permission_id')
           db.create_index('useradmin_grouppermission', ['group_id'])
           db.create_index('useradmin_grouppermission', ['hue_permission_id'])
-        except Exception:
-          db.rollback_transaction()
-          db.start_transaction()
-
+        else:
           # Adding model 'GroupPermission'
           db.create_table('useradmin_grouppermission', (
               ('hue_permission', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['useradmin.HuePermission'])),
@@ -69,12 +63,9 @@ class Migration(DataMigration):
           db.start_transaction()
           db.send_create_signal('useradmin', ['GroupPermission'])
 
-        try:
+        if 'userman_desktoppermission' in connection.introspection.table_names():
           db.rename_table('userman_desktoppermission', 'useradmin_huepermission')
-        except Exception:
-          db.rollback_transaction()
-          db.start_transaction()
-
+        else:
           # Adding model 'HuePermission'
           db.create_table('useradmin_huepermission', (
               ('action', self.gf('django.db.models.fields.CharField')(max_length=100)),

+ 3 - 7
apps/useradmin/src/useradmin/migrations/0002_add_ldap_support.py

@@ -1,10 +1,9 @@
 # encoding: utf-8
-import datetime
 from south.db import db
 from south.v2 import DataMigration
-from django.db import models
 
 from django.contrib.auth.models import User
+from django.db import connection
 from useradmin.models import create_profile_for_user
 from useradmin.models import UserProfile
 
@@ -15,13 +14,10 @@ class Migration(DataMigration):
         This migration has been customized to support upgrades from Cloudera
         Enterprise 3.5, as well as Hue 1.2
         """
-        try:
+        if 'userman_ldapgroup' in connection.introspection.table_names():
           db.rename_table('userman_ldapgroup', 'useradmin_ldapgroup')
           db.delete_column('useradmin_ldapgroup', 'hidden')
-        except Exception, e:
-          db.rollback_transaction()
-          db.start_transaction()
-
+        else:
           # Adding model 'LdapGroup'
           db.create_table('useradmin_ldapgroup', (
               ('group', self.gf('django.db.models.fields.related.ForeignKey')(related_name='group', to=orm['auth.Group'])),

+ 22 - 0
desktop/core/ext-py/South-0.8.2/PKG-INFO

@@ -0,0 +1,22 @@
+Metadata-Version: 1.1
+Name: South
+Version: 0.8.2
+Summary: South: Migrations for Django
+Home-page: http://south.aeracode.org/
+Author: Andrew Godwin & Andy McCurdy
+Author-email: south@aeracode.org
+License: UNKNOWN
+Download-URL: http://south.aeracode.org/wiki/Download
+Description: South is an intelligent database migrations library for the Django web framework. It is database-independent and DVCS-friendly, as well as a whole host of other features.
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Framework :: Django
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: System Administrators
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Software Development
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7

+ 6 - 0
desktop/core/ext-py/South-0.8.2/README

@@ -0,0 +1,6 @@
+This is South, a Django application to provide schema and data migrations.
+
+Documentation on South is currently available on our project site;
+you can find it at http://south.aeracode.org/docs/
+
+South is compatable with Django 1.2 and higher, and Python 2.6 and higher.

+ 5 - 0
desktop/core/ext-py/South-0.8.2/setup.cfg

@@ -0,0 +1,5 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+

+ 9 - 3
desktop/core/ext-py/south/setup.py → desktop/core/ext-py/South-0.8.2/setup.py

@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 
 # Use setuptools if we can
 try:
@@ -24,7 +24,10 @@ setup(
         "Intended Audience :: System Administrators",
         "License :: OSI Approved :: Apache Software License",
         "Operating System :: OS Independent",
-        "Topic :: Software Development"
+        "Topic :: Software Development",
+        "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 2.6",
+        "Programming Language :: Python :: 2.7",
     ],
     packages=[
         'south',
@@ -46,6 +49,7 @@ setup(
         'south.tests.otherfakeapp',
         'south.tests.deps_c',
         'south.tests.deps_b',
+        'south.tests.non_managed',
         'south.tests.circular_a.migrations',
         'south.tests.emptyapp.migrations',
         'south.tests.deps_a.migrations',
@@ -54,6 +58,8 @@ setup(
         'south.tests.circular_b.migrations',
         'south.tests.otherfakeapp.migrations',
         'south.tests.deps_c.migrations',
-        'south.tests.deps_b.migrations'
+        'south.tests.deps_b.migrations',
+        'south.tests.non_managed.migrations',
+        'south.utils',
     ],
 )

+ 1 - 1
desktop/core/ext-py/south/south/__init__.py → desktop/core/ext-py/South-0.8.2/south/__init__.py

@@ -2,7 +2,7 @@
 South - Useable migrations for Django apps
 """
 
-__version__ = "0.7"
+__version__ = "0.8.2"
 __authors__ = [
     "Andrew Godwin <andrew@aeracode.org>",
     "Andy McCurdy <andy@andymccurdy.com>"

+ 0 - 0
desktop/core/ext-py/south/south/creator/__init__.py → desktop/core/ext-py/South-0.8.2/south/creator/__init__.py


+ 159 - 63
desktop/core/ext-py/south/south/creator/actions.py → desktop/core/ext-py/South-0.8.2/south/creator/actions.py

@@ -4,14 +4,18 @@ Each one has a class, which can take the action description and insert code
 blocks into the forwards() and backwards() methods, in the right place.
 """
 
+from __future__ import print_function
+
 import sys
-import datetime
 
 from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
-from django.db.models.fields import FieldDoesNotExist, NOT_PROVIDED
+from django.db.models.fields import FieldDoesNotExist, NOT_PROVIDED, CharField, TextField
 
-from south import modelsinspector
+from south.modelsinspector import value_clean
 from south.creator.freezer import remove_useless_attributes, model_key
+from south.utils import datetime_utils
+from south.utils.py3 import raw_input
+
 
 class Action(object):
     """
@@ -19,6 +23,9 @@ class Action(object):
     the forwards() and backwards() method lists.
     """
     
+    prepend_forwards = False
+    prepend_backwards = False
+    
     def forwards_code(self):
         raise NotImplementedError
     
@@ -26,10 +33,16 @@ class Action(object):
         raise NotImplementedError
     
     def add_forwards(self, forwards):
-        forwards.append(self.forwards_code())
+        if self.prepend_forwards:
+            forwards.insert(0, self.forwards_code())
+        else:
+            forwards.append(self.forwards_code())
     
     def add_backwards(self, backwards):
-        backwards.append(self.backwards_code())
+        if self.prepend_backwards:
+            backwards.insert(0, self.backwards_code())
+        else:
+            backwards.append(self.backwards_code())
     
     def console_line(self):
         "Returns the string to print on the console, e.g. ' + Added field foo'"
@@ -61,11 +74,11 @@ class AddModel(Action):
         db.create_table(%(table_name)r, (
             %(field_defs)s
         ))
-        db.send_create_signal(%(app_label)r, [%(model_name)r])'''
+        db.send_create_signal(%(app_label)r, [%(model_name)r])'''[1:] + "\n"
     
     BACKWARDS_TEMPLATE = '''
         # Deleting model '%(model_name)s'
-        db.delete_table(%(table_name)r)'''
+        db.delete_table(%(table_name)r)'''[1:] + "\n"
 
     def __init__(self, model, model_def):
         self.model = model
@@ -117,20 +130,98 @@ class DeleteModel(AddModel):
 
     def backwards_code(self):
         return AddModel.forwards_code(self)
+
+
+class _NullIssuesField(object):
+    """
+    A field that might need to ask a question about rogue NULL values.
+    """
+
+    issue_with_backward_migration = False
+    irreversible = False
+
+    IRREVERSIBLE_TEMPLATE = '''
+        # User chose to not deal with backwards NULL issues for '%(model_name)s.%(field_name)s'
+        raise RuntimeError("Cannot reverse this migration. '%(model_name)s.%(field_name)s' and its values cannot be restored.")
+        
+        # The following code is provided here to aid in writing a correct migration'''
+
+    def deal_with_not_null_no_default(self, field, field_def):
+        # If it's a CharField or TextField that's blank, skip this step.
+        if isinstance(field, (CharField, TextField)) and field.blank:
+            field_def[2]['default'] = repr("")
+            return
+        # Oh dear. Ask them what to do.
+        print(" ? The field '%s.%s' does not have a default specified, yet is NOT NULL." % (
+            self.model._meta.object_name,
+            field.name,
+        ))
+        print(" ? Since you are %s, you MUST specify a default" % self.null_reason)
+        print(" ? value to use for existing rows. Would you like to:")
+        print(" ?  1. Quit now"+("." if self.issue_with_backward_migration else ", and add a default to the field in models.py" ))
+        print(" ?  2. Specify a one-off value to use for existing columns now")
+        if self.issue_with_backward_migration:
+            print(" ?  3. Disable the backwards migration by raising an exception; you can edit the migration to fix it later")
+        while True:
+            choice = raw_input(" ? Please select a choice: ")
+            if choice == "1":
+                sys.exit(1)
+            elif choice == "2":
+                break
+            elif choice == "3" and self.issue_with_backward_migration:
+                break
+            else:
+                print(" ! Invalid choice.")
+        if choice == "2":
+            self.add_one_time_default(field, field_def)
+        elif choice == "3":
+            self.irreversible = True
+
+    def add_one_time_default(self, field, field_def):
+        # OK, they want to pick their own one-time default. Who are we to refuse?
+        print(" ? Please enter Python code for your one-off default value.")
+        print(" ? The datetime module is available, so you can do e.g. datetime.date.today()")
+        while True:
+            code = raw_input(" >>> ")
+            if not code:
+                print(" ! Please enter some code, or 'exit' (with no quotes) to exit.")
+            elif code == "exit":
+                sys.exit(1)
+            else:
+                try:
+                    result = eval(code, {}, {"datetime": datetime_utils})
+                except (SyntaxError, NameError) as e:
+                    print(" ! Invalid input: %s" % e)
+                else:
+                    break
+        # Right, add the default in.
+        field_def[2]['default'] = value_clean(result)
+
+    def irreversable_code(self, field):
+        return self.IRREVERSIBLE_TEMPLATE % {
+            "model_name": self.model._meta.object_name,
+            "table_name": self.model._meta.db_table,
+            "field_name": field.name,
+            "field_column": field.column,
+        }
     
     
-class AddField(Action):
+class AddField(Action, _NullIssuesField):
     """
     Adds a field to a model. Takes a Model class and the field name.
     """
+
+    null_reason = "adding this field"
     
     FORWARDS_TEMPLATE = '''
         # Adding field '%(model_name)s.%(field_name)s'
-        db.add_column(%(table_name)r, %(field_name)r, %(field_def)s, keep_default=False)'''
+        db.add_column(%(table_name)r, %(field_name)r,
+                      %(field_def)s,
+                      keep_default=False)'''[1:] + "\n"
     
     BACKWARDS_TEMPLATE = '''
         # Deleting field '%(model_name)s.%(field_name)s'
-        db.delete_column(%(table_name)r, %(field_column)r)'''
+        db.delete_column(%(table_name)r, %(field_column)r)'''[1:] + "\n"
     
     def __init__(self, model, field, field_def):
         self.model = model
@@ -142,47 +233,13 @@ class AddField(Action):
         default = (self.field.default is not None) and (self.field.default is not NOT_PROVIDED)
         
         if not is_null and not default:
-            # Oh dear. Ask them what to do.
-            print " ? The field '%s.%s' does not have a default specified, yet is NOT NULL." % (
-                self.model._meta.object_name,
-                self.field.name,
-            )
-            print " ? Since you are adding or removing this field, you MUST specify a default"
-            print " ? value to use for existing rows. Would you like to:"
-            print " ?  1. Quit now, and add a default to the field in models.py"
-            print " ?  2. Specify a one-off value to use for existing columns now"
-            while True: 
-                choice = raw_input(" ? Please select a choice: ")
-                if choice == "1":
-                    sys.exit(1)
-                elif choice == "2":
-                    break
-                else:
-                    print " ! Invalid choice."
-            # OK, they want to pick their own one-time default. Who are we to refuse?
-            print " ? Please enter Python code for your one-off default value."
-            print " ? The datetime module is available, so you can do e.g. datetime.date.today()"
-            while True:
-                code = raw_input(" >>> ")
-                if not code:
-                    print " ! Please enter some code, or 'exit' (with no quotes) to exit."
-                elif code == "exit":
-                    sys.exit(1)
-                else:
-                    try:
-                        result = eval(code, {}, {"datetime": datetime})
-                    except (SyntaxError, NameError), e:
-                        print " ! Invalid input: %s" % e
-                    else:
-                        break
-            # Right, add the default in.
-            self.field_def[2]['default'] = repr(result)
-    
+            self.deal_with_not_null_no_default(self.field, self.field_def)
+
     def console_line(self):
         "Returns the string to print on the console, e.g. ' + Added field foo'"
         return " + Added field %s on %s.%s" % (
             self.field.name,
-            self.model._meta.app_label, 
+            self.model._meta.app_label,
             self.model._meta.object_name,
         )
     
@@ -209,7 +266,10 @@ class DeleteField(AddField):
     """
     Removes a field from a model. Takes a Model class and the field name.
     """
-    
+
+    null_reason = "removing this field"
+    issue_with_backward_migration = True
+
     def console_line(self):
         "Returns the string to print on the console, e.g. ' + Added field foo'"
         return " - Deleted field %s on %s.%s" % (
@@ -222,14 +282,18 @@ class DeleteField(AddField):
         return AddField.backwards_code(self)
 
     def backwards_code(self):
-        return AddField.forwards_code(self)
-
+        if not self.irreversible:
+            return AddField.forwards_code(self)
+        else:
+            return self.irreversable_code(self.field) + AddField.forwards_code(self)
 
 
-class ChangeField(Action):
+class ChangeField(Action, _NullIssuesField):
     """
     Changes a field's type/options on a model.
     """
+
+    null_reason = "making this field non-nullable"
     
     FORWARDS_TEMPLATE = BACKWARDS_TEMPLATE = '''
         # Changing field '%(model_name)s.%(field_name)s'
@@ -245,6 +309,16 @@ class ChangeField(Action):
         self.new_field = new_field
         self.old_def = old_def
         self.new_def = new_def
+
+        # See if they've changed a not-null field to be null
+        new_default = (self.new_field.default is not None) and (self.new_field.default is not NOT_PROVIDED)
+        old_default = (self.old_field.default is not None) and (self.old_field.default is not NOT_PROVIDED)
+        if self.old_field.null and not self.new_field.null and not new_default:
+            self.deal_with_not_null_no_default(self.new_field, self.new_def)
+        if not self.old_field.null and self.new_field.null and not old_default:
+            self.null_reason = "making this field nullable"
+            self.issue_with_backward_migration = True
+            self.deal_with_not_null_no_default(self.old_field, self.old_def)
     
     def console_line(self):
         "Returns the string to print on the console, e.g. ' + Added field foo'"
@@ -281,7 +355,11 @@ class ChangeField(Action):
         return self._code(self.old_field, self.new_field, self.new_def)
 
     def backwards_code(self):
-        return self._code(self.new_field, self.old_field, self.old_def)
+        change_code = self._code(self.new_field, self.old_field, self.old_def)
+        if not self.irreversible:
+            return change_code
+        else:
+            return self.irreversable_code(self.old_field) + change_code
 
 
 class AddUnique(Action):
@@ -291,11 +369,13 @@ class AddUnique(Action):
     
     FORWARDS_TEMPLATE = '''
         # Adding unique constraint on '%(model_name)s', fields %(field_names)s
-        db.create_unique(%(table_name)r, %(fields)r)'''
+        db.create_unique(%(table_name)r, %(fields)r)'''[1:] + "\n"
     
     BACKWARDS_TEMPLATE = '''
         # Removing unique constraint on '%(model_name)s', fields %(field_names)s
-        db.delete_unique(%(table_name)r, %(fields)r)'''
+        db.delete_unique(%(table_name)r, %(fields)r)'''[1:] + "\n"
+    
+    prepend_backwards = True
     
     def __init__(self, model, fields):
         self.model = model
@@ -332,6 +412,9 @@ class DeleteUnique(AddUnique):
     Removes a unique constraint from a model. Takes a Model class and the field names.
     """
     
+    prepend_forwards = True
+    prepend_backwards = False
+    
     def console_line(self):
         "Returns the string to print on the console, e.g. ' + Added field foo'"
         return " - Deleted unique constraint for %s on %s.%s" % (
@@ -354,11 +437,11 @@ class AddIndex(AddUnique):
     
     FORWARDS_TEMPLATE = '''
         # Adding index on '%(model_name)s', fields %(field_names)s
-        db.create_index(%(table_name)r, %(fields)r)'''
+        db.create_index(%(table_name)r, %(fields)r)'''[1:] + "\n"
     
     BACKWARDS_TEMPLATE = '''
         # Removing index on '%(model_name)s', fields %(field_names)s
-        db.delete_index(%(table_name)r, %(fields)r)'''
+        db.delete_index(%(table_name)r, %(fields)r)'''[1:] + "\n"
     
     def console_line(self):
         "Returns the string to print on the console, e.g. ' + Added field foo'"
@@ -396,16 +479,17 @@ class AddM2M(Action):
     
     FORWARDS_TEMPLATE = '''
         # Adding M2M table for field %(field_name)s on '%(model_name)s'
-        db.create_table(%(table_name)r, (
+        m2m_table_name = %(table_name)s
+        db.create_table(m2m_table_name, (
             ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
             (%(left_field)r, models.ForeignKey(orm[%(left_model_key)r], null=False)),
             (%(right_field)r, models.ForeignKey(orm[%(right_model_key)r], null=False))
         ))
-        db.create_unique(%(table_name)r, [%(left_column)r, %(right_column)r])'''
+        db.create_unique(m2m_table_name, [%(left_column)r, %(right_column)r])'''[1:] + "\n"
     
     BACKWARDS_TEMPLATE = '''
         # Removing M2M table for field %(field_name)s on '%(model_name)s'
-        db.delete_table('%(table_name)s')'''
+        db.delete_table(%(table_name)s)'''[1:] + "\n"
     
     def __init__(self, model, field):
         self.model = model
@@ -418,13 +502,25 @@ class AddM2M(Action):
             self.model._meta.app_label, 
             self.model._meta.object_name,
         )
-    
+
+    def table_name(self):
+        # This is part of a workaround for the fact that Django uses
+        # different shortening for automatically generated m2m table names 
+        # (as opposed to any explicitly specified table name)
+        f = self.field
+        explicit = f.db_table
+        if explicit:
+            return "%r" % explicit
+        else:
+            auto = "%s_%s" % (self.model._meta.db_table, f.name)
+            return 'db.shorten_name(%r)' % auto
+
     def forwards_code(self):
         
         return self.FORWARDS_TEMPLATE % {
             "model_name": self.model._meta.object_name,
             "field_name": self.field.name,
-            "table_name": self.field.m2m_db_table(),
+            "table_name": self.table_name(),
             "left_field": self.field.m2m_column_name()[:-3], # Remove the _id part
             "left_column": self.field.m2m_column_name(),
             "left_model_key": model_key(self.model),
@@ -438,7 +534,7 @@ class AddM2M(Action):
         return self.BACKWARDS_TEMPLATE % {
             "model_name": self.model._meta.object_name,
             "field_name": self.field.name,
-            "table_name": self.field.m2m_db_table(),
+            "table_name": self.table_name(),
         }
 
 
@@ -460,4 +556,4 @@ class DeleteM2M(AddM2M):
 
     def backwards_code(self):
         return AddM2M.forwards_code(self)
-    
+    

+ 87 - 43
desktop/core/ext-py/south/south/creator/changes.py → desktop/core/ext-py/South-0.8.2/south/creator/changes.py

@@ -3,11 +3,15 @@ Contains things to detect changes - either using options passed in on the
 commandline, or by using autodetection, etc.
 """
 
+from __future__ import print_function
+
 from django.db import models
 from django.contrib.contenttypes.generic import GenericRelation
+from django.utils.datastructures import SortedDict
 
 from south.creator.freezer import remove_useless_attributes, freeze_apps, model_key
 from south.utils import auto_through
+from south.utils.py3 import string_types
 
 class BaseChanges(object):
     """
@@ -21,9 +25,9 @@ class BaseChanges(object):
         Given a model and its model def (a dict of field: triple), returns three
         items: the real fields dict, the Meta dict, and the M2M fields dict.
         """
-        real_fields = {}
-        meta = {}
-        m2m_fields = {}
+        real_fields = SortedDict()
+        meta = SortedDict()
+        m2m_fields = SortedDict()
         for name, triple in model_def.items():
             if name == "Meta":
                 meta = triple
@@ -99,6 +103,16 @@ class AutoChanges(BaseChanges):
                     params['model']._meta.object_name.lower(),
                     "_".join([x.name for x in params['fields']]),
                 ))
+            elif change_name == "AddIndex":
+                parts.append("add_index_%s_%s" % (
+                    params['model']._meta.object_name.lower(),
+                    "_".join([x.name for x in params['fields']]),
+                ))
+            elif change_name == "DeleteIndex":
+                parts.append("del_index_%s_%s" % (
+                    params['model']._meta.object_name.lower(),
+                    "_".join([x.name for x in params['fields']]),
+                ))
         return ("__".join(parts))[:70]
     
     def get_changes(self):
@@ -113,8 +127,8 @@ class AutoChanges(BaseChanges):
         for key in self.old_defs:
             if key not in self.new_defs:
                 # We shouldn't delete it if it was managed=False
-                if self.old_defs[key].get("Meta", {}).get("managed", "True") != "False":
-                    old_fields, old_meta, old_m2ms = self.split_model_def(self.old_orm[key], self.old_defs[key])
+                old_fields, old_meta, old_m2ms = self.split_model_def(self.old_orm[key], self.old_defs[key])
+                if old_meta.get("managed", "True") != "False":
                     # Alright, delete it.
                     yield ("DeleteModel", {
                         "model": self.old_orm[key], 
@@ -126,6 +140,19 @@ class AutoChanges(BaseChanges):
                         field = self.old_orm[key + ":" + fieldname]
                         if auto_through(field):
                             yield ("DeleteM2M", {"model": self.old_orm[key], "field": field})
+                    # And any index/uniqueness constraints it had
+                    for attr, operation in (("unique_together", "DeleteUnique"), ("index_together", "DeleteIndex")):
+                        together = eval(old_meta.get(attr, "[]"))
+                        if together:
+                            # If it's only a single tuple, make it into the longer one
+                            if isinstance(together[0], string_types):
+                                together = [together]
+                            # For each combination, make an action for it
+                            for fields in together:
+                                yield (operation, {
+                                    "model": self.old_orm[key],
+                                    "fields": [self.old_orm[key]._meta.get_field_by_name(x)[0] for x in fields],
+                                })
                 # We always add it in here so we ignore it later
                 deleted_models.add(key)
         
@@ -133,8 +160,8 @@ class AutoChanges(BaseChanges):
         for key in self.new_defs:
             if key not in self.old_defs:
                 # We shouldn't add it if it's managed=False
-                if self.new_defs[key].get("Meta", {}).get("managed", "True") != "False":
-                    new_fields, new_meta, new_m2ms = self.split_model_def(self.current_model_from_key(key), self.new_defs[key])
+                new_fields, new_meta, new_m2ms = self.split_model_def(self.current_model_from_key(key), self.new_defs[key])
+                if new_meta.get("managed", "True") != "False":
                     yield ("AddModel", {
                         "model": self.current_model_from_key(key), 
                         "model_def": new_fields,
@@ -145,16 +172,31 @@ class AutoChanges(BaseChanges):
                         field = self.current_field_from_key(key, fieldname)
                         if auto_through(field):
                             yield ("AddM2M", {"model": self.current_model_from_key(key), "field": field})
+                    # And any index/uniqueness constraints it has
+                    for attr, operation in (("unique_together", "AddUnique"), ("index_together", "AddIndex")):
+                        together = eval(new_meta.get(attr, "[]"))
+                        if together:
+                            # If it's only a single tuple, make it into the longer one
+                            if isinstance(together[0], string_types):
+                                together = [together]
+                            # For each combination, make an action for it
+                            for fields in together:
+                                yield (operation, {
+                                    "model": self.current_model_from_key(key),
+                                    "fields": [self.current_model_from_key(key)._meta.get_field_by_name(x)[0] for x in fields],
+                                })
         
         # Now, for every model that's stayed the same, check its fields.
         for key in self.old_defs:
             if key not in deleted_models:
                 
-                still_there = set()
-                
                 old_fields, old_meta, old_m2ms = self.split_model_def(self.old_orm[key], self.old_defs[key])
                 new_fields, new_meta, new_m2ms = self.split_model_def(self.current_model_from_key(key), self.new_defs[key])
                 
+                # Do nothing for models which are now not managed.
+                if new_meta.get("managed", "True") == "False":
+                    continue
+                
                 # Find fields that have vanished.
                 for fieldname in old_fields:
                     if fieldname not in new_fields:
@@ -258,26 +300,27 @@ class AutoChanges(BaseChanges):
                     if not auto_through(old_field) and auto_through(new_field):
                         yield ("AddM2M", {"model": self.current_model_from_key(key), "field": new_field})
                 
-                ## See if the unique_togethers have changed
-                # First, normalise them into lists of sets.
-                old_unique_together = eval(old_meta.get("unique_together", "[]"))
-                new_unique_together = eval(new_meta.get("unique_together", "[]"))
-                if old_unique_together and isinstance(old_unique_together[0], basestring):
-                    old_unique_together = [old_unique_together]
-                if new_unique_together and isinstance(new_unique_together[0], basestring):
-                    new_unique_together = [new_unique_together]
-                old_unique_together = map(set, old_unique_together)
-                new_unique_together = map(set, new_unique_together)
-                # See if any appeared or disappeared
-                for item in old_unique_together:
-                    if item not in new_unique_together:
-                        yield ("DeleteUnique", {
+                ## See if the {index,unique}_togethers have changed
+                for attr, add_operation, del_operation in (("unique_together", "AddUnique", "DeleteUnique"), ("index_together", "AddIndex", "DeleteIndex")):
+                    # First, normalise them into lists of sets.
+                    old_together = eval(old_meta.get(attr, "[]"))
+                    new_together = eval(new_meta.get(attr, "[]"))
+                    if old_together and isinstance(old_together[0], string_types):
+                        old_together = [old_together]
+                    if new_together and isinstance(new_together[0], string_types):
+                        new_together = [new_together]
+                    old_together = frozenset(tuple(o) for o in old_together)
+                    new_together = frozenset(tuple(n) for n in new_together)
+                    # See if any appeared or disappeared
+                    disappeared = old_together.difference(new_together)
+                    appeared = new_together.difference(old_together)
+                    for item in disappeared:
+                        yield (del_operation, {
                             "model": self.old_orm[key],
                             "fields": [self.old_orm[key + ":" + x] for x in item],
                         })
-                for item in new_unique_together:
-                    if item not in old_unique_together:
-                        yield ("AddUnique", {
+                    for item in appeared:
+                        yield (add_operation, {
                             "model": self.current_model_from_key(key),
                             "fields": [self.current_field_from_key(key, x) for x in item],
                         })
@@ -286,7 +329,7 @@ class AutoChanges(BaseChanges):
     def is_triple(cls, triple):
         "Returns whether the argument is a triple."
         return isinstance(triple, (list, tuple)) and len(triple) == 3 and \
-            isinstance(triple[0], (str, unicode)) and \
+            isinstance(triple[0], string_types) and \
             isinstance(triple[1], (list, tuple)) and \
             isinstance(triple[2], dict)
 
@@ -363,7 +406,7 @@ class ManualChanges(BaseChanges):
             bits.append('add_field_%s' % field_name)
         for index_name in self.added_indexes:
             bits.append('add_index_%s' % index_name)
-        return '_'.join(bits)
+        return '_'.join(bits).replace('.', '_')
     
     def get_changes(self):
         # Get the model defs so we can use them for the yield later
@@ -381,7 +424,7 @@ class ManualChanges(BaseChanges):
             try:
                 model_name, field_name = field_desc.split(".")
             except (TypeError, ValueError):
-                print "%r is not a valid field description." % field_desc
+                raise ValueError("%r is not a valid field description." % field_desc)
             model = models.get_model(self.migrations.app_label(), model_name)
             real_fields, meta, m2m_fields = self.split_model_def(model, model_defs[model_key(model)])
             yield ("AddField", {
@@ -394,7 +437,7 @@ class ManualChanges(BaseChanges):
             try:
                 model_name, field_name = field_desc.split(".")
             except (TypeError, ValueError):
-                print "%r is not a valid field description." % field_desc
+                print("%r is not a valid field description." % field_desc)
             model = models.get_model(self.migrations.app_label(), model_name)
             yield ("AddIndex", {
                 "model": model,
@@ -430,19 +473,20 @@ class InitialChanges(BaseChanges):
                 "model_def": real_fields,
             })
             
-            # Then, add any uniqueness that's around
+            # Then, add any indexing/uniqueness that's around
             if meta:
-                unique_together = eval(meta.get("unique_together", "[]"))
-                if unique_together:
-                    # If it's only a single tuple, make it into the longer one
-                    if isinstance(unique_together[0], basestring):
-                        unique_together = [unique_together]
-                    # For each combination, make an action for it
-                    for fields in unique_together:
-                        yield ("AddUnique", {
-                            "model": model,
-                            "fields": [model._meta.get_field_by_name(x)[0] for x in fields],
-                        })
+                for attr, operation in (("unique_together", "AddUnique"), ("index_together", "AddIndex")):
+                    together = eval(meta.get(attr, "[]"))
+                    if together:
+                        # If it's only a single tuple, make it into the longer one
+                        if isinstance(together[0], string_types):
+                            together = [together]
+                        # For each combination, make an action for it
+                        for fields in together:
+                            yield (operation, {
+                                "model": model,
+                                "fields": [model._meta.get_field_by_name(x)[0] for x in fields],
+                            })
             
             # Finally, see if there's some M2M action
             for name, triple in m2m_fields.items():
@@ -459,4 +503,4 @@ class InitialChanges(BaseChanges):
                     yield ("AddM2M", {
                         "model": model,
                         "field": field,
-                    })
+                    })

+ 51 - 29
desktop/core/ext-py/south/south/creator/freezer.py → desktop/core/ext-py/South-0.8.2/south/creator/freezer.py

@@ -2,20 +2,23 @@
 Handles freezing of models into FakeORMs.
 """
 
+from __future__ import print_function
+
 import sys
 
 from django.db import models
+from django.db.models.base import ModelBase, Model
 from django.contrib.contenttypes.generic import GenericRelation
 
-from south.orm import FakeORM
-from south.utils import auto_model
+from south.utils import get_attribute, auto_through
 from south import modelsinspector
+from south.utils.py3 import string_types
 
 def freeze_apps(apps):
     """
     Takes a list of app labels, and returns a string of their frozen form.
     """
-    if isinstance(apps, basestring):
+    if isinstance(apps, string_types):
         apps = [apps]
     frozen_models = set()
     # For each app, add in all its models
@@ -29,21 +32,26 @@ def freeze_apps(apps):
         frozen_models.update(model_dependencies(model))
     # Serialise!
     model_defs = {}
+    model_classes = {}
     for model in frozen_models:
         model_defs[model_key(model)] = prep_for_freeze(model)
+        model_classes[model_key(model)] = model
     # Check for any custom fields that failed to freeze.
     missing_fields = False
     for key, fields in model_defs.items():
         for field_name, value in fields.items():
             if value is None:
                 missing_fields = True
-                print " ! Cannot freeze field '%s.%s'" % (key, field_name)
+                model_class = model_classes[key]
+                field_class = model_class._meta.get_field_by_name(field_name)[0]
+                print(" ! Cannot freeze field '%s.%s'" % (key, field_name))
+                print(" ! (this field has class %s.%s)" % (field_class.__class__.__module__, field_class.__class__.__name__))
     if missing_fields:
-        print ""
-        print " ! South cannot introspect some fields; this is probably because they are custom"
-        print " ! fields. If they worked in 0.6 or below, this is because we have removed the"
-        print " ! models parser (it often broke things)."
-        print " ! To fix this, read http://south.aeracode.org/wiki/MyFieldsDontWork"
+        print("")
+        print(" ! South cannot introspect some fields; this is probably because they are custom")
+        print(" ! fields. If they worked in 0.6 or below, this is because we have removed the")
+        print(" ! models parser (it often broke things).")
+        print(" ! To fix this, read http://south.aeracode.org/wiki/MyFieldsDontWork")
         sys.exit(1)
     
     return model_defs
@@ -85,10 +93,10 @@ def model_dependencies(model, checked_models=None):
     checked_models = checked_models or set()
     # Get deps for each field
     for field in model._meta.fields + model._meta.many_to_many:
-        depends.update(field_dependencies(field))
+        depends.update(field_dependencies(field, checked_models))
     # Add in any non-abstract bases
     for base in model.__bases__:
-        if isinstance(base, models.Model) and not base._meta.abstract:
+        if issubclass(base, models.Model) and hasattr(base, '_meta') and not base._meta.abstract:
             depends.add(base)
     # Now recurse
     new_to_check = depends - checked_models
@@ -109,21 +117,35 @@ def model_dependencies(model, checked_models=None):
 def field_dependencies(field, checked_models=None):
     checked_models = checked_models or set()
     depends = set()
-    if isinstance(field, (models.OneToOneField, models.ForeignKey, models.ManyToManyField, GenericRelation)):
-        if field.rel.to in checked_models:
-            return depends
-        checked_models.add(field.rel.to)
-        depends.add(field.rel.to)
-        depends.update(field_dependencies(field.rel.to._meta.pk, checked_models))
-        # Also include M2M throughs
-        if isinstance(field, models.ManyToManyField):
-            if field.rel.through:
-                if hasattr(field.rel, "through_model"): # 1.1 and below
-                    depends.add(field.rel.through_model)
-                else:
-                    # Make sure it's not an automatic one
-                    if not auto_model(field.rel.through):
-                        depends.add(field.rel.through) # 1.2 and up
+    arg_defs, kwarg_defs = modelsinspector.matching_details(field)
+    for attrname, options in arg_defs + list(kwarg_defs.values()):
+        if options.get("ignore_if_auto_through", False) and auto_through(field):
+            continue
+        if options.get("is_value", False):
+            value = attrname
+        elif attrname == 'rel.through' and hasattr(getattr(field, 'rel', None), 'through_model'):
+            # Hack for django 1.1 and below, where the through model is stored
+            # in rel.through_model while rel.through stores only the model name.
+            value = field.rel.through_model
+        else:
+            try:
+                value = get_attribute(field, attrname)
+            except AttributeError:
+                if options.get("ignore_missing", False):
+                    continue
+                raise
+        if isinstance(value, Model):
+            value = value.__class__
+        if not isinstance(value, ModelBase):
+            continue
+        if getattr(value._meta, "proxy", False):
+            value = value._meta.proxy_for_model
+        if value in checked_models:
+            continue
+        checked_models.add(value)
+        depends.add(value)
+        depends.update(model_dependencies(value, checked_models))
+
     return depends
 
 ### Prettyprinters
@@ -142,8 +164,8 @@ def pprint_fields(fields):
 
 ### Output sanitisers
 
-USELESS_KEYWORDS = ["choices", "help_text", "upload_to", "verbose_name", "storage"]
-USELESS_DB_KEYWORDS = ["related_name", "default"] # Important for ORM, not for DB.
+USELESS_KEYWORDS = ["choices", "help_text", "verbose_name"]
+USELESS_DB_KEYWORDS = ["related_name", "default", "blank"] # Important for ORM, not for DB.
 INDEX_KEYWORDS = ["db_index"]
 
 def remove_useless_attributes(field, db=False, indexes=False):
@@ -167,4 +189,4 @@ def remove_useless_meta(meta):
         for name in USELESS_META:
             if name in meta:
                 del meta[name]
-    return meta
+    return meta

+ 10 - 2
desktop/core/ext-py/south/south/db/__init__.py → desktop/core/ext-py/South-0.8.2/south/db/__init__.py

@@ -10,12 +10,19 @@ engine_modules = {
     'django.db.backends.postgresql_psycopg2': 'postgresql_psycopg2',
     'django.db.backends.sqlite3': 'sqlite3',
     'django.db.backends.mysql': 'mysql',
+    'mysql_oursql.standard': 'mysql',
     'django.db.backends.oracle': 'oracle',
-    'sql_server.pyodbc': 'sql_server.pyodbc',
+    'sql_server.pyodbc': 'sql_server.pyodbc', #django-pyodbc-azure
+    'django_pyodbc': 'sql_server.pyodbc', #django-pyodbc
+    'sqlserver_ado': 'sql_server.pyodbc', #django-mssql
+    'firebird': 'firebird', #django-firebird
     'django.contrib.gis.db.backends.postgis': 'postgresql_psycopg2',
     'django.contrib.gis.db.backends.spatialite': 'sqlite3',
     'django.contrib.gis.db.backends.mysql': 'mysql',
     'django.contrib.gis.db.backends.oracle': 'oracle',
+    'doj.backends.zxjdbc.postgresql': 'postgresql_psycopg2', #django-jython
+    'doj.backends.zxjdbc.mysql': 'mysql', #django-jython
+    'doj.backends.zxjdbc.oracle': 'oracle', #django-jython
 }
 
 # First, work out if we're multi-db or not, and which databases we have
@@ -34,8 +41,9 @@ else:
     # Loop over the defined databases, gathering up their engines
     db_engines = dict([
         # Note we check to see if contrib.gis has overridden us.
-        (alias, "south.db.%s" % engine_modules.get(db_settings['ENGINE'], None))
+        (alias, "south.db.%s" % engine_modules[db_settings['ENGINE']])
         for alias, db_settings in settings.DATABASES.items()
+        if db_settings['ENGINE'] in engine_modules
     ])
     # Update with any overrides
     db_engines.update(getattr(settings, "SOUTH_DATABASE_ADAPTERS", {}))

+ 353 - 0
desktop/core/ext-py/South-0.8.2/south/db/firebird.py

@@ -0,0 +1,353 @@
+# firebird
+
+from __future__ import print_function
+
+import datetime
+
+from django.db import connection, models
+from django.core.management.color import no_style
+from django.db.utils import DatabaseError
+
+from south.db import generic
+from south.utils.py3 import string_types
+
+class DatabaseOperations(generic.DatabaseOperations):
+    backend_name = 'firebird'
+    alter_string_set_type = 'ALTER %(column)s TYPE %(type)s'
+    alter_string_set_default =  'ALTER %(column)s SET DEFAULT %(default)s;'
+    alter_string_drop_null = ''
+    add_column_string = 'ALTER TABLE %s ADD %s;'
+    delete_column_string = 'ALTER TABLE %s DROP %s;'
+    rename_table_sql = ''
+
+    # Features
+    allows_combined_alters = False
+    has_booleans = False
+
+    def _fill_constraint_cache(self, db_name, table_name):
+        self._constraint_cache.setdefault(db_name, {})
+        self._constraint_cache[db_name][table_name] = {}
+
+        rows = self.execute("""
+            SELECT
+                rc.RDB$CONSTRAINT_NAME,
+                rc.RDB$CONSTRAINT_TYPE,
+                cc.RDB$TRIGGER_NAME
+            FROM rdb$relation_constraints rc
+            JOIN rdb$check_constraints cc
+            ON rc.rdb$constraint_name = cc.rdb$constraint_name
+            WHERE rc.rdb$constraint_type = 'NOT NULL'
+            AND rc.rdb$relation_name = '%s'
+            """ % table_name)
+
+        for constraint, kind, column in rows:
+           self._constraint_cache[db_name][table_name].setdefault(column, set())
+           self._constraint_cache[db_name][table_name][column].add((kind, constraint))
+        return
+
+    def _alter_column_set_null(self, table_name, column_name, is_null):
+        sql = """
+            UPDATE RDB$RELATION_FIELDS SET RDB$NULL_FLAG = %(null_flag)s
+            WHERE RDB$FIELD_NAME = '%(column)s'
+            AND RDB$RELATION_NAME = '%(table_name)s'
+        """
+        null_flag = 'NULL' if is_null else '1'
+        return sql % {
+            'null_flag': null_flag,
+            'column': column_name.upper(),
+            'table_name': table_name.upper()
+        }
+
+    def _column_has_default(self, params):
+        sql = """
+            SELECT a.RDB$DEFAULT_VALUE
+            FROM RDB$RELATION_FIELDS a
+            WHERE a.RDB$FIELD_NAME = '%(column)s'
+            AND a.RDB$RELATION_NAME = '%(table_name)s'
+        """
+        value = self.execute(sql % params)
+        return True if value else False
+
+
+    def _alter_set_defaults(self, field, name, params, sqls):
+        "Subcommand of alter_column that sets default values (overrideable)"
+        # Historically, we used to set defaults here.
+        # But since South 0.8, we don't ever set defaults on alter-column -- we only
+        # use database-level defaults as scaffolding when adding columns.
+        # However, we still sometimes need to remove defaults in alter-column.
+        if self._column_has_default(params):
+            sqls.append(('ALTER COLUMN %s DROP DEFAULT' % (self.quote_name(name),), []))
+
+
+    @generic.invalidate_table_constraints
+    def create_table(self, table_name, fields):
+        qn = self.quote_name(table_name)
+        columns = []
+        autoinc_sql = ''
+
+        for field_name, field in fields:
+            col = self.column_sql(table_name, field_name, field)
+            if not col:
+                continue
+
+            columns.append(col)
+            if isinstance(field, models.AutoField):
+                field_name = field.db_column or field.column
+                autoinc_sql = connection.ops.autoinc_sql(table_name, field_name)
+
+        sql = 'CREATE TABLE %s (%s);' % (qn, ', '.join([col for col in columns]))
+        self.execute(sql)
+        if autoinc_sql:
+            self.execute(autoinc_sql[0])
+            self.execute(autoinc_sql[1])
+
+    def rename_table(self, old_table_name, table_name):
+        """
+        Renames table is not supported by firebird.
+        This involve recreate all related objects (store procedure, views, triggers, etc)
+        """
+        pass
+
+    @generic.invalidate_table_constraints
+    def delete_table(self, table_name, cascade=False):
+        """
+        Deletes the table 'table_name'.
+        Firebird will also delete any triggers associated with the table.
+        """
+        super(DatabaseOperations, self).delete_table(table_name, cascade=False)
+
+        # Also, drop sequence if exists
+        sql = connection.ops.drop_sequence_sql(table_name)
+        if sql:
+            try:
+                self.execute(sql)
+            except:
+                pass
+
+    def column_sql(self, table_name, field_name, field, tablespace='', with_name=True, field_prepared=False):
+        """
+        Creates the SQL snippet for a column. Used by add_column and add_table.
+        """
+
+        # If the field hasn't already been told its attribute name, do so.
+        if not field_prepared:
+            field.set_attributes_from_name(field_name)
+
+        # hook for the field to do any resolution prior to it's attributes being queried
+        if hasattr(field, 'south_init'):
+            field.south_init()
+
+        # Possible hook to fiddle with the fields (e.g. defaults & TEXT on MySQL)
+        field = self._field_sanity(field)
+
+        try:
+            sql = field.db_type(connection=self._get_connection())
+        except TypeError:
+            sql = field.db_type()
+
+        if sql:
+            # Some callers, like the sqlite stuff, just want the extended type.
+            if with_name:
+                field_output = [self.quote_name(field.column), sql]
+            else:
+                field_output = [sql]
+
+            if field.primary_key:
+                field_output.append('NOT NULL PRIMARY KEY')
+            elif field.unique:
+                # Just use UNIQUE (no indexes any more, we have delete_unique)
+                field_output.append('UNIQUE')
+
+            sql = ' '.join(field_output)
+            sqlparams = ()
+
+            # if the field is "NOT NULL" and a default value is provided, create the column with it
+            # this allows the addition of a NOT NULL field to a table with existing rows
+            if not getattr(field, '_suppress_default', False):
+                if field.has_default():
+                    default = field.get_default()
+                    # If the default is actually None, don't add a default term
+                    if default is not None:
+                        # If the default is a callable, then call it!
+                        if callable(default):
+                            default = default()
+                        # Now do some very cheap quoting. TODO: Redesign return values to avoid this.
+                        if isinstance(default, string_types):
+                            default = "'%s'" % default.replace("'", "''")
+                        elif isinstance(default, (datetime.date, datetime.time, datetime.datetime)):
+                            default = "'%s'" % default
+                        elif isinstance(default, bool):
+                            default = int(default)
+                        # Escape any % signs in the output (bug #317)
+                        if isinstance(default, string_types):
+                            default = default.replace("%", "%%")
+                        # Add it in
+                        sql += " DEFAULT %s"
+                        sqlparams = (default)
+                elif (not field.null and field.blank) or (field.get_default() == ''):
+                    if field.empty_strings_allowed and self._get_connection().features.interprets_empty_strings_as_nulls:
+                        sql += " DEFAULT ''"
+                    # Error here would be nice, but doesn't seem to play fair.
+                    #else:
+                    #    raise ValueError("Attempting to add a non null column that isn't character based without an explicit default value.")
+
+            # Firebird need set not null after of default value keyword
+            if not field.primary_key and not field.null:
+                sql += ' NOT NULL'
+
+            if field.rel and self.supports_foreign_keys:
+                self.add_deferred_sql(
+                    self.foreign_key_sql(
+                        table_name,
+                        field.column,
+                        field.rel.to._meta.db_table,
+                        field.rel.to._meta.get_field(field.rel.field_name).column
+                    )
+                )
+
+        # Things like the contrib.gis module fields have this in 1.1 and below
+        if hasattr(field, 'post_create_sql'):
+            for stmt in field.post_create_sql(no_style(), table_name):
+                self.add_deferred_sql(stmt)
+
+        # In 1.2 and above, you have to ask the DatabaseCreation stuff for it.
+        # This also creates normal indexes in 1.1.
+        if hasattr(self._get_connection().creation, "sql_indexes_for_field"):
+            # Make a fake model to pass in, with only db_table
+            model = self.mock_model("FakeModelForGISCreation", table_name)
+            for stmt in self._get_connection().creation.sql_indexes_for_field(model, field, no_style()):
+                self.add_deferred_sql(stmt)
+
+        if sql:
+            return sql % sqlparams
+        else:
+            return None
+
+
+    def _drop_constraints(self, table_name, name, field):
+        if self.has_check_constraints:
+            check_constraints = self._constraints_affecting_columns(table_name, [name], "CHECK")
+            for constraint in check_constraints:
+                self.execute(self.delete_check_sql % {
+                    'table': self.quote_name(table_name),
+                    'constraint': self.quote_name(constraint),
+                })
+
+        # Drop or add UNIQUE constraint
+        unique_constraint = list(self._constraints_affecting_columns(table_name, [name], "UNIQUE"))
+        if field.unique and not unique_constraint:
+            self.create_unique(table_name, [name])
+        elif not field.unique and unique_constraint:
+            self.delete_unique(table_name, [name])
+
+        # Drop all foreign key constraints
+        try:
+            self.delete_foreign_key(table_name, name)
+        except ValueError:
+            # There weren't any
+            pass
+
+
+    @generic.invalidate_table_constraints
+    def alter_column(self, table_name, name, field, explicit_name=True, ignore_constraints=False):
+        """
+        Alters the given column name so it will match the given field.
+        Note that conversion between the two by the database must be possible.
+        Will not automatically add _id by default; to have this behavour, pass
+        explicit_name=False.
+
+        @param table_name: The name of the table to add the column to
+        @param name: The name of the column to alter
+        @param field: The new field definition to use
+        """
+
+        if self.dry_run:
+            if self.debug:
+                print('   - no dry run output for alter_column() due to dynamic DDL, sorry')
+            return
+
+
+        # hook for the field to do any resolution prior to it's attributes being queried
+        if hasattr(field, 'south_init'):
+            field.south_init()
+
+        # Add _id or whatever if we need to
+        field.set_attributes_from_name(name)
+        if not explicit_name:
+            name = field.column
+        else:
+            field.column = name
+
+        if not ignore_constraints:
+            # Drop all check constraints. Note that constraints will be added back
+            # with self.alter_string_set_type and self.alter_string_drop_null.
+            self._drop_constraints(table_name, name, field)
+
+        # First, change the type
+        params = {
+            "column": self.quote_name(name),
+            "type": self._db_type_for_alter_column(field),
+            "table_name": table_name
+        }
+
+        # SQLs is a list of (SQL, values) pairs.
+        sqls = []
+        sqls_extra = []
+
+        # Only alter the column if it has a type (Geometry ones sometimes don't)
+        if params["type"] is not None:
+            sqls.append((self.alter_string_set_type % params, []))
+
+        # Add any field- and backend- specific modifications
+        self._alter_add_column_mods(field, name, params, sqls)
+
+        # Next, nullity: modified, firebird doesn't support DROP NOT NULL
+        sqls_extra.append(self._alter_column_set_null(table_name, name, field.null))
+
+        # Next, set any default
+        self._alter_set_defaults(field, name, params, sqls)
+
+        # Finally, actually change the column
+        if self.allows_combined_alters:
+            sqls, values = list(zip(*sqls))
+            self.execute(
+                "ALTER TABLE %s %s;" % (self.quote_name(table_name), ", ".join(sqls)),
+                generic.flatten(values),
+            )
+        else:
+            # Databases like e.g. MySQL don't like more than one alter at once.
+            for sql, values in sqls:
+                try:
+                    self.execute("ALTER TABLE %s %s;" % (self.quote_name(table_name), sql), values)
+                except DatabaseError as e:
+                    print(e)
+
+
+        # Execute extra sql, which don't need ALTER TABLE statement
+        for sql in sqls_extra:
+            self.execute(sql)
+
+        if not ignore_constraints:
+            # Add back FK constraints if needed
+            if field.rel and self.supports_foreign_keys:
+                self.execute(
+                    self.foreign_key_sql(
+                        table_name,
+                        field.column,
+                        field.rel.to._meta.db_table,
+                        field.rel.to._meta.get_field(field.rel.field_name).column
+                    )
+                )
+
+    @generic.copy_column_constraints
+    @generic.delete_column_constraints
+    def rename_column(self, table_name, old, new):
+        if old == new:
+            # Short-circuit out
+            return []
+
+        self.execute('ALTER TABLE %s ALTER %s TO %s;' % (
+            self.quote_name(table_name),
+            self.quote_name(old),
+            self.quote_name(new),
+        ))

Diff do ficheiro suprimidas por serem muito extensas
+ 418 - 163
desktop/core/ext-py/South-0.8.2/south/db/generic.py


+ 283 - 0
desktop/core/ext-py/South-0.8.2/south/db/mysql.py

@@ -0,0 +1,283 @@
+# MySQL-specific implementations for south
+# Original author: Andrew Godwin
+# Patches by: F. Gabriel Gosselin <gabrielNOSPAM@evidens.ca>
+
+from south.db import generic
+from south.db.generic import DryRunError, INVALID
+from south.logger import get_logger
+
+
+def delete_column_constraints(func):
+    """
+    Decorates column operation functions for MySQL.
+    Deletes the constraints from the database and clears local cache.
+    """
+    def _column_rm(self, table_name, column_name, *args, **opts):
+        # Delete foreign key constraints
+        try:
+            self.delete_foreign_key(table_name, column_name)
+        except ValueError:
+            pass  # If no foreign key on column, OK because it checks first
+        # Delete constraints referring to this column
+        try:
+            reverse = self._lookup_reverse_constraint(table_name, column_name)
+            for cname, rtable, rcolumn in reverse:
+                self.delete_foreign_key(rtable, rcolumn)
+        except DryRunError:
+            pass
+        return func(self, table_name, column_name, *args, **opts)
+    return _column_rm
+
+
+def copy_column_constraints(func):
+    """
+    Decorates column operation functions for MySQL.
+    Determines existing constraints and copies them to a new column
+    """
+    def _column_cp(self, table_name, column_old, column_new, *args, **opts):
+        # Copy foreign key constraint
+        try:
+            constraint = self._find_foreign_constraints(table_name, column_old)[0]
+            (ftable, fcolumn) = self._lookup_constraint_references(table_name, constraint)
+            if ftable and fcolumn:
+                fk_sql = self.foreign_key_sql(
+                            table_name, column_new, ftable, fcolumn)
+                get_logger().debug("Foreign key SQL: " + fk_sql)
+                self.add_deferred_sql(fk_sql)
+        except IndexError:
+            pass  # No constraint exists so ignore
+        except DryRunError:
+            pass
+        # Copy constraints referring to this column
+        try:
+            reverse = self._lookup_reverse_constraint(table_name, column_old)
+            for cname, rtable, rcolumn in reverse:
+                fk_sql = self.foreign_key_sql(
+                        rtable, rcolumn, table_name, column_new)
+                self.add_deferred_sql(fk_sql)
+        except DryRunError:
+            pass
+        return func(self, table_name, column_old, column_new, *args, **opts)
+    return _column_cp
+
+
+def invalidate_table_constraints(func):
+    """
+    For MySQL we grab all table constraints simultaneously, so this is
+    effective.
+    It further solves the issues of invalidating referred table constraints.
+    """
+    def _cache_clear(self, table, *args, **opts):
+        db_name = self._get_setting('NAME')
+        if db_name in self._constraint_cache:
+            del self._constraint_cache[db_name]
+        if db_name in self._reverse_cache:
+            del self._reverse_cache[db_name]
+        if db_name in self._constraint_references:
+            del self._constraint_references[db_name]
+        return func(self, table, *args, **opts)
+    return _cache_clear
+
+
+class DatabaseOperations(generic.DatabaseOperations):
+    """
+    MySQL implementation of database operations.
+
+    MySQL has no DDL transaction support This can confuse people when they ask
+    how to roll back - hence the dry runs, etc., found in the migration code.
+    """
+
+    backend_name = "mysql"
+    alter_string_set_type = ''
+    alter_string_set_null = 'MODIFY %(column)s %(type)s NULL;'
+    alter_string_drop_null = 'MODIFY %(column)s %(type)s NOT NULL;'
+    drop_index_string = 'DROP INDEX %(index_name)s ON %(table_name)s'
+    delete_primary_key_sql = "ALTER TABLE %(table)s DROP PRIMARY KEY"
+    delete_foreign_key_sql = "ALTER TABLE %(table)s DROP FOREIGN KEY %(constraint)s"
+    delete_unique_sql = "ALTER TABLE %s DROP INDEX %s"
+    rename_table_sql = "RENAME TABLE %s TO %s;"
+
+    allows_combined_alters = False
+    has_check_constraints = False
+    raises_default_errors = False
+
+    geom_types = ['geometry', 'point', 'linestring', 'polygon']
+    text_types = ['text', 'blob']
+
+    def __init__(self, db_alias):
+        self._constraint_references = {}
+        self._reverse_cache = {}
+        super(DatabaseOperations, self).__init__(db_alias)
+        if self._has_setting('STORAGE_ENGINE') and self._get_setting('STORAGE_ENGINE'):
+            self.create_table_sql = self.create_table_sql + ' ENGINE=%s' % self._get_setting('STORAGE_ENGINE')
+
+    def _is_valid_cache(self, db_name, table_name):
+        cache = self._constraint_cache
+        # we cache the whole db so if there are any tables table_name is valid
+        return db_name in cache and cache[db_name].get(table_name, None) is not INVALID
+
+    def _fill_constraint_cache(self, db_name, table_name):
+        # for MySQL grab all constraints for this database.  It's just as cheap as a single column.
+        self._constraint_cache[db_name] = {}
+        self._constraint_cache[db_name][table_name] = {}
+        self._reverse_cache[db_name] = {}
+        self._constraint_references[db_name] = {}
+
+        name_query = """
+            SELECT kc.`constraint_name`, kc.`column_name`, kc.`table_name`,
+                kc.`referenced_table_name`, kc.`referenced_column_name`
+            FROM information_schema.key_column_usage AS kc
+            WHERE
+                kc.table_schema = %s
+        """
+        rows = self.execute(name_query, [db_name])
+        if not rows:
+            return
+        cnames = {}
+        for constraint, column, table, ref_table, ref_column in rows:
+            key = (table, constraint)
+            cnames.setdefault(key, set())
+            cnames[key].add((column, ref_table, ref_column))
+
+        type_query = """
+            SELECT c.constraint_name, c.table_name, c.constraint_type
+            FROM information_schema.table_constraints AS c
+            WHERE
+                c.table_schema = %s
+        """
+        rows = self.execute(type_query, [db_name])
+        for constraint, table, kind in rows:
+            key = (table, constraint)
+            self._constraint_cache[db_name].setdefault(table, {})
+            try:
+                cols = cnames[key]
+            except KeyError:
+                cols = set()
+            for column_set in cols:
+                (column, ref_table, ref_column) = column_set
+                self._constraint_cache[db_name][table].setdefault(column, set())
+                if kind == 'FOREIGN KEY':
+                    self._constraint_cache[db_name][table][column].add((kind,
+                        constraint))
+                    # Create constraint lookup, see constraint_references
+                    self._constraint_references[db_name][(table,
+                        constraint)] = (ref_table, ref_column)
+                    # Create reverse table lookup, reverse_lookup
+                    self._reverse_cache[db_name].setdefault(ref_table, {})
+                    self._reverse_cache[db_name][ref_table].setdefault(ref_column,
+                            set())
+                    self._reverse_cache[db_name][ref_table][ref_column].add(
+                            (constraint, table, column))
+                else:
+                    self._constraint_cache[db_name][table][column].add((kind,
+                    constraint))
+
+    def connection_init(self):
+        """
+        Run before any SQL to let database-specific config be sent as a command,
+        e.g. which storage engine (MySQL) or transaction serialisability level.
+        """
+        cursor = self._get_connection().cursor()
+        if self._has_setting('STORAGE_ENGINE') and self._get_setting('STORAGE_ENGINE'):
+            cursor.execute("SET storage_engine=%s;" % self._get_setting('STORAGE_ENGINE'))
+
+    def start_transaction(self):
+        super(DatabaseOperations, self).start_transaction()
+        self.execute("SET FOREIGN_KEY_CHECKS=0;")
+
+    @copy_column_constraints
+    @delete_column_constraints
+    @invalidate_table_constraints
+    def rename_column(self, table_name, old, new):
+        if old == new or self.dry_run:
+            return []
+
+        rows = [x for x in self.execute('DESCRIBE %s' % (self.quote_name(table_name),)) if x[0] == old]
+
+        if not rows:
+            raise ValueError("No column '%s' in '%s'." % (old, table_name))
+
+        params = (
+            self.quote_name(table_name),
+            self.quote_name(old),
+            self.quote_name(new),
+            rows[0][1],
+            rows[0][2] == "YES" and "NULL" or "NOT NULL",
+            rows[0][4] and "DEFAULT " or "",
+            rows[0][4] and "%s" or "",
+            rows[0][5] or "",
+        )
+
+        sql = 'ALTER TABLE %s CHANGE COLUMN %s %s %s %s %s %s %s;' % params
+
+        if rows[0][4]:
+            self.execute(sql, (rows[0][4],))
+        else:
+            self.execute(sql)
+
+    @delete_column_constraints
+    def delete_column(self, table_name, name):
+        super(DatabaseOperations, self).delete_column(table_name, name)
+
+    @invalidate_table_constraints
+    def rename_table(self, old_table_name, table_name):
+        super(DatabaseOperations, self).rename_table(old_table_name,
+                table_name)
+
+    @invalidate_table_constraints
+    def delete_table(self, table_name):
+        super(DatabaseOperations, self).delete_table(table_name)
+
+    def _lookup_constraint_references(self, table_name, cname):
+        """
+        Provided an existing table and constraint, returns tuple of (foreign
+        table, column)
+        """
+        db_name = self._get_setting('NAME')
+        try:
+            return self._constraint_references[db_name][(table_name, cname)]
+        except KeyError:
+            return None
+
+    def _lookup_reverse_constraint(self, table_name, column_name=None):
+        """Look for the column referenced by a foreign constraint"""
+        db_name = self._get_setting('NAME')
+        if self.dry_run:
+            raise DryRunError("Cannot get constraints for columns.")
+
+        if not self._is_valid_cache(db_name, table_name):
+            # Piggy-back on lookup_constraint, ensures cache exists
+            self.lookup_constraint(db_name, table_name)
+
+        try:
+            table = self._reverse_cache[db_name][table_name]
+            if column_name == None:
+                return [(y, tuple(y)) for x, y in table.items()]
+            else:
+                return tuple(table[column_name])
+        except KeyError:
+            return []
+
+    def _field_sanity(self, field):
+        """
+        This particular override stops us sending DEFAULTs for BLOB/TEXT columns.
+        """
+        #  MySQL does not support defaults for geometry columns also
+        type = self._db_type_for_alter_column(field).lower()
+        is_geom = True in [type.find(t) > -1 for t in self.geom_types]
+        is_text = True in [type.find(t) > -1 for t in self.text_types]
+
+        if is_geom or is_text:
+            field._suppress_default = True
+        return field
+
+    def _alter_set_defaults(self, field, name, params, sqls):
+        """
+        MySQL does not support defaults on text or blob columns.
+        """
+        type = params['type']
+        #  MySQL does not support defaults for geometry columns also
+        is_geom = True in [type.find(t) > -1 for t in self.geom_types]
+        is_text = True in [type.find(t) > -1 for t in self.text_types]
+        if not is_geom and not is_text:
+            super(DatabaseOperations, self)._alter_set_defaults(field, name, params, sqls)

+ 326 - 0
desktop/core/ext-py/South-0.8.2/south/db/oracle.py

@@ -0,0 +1,326 @@
+from __future__ import print_function
+
+import os.path
+import sys
+import re
+import warnings
+import cx_Oracle
+
+
+from django.db import connection, models
+from django.db.backends.util import truncate_name
+from django.core.management.color import no_style
+from django.db.models.fields import NOT_PROVIDED
+from django.db.utils import DatabaseError
+
+# In revision r16016 function get_sequence_name has been transformed into
+# method of DatabaseOperations class. To make code backward-compatible we
+# need to handle both situations.
+try:
+    from django.db.backends.oracle.base import get_sequence_name\
+        as original_get_sequence_name
+except ImportError:
+    original_get_sequence_name = None
+
+from south.db import generic
+
+class DatabaseOperations(generic.DatabaseOperations):    
+    """
+    Oracle implementation of database operations.    
+    """
+    backend_name = 'oracle'
+
+    alter_string_set_type =     'ALTER TABLE %(table_name)s MODIFY %(column)s %(type)s %(nullity)s;'
+    alter_string_set_default =  'ALTER TABLE %(table_name)s MODIFY %(column)s DEFAULT %(default)s;'
+    alter_string_update_nulls_to_default = \
+                                'UPDATE %(table_name)s SET %(column)s = %(default)s WHERE %(column)s IS NULL;'
+    add_column_string =         'ALTER TABLE %s ADD %s;'
+    delete_column_string =      'ALTER TABLE %s DROP COLUMN %s;'
+    add_constraint_string =     'ALTER TABLE %(table_name)s ADD CONSTRAINT %(constraint)s %(clause)s'
+
+    allows_combined_alters = False
+    has_booleans = False
+    
+    constraints_dict = {
+        'P': 'PRIMARY KEY',
+        'U': 'UNIQUE',
+        'C': 'CHECK',
+        'R': 'FOREIGN KEY'
+    }
+
+    def get_sequence_name(self, table_name):
+        if original_get_sequence_name is None:
+            return self._get_connection().ops._get_sequence_name(table_name)
+        else:
+            return original_get_sequence_name(table_name)
+
+    #TODO: This will cause very obscure bugs if anyone uses a column name or string value
+    #      that looks like a column definition (with 'CHECK', 'DEFAULT' and/or 'NULL' in it)
+    #      e.g. "CHECK MATE" varchar(10) DEFAULT 'NULL'
+    def adj_column_sql(self, col):
+        # Syntax fixes -- Oracle is picky about clause order
+        col = re.sub('(?P<constr>CHECK \(.*\))(?P<any>.*)(?P<default>DEFAULT \d+)', 
+                     lambda mo: '%s %s%s'%(mo.group('default'), mo.group('constr'), mo.group('any')), col) #syntax fix for boolean/integer field only
+        col = re.sub('(?P<not_null>(NOT )?NULL) (?P<misc>(.* )?)(?P<default>DEFAULT.+)',
+                     lambda mo: '%s %s %s'%(mo.group('default'),mo.group('not_null'),mo.group('misc') or ''), col) #fix order of NULL/NOT NULL and DEFAULT
+        return col
+
+    def check_meta(self, table_name):
+        return table_name in [ m._meta.db_table for m in models.get_models() ] #caching provided by Django
+    
+    def normalize_name(self, name):
+        """
+        Get the properly shortened and uppercased identifier as returned by quote_name(), but without the actual quotes.
+        """
+        nn = self.quote_name(name)
+        if nn[0] == '"' and nn[-1] == '"':
+            nn = nn[1:-1]
+        return nn
+
+    @generic.invalidate_table_constraints
+    def create_table(self, table_name, fields): 
+        qn = self.quote_name(table_name)
+        columns = []
+        autoinc_sql = ''
+
+
+        for field_name, field in fields:
+            
+            field = self._field_sanity(field)
+
+            # avoid default values in CREATE TABLE statements (#925)
+            field._suppress_default = True
+
+
+            col = self.column_sql(table_name, field_name, field)
+            if not col:
+                continue
+            col = self.adj_column_sql(col)
+
+            columns.append(col)
+            if isinstance(field, models.AutoField):
+                autoinc_sql = connection.ops.autoinc_sql(table_name, field_name)
+
+        sql = 'CREATE TABLE %s (%s);' % (qn, ', '.join([col for col in columns]))
+        self.execute(sql)
+        if autoinc_sql:
+            self.execute(autoinc_sql[0])
+            self.execute(autoinc_sql[1])
+
+    @generic.invalidate_table_constraints
+    def delete_table(self, table_name, cascade=True):
+        qn = self.quote_name(table_name)
+
+        # Note: PURGE is not valid syntax for Oracle 9i (it was added in 10)
+        if cascade:
+            self.execute('DROP TABLE %s CASCADE CONSTRAINTS;' % qn)
+        else:
+            self.execute('DROP TABLE %s;' % qn)
+        
+        # If the table has an AutoField a sequence was created.
+        sequence_sql = """
+DECLARE
+    i INTEGER;
+BEGIN
+    SELECT COUNT(*) INTO i FROM USER_CATALOG
+        WHERE TABLE_NAME = '%(sq_name)s' AND TABLE_TYPE = 'SEQUENCE';
+    IF i = 1 THEN
+        EXECUTE IMMEDIATE 'DROP SEQUENCE "%(sq_name)s"';
+    END IF;
+END;
+/""" % {'sq_name': self.get_sequence_name(table_name)}
+        self.execute(sequence_sql)
+
+    @generic.invalidate_table_constraints
+    def alter_column(self, table_name, name, field, explicit_name=True):
+        
+        if self.dry_run:
+            if self.debug:
+                print('   - no dry run output for alter_column() due to dynamic DDL, sorry')
+            return
+
+        qn = self.quote_name(table_name)
+
+        # hook for the field to do any resolution prior to it's attributes being queried
+        if hasattr(field, 'south_init'):
+            field.south_init()
+        field = self._field_sanity(field)
+
+        # Add _id or whatever if we need to
+        field.set_attributes_from_name(name)
+        if not explicit_name:
+            name = field.column
+        qn_col = self.quote_name(name)
+
+        # First, change the type
+        # This will actually also add any CHECK constraints needed,
+        # since e.g. 'type' for a BooleanField is 'NUMBER(1) CHECK (%(qn_column)s IN (0,1))'
+        params = {
+            'table_name':qn,
+            'column': qn_col,
+            'type': self._db_type_for_alter_column(field),
+            'nullity': 'NOT NULL',
+            'default': 'NULL'
+        }
+        if field.null:
+            params['nullity'] = 'NULL'
+
+        sql_templates = [
+            (self.alter_string_set_type, params, []),
+            (self.alter_string_set_default, params, []),
+        ]
+        if not field.null and field.has_default():
+            # Use default for rows that had nulls. To support the case where
+            # the new default does not fit the old type, we need to first change
+            # the column type to the new type, but null=True; then set the default;
+            # then complete the type change. 
+            def change_params(**kw):
+                "A little helper for non-destructively changing the params"
+                p = params.copy()
+                p.update(kw)
+                return p
+            sql_templates[:0] = [
+                (self.alter_string_set_type, change_params(nullity='NULL'),[]),
+                (self.alter_string_update_nulls_to_default, change_params(default="%s"), [field.get_default()]),
+            ]
+
+
+        # drop CHECK constraints. Make sure this is executed before the ALTER TABLE statements
+        # generated above, since those statements recreate the constraints we delete here.
+        check_constraints = self._constraints_affecting_columns(table_name, [name], "CHECK")
+        for constraint in check_constraints:
+            self.execute(self.delete_check_sql % {
+                'table': self.quote_name(table_name),
+                'constraint': self.quote_name(constraint),
+            })
+
+        for sql_template, params, args in sql_templates:
+            try:
+                self.execute(sql_template % params, args, print_all_errors=False)
+            except DatabaseError as exc:
+                description = str(exc)
+                # Oracle complains if a column is already NULL/NOT NULL
+                if 'ORA-01442' in description or 'ORA-01451' in description:
+                    # so we just drop NULL/NOT NULL part from target sql and retry
+                    params['nullity'] = ''
+                    sql = sql_template % params
+                    self.execute(sql)
+                # Oracle also has issues if we try to change a regular column
+                # to a LOB or vice versa (also REF, object, VARRAY or nested
+                # table, but these don't come up much in Django apps)
+                elif 'ORA-22858' in description or 'ORA-22859' in description:
+                    self._alter_column_lob_workaround(table_name, name, field)
+                else:
+                    self._print_sql_error(exc, sql_template % params)
+                    raise
+
+    def _alter_column_lob_workaround(self, table_name, name, field):
+        """
+        Oracle refuses to change a column type from/to LOB to/from a regular
+        column. In Django, this shows up when the field is changed from/to
+        a TextField.
+        What we need to do instead is:
+        - Rename the original column
+        - Add the desired field as new
+        - Update the table to transfer values from old to new
+        - Drop old column
+        """
+        renamed = self._generate_temp_name(name)
+        self.rename_column(table_name, name, renamed)
+        self.add_column(table_name, name, field, keep_default=False)
+        self.execute("UPDATE %s set %s=%s" % (
+            self.quote_name(table_name),
+            self.quote_name(name),
+            self.quote_name(renamed),
+        ))
+        self.delete_column(table_name, renamed)
+
+    def _generate_temp_name(self, for_name):
+        suffix = hex(hash(for_name)).upper()[1:]
+        return self.normalize_name(for_name + "_" + suffix)
+    
+    @generic.copy_column_constraints #TODO: Appears to be nulled by the delete decorator below...
+    @generic.delete_column_constraints
+    def rename_column(self, table_name, old, new):
+        if old == new:
+            # Short-circuit out
+            return []
+        self.execute('ALTER TABLE %s RENAME COLUMN %s TO %s;' % (
+            self.quote_name(table_name),
+            self.quote_name(old),
+            self.quote_name(new),
+        ))
+
+    @generic.invalidate_table_constraints
+    def add_column(self, table_name, name, field, keep_default=False):
+        field = self._field_sanity(field)
+        sql = self.column_sql(table_name, name, field)
+        sql = self.adj_column_sql(sql)
+
+        if sql:
+            params = (
+                self.quote_name(table_name),
+                sql
+            )
+            sql = self.add_column_string % params
+            self.execute(sql)
+
+            # Now, drop the default if we need to
+            if not keep_default and field.default is not None:
+                field.default = NOT_PROVIDED
+                self.alter_column(table_name, name, field, explicit_name=False)
+
+    def delete_column(self, table_name, name):
+        return super(DatabaseOperations, self).delete_column(self.quote_name(table_name), name)
+
+    def lookup_constraint(self, db_name, table_name, column_name=None):
+        if column_name:
+            # Column names in the constraint cache come from the database,
+            # make sure we use the properly shortened/uppercased version
+            # for lookup.
+            column_name = self.normalize_name(column_name)
+        return super(DatabaseOperations, self).lookup_constraint(db_name, table_name, column_name)
+
+    def _constraints_affecting_columns(self, table_name, columns, type="UNIQUE"):
+        if columns:
+            columns = [self.normalize_name(c) for c in columns]
+        return super(DatabaseOperations, self)._constraints_affecting_columns(table_name, columns, type)
+
+    def _field_sanity(self, field):
+        """
+        This particular override stops us sending DEFAULTs for BooleanField.
+        """
+        if isinstance(field, models.BooleanField) and field.has_default():
+            field.default = int(field.to_python(field.get_default()))
+        # On Oracle, empty strings are null
+        if isinstance(field, (models.CharField, models.TextField)):
+            field.null = field.empty_strings_allowed
+        return field
+
+
+    def _default_value_workaround(self, value):
+        from datetime import date,time,datetime
+        if isinstance(value, (date,time,datetime)):
+            return "'%s'" % value
+        else:
+            return super(DatabaseOperations, self)._default_value_workaround(value)
+
+    def _fill_constraint_cache(self, db_name, table_name):
+        self._constraint_cache.setdefault(db_name, {}) 
+        self._constraint_cache[db_name][table_name] = {} 
+
+        rows = self.execute("""
+            SELECT user_cons_columns.constraint_name,
+                   user_cons_columns.column_name,
+                   user_constraints.constraint_type
+            FROM user_constraints
+            JOIN user_cons_columns ON
+                 user_constraints.table_name = user_cons_columns.table_name AND 
+                 user_constraints.constraint_name = user_cons_columns.constraint_name
+            WHERE user_constraints.table_name = '%s'
+        """ % self.normalize_name(table_name))
+
+        for constraint, column, kind in rows:
+            self._constraint_cache[db_name][table_name].setdefault(column, set())
+            self._constraint_cache[db_name][table_name][column].add((self.constraints_dict[kind], constraint))
+        return

+ 96 - 0
desktop/core/ext-py/South-0.8.2/south/db/postgresql_psycopg2.py

@@ -0,0 +1,96 @@
+from __future__ import print_function
+
+import uuid
+from django.db.backends.util import truncate_name
+from south.db import generic
+
+
+class DatabaseOperations(generic.DatabaseOperations):
+
+    """
+    PsycoPG2 implementation of database operations.
+    """
+
+    backend_name = "postgres"
+
+    def create_index_name(self, table_name, column_names, suffix=""):
+        """
+        Generate a unique name for the index
+
+        Django's logic for naming field indexes is different in the
+        postgresql_psycopg2 backend, so we follow that for single-column
+        indexes.
+        """
+
+        if len(column_names) == 1:
+            return truncate_name(
+                '%s_%s%s' % (table_name, column_names[0], suffix),
+                self._get_connection().ops.max_name_length()
+            )
+        return super(DatabaseOperations, self).create_index_name(table_name, column_names, suffix)
+
+    @generic.copy_column_constraints
+    @generic.delete_column_constraints
+    def rename_column(self, table_name, old, new):
+        if old == new:
+            # Short-circuit out
+            return []
+        self.execute('ALTER TABLE %s RENAME COLUMN %s TO %s;' % (
+            self.quote_name(table_name),
+            self.quote_name(old),
+            self.quote_name(new),
+        ))
+
+    @generic.invalidate_table_constraints
+    def rename_table(self, old_table_name, table_name):
+        "will rename the table and an associated ID sequence and primary key index"
+        # First, rename the table
+        generic.DatabaseOperations.rename_table(self, old_table_name, table_name)
+        # Then, try renaming the ID sequence
+        # (if you're using other AutoFields... your problem, unfortunately)
+
+        if self.execute(
+            """
+            SELECT 1
+            FROM information_schema.sequences
+            WHERE sequence_name = %s
+            """,
+            [old_table_name + '_id_seq']
+        ):
+            generic.DatabaseOperations.rename_table(self, old_table_name + "_id_seq", table_name + "_id_seq")
+
+        # Rename primary key index, will not rename other indices on
+        # the table that are used by django (e.g. foreign keys). Until
+        # figure out how, you need to do this yourself.
+
+        pkey_index_names = self.execute(
+            """
+            SELECT pg_index.indexrelid::regclass
+            FROM pg_index, pg_attribute
+            WHERE
+              indrelid = %s::regclass AND
+              pg_attribute.attrelid = indrelid AND
+              pg_attribute.attnum = any(pg_index.indkey)
+              AND indisprimary
+            """,
+            [table_name]
+        )
+        if old_table_name + "_pkey" in pkey_index_names:
+            generic.DatabaseOperations.rename_table(self, old_table_name + "_pkey", table_name + "_pkey")
+
+    def rename_index(self, old_index_name, index_name):
+        "Rename an index individually"
+        generic.DatabaseOperations.rename_table(self, old_index_name, index_name)
+
+    def _default_value_workaround(self, value):
+        "Support for UUIDs on psql"
+        if isinstance(value, uuid.UUID):
+            return str(value)
+        else:
+            return super(DatabaseOperations, self)._default_value_workaround(value)
+
+    def _db_type_for_alter_column(self, field):
+        return self._db_positive_type_for_alter_column(DatabaseOperations, field)
+
+    def _alter_add_column_mods(self, field, name, params, sqls):
+        return self._alter_add_positive_check(DatabaseOperations, field, name, params, sqls)

+ 0 - 0
desktop/core/ext-py/south/south/db/sql_server/__init__.py → desktop/core/ext-py/South-0.8.2/south/db/sql_server/__init__.py


+ 444 - 0
desktop/core/ext-py/South-0.8.2/south/db/sql_server/pyodbc.py

@@ -0,0 +1,444 @@
+from datetime import date, datetime, time
+from warnings import warn
+from django.db import models
+from django.db.models import fields
+from south.db import generic
+from south.db.generic import delete_column_constraints, invalidate_table_constraints, copy_column_constraints
+from south.exceptions import ConstraintDropped
+from south.utils.py3 import string_types
+try:
+    from django.utils.encoding import smart_text                    # Django >= 1.5
+except ImportError:
+    from django.utils.encoding import smart_unicode as smart_text   # Django < 1.5
+from django.core.management.color import no_style
+
+class DatabaseOperations(generic.DatabaseOperations):
+    """
+    django-pyodbc (sql_server.pyodbc) implementation of database operations.
+    """
+    
+    backend_name = "pyodbc"
+    
+    add_column_string = 'ALTER TABLE %s ADD %s;'
+    alter_string_set_type = 'ALTER COLUMN %(column)s %(type)s'
+    alter_string_set_null = 'ALTER COLUMN %(column)s %(type)s NULL'
+    alter_string_drop_null = 'ALTER COLUMN %(column)s %(type)s NOT NULL'
+    
+    allows_combined_alters = False
+
+    drop_index_string = 'DROP INDEX %(index_name)s ON %(table_name)s'
+    drop_constraint_string = 'ALTER TABLE %(table_name)s DROP CONSTRAINT %(constraint_name)s'
+    delete_column_string = 'ALTER TABLE %s DROP COLUMN %s'
+
+    #create_check_constraint_sql = "ALTER TABLE %(table)s " + \
+    #                              generic.DatabaseOperations.add_check_constraint_fragment 
+    create_foreign_key_sql = "ALTER TABLE %(table)s ADD CONSTRAINT %(constraint)s " + \
+                             "FOREIGN KEY (%(column)s) REFERENCES %(target)s"
+    create_unique_sql = "ALTER TABLE %(table)s ADD CONSTRAINT %(constraint)s UNIQUE (%(columns)s)"
+    
+    
+    default_schema_name = "dbo"
+    
+    has_booleans = False
+
+
+    @delete_column_constraints
+    def delete_column(self, table_name, name):
+        q_table_name, q_name = (self.quote_name(table_name), self.quote_name(name))
+
+        # Zap the constraints
+        for const in self._find_constraints_for_column(table_name,name):
+            params = {'table_name':q_table_name, 'constraint_name': const}
+            sql = self.drop_constraint_string % params
+            self.execute(sql, [])
+
+        # Zap the indexes
+        for ind in self._find_indexes_for_column(table_name,name):
+            params = {'table_name':q_table_name, 'index_name': ind}
+            sql = self.drop_index_string % params
+            self.execute(sql, [])
+
+        # Zap default if exists
+        drop_default = self.drop_column_default_sql(table_name, name)
+        if drop_default:
+            sql = "ALTER TABLE [%s] %s" % (table_name, drop_default)
+            self.execute(sql, [])
+
+        # Finally zap the column itself
+        self.execute(self.delete_column_string % (q_table_name, q_name), [])
+
+    def _find_indexes_for_column(self, table_name, name):
+        "Find the indexes that apply to a column, needed when deleting"
+
+        sql = """
+        SELECT si.name, si.id, sik.colid, sc.name
+        FROM dbo.sysindexes si WITH (NOLOCK)
+        INNER JOIN dbo.sysindexkeys sik WITH (NOLOCK)
+            ON  sik.id = si.id
+            AND sik.indid = si.indid
+        INNER JOIN dbo.syscolumns sc WITH (NOLOCK)
+            ON  si.id = sc.id
+            AND sik.colid = sc.colid
+        WHERE si.indid !=0
+            AND si.id = OBJECT_ID('%s')
+            AND sc.name = '%s'
+        """
+        idx = self.execute(sql % (table_name, name), [])
+        return [i[0] for i in idx]
+
+
+    def _find_constraints_for_column(self, table_name, name, just_names=True):
+        """
+        Find the constraints that apply to a column, needed when deleting. Defaults not included.
+        This is more general than the parent _constraints_affecting_columns, as on MSSQL this
+        includes PK and FK constraints.
+        """
+
+        sql = """
+         SELECT CC.[CONSTRAINT_NAME]
+              ,TC.[CONSTRAINT_TYPE]
+              ,CHK.[CHECK_CLAUSE]
+              ,RFD.TABLE_SCHEMA
+              ,RFD.TABLE_NAME
+              ,RFD.COLUMN_NAME
+              -- used for normalized names
+              ,CC.TABLE_NAME
+              ,CC.COLUMN_NAME
+          FROM [INFORMATION_SCHEMA].[TABLE_CONSTRAINTS] TC
+          JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE CC
+               ON TC.CONSTRAINT_CATALOG = CC.CONSTRAINT_CATALOG 
+              AND TC.CONSTRAINT_SCHEMA = CC.CONSTRAINT_SCHEMA
+              AND TC.CONSTRAINT_NAME = CC.CONSTRAINT_NAME
+          LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS CHK
+               ON CHK.CONSTRAINT_CATALOG = CC.CONSTRAINT_CATALOG
+              AND CHK.CONSTRAINT_SCHEMA = CC.CONSTRAINT_SCHEMA
+              AND CHK.CONSTRAINT_NAME = CC.CONSTRAINT_NAME
+              AND 'CHECK' = TC.CONSTRAINT_TYPE
+          LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS REF
+               ON REF.CONSTRAINT_CATALOG = CC.CONSTRAINT_CATALOG
+              AND REF.CONSTRAINT_SCHEMA = CC.CONSTRAINT_SCHEMA
+              AND REF.CONSTRAINT_NAME = CC.CONSTRAINT_NAME
+              AND 'FOREIGN KEY' = TC.CONSTRAINT_TYPE
+          LEFT JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE RFD
+               ON RFD.CONSTRAINT_CATALOG = REF.UNIQUE_CONSTRAINT_CATALOG
+              AND RFD.CONSTRAINT_SCHEMA = REF.UNIQUE_CONSTRAINT_SCHEMA
+              AND RFD.CONSTRAINT_NAME = REF.UNIQUE_CONSTRAINT_NAME
+          WHERE CC.CONSTRAINT_CATALOG = CC.TABLE_CATALOG
+            AND CC.CONSTRAINT_SCHEMA = CC.TABLE_SCHEMA
+            AND CC.TABLE_CATALOG = %s
+            AND CC.TABLE_SCHEMA = %s
+            AND CC.TABLE_NAME = %s
+            AND CC.COLUMN_NAME = %s 
+        """
+        db_name = self._get_setting('name')
+        schema_name = self._get_schema_name()
+        table = self.execute(sql, [db_name, schema_name, table_name, name])
+        
+        if just_names:
+            return [r[0] for r in table]
+        
+        all = {}
+        for r in table:
+            cons_name, type = r[:2]
+            if type=='PRIMARY KEY' or type=='UNIQUE':
+                cons = all.setdefault(cons_name, (type,[]))
+                sql = '''
+                SELECT COLUMN_NAME
+                FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE RFD
+                WHERE RFD.CONSTRAINT_CATALOG = %s
+                  AND RFD.CONSTRAINT_SCHEMA = %s
+                  AND RFD.TABLE_NAME = %s
+                  AND RFD.CONSTRAINT_NAME = %s
+                '''
+                columns = self.execute(sql, [db_name, schema_name, table_name, cons_name])
+                cons[1].extend(col for col, in columns)
+            elif type=='CHECK':
+                cons = (type, r[2])
+            elif type=='FOREIGN KEY':
+                if cons_name in all:
+                    raise NotImplementedError("Multiple-column foreign keys are not supported")
+                else:
+                    cons = (type, r[3:6])
+            else:
+                raise NotImplementedError("Don't know how to handle constraints of type "+ type)
+            all[cons_name] = cons
+        return all
+
+    @invalidate_table_constraints        
+    def alter_column(self, table_name, name, field, explicit_name=True, ignore_constraints=False):
+        """
+        Alters the given column name so it will match the given field.
+        Note that conversion between the two by the database must be possible.
+        Will not automatically add _id by default; to have this behavour, pass
+        explicit_name=False.
+
+        @param table_name: The name of the table to add the column to
+        @param name: The name of the column to alter
+        @param field: The new field definition to use
+        """
+        self._fix_field_definition(field)
+
+        if not ignore_constraints:
+            qn = self.quote_name
+            sch = qn(self._get_schema_name())
+            tab = qn(table_name)
+            table = ".".join([sch, tab])
+            try:
+                self.delete_foreign_key(table_name, name)
+            except ValueError:
+                # no FK constraint on this field. That's OK.
+                pass
+            constraints = self._find_constraints_for_column(table_name, name, False)
+            for constraint in constraints.keys():
+                params = dict(table_name = table,
+                              constraint_name = qn(constraint))
+                sql = self.drop_constraint_string % params
+                self.execute(sql, [])
+                
+        ret_val = super(DatabaseOperations, self).alter_column(table_name, name, field, explicit_name, ignore_constraints=True)
+        
+        if not ignore_constraints:
+            for cname, (ctype,args) in constraints.items():
+                params = dict(table = table,
+                              constraint = qn(cname))
+                if ctype=='UNIQUE':
+                    params['columns'] = ", ".join(map(qn,args))
+                    sql = self.create_unique_sql % params
+                elif ctype=='PRIMARY KEY':
+                    params['columns'] = ", ".join(map(qn,args))
+                    sql = self.create_primary_key_string % params
+                elif ctype=='FOREIGN KEY':
+                    continue
+                    # Foreign keys taken care of below 
+                    #target = "%s.%s(%s)" % tuple(map(qn,args))
+                    #params.update(column = qn(name), target = target)
+                    #sql = self.create_foreign_key_sql % params
+                elif ctype=='CHECK':
+                    warn(ConstraintDropped("CHECK "+ args, table_name, name))
+                    continue
+                    #TODO: Some check constraints should be restored; but not before the generic
+                    #      backend restores them.
+                    #params['check'] = args
+                    #sql = self.create_check_constraint_sql % params
+                else:
+                    raise NotImplementedError("Don't know how to handle constraints of type "+ type)                    
+                self.execute(sql, [])
+            # Create foreign key if necessary
+            if field.rel and self.supports_foreign_keys:
+                self.execute(
+                    self.foreign_key_sql(
+                        table_name,
+                        field.column,
+                        field.rel.to._meta.db_table,
+                        field.rel.to._meta.get_field(field.rel.field_name).column
+                    )
+                )
+                model = self.mock_model("FakeModelForIndexCreation", table_name)
+                for stmt in self._get_connection().creation.sql_indexes_for_field(model, field, no_style()):
+                    self.execute(stmt)
+
+
+        return ret_val
+    
+    def _alter_set_defaults(self, field, name, params, sqls): 
+        "Subcommand of alter_column that sets default values (overrideable)"
+        # Historically, we used to set defaults here.
+        # But since South 0.8, we don't ever set defaults on alter-column -- we only
+        # use database-level defaults as scaffolding when adding columns.
+        # However, we still sometimes need to remove defaults in alter-column.
+        table_name = self.quote_name(params['table_name'])
+        drop_default = self.drop_column_default_sql(table_name, name)
+        if drop_default:
+            sqls.append((drop_default, []))
+            
+    def _value_to_unquoted_literal(self, field, value):
+        # Start with the field's own translation
+        conn = self._get_connection()
+        value = field.get_db_prep_save(value, connection=conn)
+        # This is still a Python object -- nobody expects to need a literal.
+        if isinstance(value, string_types):
+            return smart_text(value)
+        elif isinstance(value, (date,time,datetime)):
+            return value.isoformat()
+        else:
+            #TODO: Anybody else needs special translations?
+            return str(value) 
+    def _default_value_workaround(self, value):
+        if isinstance(value, (date,time,datetime)):
+            return value.isoformat()
+        else:
+            return super(DatabaseOperations, self)._default_value_workaround(value)
+        
+    def _quote_string(self, s):
+        return "'" + s.replace("'","''") + "'"
+    
+
+    def drop_column_default_sql(self, table_name, name, q_name=None):
+        "MSSQL specific drop default, which is a pain"
+
+        sql = """
+        SELECT object_name(cdefault)
+        FROM syscolumns
+        WHERE id = object_id('%s')
+        AND name = '%s'
+        """
+        cons = self.execute(sql % (table_name, name), [])
+        if cons and cons[0] and cons[0][0]:
+            return "DROP CONSTRAINT %s" % cons[0][0]
+        return None
+
+    def _fix_field_definition(self, field):
+        if isinstance(field, (fields.BooleanField, fields.NullBooleanField)):
+            if field.default == True:
+                field.default = 1
+            if field.default == False:
+                field.default = 0
+
+    # This is copied from South's generic add_column, with two modifications:
+    # 1) The sql-server-specific call to _fix_field_definition
+    # 2) Removing a default, when needed, by calling drop_default and not the more general alter_column
+    @invalidate_table_constraints
+    def add_column(self, table_name, name, field, keep_default=False):
+        """
+        Adds the column 'name' to the table 'table_name'.
+        Uses the 'field' paramater, a django.db.models.fields.Field instance,
+        to generate the necessary sql
+
+        @param table_name: The name of the table to add the column to
+        @param name: The name of the column to add
+        @param field: The field to use
+        """
+        self._fix_field_definition(field)
+        sql = self.column_sql(table_name, name, field)
+        if sql:
+            params = (
+                self.quote_name(table_name),
+                sql,
+            )
+            sql = self.add_column_string % params
+            self.execute(sql)
+
+            # Now, drop the default if we need to
+            if not keep_default and field.default is not None:
+                field.default = fields.NOT_PROVIDED
+                #self.alter_column(table_name, name, field, explicit_name=False, ignore_constraints=True)
+                self.drop_default(table_name, name, field)
+
+    @invalidate_table_constraints
+    def drop_default(self, table_name, name, field):
+        fragment = self.drop_column_default_sql(table_name, name)
+        if fragment:
+            table_name = self.quote_name(table_name)
+            sql = " ".join(["ALTER TABLE", table_name, fragment])
+            self.execute(sql)        
+
+
+    @invalidate_table_constraints
+    def create_table(self, table_name, field_defs):
+        # Tweak stuff as needed
+        for _, f in field_defs:
+            self._fix_field_definition(f)
+
+        # Run
+        super(DatabaseOperations, self).create_table(table_name, field_defs)
+
+    def _find_referencing_fks(self, table_name):
+        "MSSQL does not support cascading FKs when dropping tables, we need to implement."
+
+        # FK -- Foreign Keys
+        # UCTU -- Unique Constraints Table Usage
+        # FKTU -- Foreign Key Table Usage
+        # (last two are both really CONSTRAINT_TABLE_USAGE, different join conditions)
+        sql = """
+        SELECT FKTU.TABLE_SCHEMA as REFING_TABLE_SCHEMA,
+               FKTU.TABLE_NAME as REFING_TABLE_NAME,
+               FK.[CONSTRAINT_NAME] as FK_NAME
+        FROM [INFORMATION_SCHEMA].[REFERENTIAL_CONSTRAINTS] FK
+        JOIN [INFORMATION_SCHEMA].[CONSTRAINT_TABLE_USAGE] UCTU
+          ON FK.UNIQUE_CONSTRAINT_CATALOG = UCTU.CONSTRAINT_CATALOG and
+             FK.UNIQUE_CONSTRAINT_NAME = UCTU.CONSTRAINT_NAME and
+             FK.UNIQUE_CONSTRAINT_SCHEMA = UCTU.CONSTRAINT_SCHEMA
+        JOIN [INFORMATION_SCHEMA].[CONSTRAINT_TABLE_USAGE] FKTU
+          ON FK.CONSTRAINT_CATALOG = FKTU.CONSTRAINT_CATALOG and
+             FK.CONSTRAINT_NAME = FKTU.CONSTRAINT_NAME and
+             FK.CONSTRAINT_SCHEMA = FKTU.CONSTRAINT_SCHEMA
+        WHERE FK.CONSTRAINT_CATALOG = %s
+          AND UCTU.TABLE_SCHEMA = %s -- REFD_TABLE_SCHEMA
+          AND UCTU.TABLE_NAME = %s -- REFD_TABLE_NAME
+        """
+        db_name = self._get_setting('name')
+        schema_name = self._get_schema_name()
+        return self.execute(sql, [db_name, schema_name, table_name])
+                
+    @invalidate_table_constraints
+    def delete_table(self, table_name, cascade=True):
+        """
+        Deletes the table 'table_name'.
+        """
+        if cascade:
+            refing = self._find_referencing_fks(table_name)
+            for schmea, table, constraint in refing:
+                table = ".".join(map (self.quote_name, [schmea, table]))
+                params = dict(table_name = table,
+                              constraint_name = self.quote_name(constraint))
+                sql = self.drop_constraint_string % params
+                self.execute(sql, [])
+            cascade = False
+        super(DatabaseOperations, self).delete_table(table_name, cascade)
+            
+    @copy_column_constraints
+    @delete_column_constraints
+    def rename_column(self, table_name, old, new):
+        """
+        Renames the column of 'table_name' from 'old' to 'new'.
+        WARNING - This isn't transactional on MSSQL!
+        """
+        if old == new:
+            # No Operation
+            return
+        # Examples on the MS site show the table name not being quoted...
+        params = (table_name, self.quote_name(old), self.quote_name(new))
+        self.execute("EXEC sp_rename '%s.%s', %s, 'COLUMN'" % params)
+
+    @invalidate_table_constraints
+    def rename_table(self, old_table_name, table_name):
+        """
+        Renames the table 'old_table_name' to 'table_name'.
+        WARNING - This isn't transactional on MSSQL!
+        """
+        if old_table_name == table_name:
+            # No Operation
+            return
+        params = (self.quote_name(old_table_name), self.quote_name(table_name))
+        self.execute('EXEC sp_rename %s, %s' % params)
+
+    def _db_type_for_alter_column(self, field): 
+        return self._db_positive_type_for_alter_column(DatabaseOperations, field)
+
+    def _alter_add_column_mods(self, field, name, params, sqls):
+        return self._alter_add_positive_check(DatabaseOperations, field, name, params, sqls)
+
+    @invalidate_table_constraints
+    def delete_foreign_key(self, table_name, column):
+        super(DatabaseOperations, self).delete_foreign_key(table_name, column)
+        # A FK also implies a non-unique index
+        find_index_sql = """
+            SELECT i.name -- s.name, t.name,  c.name
+            FROM sys.tables t
+            INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
+            INNER JOIN sys.indexes i ON i.object_id = t.object_id
+            INNER JOIN sys.index_columns ic ON ic.object_id = t.object_id
+                                            AND ic.index_id = i.index_id
+            INNER JOIN sys.columns c ON c.object_id = t.object_id 
+                                     AND ic.column_id = c.column_id
+            WHERE i.is_unique=0 AND i.is_primary_key=0 AND i.is_unique_constraint=0
+              AND s.name = %s
+              AND t.name = %s
+              AND c.name = %s
+            """
+        schema = self._get_schema_name()
+        indexes = self.execute(find_index_sql, [schema, table_name, column])
+        qn = self.quote_name
+        for index in (i[0] for i in indexes if i[0]): # "if i[0]" added because an empty name may return
+            self.execute("DROP INDEX %s on %s.%s" % (qn(index), qn(schema), qn(table_name) ))
+            

+ 265 - 0
desktop/core/ext-py/South-0.8.2/south/db/sqlite3.py

@@ -0,0 +1,265 @@
+from south.db import generic
+
+    
+class DatabaseOperations(generic.DatabaseOperations):
+
+    """
+    SQLite3 implementation of database operations.
+    """
+    
+    backend_name = "sqlite3"
+
+    # SQLite ignores several constraints. I wish I could.
+    supports_foreign_keys = False
+    has_check_constraints = False
+    has_booleans = False
+
+    def add_column(self, table_name, name, field, *args, **kwds):
+        """
+        Adds a column.
+        """
+        # If it's not nullable, and has no default, raise an error (SQLite is picky)
+        if (not field.null and
+           (not field.has_default() or field.get_default() is None) and
+           not field.empty_strings_allowed):
+            raise ValueError("You cannot add a null=False column without a default value.")
+        # Initialise the field.
+        field.set_attributes_from_name(name)
+        # We add columns by remaking the table; even though SQLite supports
+        # adding columns, it doesn't support adding PRIMARY KEY or UNIQUE cols.
+        # We define fields with no default; a default will be used, though, to fill up the remade table
+        field_default = None
+        if not getattr(field, '_suppress_default', False):
+            default = field.get_default()
+            if default is not None:
+                field_default = "'%s'" % field.get_db_prep_save(default, connection=self._get_connection())
+        field._suppress_default = True
+        self._remake_table(table_name, added={
+            field.column: (self._column_sql_for_create(table_name, name, field, False), field_default)
+        })
+
+    def _get_full_table_description(self, connection, cursor, table_name):
+        cursor.execute('PRAGMA table_info(%s)' % connection.ops.quote_name(table_name))
+        # cid, name, type, notnull, dflt_value, pk
+        return [{'name': field[1],
+                 'type': field[2],
+                 'null_ok': not field[3],
+                 'dflt_value': field[4],
+                 'pk': field[5]     # undocumented
+                 } for field in cursor.fetchall()]
+
+    @generic.invalidate_table_constraints
+    def _remake_table(self, table_name, added={}, renames={}, deleted=[], altered={}, primary_key_override=None, uniques_deleted=[]):
+        """
+        Given a table and three sets of changes (renames, deletes, alters),
+        recreates it with the modified schema.
+        """
+        # Dry runs get skipped completely
+        if self.dry_run:
+            return
+        # Temporary table's name
+        temp_name = "_south_new_" + table_name
+        # Work out the (possibly new) definitions of each column
+        definitions = {}
+        cursor = self._get_connection().cursor()
+        # Get the index descriptions
+        indexes = self._get_connection().introspection.get_indexes(cursor, table_name)
+        multi_indexes = self._get_multi_indexes(table_name)
+        # Work out new column defs.
+        for column_info in self._get_full_table_description(self._get_connection(), cursor, table_name):
+            name = column_info['name']
+            if name in deleted:
+                continue
+            # Get the type, ignoring PRIMARY KEY (we need to be consistent)
+            type = column_info['type'].replace("PRIMARY KEY", "")
+            # Add on primary key, not null or unique if needed.
+            if (primary_key_override and primary_key_override == name) or \
+               (not primary_key_override and name in indexes and
+                indexes[name]['primary_key']):
+                type += " PRIMARY KEY"
+            elif not column_info['null_ok']:
+                type += " NOT NULL"
+            if (name in indexes and indexes[name]['unique'] and
+                name not in uniques_deleted):
+                type += " UNIQUE"
+            if column_info['dflt_value'] is not None:
+                type += " DEFAULT " + column_info['dflt_value']
+            # Deal with a rename
+            if name in renames:
+                name = renames[name]
+            # Add to the defs
+            definitions[name] = type
+        # Add on altered columns
+        for name, type in altered.items():
+            if (primary_key_override and primary_key_override == name) or \
+               (not primary_key_override and name in indexes and
+                indexes[name]['primary_key']):
+                type += " PRIMARY KEY"
+            if (name in indexes and indexes[name]['unique'] and
+                name not in uniques_deleted):
+                type += " UNIQUE"
+            definitions[name] = type
+        # Add on the new columns
+        for name, (type,_) in added.items():
+            if (primary_key_override and primary_key_override == name):
+                type += " PRIMARY KEY"
+            definitions[name] = type
+        # Alright, Make the table
+        self.execute("CREATE TABLE %s (%s)" % (
+            self.quote_name(temp_name),
+            ", ".join(["%s %s" % (self.quote_name(cname), ctype) for cname, ctype in definitions.items()]),
+        ))
+        # Copy over the data
+        self._copy_data(table_name, temp_name, renames, added)
+        # Delete the old table, move our new one over it
+        self.delete_table(table_name)
+        self.rename_table(temp_name, table_name)
+        # Recreate multi-valued indexes
+        # We can't do that before since it's impossible to rename indexes
+        # and index name scope is global
+        self._make_multi_indexes(table_name, multi_indexes, renames=renames, deleted=deleted, uniques_deleted=uniques_deleted)
+    
+    def _copy_data(self, src, dst, field_renames={}, added={}):
+        "Used to copy data into a new table"
+        # Make a list of all the fields to select
+        cursor = self._get_connection().cursor()
+        src_fields = [column_info[0] for column_info in self._get_connection().introspection.get_table_description(cursor, src)]
+        dst_fields = [column_info[0] for column_info in self._get_connection().introspection.get_table_description(cursor, dst)]
+        src_fields_new = []
+        dst_fields_new = []
+        for field in src_fields:
+            if field in field_renames:
+                dst_fields_new.append(self.quote_name(field_renames[field]))
+            elif field in dst_fields:
+                dst_fields_new.append(self.quote_name(field))
+            else:
+                continue
+            src_fields_new.append(self.quote_name(field))
+        for field, (_,default) in added.items():
+            if default is not None:
+                field = self.quote_name(field)
+                src_fields_new.append("%s as %s" % (default, field))
+                dst_fields_new.append(field)
+        # Copy over the data
+        self.execute("INSERT INTO %s (%s) SELECT %s FROM %s;" % (
+            self.quote_name(dst),
+            ', '.join(dst_fields_new),
+            ', '.join(src_fields_new),
+            self.quote_name(src),
+        ))
+
+    def _create_unique(self, table_name, columns):
+        self.execute("CREATE UNIQUE INDEX %s ON %s(%s);" % (
+            self.quote_name('%s_%s' % (table_name, '__'.join(columns))),
+            self.quote_name(table_name),
+            ', '.join(self.quote_name(c) for c in columns),
+        ))
+
+    def _get_multi_indexes(self, table_name):
+        indexes = []
+        cursor = self._get_connection().cursor()
+        cursor.execute('PRAGMA index_list(%s)' % self.quote_name(table_name))
+        # seq, name, unique
+        for index, unique in [(field[1], field[2]) for field in cursor.fetchall()]:
+            if not unique:
+                continue
+            cursor.execute('PRAGMA index_info(%s)' % self.quote_name(index))
+            info = cursor.fetchall()
+            if len(info) == 1:
+                continue
+            columns = []
+            for field in info:
+                columns.append(field[2])
+            indexes.append(columns)
+        return indexes
+
+    def _make_multi_indexes(self, table_name, indexes, deleted=[], renames={}, uniques_deleted=[]):
+        for index in indexes:
+            columns = []
+
+            for name in index:
+                # Handle deletion
+                if name in deleted:
+                    columns = []
+                    break
+
+                # Handle renames
+                if name in renames:
+                    name = renames[name]
+                columns.append(name)
+
+            if columns and set(columns) != set(uniques_deleted):
+                self._create_unique(table_name, columns)
+    
+    def _column_sql_for_create(self, table_name, name, field, explicit_name=True):
+        "Given a field and its name, returns the full type for the CREATE TABLE (without unique/pk)"
+        field.set_attributes_from_name(name)
+        if not explicit_name:
+            name = field.db_column
+        else:
+            field.column = name
+        sql = self.column_sql(table_name, name, field, with_name=False, field_prepared=True)
+        # Remove keywords we don't want (this should be type only, not constraint)
+        if sql:
+            sql = sql.replace("PRIMARY KEY", "")
+        return sql
+    
+    def alter_column(self, table_name, name, field, explicit_name=True, ignore_constraints=False):
+        """
+        Changes a column's SQL definition.
+
+        Note that this sqlite3 implementation ignores the ignore_constraints argument.
+        The argument is accepted for API compatibility with the generic
+        DatabaseOperations.alter_column() method.
+        """
+        # Change nulls to default if needed
+        if not field.null and field.has_default():
+            params = {
+                "column": self.quote_name(name),
+                "table_name": self.quote_name(table_name)
+            }            
+            self._update_nulls_to_default(params, field)
+        # Remake the table correctly
+        field._suppress_default = True
+        self._remake_table(table_name, altered={
+            name: self._column_sql_for_create(table_name, name, field, explicit_name),
+        })
+
+    def delete_column(self, table_name, column_name):
+        """
+        Deletes a column.
+        """
+        self._remake_table(table_name, deleted=[column_name])
+    
+    def rename_column(self, table_name, old, new):
+        """
+        Renames a column from one name to another.
+        """
+        self._remake_table(table_name, renames={old: new})
+    
+    def create_unique(self, table_name, columns):
+        """
+        Create an unique index on columns
+        """
+        self._create_unique(table_name, columns)
+    
+    def delete_unique(self, table_name, columns):
+        """
+        Delete an unique index
+        """
+        self._remake_table(table_name, uniques_deleted=columns)
+    
+    def create_primary_key(self, table_name, columns):
+        if not isinstance(columns, (list, tuple)):
+            columns = [columns]
+        assert len(columns) == 1, "SQLite backend does not support multi-column primary keys"
+        self._remake_table(table_name, primary_key_override=columns[0])
+
+    # Not implemented this yet.
+    def delete_primary_key(self, table_name):
+        # By passing True in, we make sure we wipe all existing PKs.
+        self._remake_table(table_name, primary_key_override=True)
+    
+    # No cascades on deletes
+    def delete_table(self, table_name, cascade=True):
+        generic.DatabaseOperations.delete_table(self, table_name, False)

+ 23 - 5
desktop/core/ext-py/south/south/exceptions.py → desktop/core/ext-py/South-0.8.2/south/exceptions.py

@@ -1,8 +1,12 @@
-from traceback import format_exception
+from __future__ import print_function
+
+from traceback import format_exception, format_exc
 
 class SouthError(RuntimeError):
     pass
 
+class SouthWarning(RuntimeWarning):
+    pass
 
 class BrokenMigration(SouthError):
     def __init__(self, migration, exc_info):
@@ -10,6 +14,8 @@ class BrokenMigration(SouthError):
         self.exc_info = exc_info
         if self.exc_info:
             self.traceback = ''.join(format_exception(*self.exc_info))
+        else:
+            self.traceback = format_exc()
 
     def __str__(self):
         return ("While loading migration '%(migration)s':\n"
@@ -45,7 +51,7 @@ class MultiplePrefixMatches(SouthError):
         self.matches = matches
 
     def __str__(self):
-        self.matches_list = "\n    ".join([unicode(m) for m in self.matches])
+        self.matches_list = "\n    ".join([str(m) for m in self.matches])
         return ("Prefix '%(prefix)s' matches more than one migration:\n"
                 "    %(matches_list)s") % self.__dict__
 
@@ -55,7 +61,7 @@ class GhostMigrations(SouthError):
         self.ghosts = ghosts
 
     def __str__(self):
-        self.ghosts_list = "\n    ".join([unicode(m) for m in self.ghosts])
+        self.ghosts_list = "\n    ".join([str(m) for m in self.ghosts])
         return ("\n\n ! These migrations are in the database but not on disk:\n"
                 "    %(ghosts_list)s\n"
                 " ! I'm not trusting myself; either fix this yourself by fiddling\n"
@@ -68,7 +74,7 @@ class CircularDependency(SouthError):
         self.trace = trace
 
     def __str__(self):
-        trace = " -> ".join([unicode(s) for s in self.trace])
+        trace = " -> ".join([str(s) for s in self.trace])
         return ("Found circular dependency:\n"
                 "    %s") % trace
 
@@ -98,7 +104,7 @@ class DependsOnUnknownMigration(SouthError):
         self.depends_on = depends_on
 
     def __str__(self):
-        print "Migration '%(migration)s' depends on unknown migration '%(depends_on)s'." % self.__dict__
+        print("Migration '%(migration)s' depends on unknown migration '%(depends_on)s'." % self.__dict__)
 
 
 class DependsOnUnmigratedApplication(SouthError):
@@ -135,3 +141,15 @@ class UnfreezeMeLater(Exception):
 class ImpossibleORMUnfreeze(SouthError):
     """Raised if the ORM can't manage to unfreeze all the models in a linear fashion."""
     pass
+
+class ConstraintDropped(SouthWarning):
+    def __init__(self, constraint, table, column=None):
+        self.table = table
+        if column:
+            self.column = ".%s" % column
+        else:
+            self.column = ""
+        self.constraint = constraint
+    
+    def __str__(self):
+        return "Constraint %(constraint)s was dropped from %(table)s%(column)s -- was this intended?" % self.__dict__  

+ 0 - 0
desktop/core/ext-py/south/south/hacks/__init__.py → desktop/core/ext-py/South-0.8.2/south/hacks/__init__.py


+ 110 - 0
desktop/core/ext-py/South-0.8.2/south/hacks/django_1_0.py

@@ -0,0 +1,110 @@
+"""
+Hacks for the Django 1.0/1.0.2 releases.
+"""
+
+import django
+from django.conf import settings
+from django.db.backends.creation import BaseDatabaseCreation
+from django.db.models.loading import cache
+from django.core import management
+from django.core.management.commands.flush import Command as FlushCommand
+from django.utils.datastructures import SortedDict
+
+from south.utils.py3 import string_types
+
+class SkipFlushCommand(FlushCommand):
+    def handle_noargs(self, **options):
+        # no-op to avoid calling flush
+        return
+
+class Hacks:
+    
+    def set_installed_apps(self, apps):
+        """
+        Sets Django's INSTALLED_APPS setting to be effectively the list passed in.
+        """
+        
+        # Make sure it's a list.
+        apps = list(apps)
+        
+        # Make sure it contains strings
+        if apps:
+            assert isinstance(apps[0], string_types), "The argument to set_installed_apps must be a list of strings."
+        
+        # Monkeypatch in!
+        settings.INSTALLED_APPS, settings.OLD_INSTALLED_APPS = (
+            apps,
+            settings.INSTALLED_APPS,
+        )
+        self._redo_app_cache()
+    
+    
+    def reset_installed_apps(self):
+        """
+        Undoes the effect of set_installed_apps.
+        """
+        settings.INSTALLED_APPS = settings.OLD_INSTALLED_APPS
+        self._redo_app_cache()
+    
+    
+    def _redo_app_cache(self):
+        """
+        Used to repopulate AppCache after fiddling with INSTALLED_APPS.
+        """
+        cache.loaded = False
+        cache.handled = set() if django.VERSION >= (1, 6) else {}
+        cache.postponed = []
+        cache.app_store = SortedDict()
+        cache.app_models = SortedDict()
+        cache.app_errors = {}
+        cache._populate()
+    
+    
+    def clear_app_cache(self):
+        """
+        Clears the contents of AppCache to a blank state, so new models
+        from the ORM can be added.
+        """
+        self.old_app_models, cache.app_models = cache.app_models, {}
+    
+    
+    def unclear_app_cache(self):
+        """
+        Reversed the effects of clear_app_cache.
+        """
+        cache.app_models = self.old_app_models
+        cache._get_models_cache = {}
+    
+    
+    def repopulate_app_cache(self):
+        """
+        Rebuilds AppCache with the real model definitions.
+        """
+        cache._populate()
+
+    def store_app_cache_state(self):
+        self.stored_app_cache_state = dict(**cache.__dict__)
+
+    def restore_app_cache_state(self):
+        cache.__dict__ = self.stored_app_cache_state
+
+    def patch_flush_during_test_db_creation(self):
+        """
+        Patches BaseDatabaseCreation.create_test_db to not flush database
+        """
+
+        def patch(f):
+            def wrapper(*args, **kwargs):
+                # hold onto the original and replace flush command with a no-op
+                original_flush_command = management._commands['flush']
+                try:
+                    management._commands['flush'] = SkipFlushCommand()
+                    # run create_test_db
+                    return f(*args, **kwargs)
+                finally:
+                    # unpatch flush back to the original
+                    management._commands['flush'] = original_flush_command
+            return wrapper
+            
+        BaseDatabaseCreation.create_test_db = patch(BaseDatabaseCreation.create_test_db)
+

+ 4 - 0
desktop/core/ext-py/south/south/introspection_plugins/__init__.py → desktop/core/ext-py/South-0.8.2/south/introspection_plugins/__init__.py

@@ -3,5 +3,9 @@
 
 # These imports trigger the lower-down files
 import south.introspection_plugins.geodjango
+import south.introspection_plugins.django_audit_log
 import south.introspection_plugins.django_tagging
 import south.introspection_plugins.django_taggit
+import south.introspection_plugins.django_objectpermissions
+import south.introspection_plugins.annoying_autoonetoone
+

+ 11 - 0
desktop/core/ext-py/South-0.8.2/south/introspection_plugins/annoying_autoonetoone.py

@@ -0,0 +1,11 @@
+from django.conf import settings
+from south.modelsinspector import add_introspection_rules
+
+if 'annoying' in settings.INSTALLED_APPS:
+    try:
+        from annoying.fields import AutoOneToOneField
+    except ImportError:
+        pass
+    else:
+        #django-annoying's AutoOneToOneField is essentially a OneToOneField.
+        add_introspection_rules([], ["^annoying\.fields\.AutoOneToOneField"])

+ 30 - 0
desktop/core/ext-py/South-0.8.2/south/introspection_plugins/django_audit_log.py

@@ -0,0 +1,30 @@
+"""                                                 
+South introspection rules for django-audit-log
+"""                                                 
+                                                    
+from django.contrib.auth.models import User
+from django.conf import settings
+from south.modelsinspector import add_introspection_rules
+
+if "audit_log" in settings.INSTALLED_APPS:
+    try:                                                
+        # Try and import the field so we can see if audit_log is available
+        from audit_log.models import fields
+
+        # Make sure the `to` and `null` parameters will be ignored
+        rules = [(                                     
+            (fields.LastUserField,),                   
+            [],                                        
+            {                                          
+                'to': ['rel.to', {'default': User}],   
+                'null': ['null', {'default': True}],   
+            },                                         
+        )]                                             
+
+        # Add the rules for the `LastUserField`
+        add_introspection_rules(                           
+            rules,                                         
+            ['^audit_log\.models\.fields\.LastUserField'], 
+        )                                                  
+    except ImportError:                                 
+        pass

+ 16 - 0
desktop/core/ext-py/South-0.8.2/south/introspection_plugins/django_objectpermissions.py

@@ -0,0 +1,16 @@
+"""
+South introspection rules for django-objectpermissions
+"""
+
+from django.conf import settings
+from south.modelsinspector import add_ignored_fields
+
+if 'objectpermissions' in settings.INSTALLED_APPS:
+    try:
+        from objectpermissions.models import UserPermissionRelation, GroupPermissionRelation
+    except ImportError:
+        pass
+    else:
+        add_ignored_fields(["^objectpermissions\.models\.UserPermissionRelation",
+                            "^objectpermissions\.models\.GroupPermissionRelation"])
+

+ 24 - 0
desktop/core/ext-py/South-0.8.2/south/introspection_plugins/django_tagging.py

@@ -0,0 +1,24 @@
+from south.modelsinspector import add_introspection_rules
+from django.conf import settings
+
+if "tagging" in settings.INSTALLED_APPS:
+    try:
+        from tagging.fields import TagField
+    except ImportError:
+        pass
+    else:
+        rules = [
+            (
+                (TagField, ),
+                [],
+                {
+                    "blank": ["blank", {"default": True}],
+                    "max_length": ["max_length", {"default": 255}],
+                },
+            ),
+        ]
+        add_introspection_rules(rules, ["^tagging\.fields",])
+
+if "tagging_autocomplete" in settings.INSTALLED_APPS:
+    add_introspection_rules([], ["^tagging_autocomplete\.models\.TagAutocompleteField"])
+

+ 14 - 0
desktop/core/ext-py/South-0.8.2/south/introspection_plugins/django_taggit.py

@@ -0,0 +1,14 @@
+"""
+South introspection rules for django-taggit
+"""
+
+from django.conf import settings
+from south.modelsinspector import add_ignored_fields
+
+if 'taggit' in settings.INSTALLED_APPS:
+    try:
+        from taggit.managers import TaggableManager
+    except ImportError:
+        pass
+    else:
+        add_ignored_fields(["^taggit\.managers"])

+ 21 - 0
desktop/core/ext-py/South-0.8.2/south/introspection_plugins/django_timezones.py

@@ -0,0 +1,21 @@
+from south.modelsinspector import add_introspection_rules
+from django.conf import settings
+
+if "timezones" in settings.INSTALLED_APPS:
+    try:
+        from timezones.fields import TimeZoneField
+    except ImportError:
+        pass
+    else:
+        rules = [
+            (
+                (TimeZoneField, ),
+                [],
+                {
+                    "blank": ["blank", {"default": True}],
+                    "max_length": ["max_length", {"default": 100}],
+                },
+            ),
+        ]
+        add_introspection_rules(rules, ["^timezones\.fields",])
+

+ 1 - 0
desktop/core/ext-py/south/south/introspection_plugins/geodjango.py → desktop/core/ext-py/South-0.8.2/south/introspection_plugins/geodjango.py

@@ -24,6 +24,7 @@ if has_gis:
                     "srid": ["srid", {"default": 4326}],
                     "spatial_index": ["spatial_index", {"default": True}],
                     "dim": ["dim", {"default": 2}],
+                    "geography": ["geography", {"default": False}],
                 },
             ),
         ]

+ 10 - 6
desktop/core/ext-py/south/south/logger.py → desktop/core/ext-py/South-0.8.2/south/logger.py

@@ -7,10 +7,6 @@ class NullHandler(logging.Handler):
     def emit(self, record):
         pass
 
-_logger = logging.getLogger("south")
-_logger.addHandler(NullHandler())
-_logger.setLevel(logging.DEBUG)
-
 def get_logger():
     "Attach a file handler to the logger if there isn't one already."
     debug_on = getattr(settings, "SOUTH_LOGGING_ON", False)
@@ -22,7 +18,7 @@ def get_logger():
                 _logger.addHandler(logging.FileHandler(logging_file))
                 _logger.setLevel(logging.DEBUG)
         else:
-            raise IOError, "SOUTH_LOGGING_ON is True. You also need a SOUTH_LOGGING_FILE setting."
+            raise IOError("SOUTH_LOGGING_ON is True. You also need a SOUTH_LOGGING_FILE setting.")
     
     return _logger
 
@@ -31,4 +27,12 @@ def close_logger():
     for handler in _logger.handlers:
         _logger.removeHandler(handler)
         if isinstance(handler, logging.FileHandler):
-            handler.close()
+            handler.close()
+
+def init_logger():
+    "Initialize the south logger"
+    logger = logging.getLogger("south")
+    logger.addHandler(NullHandler())
+    return logger
+
+_logger = init_logger()

+ 0 - 0
desktop/core/ext-py/south/south/management/__init__.py → desktop/core/ext-py/South-0.8.2/south/management/__init__.py


+ 40 - 0
desktop/core/ext-py/South-0.8.2/south/management/commands/__init__.py

@@ -0,0 +1,40 @@
+
+# Common framework for syncdb actions
+
+import copy
+
+from django.core import management
+from django.conf import settings
+
+# Make sure the template loader cache is fixed _now_ (#448)
+import django.template.loaders.app_directories
+
+from south.hacks import hacks
+from south.management.commands.syncdb import Command as SyncCommand
+
+class MigrateAndSyncCommand(SyncCommand):
+    """Used for situations where "syncdb" is called by test frameworks."""
+
+    option_list = copy.deepcopy(SyncCommand.option_list)
+
+    for opt in option_list:
+        if "--migrate" == opt.get_opt_string():
+            opt.default = True
+            break
+
+def patch_for_test_db_setup():
+    # Load the commands cache
+    management.get_commands()
+    # Repoint to the correct version of syncdb
+    if hasattr(settings, "SOUTH_TESTS_MIGRATE") and not settings.SOUTH_TESTS_MIGRATE:
+        # point at the core syncdb command when creating tests
+        # tests should always be up to date with the most recent model structure
+        management._commands['syncdb'] = 'django.core'
+    else:
+        management._commands['syncdb'] = MigrateAndSyncCommand()
+        # Avoid flushing data migrations.
+        # http://code.djangoproject.com/ticket/14661 introduced change that flushed custom
+        # sql during the test database creation (thus flushing the data migrations).
+        # we patch flush to be no-op during create_test_db, but still allow flushing
+        # after each test for non-transactional backends.
+        hacks.patch_flush_during_test_db_creation()

+ 28 - 12
desktop/core/ext-py/south/south/management/commands/convert_to_south.py → desktop/core/ext-py/South-0.8.2/south/management/commands/convert_to_south.py

@@ -2,6 +2,8 @@
 Quick conversion command module.
 """
 
+from __future__ import print_function
+
 from optparse import make_option
 import sys
 
@@ -22,9 +24,15 @@ class Command(BaseCommand):
     if '--verbosity' not in [opt.get_opt_string() for opt in BaseCommand.option_list]:
         option_list += (
             make_option('--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'),
+                type='choice', choices=['0', '1', '2'],
+                help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
         )
+    option_list += (
+        make_option('--delete-ghost-migrations', action='store_true', dest='delete_ghosts', default=False,
+            help="Tells South to delete any 'ghost' migrations (ones in the database but not on disk)."),
+        make_option('--ignore-ghost-migrations', action='store_true', dest='ignore_ghosts', default=False,
+            help="Tells South to ignore any 'ghost' migrations (ones in the database but not on disk) and continue to apply new migrations."), 
+    )
 
     help = "Quickly converts the named application to use South if it is currently using syncdb."
 
@@ -32,7 +40,7 @@ class Command(BaseCommand):
         
         # Make sure we have an app
         if not app:
-            print "Please specify an app to convert."
+            print("Please specify an app to convert.")
             return
         
         # See if the app exists
@@ -40,14 +48,14 @@ class Command(BaseCommand):
         try:
             app_module = models.get_app(app)
         except ImproperlyConfigured:
-            print "There is no enabled application matching '%s'." % app
+            print("There is no enabled application matching '%s'." % app)
             return
         
         # Try to get its list of models
         model_list = models.get_models(app_module)
         if not model_list:
-            print "This application has no models; this command is for applications that already have models syncdb'd."
-            print "Make some models, and then use ./manage.py startmigration %s --initial instead." % app
+            print("This application has no models; this command is for applications that already have models syncdb'd.")
+            print("Make some models, and then use ./manage.py schemamigration %s --initial instead." % app)
             return
         
         # Ask South if it thinks it's already got migrations
@@ -56,7 +64,7 @@ class Command(BaseCommand):
         except NoMigrations:
             pass
         else:
-            print "This application is already managed by South."
+            print("This application is already managed by South.")
             return
         
         # Finally! It seems we've got a candidate, so do the two-command trick
@@ -71,9 +79,17 @@ class Command(BaseCommand):
         Migrations._clear_cache()
         
         # Now, migrate
-        management.call_command("migrate", app, "0001", fake=True, verbosity=verbosity)
+        management.call_command(
+            "migrate",
+            app,
+            "0001",
+            fake=True,
+            verbosity=verbosity,
+            ignore_ghosts=options.get("ignore_ghosts", False),
+            delete_ghosts=options.get("delete_ghosts", False),
+        )
         
-        print 
-        print "App '%s' converted. Note that South assumed the application's models matched the database" % app
-        print "(i.e. you haven't changed it since last syncdb); if you have, you should delete the %s/migrations" % app
-        print "directory, revert models.py so it matches the database, and try again."
+        print() 
+        print("App '%s' converted. Note that South assumed the application's models matched the database" % app)
+        print("(i.e. you haven't changed it since last syncdb); if you have, you should delete the %s/migrations" % app)
+        print("directory, revert models.py so it matches the database, and try again.")

+ 31 - 19
desktop/core/ext-py/south/south/management/commands/datamigration.py → desktop/core/ext-py/South-0.8.2/south/management/commands/datamigration.py

@@ -2,6 +2,8 @@
 Data migration creation command
 """
 
+from __future__ import print_function
+
 import sys
 import os
 import re
@@ -32,6 +34,8 @@ class Command(BaseCommand):
     usage_str = "Usage: ./manage.py datamigration appname migrationname [--stdout] [--freeze appname]"
     
     def handle(self, app=None, name="", freeze_list=None, stdout=False, verbosity=1, **options):
+
+        verbosity = int(verbosity)
         
         # Any supposed lists that are None become empty lists
         freeze_list = freeze_list or []
@@ -44,19 +48,21 @@ class Command(BaseCommand):
         if re.search('[^_\w]', name) and name != "-":
             self.error("Migration names should contain only alphanumeric characters and underscores.")
         
-        # if not name, there's an error
+        # If not name, there's an error
         if not name:
-            self.error("You must provide a name for this migration\n" + self.usage_str)
+            self.error("You must provide a name for this migration.\n" + self.usage_str)
         
         if not app:
             self.error("You must provide an app to create a migration for.\n" + self.usage_str)
-        
-        # Get the Migrations for this app (creating the migrations dir if needed)
+
+        # Ensure that verbosity is not a string (Python 3)
         try:
-            migrations = Migrations(app)
-        except NoMigrations:
-            Migrations.create_migrations_directory(app, verbose=verbosity > 0)
-            migrations = Migrations(app)
+            verbosity = int(verbosity)
+        except ValueError:
+            self.error("Verbosity must be an number.\n" + self.usage_str)
+            
+        # Get the Migrations for this app (creating the migrations dir if needed)
+        migrations = Migrations(app, force_creation=True, verbose_creation=verbosity > 0)
         
         # See what filename is next in line. We assume they use numbers.
         new_filename = migrations.next_filename(name)
@@ -65,20 +71,20 @@ class Command(BaseCommand):
         apps_to_freeze = self.calc_frozen_apps(migrations, freeze_list)
         
         # So, what's in this file, then?
-        file_contents = MIGRATION_TEMPLATE % {
+        file_contents = self.get_migration_template() % {
             "frozen_models":  freezer.freeze_apps_to_string(apps_to_freeze),
             "complete_apps": apps_to_freeze and "complete_apps = [%s]" % (", ".join(map(repr, apps_to_freeze))) or ""
         }
         
         # - is a special name which means 'print to stdout'
         if name == "-":
-            print file_contents
+            print(file_contents)
         # Write the migration file if the name isn't -
         else:
             fp = open(os.path.join(migrations.migrations_dir(), new_filename), "w")
             fp.write(file_contents)
             fp.close()
-            print >>sys.stderr, "Created %s." % new_filename
+            print("Created %s." % new_filename, file=sys.stderr)
     
     def calc_frozen_apps(self, migrations, freeze_list):
         """
@@ -102,26 +108,32 @@ class Command(BaseCommand):
         """
         Prints the error, and exits with the given code.
         """
-        print >>sys.stderr, message
+        print(message, file=sys.stderr)
         sys.exit(code)
 
+    def get_migration_template(self):
+        return MIGRATION_TEMPLATE
+
 
-MIGRATION_TEMPLATE = """# encoding: utf-8
+MIGRATION_TEMPLATE = """# -*- coding: utf-8 -*-
 import datetime
 from south.db import db
 from south.v2 import DataMigration
 from django.db import models
 
 class Migration(DataMigration):
-    
+
     def forwards(self, orm):
         "Write your forwards methods here."
-    
-    
+        # Note: Don't use "from appname.models import ModelName". 
+        # Use orm.ModelName to refer to models in this application,
+        # and orm['appname.ModelName'] for models in other applications.
+
     def backwards(self, orm):
         "Write your backwards methods here."
-    
+
     models = %(frozen_models)s
-    
+
     %(complete_apps)s
-"""
+    symmetrical = True
+"""

+ 63 - 0
desktop/core/ext-py/South-0.8.2/south/management/commands/graphmigrations.py

@@ -0,0 +1,63 @@
+"""
+Outputs a graphviz dot file of the dependencies.
+"""
+
+from __future__ import print_function
+
+from optparse import make_option
+import re
+import textwrap
+
+from django.core.management.base import BaseCommand
+from django.core.management.color import no_style
+
+from south.migration import Migrations, all_migrations
+
+class Command(BaseCommand):
+
+    help = "Outputs a GraphViz dot file of all migration dependencies to stdout."
+    
+    def handle(self, **options):
+        
+        # Resolve dependencies
+        Migrations.calculate_dependencies()
+
+        colors = [ 'crimson', 'darkgreen', 'darkgoldenrod', 'navy',
+                'brown', 'darkorange', 'aquamarine' , 'blueviolet' ]
+        color_index = 0
+        wrapper = textwrap.TextWrapper(width=40)
+        
+        print("digraph G {")
+        
+        # Group each app in a subgraph
+        for migrations in all_migrations():
+            print("  subgraph %s {" % migrations.app_label())
+            print("    node [color=%s];" % colors[color_index])
+            for migration in migrations:
+                # Munge the label - text wrap and change _ to spaces
+                label = "%s - %s" % (
+                        migration.app_label(), migration.name())
+                label = re.sub(r"_+", " ", label)
+                label=  "\\n".join(wrapper.wrap(label))
+                print('    "%s.%s" [label="%s"];' % (
+                        migration.app_label(), migration.name(), label))
+            print("  }")
+            color_index = (color_index + 1) % len(colors)
+
+        # For every migration, print its links.
+        for migrations in all_migrations():
+            for migration in migrations:
+                for other in migration.dependencies:
+                    # Added weight tends to keep migrations from the same app
+                    # in vertical alignment
+                    attrs = "[weight=2.0]"
+                    # But the more interesting edges are those between apps
+                    if other.app_label() != migration.app_label():
+                        attrs = "[style=bold]"
+                    print('  "%s.%s" -> "%s.%s" %s;' % (
+                        other.app_label(), other.name(),
+                        migration.app_label(), migration.name(),
+                        attrs
+                    ))
+            
+        print("}");

+ 264 - 0
desktop/core/ext-py/South-0.8.2/south/management/commands/migrate.py

@@ -0,0 +1,264 @@
+"""
+Migrate management command.
+"""
+
+from __future__ import print_function
+
+import os.path, re, sys
+from functools import reduce
+from optparse import make_option
+
+from django.core.management.base import BaseCommand
+from django.conf import settings
+from django.utils.importlib import import_module
+
+from south import migration
+from south.migration import Migrations
+from south.exceptions import NoMigrations
+from south.db import DEFAULT_DB_ALIAS
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list + (
+        make_option('--all', action='store_true', dest='all_apps', default=False,
+            help='Run the specified migration for all apps.'),
+        make_option('--list', action='store_true', dest='show_list', default=False,
+            help='List migrations noting those that have been applied'),
+        make_option('--changes', action='store_true', dest='show_changes', default=False,
+            help='List changes for migrations'),
+        make_option('--skip', action='store_true', dest='skip', default=False,
+            help='Will skip over out-of-order missing migrations'),
+        make_option('--merge', action='store_true', dest='merge', default=False,
+            help='Will run out-of-order missing migrations as they are - no rollbacks.'),
+        make_option('--no-initial-data', action='store_true', dest='no_initial_data', default=False,
+            help='Skips loading initial data if specified.'),
+        make_option('--fake', action='store_true', dest='fake', default=False,
+            help="Pretends to do the migrations, but doesn't actually execute them."),
+        make_option('--db-dry-run', action='store_true', dest='db_dry_run', default=False,
+            help="Doesn't execute the SQL generated by the db methods, and doesn't store a record that the migration(s) occurred. Useful to test migrations before applying them."),
+        make_option('--delete-ghost-migrations', action='store_true', dest='delete_ghosts', default=False,
+            help="Tells South to delete any 'ghost' migrations (ones in the database but not on disk)."),
+        make_option('--ignore-ghost-migrations', action='store_true', dest='ignore_ghosts', default=False,
+            help="Tells South to ignore any 'ghost' migrations (ones in the database but not on disk) and continue to apply new migrations."),
+        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('--database', action='store', dest='database',
+            default=DEFAULT_DB_ALIAS, help='Nominates a database to synchronize. '
+                'Defaults to the "default" database.'),
+    )
+    if '--verbosity' not in [opt.get_opt_string() for opt in BaseCommand.option_list]:
+        option_list += (
+            make_option('--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 = "Runs migrations for all apps."
+    args = "[appname] [migrationname|zero] [--all] [--list] [--skip] [--merge] [--no-initial-data] [--fake] [--db-dry-run] [--database=dbalias]"
+
+    def handle(self, app=None, target=None, skip=False, merge=False, backwards=False, fake=False, db_dry_run=False, show_list=False, show_changes=False, database=DEFAULT_DB_ALIAS, delete_ghosts=False, ignore_ghosts=False, **options):
+        
+        # NOTE: THIS IS DUPLICATED FROM django.core.management.commands.syncdb
+        # This code imports any module named 'management' in INSTALLED_APPS.
+        # The 'management' module is the preferred way of listening to post_syncdb
+        # signals, and since we're sending those out with create_table migrations,
+        # we need apps to behave correctly.
+        for app_name in settings.INSTALLED_APPS:
+            try:
+                import_module('.management', app_name)
+            except ImportError as exc:
+                msg = exc.args[0]
+                if not msg.startswith('No module named') or 'management' not in msg:
+                    raise
+        # END DJANGO DUPE CODE
+        
+        # if all_apps flag is set, shift app over to target
+        if options.get('all_apps', False):
+            target = app
+            app = None
+
+        # Migrate each app
+        if app:
+            try:
+                apps = [Migrations(app)]
+            except NoMigrations:
+                print("The app '%s' does not appear to use migrations." % app)
+                print("./manage.py migrate " + self.args)
+                return
+        else:
+            apps = list(migration.all_migrations())
+        
+        # Do we need to show the list of migrations?
+        if show_list and apps:
+            list_migrations(apps, database, **options)
+            
+        if show_changes and apps:
+            show_migration_changes(apps)
+        
+        if not (show_list or show_changes):
+            
+            for app in apps:
+                result = migration.migrate_app(
+                    app,
+                    target_name = target,
+                    fake = fake,
+                    db_dry_run = db_dry_run,
+                    verbosity = int(options.get('verbosity', 0)),
+                    interactive = options.get('interactive', True),
+                    load_initial_data = not options.get('no_initial_data', False),
+                    merge = merge,
+                    skip = skip,
+                    database = database,
+                    delete_ghosts = delete_ghosts,
+                    ignore_ghosts = ignore_ghosts,
+                )
+                if result is False:
+                    sys.exit(1) # Migration failed, so the command fails.
+
+
+def list_migrations(apps, database = DEFAULT_DB_ALIAS, **options):
+    """
+    Prints a list of all available migrations, and which ones are currently applied.
+    Accepts a list of Migrations instances.
+    """
+    from south.models import MigrationHistory
+    applied_migrations = MigrationHistory.objects.filter(app_name__in=[app.app_label() for app in apps])
+    if database != DEFAULT_DB_ALIAS:
+        applied_migrations = applied_migrations.using(database)
+    applied_migrations_lookup = dict(('%s.%s' % (mi.app_name, mi.migration), mi) for mi in applied_migrations)
+
+    print()
+    for app in apps:
+        print(" " + app.app_label())
+        # Get the migrations object
+        for migration in app:
+            full_name = migration.app_label() + "." + migration.name()
+            if full_name in applied_migrations_lookup:
+                applied_migration = applied_migrations_lookup[full_name]
+                print(format_migration_list_item(migration.name(), applied=applied_migration.applied, **options))
+            else:
+                print(format_migration_list_item(migration.name(), applied=False, **options))
+        print()
+
+def show_migration_changes(apps):
+    """
+    Prints a list of all available migrations, and which ones are currently applied.
+    Accepts a list of Migrations instances.
+    
+    Much simpler, less clear, and much less robust version:
+        grep "ing " migrations/*.py
+    """
+    for app in apps:
+        print(app.app_label())
+        # Get the migrations objects
+        migrations = [migration for migration in app]
+        # we use reduce to compare models in pairs, not to generate a value
+        reduce(diff_migrations, migrations)
+
+def format_migration_list_item(name, applied=True, **options):
+    if applied:
+        if int(options.get('verbosity')) >= 2:
+            return '  (*) %-80s  (applied %s)' % (name, applied)
+        else:
+            return '  (*) %s' % name
+    else:
+        return '  ( ) %s' % name
+                            
+def diff_migrations(migration1, migration2):
+    
+    def model_name(models, model):
+        return models[model].get('Meta', {}).get('object_name', model)
+        
+    def field_name(models, model, field):
+        return '%s.%s' % (model_name(models, model), field)
+        
+    print("  " + migration2.name())
+    
+    models1 = migration1.migration_class().models
+    models2 = migration2.migration_class().models
+
+    # find new models
+    for model in models2.keys():
+        if not model in models1.keys():
+            print('    added model %s' % model_name(models2, model))
+ 
+    # find removed models
+    for model in models1.keys():
+        if not model in models2.keys():
+            print('    removed model %s' % model_name(models1, model))
+            
+    # compare models
+    for model in models1:
+        if model in models2:
+        
+            # find added fields
+            for field in models2[model]:
+                if not field in models1[model]:
+                    print('    added field %s' % field_name(models2, model, field))
+
+            # find removed fields
+            for field in models1[model]:
+                if not field in models2[model]:
+                    print('    removed field %s' % field_name(models1, model, field))
+                
+            # compare fields
+            for field in models1[model]:
+                if field in models2[model]:
+                
+                    name = field_name(models1, model, field)
+                
+                    # compare field attributes
+                    field_value1 = models1[model][field]
+                    field_value2 = models2[model][field]
+                    
+                    # if a field has become a class, or vice versa
+                    if type(field_value1) != type(field_value2):
+                        print('    type of %s changed from %s to %s' % (
+                            name, field_value1, field_value2))
+                    
+                    # if class
+                    elif isinstance(field_value1, dict):
+                        # print '    %s is a class' % name
+                        pass
+                    
+                    # else regular field
+                    else:
+                    
+                        type1, attr_list1, field_attrs1 = models1[model][field]
+                        type2, attr_list2, field_attrs2 = models2[model][field]
+                        
+                        if type1 != type2:
+                            print('    %s type changed from %s to %s' % (
+                                name, type1, type2))
+    
+                        if attr_list1 != []:
+                            print('    %s list %s is not []' % (
+                                name, attr_list1))
+                        if attr_list2 != []:
+                            print('    %s list %s is not []' % (
+                                name, attr_list2))    
+                        if attr_list1 != attr_list2:
+                            print('    %s list changed from %s to %s' % (
+                                name, attr_list1, attr_list2))                
+                                        
+                        # find added field attributes
+                        for attr in field_attrs2:
+                            if not attr in field_attrs1:
+                                print('    added %s attribute %s=%s' % (
+                                    name, attr, field_attrs2[attr]))
+                                
+                        # find removed field attributes
+                        for attr in field_attrs1:
+                            if not attr in field_attrs2:
+                                print('    removed attribute %s(%s=%s)' % (
+                                    name, attr, field_attrs1[attr]))
+                            
+                        # compare field attributes
+                        for attr in field_attrs1:
+                            if attr in field_attrs2:
+                            
+                                value1 = field_attrs1[attr]
+                                value2 = field_attrs2[attr]
+                                if value1 != value2:
+                                    print('    %s attribute %s changed from %s to %s' % (
+                                        name, attr, value1, value2))
+    
+    return migration2

+ 67 - 0
desktop/core/ext-py/South-0.8.2/south/management/commands/migrationcheck.py

@@ -0,0 +1,67 @@
+from django.core.exceptions import ImproperlyConfigured
+from django.core.management import call_command, CommandError
+from django.core.management.base import BaseCommand
+from django.conf import settings
+from django.db.models import loading
+from django.test import simple
+
+from south.migration import Migrations
+from south.exceptions import NoMigrations
+from south.hacks import hacks
+
+class Command(BaseCommand):
+    help = "Runs migrations for each app in turn, detecting missing depends_on values."
+    usage_str = "Usage: ./manage.py migrationcheck"
+
+    def handle(self, check_app_name=None, **options):
+        runner = simple.DjangoTestSuiteRunner(verbosity=0)
+        err_msg = "Failed to migrate %s; see output for hints at missing dependencies:\n"
+        hacks.patch_flush_during_test_db_creation()
+        failures = 0
+        if check_app_name is None:
+            app_names = settings.INSTALLED_APPS
+        else:
+            app_names = [check_app_name]
+        for app_name in app_names:
+            app_label = app_name.split(".")[-1]
+            if app_name == 'south':
+                continue
+
+            try:
+                Migrations(app_name)
+            except (NoMigrations, ImproperlyConfigured):
+                continue
+            app = loading.get_app(app_label)
+
+            verbosity = int(options.get('verbosity', 1))
+            if verbosity >= 1:
+                self.stderr.write("processing %s\n" % app_name)
+
+            old_config = runner.setup_databases()
+            try:
+                call_command('migrate', app_label, noinput=True, verbosity=verbosity)
+                for model in loading.get_models(app):
+                    dummy = model._default_manager.exists()
+            except (KeyboardInterrupt, SystemExit):
+                raise
+            except Exception as e:
+                failures += 1
+                if verbosity >= 1:
+                    self.stderr.write(err_msg % app_name)
+                    self.stderr.write("%s\n" % e)
+            finally:
+                runner.teardown_databases(old_config)
+        if failures > 0:
+            raise CommandError("Missing depends_on found in %s app(s)." % failures)
+        self.stderr.write("No missing depends_on found.\n")
+#
+#for each app:
+#    start with blank db.
+#    syncdb only south (and contrib?)
+#
+#    migrate a single app all the way up.  any errors is missing depends_on.
+#    for all models of that app, try the default manager:
+#        from django.db.models import loading
+#        for m in loading.get_models(loading.get_app('a')):
+#            m._default_manager.exists()
+#    Any error is also a missing depends on.

+ 64 - 30
desktop/core/ext-py/south/south/management/commands/schemamigration.py → desktop/core/ext-py/South-0.8.2/south/management/commands/schemamigration.py

@@ -2,13 +2,14 @@
 Startmigration command, version 2.
 """
 
+from __future__ import print_function
+
 import sys
 import os
 import re
 import string
 import random
 import inspect
-import parser
 from optparse import make_option
 
 try:
@@ -18,10 +19,12 @@ except NameError:
 
 from django.core.management.base import BaseCommand
 from django.core.management.color import no_style
+from django.core.exceptions import ImproperlyConfigured
 from django.db import models
 from django.conf import settings
 
-from south.migration import Migrations
+from south.migration import Migrations, migrate_app
+from south.models import MigrationHistory
 from south.exceptions import NoMigrations
 from south.creator import changes, actions, freezer
 from south.management.commands.datamigration import Command as DataCommand
@@ -29,7 +32,7 @@ from south.management.commands.datamigration import Command as DataCommand
 class Command(DataCommand):
     option_list = DataCommand.option_list + (
         make_option('--add-model', action='append', dest='added_model_list', type='string',
-            help='Generate a Create Table migration for the specified model.  Add multiple models to this migration with subsequent --model parameters.'),
+            help='Generate a Create Table migration for the specified model.  Add multiple models to this migration with subsequent --add-model parameters.'),
         make_option('--add-field', action='append', dest='added_field_list', type='string',
             help='Generate an Add Column migration for the specified modelname.fieldname - you can use this multiple times to add more than one column.'),
         make_option('--add-index', action='append', dest='added_index_list', type='string',
@@ -40,11 +43,13 @@ class Command(DataCommand):
             help='Attempt to automatically detect differences from the last migration.'),
         make_option('--empty', action='store_true', dest='empty', default=False,
             help='Make a blank migration.'),
+        make_option('--update', action='store_true', dest='update', default=False,
+                    help='Update the most recent migration instead of creating a new one. Rollback this migration if it is already applied.'),
     )
     help = "Creates a new template schema migration for the given app"
     usage_str = "Usage: ./manage.py schemamigration appname migrationname [--empty] [--initial] [--auto] [--add-model ModelName] [--add-field ModelName.field_name] [--stdout]"
     
-    def handle(self, app=None, name="", added_model_list=None, added_field_list=None, freeze_list=None, initial=False, auto=False, stdout=False, added_index_list=None, verbosity=1, empty=False, **options):
+    def handle(self, app=None, name="", added_model_list=None, added_field_list=None, freeze_list=None, initial=False, auto=False, stdout=False, added_index_list=None, verbosity=1, empty=False, update=False, **options):
         
         # Any supposed lists that are None become empty lists
         added_model_list = added_model_list or []
@@ -69,15 +74,23 @@ class Command(DataCommand):
         
         if not app:
             self.error("You must provide an app to create a migration for.\n" + self.usage_str)
-        
+	    
+        # See if the app exists
+        app = app.split(".")[-1]
+        try:
+            app_module = models.get_app(app)
+        except ImproperlyConfigured:
+            print("There is no enabled application matching '%s'." % app)
+            return
+	
         # Get the Migrations for this app (creating the migrations dir if needed)
-        migrations = Migrations(app, force_creation=True, verbose_creation=verbosity > 0)
+        migrations = Migrations(app, force_creation=True, verbose_creation=int(verbosity) > 0)
         
         # What actions do we need to do?
         if auto:
             # Get the old migration
             try:
-                last_migration = migrations[-1]
+                last_migration = migrations[-2 if update else -1]
             except IndexError:
                 self.error("You cannot use --auto on an app with no migrations. Try --initial.")
             # Make sure it has stored models
@@ -115,19 +128,22 @@ class Command(DataCommand):
             elif empty:
                 change_source = None
             else:
-                print >>sys.stderr, "You have not passed any of --initial, --auto, --empty, --add-model, --add-field or --add-index."
+                print("You have not passed any of --initial, --auto, --empty, --add-model, --add-field or --add-index.", file=sys.stderr)
                 sys.exit(1)
+
+        # Validate this so we can access the last migration without worrying
+        if update and not migrations:
+            self.error("You cannot use --update on an app with no migrations.")
         
         # if not name, there's an error
         if not name:
             if change_source:
                 name = change_source.suggest_name()
+            if update:
+                name = re.sub(r'^\d{4}_', '', migrations[-1].name())
             if not name:
                 self.error("You must provide a name for this migration\n" + self.usage_str)
         
-        # See what filename is next in line. We assume they use numbers.
-        new_filename = migrations.next_filename(name)
-        
         # Get the actions, and then insert them into the actions lists
         forwards_actions = []
         backwards_actions = []
@@ -142,7 +158,7 @@ class Command(DataCommand):
                     action = action_class(**params)
                     action.add_forwards(forwards_actions)
                     action.add_backwards(backwards_actions)
-                    print >>sys.stderr, action.console_line()
+                    print(action.console_line(), file=sys.stderr)
         
         # Nowt happen? That's not good for --auto.
         if auto and not forwards_actions:
@@ -152,44 +168,62 @@ class Command(DataCommand):
         apps_to_freeze = self.calc_frozen_apps(migrations, freeze_list)
         
         # So, what's in this file, then?
-        file_contents = MIGRATION_TEMPLATE % {
-            "forwards": "\n".join(forwards_actions or ["pass"]), 
-            "backwards": "\n".join(backwards_actions or ["pass"]), 
+        file_contents = self.get_migration_template() % {
+            "forwards": "\n".join(forwards_actions or ["        pass"]),
+            "backwards": "\n".join(backwards_actions or ["        pass"]),
             "frozen_models":  freezer.freeze_apps_to_string(apps_to_freeze),
             "complete_apps": apps_to_freeze and "complete_apps = [%s]" % (", ".join(map(repr, apps_to_freeze))) or ""
         }
-        
+
+        # Deal with update mode as late as possible, avoid a rollback as long
+        # as something else can go wrong.
+        if update:
+            last_migration = migrations[-1]
+            if MigrationHistory.objects.filter(applied__isnull=False, app_name=app, migration=last_migration.name()):
+                print("Migration to be updated, %s, is already applied, rolling it back now..." % last_migration.name(), file=sys.stderr)
+                migrate_app(migrations, 'current-1', verbosity=verbosity)
+            for ext in ('py', 'pyc'):
+                old_filename = "%s.%s" % (os.path.join(migrations.migrations_dir(), last_migration.filename), ext)
+                if os.path.isfile(old_filename):
+                    os.unlink(old_filename)
+            migrations.remove(last_migration)
+
+        # See what filename is next in line. We assume they use numbers.
+        new_filename = migrations.next_filename(name)
+
         # - is a special name which means 'print to stdout'
         if name == "-":
-            print file_contents
+            print(file_contents)
         # Write the migration file if the name isn't -
         else:
             fp = open(os.path.join(migrations.migrations_dir(), new_filename), "w")
             fp.write(file_contents)
             fp.close()
+            verb = 'Updated' if update else 'Created'
             if empty:
-                print >>sys.stderr, "Created %s. You must now edit this migration and add the code for each direction." % new_filename
+                print("%s %s. You must now edit this migration and add the code for each direction." % (verb, new_filename), file=sys.stderr)
             else:
-                print >>sys.stderr, "Created %s. You can now apply this migration with: ./manage.py migrate %s" % (new_filename, app)
+                print("%s %s. You can now apply this migration with: ./manage.py migrate %s" % (verb, new_filename, app), file=sys.stderr)
 
+    def get_migration_template(self):
+        return MIGRATION_TEMPLATE
 
-MIGRATION_TEMPLATE = """# encoding: utf-8
+
+MIGRATION_TEMPLATE = """# -*- coding: utf-8 -*-
 import datetime
 from south.db import db
 from south.v2 import SchemaMigration
 from django.db import models
 
+
 class Migration(SchemaMigration):
-    
+
     def forwards(self, orm):
-        %(forwards)s
-    
-    
+%(forwards)s
+
     def backwards(self, orm):
-        %(backwards)s
-    
-    
+%(backwards)s
+
     models = %(frozen_models)s
-    
-    %(complete_apps)s
-"""
+
+    %(complete_apps)s"""

+ 5 - 3
desktop/core/ext-py/south/south/management/commands/startmigration.py → desktop/core/ext-py/South-0.8.2/south/management/commands/startmigration.py

@@ -2,6 +2,8 @@
 Now-obsolete startmigration command.
 """
 
+from __future__ import print_function
+
 from optparse import make_option
 
 from django.core.management.base import BaseCommand
@@ -10,7 +12,7 @@ from django.core.management.color import no_style
 class Command(BaseCommand):
     option_list = BaseCommand.option_list + (
         make_option('--model', action='append', dest='added_model_list', type='string',
-            help='Generate a Create Table migration for the specified model.  Add multiple models to this migration with subsequent --model parameters.'),
+            help='Generate a Create Table migration for the specified model.  Add multiple models to this migration with subsequent --add-model parameters.'),
         make_option('--add-field', action='append', dest='added_field_list', type='string',
             help='Generate an Add Column migration for the specified modelname.fieldname - you can use this multiple times to add more than one column.'),
         make_option('--add-index', action='append', dest='added_index_list', type='string',
@@ -24,8 +26,8 @@ class Command(BaseCommand):
         make_option('--stdout', action='store_true', dest='stdout', default=False,
             help='Print the migration to stdout instead of writing it to a file.'),
     )
-    help = "Depereciated command"
+    help = "Deprecated command"
     
     def handle(self, app=None, name="", added_model_list=None, added_field_list=None, initial=False, freeze_list=None, auto=False, stdout=False, added_index_list=None, **options):
         
-        print "The 'startmigration' command is now deprecated; please use the new 'schemamigration' and 'datamigration' commands."
+        print("The 'startmigration' command is now deprecated; please use the new 'schemamigration' and 'datamigration' commands.")

+ 27 - 23
desktop/core/ext-py/south/south/management/commands/syncdb.py → desktop/core/ext-py/South-0.8.2/south/management/commands/syncdb.py

@@ -2,6 +2,8 @@
 Overridden syncdb command
 """
 
+from __future__ import print_function
+
 import sys
 from optparse import make_option
 
@@ -37,6 +39,23 @@ class Command(NoArgsCommand):
     help = "Create the database tables for all apps in INSTALLED_APPS whose tables haven't already been created, except those which use migrations."
 
     def handle_noargs(self, migrate_all=False, **options):
+        
+        # Import the 'management' module within each installed app, to register
+        # dispatcher events.
+        # This is copied from Django, to fix bug #511.
+        try:
+            from django.utils.importlib import import_module
+        except ImportError:
+            pass # TODO: Remove, only for Django1.0
+        else:
+            for app_name in settings.INSTALLED_APPS:
+                try:
+                    import_module('.management', app_name)
+                except ImportError as exc:
+                    msg = exc.args[0]
+                    if not msg.startswith('No module named') or 'management' not in msg:
+                        raise
+        
         # Work out what uses migrations and so doesn't need syncing
         apps_needing_sync = []
         apps_migrated = []
@@ -57,7 +76,7 @@ class Command(NoArgsCommand):
         
         # Run syncdb on only the ones needed
         if verbosity:
-            print "Syncing..."
+            print("Syncing...")
         
         old_installed, settings.INSTALLED_APPS = settings.INSTALLED_APPS, apps_needing_sync
         old_app_store, cache.app_store = cache.app_store, SortedDict([
@@ -78,34 +97,19 @@ class Command(NoArgsCommand):
         # Migrate if needed
         if options.get('migrate', True):
             if verbosity:
-                print "Migrating..."
+                print("Migrating...")
+            # convert from store_true to store_false
+            options['no_initial_data'] = not options.get('load_initial_data', True)
             management.call_command('migrate', **options)
         
         # Be obvious about what we did
         if verbosity:
-            print "\nSynced:\n > %s" % "\n > ".join(apps_needing_sync)
+            print("\nSynced:\n > %s" % "\n > ".join(apps_needing_sync))
         
         if options.get('migrate', True):
             if verbosity:
-                print "\nMigrated:\n - %s" % "\n - ".join(apps_migrated)
+                print("\nMigrated:\n - %s" % "\n - ".join(apps_migrated))
         else:
             if verbosity:
-                print "\nNot synced (use migrations):\n - %s" % "\n - ".join(apps_migrated)
-                print "(use ./manage.py migrate to migrate these)"
-
-class MigrateAndSyncCommand(Command):
-    """Used for situations where "syncdb" is called by test frameworks."""
-    option_list = Command.option_list
-    for opt in option_list:
-        if "--migrate" == opt.get_opt_string():
-            opt.default = True
-            break
-
-def patch_for_test_db_setup():
-      management.get_commands()
-      if hasattr(settings, "SOUTH_TESTS_MIGRATE") and not settings.SOUTH_TESTS_MIGRATE:
-          # point at the core syncdb command when creating tests
-          # tests should always be up to date with the most recent model structure
-          management._commands['syncdb'] = 'django.core'
-      else:
-          management._commands['syncdb'] = MigrateAndSyncCommand()
+                print("\nNot synced (use migrations):\n - %s" % "\n - ".join(apps_migrated))
+                print("(use ./manage.py migrate to migrate these)")

+ 3 - 2
desktop/core/ext-py/south/south/management/commands/test.py → desktop/core/ext-py/South-0.8.2/south/management/commands/test.py

@@ -1,7 +1,8 @@
 from django.core.management.commands import test
-import south.management.commands.syncdb
+
+from south.management.commands import patch_for_test_db_setup
 
 class Command(test.Command):
     def handle(self, *args, **kwargs):
-        south.management.commands.syncdb.patch_for_test_db_setup()
+        patch_for_test_db_setup()
         super(Command, self).handle(*args, **kwargs)

+ 3 - 2
desktop/core/ext-py/south/south/management/commands/testserver.py → desktop/core/ext-py/South-0.8.2/south/management/commands/testserver.py

@@ -1,7 +1,8 @@
 from django.core.management.commands import testserver
-import south.management.commands.syncdb
+
+from south.management.commands import patch_for_test_db_setup
 
 class Command(testserver.Command):
     def handle(self, *args, **kwargs):
-        south.management.commands.syncdb.patch_for_test_db_setup()
+        patch_for_test_db_setup()
         super(Command, self).handle(*args, **kwargs)

+ 63 - 36
desktop/core/ext-py/south/south/migration/__init__.py → desktop/core/ext-py/South-0.8.2/south/migration/__init__.py

@@ -2,6 +2,8 @@
 Main migration logic.
 """
 
+from __future__ import print_function
+
 import sys
 
 from django.core.exceptions import ImproperlyConfigured
@@ -36,17 +38,6 @@ def problems(pending, done):
         if last and migration not in done:
             yield last, migration
 
-def problems(pending, done):
-    last = None
-    if not pending:
-        raise StopIteration()
-    for migration in pending:
-        if migration in done:
-            last = migration
-            continue
-        if last and migration not in done:
-            yield last, migration
-
 def forwards_problems(pending, done, verbosity):
     """
     Takes the list of linearised pending migrations, and the set of done ones,
@@ -61,22 +52,27 @@ def inner_problem_check(problems, done, verbosity):
     "Takes a set of possible problems and gets the actual issues out of it."
     result = []
     for last, migration in problems:
+        checked = set([])
         # 'Last' is the last applied migration. Step back from it until we
         # either find nothing wrong, or we find something.
         to_check = list(last.dependencies)
         while to_check:
             checking = to_check.pop()
+            if checking in checked:
+                continue
+            checked.add(checking)
+
             if checking not in done:
                 # That's bad. Error.
                 if verbosity:
-                    print (" ! Migration %s should not have been applied "
-                           "before %s but was." % (last, checking))
+                    print((" ! Migration %s should not have been applied "
+                           "before %s but was." % (last, checking)))
                 result.append((last, checking))
             else:
                 to_check.extend(checking.dependencies)
     return result
 
-def check_migration_histories(histories, delete_ghosts=False):
+def check_migration_histories(histories, delete_ghosts=False, ignore_ghosts=False):
     "Checks that there's no 'ghost' migrations in the database."
     exists = SortedSet()
     ghosts = []
@@ -95,7 +91,7 @@ def check_migration_histories(histories, delete_ghosts=False):
         if delete_ghosts:
             for h in ghosts:
                 h.delete()
-        else:
+        elif not ignore_ghosts:
             raise exceptions.GhostMigrations(ghosts)
     return exists
 
@@ -114,7 +110,7 @@ def get_dependencies(target, migrations):
             backwards = migration_before_here.backwards_plan
     return forwards, backwards
 
-def get_direction(target, applied, migrations, verbosity):
+def get_direction(target, applied, migrations, verbosity, interactive):
     # Get the forwards and reverse dependencies for this target
     forwards, backwards = get_dependencies(target, migrations)
     # Is the whole forward branch applied?
@@ -129,7 +125,7 @@ def get_direction(target, applied, migrations, verbosity):
         # the forwards trace, we just need to go forwards to our
         # target (and check for badness)
         problems = forwards_problems(forwards, applied, verbosity)
-        direction = Forwards(verbosity=verbosity)
+        direction = Forwards(verbosity=verbosity, interactive=interactive)
     if not problems:
         # What about the whole backward trace then?
         backwards = backwards()
@@ -139,7 +135,7 @@ def get_direction(target, applied, migrations, verbosity):
             # all the higher migrations) then we need to go backwards
             workplan = to_unapply(backwards, applied)
             problems = backwards_problems(backwards, applied, verbosity)
-            direction = Backwards(verbosity=verbosity)
+            direction = Backwards(verbosity=verbosity, interactive=interactive)
     return direction, problems, workplan
 
 def get_migrator(direction, db_dry_run, fake, load_initial_data):
@@ -153,7 +149,15 @@ def get_migrator(direction, db_dry_run, fake, load_initial_data):
         direction = LoadInitialDataMigrator(migrator=direction)
     return direction
 
-def migrate_app(migrations, target_name=None, merge=False, fake=False, db_dry_run=False, yes=False, verbosity=0, load_initial_data=False, skip=False, database=DEFAULT_DB_ALIAS, delete_ghosts=False):
+def get_unapplied_migrations(migrations, applied_migrations):
+    applied_migration_names = ['%s.%s' % (mi.app_name,mi.migration) for mi in applied_migrations]
+
+    for migration in migrations:
+        is_applied = '%s.%s' % (migration.app_label(), migration.name()) in applied_migration_names
+        if not is_applied:
+            yield migration
+
+def migrate_app(migrations, target_name=None, merge=False, fake=False, db_dry_run=False, yes=False, verbosity=0, load_initial_data=False, skip=False, database=DEFAULT_DB_ALIAS, delete_ghosts=False, ignore_ghosts=False, interactive=False):
     app_label = migrations.app_label()
 
     verbosity = int(verbosity)
@@ -162,36 +166,50 @@ def migrate_app(migrations, target_name=None, merge=False, fake=False, db_dry_ru
     
     # If there aren't any, quit quizically
     if not migrations:
-        print "? You have no migrations for the '%s' app. You might want some." % app_label
+        print("? You have no migrations for the '%s' app. You might want some." % app_label)
         return
     
     # Load the entire dependency graph
     Migrations.calculate_dependencies()
     
     # Check there's no strange ones in the database
-    applied = MigrationHistory.objects.filter(applied__isnull=False)
-    # If we're using a different database, use that
-    if database != DEFAULT_DB_ALIAS:
-        applied = applied.using(database)
-        south.db.db = south.db.dbs[database]
-        # We now have to make sure the migrations are all reloaded, as they'll
-        # have imported the old value of south.db.db.
-        Migrations.invalidate_all_modules()
+    applied_all = MigrationHistory.objects.filter(applied__isnull=False).order_by('applied').using(database)
+    applied = applied_all.filter(app_name=app_label).using(database)
+    south.db.db = south.db.dbs[database]
+    Migrations.invalidate_all_modules()
     
     south.db.db.debug = (verbosity > 1)
-    applied = check_migration_histories(applied, delete_ghosts)
+
+    if target_name == 'current-1':
+        if applied.count() > 1:
+            previous_migration = applied[applied.count() - 2]
+            if verbosity:
+                print('previous_migration: %s (applied: %s)' % (previous_migration.migration, previous_migration.applied))
+            target_name = previous_migration.migration
+        else:
+            if verbosity:
+                print('previous_migration: zero')
+            target_name = 'zero'
+    elif target_name == 'current+1':
+        try:
+            first_unapplied_migration = get_unapplied_migrations(migrations, applied).next()
+            target_name = first_unapplied_migration.name()
+        except StopIteration:
+            target_name = None
+    
+    applied_all = check_migration_histories(applied_all, delete_ghosts, ignore_ghosts)
     
     # Guess the target_name
     target = migrations.guess_migration(target_name)
     if verbosity:
         if target_name not in ('zero', None) and target.name() != target_name:
-            print " - Soft matched migration %s to %s." % (target_name,
-                                                           target.name())
-        print "Running migrations for %s:" % app_label
+            print(" - Soft matched migration %s to %s." % (target_name,
+                                                           target.name()))
+        print("Running migrations for %s:" % app_label)
     
     # Get the forwards and reverse dependencies for this target
-    direction, problems, workplan = get_direction(target, applied,
-                                                  migrations, verbosity)
+    direction, problems, workplan = get_direction(target, applied_all, migrations,
+                                                  verbosity, interactive)
     if problems and not (merge or skip):
         raise exceptions.InconsistentMigrationHistory(problems)
     
@@ -203,6 +221,15 @@ def migrate_app(migrations, target_name=None, merge=False, fake=False, db_dry_ru
         # Finally, fire off the post-migrate signal
         if success:
             post_migrate.send(None, app=app_label)
-    elif verbosity:
-        print '- Nothing to migrate.'
+    else:
+        if verbosity:
+            # Say there's nothing.
+            print('- Nothing to migrate.')
+        # If we have initial data enabled, and we're at the most recent
+        # migration, do initial data.
+        # Note: We use a fake Forwards() migrator here. It's never used really.
+        if load_initial_data:
+            migrator = LoadInitialDataMigrator(migrator=Forwards(verbosity=verbosity))
+            migrator.load_initial_data(target, db=database)
+        # Send signal.
         post_migrate.send(None, app=app_label)

+ 36 - 22
desktop/core/ext-py/south/south/migration/base.py → desktop/core/ext-py/South-0.8.2/south/migration/base.py

@@ -1,5 +1,8 @@
+from __future__ import print_function
+
 from collections import deque
 import datetime
+from imp import reload
 import os
 import re
 import sys
@@ -7,13 +10,14 @@ import sys
 from django.core.exceptions import ImproperlyConfigured
 from django.db import models
 from django.conf import settings
+from django.utils import importlib
 
 from south import exceptions
 from south.migration.utils import depends, dfs, flatten, get_app_label
 from south.orm import FakeORM
-from south.utils import memoize, ask_for_it_by_name
+from south.utils import memoize, ask_for_it_by_name, datetime_utils
 from south.migration.utils import app_label_to_app_module
-
+from south.utils.py3 import string_types, with_metaclass
 
 def all_migrations(applications=None):
     """
@@ -33,7 +37,7 @@ def all_migrations(applications=None):
 
 def application_to_app_label(application):
     "Works out the app label from either the app label, the app name, or the module"
-    if isinstance(application, basestring):
+    if isinstance(application, string_types):
         app_label = application.split('.')[-1]
     else:
         app_label = application.__name__.split('.')[-1]
@@ -66,13 +70,11 @@ class MigrationsMetaclass(type):
         self.instances = {}
 
 
-class Migrations(list):
+class Migrations(with_metaclass(MigrationsMetaclass, list)):
     """
     Holds a list of Migration objects for a particular app.
     """
     
-    __metaclass__ = MigrationsMetaclass
-    
     if getattr(settings, "SOUTH_USE_PYC", False):
         MIGRATION_FILENAME = re.compile(r'(?!__init__)' # Don't match __init__.py
                                         r'[0-9a-zA-Z_]*' # Don't match dotfiles, or names with dots/invalid chars in them
@@ -93,14 +95,14 @@ class Migrations(list):
         # Make the directory if it's not already there
         if not os.path.isdir(migrations_dir):
             if verbose:
-                print "Creating migrations directory at '%s'..." % migrations_dir
+                print("Creating migrations directory at '%s'..." % migrations_dir)
             os.mkdir(migrations_dir)
         # Same for __init__.py
         init_path = os.path.join(migrations_dir, "__init__.py")
         if not os.path.isfile(init_path):
             # Touch the init py file
             if verbose:
-                print "Creating __init__.py in '%s'..." % migrations_dir
+                print("Creating __init__.py in '%s'..." % migrations_dir)
             open(init_path, "w").close()
     
     def migrations_dir(self):
@@ -111,11 +113,11 @@ class Migrations(list):
         """
         module_path = self.migrations_module()
         try:
-            module = __import__(module_path, {}, {}, [''])
+            module = importlib.import_module(module_path)
         except ImportError:
             # There's no migrations module made yet; guess!
             try:
-                parent = __import__(".".join(module_path.split(".")[:-1]), {}, {}, [''])
+                parent = importlib.import_module(".".join(module_path.split(".")[:-1]))
             except ImportError:
                 # The parent doesn't even exist, that's an issue.
                 raise exceptions.InvalidMigrationModule(
@@ -131,10 +133,11 @@ class Migrations(list):
     
     def migrations_module(self):
         "Returns the module name of the migrations module for this"
+        app_label = application_to_app_label(self.application)
         if hasattr(settings, "SOUTH_MIGRATION_MODULES"):
-            if self.application.__name__ in settings.SOUTH_MIGRATION_MODULES:
+            if app_label in settings.SOUTH_MIGRATION_MODULES:
                 # There's an override.
-                return settings.SOUTH_MIGRATION_MODULES[self.application.__name__]
+                return settings.SOUTH_MIGRATION_MODULES[app_label]
         return self._application.__name__ + '.migrations'
 
     def get_application(self):
@@ -148,12 +151,12 @@ class Migrations(list):
         self._application = application
         if not hasattr(application, 'migrations'):
             try:
-                module = __import__(self.migrations_module(), {}, {}, [''])
+                module = importlib.import_module(self.migrations_module())
                 self._migrations = application.migrations = module
             except ImportError:
                 if force_creation:
                     self.create_migrations_directory(verbose_creation)
-                    module = __import__(self.migrations_module(), {}, {}, [''])
+                    module = importlib.import_module(self.migrations_module())
                     self._migrations = application.migrations = module
                 else:
                     raise exceptions.NoMigrations(application)
@@ -188,7 +191,7 @@ class Migrations(list):
         return self._cache[name]
 
     def __getitem__(self, value):
-        if isinstance(value, basestring):
+        if isinstance(value, string_types):
             return self.migration(value)
         return super(Migrations, self).__getitem__(value)
 
@@ -216,12 +219,15 @@ class Migrations(list):
     def full_name(self):
         return self._migrations.__name__
 
-    @staticmethod
-    def calculate_dependencies():
+    @classmethod
+    def calculate_dependencies(cls, force=False):
         "Goes through all the migrations, and works out the dependencies."
+        if getattr(cls, "_dependencies_done", False) and not force:
+            return
         for migrations in all_migrations():
             for migration in migrations:
                 migration.calculate_dependencies()
+        cls._dependencies_done = True
     
     @staticmethod
     def invalidate_all_modules():
@@ -265,7 +271,13 @@ class Migration(object):
         return self.app_label() + ':' + self.name()
 
     def __repr__(self):
-        return u'<Migration: %s>' % unicode(self)
+        return '<Migration: %s>' % str(self)
+
+    def __eq__(self, other):
+        return self.app_label() == other.app_label() and self.name() == other.name()
+
+    def __hash__(self):
+        return hash(str(self))
 
     def app_label(self):
         return self.migrations.app_label()
@@ -287,14 +299,14 @@ class Migration(object):
             migration = sys.modules[full_name]
         except KeyError:
             try:
-                migration = __import__(full_name, '', '', ['Migration'])
-            except ImportError, e:
+                migration = __import__(full_name, {}, {}, ['Migration'])
+            except ImportError as e:
                 raise exceptions.UnknownMigration(self, sys.exc_info())
-            except Exception, e:
+            except Exception as e:
                 raise exceptions.BrokenMigration(self, sys.exc_info())
         # Override some imports
         migration._ = lambda x: x  # Fake i18n
-        migration.datetime = datetime
+        migration.datetime = datetime_utils
         return migration
     migration = memoize(migration)
 
@@ -407,6 +419,8 @@ class Migration(object):
             return False
 
     def prev_orm(self):
+        if getattr(self.migration_class(), 'symmetrical', False):
+            return self.orm()
         previous = self.previous()
         if previous is None:
             # First migration? The 'previous ORM' is empty.

+ 74 - 53
desktop/core/ext-py/south/south/migration/migrators.py → desktop/core/ext-py/South-0.8.2/south/migration/migrators.py

@@ -1,5 +1,6 @@
-from copy import copy
-from cStringIO import StringIO
+from __future__ import print_function
+
+from copy import copy, deepcopy
 import datetime
 import inspect
 import sys
@@ -14,11 +15,13 @@ from south import exceptions
 from south.db import DEFAULT_DB_ALIAS
 from south.models import MigrationHistory
 from south.signals import ran_migration
+from south.utils.py3 import StringIO
 
 
 class Migrator(object):
-    def __init__(self, verbosity=0):
+    def __init__(self, verbosity=0, interactive=False):
         self.verbosity = int(verbosity)
+        self.interactive = bool(interactive)
 
     @staticmethod
     def title(target):
@@ -26,7 +29,7 @@ class Migrator(object):
 
     def print_title(self, target):
         if self.verbosity:
-            print self.title(target)
+            print(self.title(target))
         
     @staticmethod
     def status(target):
@@ -35,7 +38,7 @@ class Migrator(object):
     def print_status(self, migration):
         status = self.status(migration)
         if self.verbosity and status:
-            print status
+            print(status)
 
     @staticmethod
     def orm(migration):
@@ -60,50 +63,55 @@ class Migrator(object):
         raise NotImplementedError()
 
     def run_migration_error(self, migration, extra_info=''):
-        return (' ! Error found during real run of migration! Aborting.\n'
-                '\n'
-                ' ! Since you have a database that does not support running\n'
-                ' ! schema-altering statements in transactions, we have had \n'
-                ' ! to leave it in an interim state between migrations.\n'
-                '%s\n'
-                ' ! The South developers regret this has happened, and would\n'
-                ' ! like to gently persuade you to consider a slightly\n'
-                ' ! easier-to-deal-with DBMS.\n') % extra_info
-
-    def run_migration(self, migration):
+        return (
+            ' ! Error found during real run of migration! Aborting.\n'
+            '\n'
+            ' ! Since you have a database that does not support running\n'
+            ' ! schema-altering statements in transactions, we have had \n'
+            ' ! to leave it in an interim state between migrations.\n'
+            '%s\n'
+            ' ! The South developers regret this has happened, and would\n'
+            ' ! like to gently persuade you to consider a slightly\n'
+            ' ! easier-to-deal-with DBMS (one that supports DDL transactions)\n'
+            ' ! NOTE: The error which caused the migration to fail is further up.'
+        ) % extra_info
+
+    def run_migration(self, migration, database):
         migration_function = self.direction(migration)
         south.db.db.start_transaction()
         try:
             migration_function()
             south.db.db.execute_deferred_sql()
+            if not isinstance(getattr(self, '_wrapper', self), DryRunMigrator):
+                # record us as having done this in the same transaction,
+                # since we're not in a dry run
+                self.record(migration, database)
         except:
             south.db.db.rollback_transaction()
             if not south.db.db.has_ddl_transactions:
-                print self.run_migration_error(migration)
+                print(self.run_migration_error(migration))
+            print("Error in migration: %s" % migration)
             raise
         else:
-            south.db.db.commit_transaction()
+            try:
+                south.db.db.commit_transaction()
+            except:
+                print("Error during commit in migration: %s" % migration)
+                raise
+                
 
-    def run(self, migration):
+    def run(self, migration, database):
         # Get the correct ORM.
         south.db.db.current_orm = self.orm(migration)
-        # If the database doesn't support running DDL inside a transaction
-        # *cough*MySQL*cough* then do a dry run first.
-        if not south.db.db.has_ddl_transactions:
-            dry_run = DryRunMigrator(migrator=self, ignore_fail=False)
-            dry_run.run_migration(migration)
-        return self.run_migration(migration)
-
-    def done_migrate(self, migration, database):
-        south.db.db.start_transaction()
-        try:
-            # Record us as having done this
-            self.record(migration, database)
-        except:
-            south.db.db.rollback_transaction()
-            raise
-        else:
-            south.db.db.commit_transaction()
+        # If we're not already in a dry run, and the database doesn't support
+        # running DDL inside a transaction, *cough*MySQL*cough* then do a dry
+        # run first.
+        if not isinstance(getattr(self, '_wrapper', self), DryRunMigrator):
+            if not south.db.db.has_ddl_transactions:
+                dry_run = DryRunMigrator(migrator=self, ignore_fail=False)
+                dry_run.run_migration(migration, database)
+        return self.run_migration(migration, database)
+
 
     def send_ran_migration(self, migration):
         ran_migration.send(None,
@@ -118,8 +126,7 @@ class Migrator(object):
         app = migration.migrations._migrations
         migration_name = migration.name()
         self.print_status(migration)
-        result = self.run(migration)
-        self.done_migrate(migration, database)
+        result = self.run(migration, database)
         self.send_ran_migration(migration)
         return result
 
@@ -131,9 +138,10 @@ class MigratorWrapper(object):
     def __init__(self, migrator, *args, **kwargs):
         self._migrator = copy(migrator)
         attributes = dict([(k, getattr(self, k))
-                           for k in self.__class__.__dict__.iterkeys()
+                           for k in self.__class__.__dict__
                            if not k.startswith('__')])
         self._migrator.__dict__.update(attributes)
+        self._migrator.__dict__['_wrapper'] = self
 
     def __getattr__(self, name):
         return getattr(self._migrator, name)
@@ -145,10 +153,13 @@ class DryRunMigrator(MigratorWrapper):
         self._ignore_fail = ignore_fail
 
     def _run_migration(self, migration):
-        if migration.no_dry_run() and self.verbosity:
-            print " - Migration '%s' is marked for no-dry-run." % migration
+        if migration.no_dry_run():
+            if self.verbosity:
+                print(" - Migration '%s' is marked for no-dry-run." % migration)
             return
         south.db.db.dry_run = True
+        # preserve the constraint cache as it can be mutated by the dry run
+        constraint_cache = deepcopy(south.db.db._constraint_cache)
         if self._ignore_fail:
             south.db.db.debug, old_debug = False, south.db.db.debug
         pending_creates = south.db.db.get_pending_creates()
@@ -166,8 +177,11 @@ class DryRunMigrator(MigratorWrapper):
                 south.db.db.debug = old_debug
             south.db.db.clear_run_data(pending_creates)
             south.db.db.dry_run = False
+            # restore the preserved constraint cache from before dry run was
+            # executed
+            south.db.db._constraint_cache = constraint_cache
 
-    def run_migration(self, migration):
+    def run_migration(self, migration, database):
         try:
             self._run_migration(migration)
         except exceptions.FailedDryRun:
@@ -175,29 +189,29 @@ class DryRunMigrator(MigratorWrapper):
                 return False
             raise
 
-    def done_migrate(self, *args, **kwargs):
-        pass
-
     def send_ran_migration(self, *args, **kwargs):
         pass
 
 
 class FakeMigrator(MigratorWrapper):
-    def run(self, migration):
+    def run(self, migration, database):
+        # Don't actually run, just record as if ran
+        self.record(migration, database)
         if self.verbosity:
-            print '   (faked)'
+            print('   (faked)')
 
     def send_ran_migration(self, *args, **kwargs):
         pass
 
 
 class LoadInitialDataMigrator(MigratorWrapper):
-    def load_initial_data(self, target):
+    
+    def load_initial_data(self, target, db='default'):
         if target is None or target != target.migrations[-1]:
             return
         # Load initial data, if we ended up at target
         if self.verbosity:
-            print " - Loading initial data for %s." % target.app_label()
+            print(" - Loading initial data for %s." % target.app_label())
         # Override Django's get_apps call temporarily to only load from the
         # current app
         old_get_apps = models.get_apps
@@ -205,7 +219,7 @@ class LoadInitialDataMigrator(MigratorWrapper):
         models.get_apps = new_get_apps
         loaddata.get_apps = new_get_apps
         try:
-            call_command('loaddata', 'initial_data', verbosity=self.verbosity)
+            call_command('loaddata', 'initial_data', verbosity=self.verbosity, database=db)
         finally:
             models.get_apps = old_get_apps
             loaddata.get_apps = old_get_apps
@@ -214,7 +228,7 @@ class LoadInitialDataMigrator(MigratorWrapper):
         migrator = self._migrator
         result = migrator.__class__.migrate_many(migrator, target, migrations, database)
         if result:
-            self.load_initial_data(target)
+            self.load_initial_data(target, db=database)
         return True
 
 
@@ -248,7 +262,11 @@ class Forwards(Migrator):
     def record(migration, database):
         # Record us as having done this
         record = MigrationHistory.for_migration(migration, database)
-        record.applied = datetime.datetime.utcnow()
+        try:
+            from django.utils.timezone import now
+            record.applied = now()
+        except ImportError:
+            record.applied = datetime.datetime.utcnow()
         if database != DEFAULT_DB_ALIAS:
             record.save(using=database)
         else:
@@ -256,6 +274,8 @@ class Forwards(Migrator):
             record.save()
 
     def format_backwards(self, migration):
+        if migration.no_dry_run():
+            return "   (migration cannot be dry-run; cannot discover commands)"
         old_debug, old_dry_run = south.db.db.debug, south.db.db.dry_run
         south.db.db.debug = south.db.db.dry_run = True
         stdout = sys.stdout
@@ -286,7 +306,8 @@ class Forwards(Migrator):
                     return False
         finally:
             # Call any pending post_syncdb signals
-            south.db.db.send_pending_create_signals()
+            south.db.db.send_pending_create_signals(verbosity=self.verbosity,
+                                                    interactive=self.interactive)
         return True
 
 

+ 94 - 0
desktop/core/ext-py/South-0.8.2/south/migration/utils.py

@@ -0,0 +1,94 @@
+import sys
+from collections import deque
+
+from django.utils.datastructures import SortedDict
+from django.db import models
+
+from south import exceptions
+
+
+class SortedSet(SortedDict):
+    def __init__(self, data=tuple()):
+        self.extend(data)
+
+    def __str__(self):
+        return "SortedSet(%s)" % list(self)
+
+    def add(self, value):
+        self[value] = True
+
+    def remove(self, value):
+        del self[value]
+
+    def extend(self, iterable):
+        [self.add(k) for k in iterable]
+
+
+def get_app_label(app):
+    """
+    Returns the _internal_ app label for the given app module.
+    i.e. for <module django.contrib.auth.models> will return 'auth'
+    """
+    return app.__name__.split('.')[-2]
+
+
+def app_label_to_app_module(app_label):
+    """
+    Given the app label, returns the module of the app itself (unlike models.get_app,
+    which returns the models module)
+    """
+    # Get the models module
+    app = models.get_app(app_label)
+    module_name = ".".join(app.__name__.split(".")[:-1])
+    try:
+        module = sys.modules[module_name]
+    except KeyError:
+        __import__(module_name, {}, {}, [''])
+        module = sys.modules[module_name]
+    return module
+
+
+def flatten(*stack):
+    stack = deque(stack)
+    while stack:
+        try:
+            x = next(stack[0])
+        except TypeError:
+            stack[0] = iter(stack[0])
+            x = next(stack[0])
+        except StopIteration:
+            stack.popleft()
+            continue
+        if hasattr(x, '__iter__') and not isinstance(x, str):
+            stack.appendleft(x)
+        else:
+            yield x
+
+dependency_cache = {}
+
+def _dfs(start, get_children, path):
+    if (start, get_children) in dependency_cache:
+        return dependency_cache[(start, get_children)]
+
+    results = []
+    if start in path:
+        raise exceptions.CircularDependency(path[path.index(start):] + [start])
+    path.append(start)
+    results.append(start)
+    children = sorted(get_children(start), key=lambda x: str(x))
+    
+    # We need to apply all the migrations this one depends on
+    for n in children:
+        results = _dfs(n, get_children, path) + results
+
+    path.pop()
+
+    results = list(SortedSet(results))
+    dependency_cache[(start, get_children)] = results
+    return results
+
+def dfs(start, get_children):
+    return _dfs(start, get_children, [])
+
+def depends(start, get_children):
+    return dfs(start, get_children)

+ 1 - 1
desktop/core/ext-py/south/south/models.py → desktop/core/ext-py/South-0.8.2/south/models.py

@@ -33,5 +33,5 @@ class MigrationHistory(models.Model):
     def get_migration(self):
         return self.get_migrations().migration(self.migration)
     
-    def __unicode__(self):
+    def __str__(self):
         return "<%s: %s>" % (self.app_name, self.migration)

+ 112 - 19
desktop/core/ext-py/south/south/modelsinspector.py → desktop/core/ext-py/South-0.8.2/south/modelsinspector.py

@@ -3,11 +3,14 @@ Like the old south.modelsparser, but using introspection where possible
 rather than direct inspection of models.py.
 """
 
+from __future__ import print_function
+
 import datetime
 import re
 import decimal
 
 from south.utils import get_attribute, auto_through
+from south.utils.py3 import text_type
 
 from django.db import models
 from django.db.models.base import ModelBase, Model
@@ -16,9 +19,42 @@ from django.conf import settings
 from django.utils.functional import Promise
 from django.contrib.contenttypes import generic
 from django.utils.datastructures import SortedDict
+from django.utils import datetime_safe
 
 NOISY = False
 
+try:
+    from django.utils import timezone
+except ImportError:
+    timezone = False
+
+
+# Define any converter functions first to prevent NameErrors
+
+def convert_on_delete_handler(value):
+    django_db_models_module = 'models'  # relative to standard import 'django.db'
+    if hasattr(models, "PROTECT"):
+        if value in (models.CASCADE, models.PROTECT, models.DO_NOTHING, models.SET_DEFAULT):
+            # straightforward functions
+            return '%s.%s' % (django_db_models_module, value.__name__)
+        else:
+            # This is totally dependent on the implementation of django.db.models.deletion.SET
+            func_name = getattr(value, '__name__', None)
+            if func_name == 'set_on_delete':
+                # we must inspect the function closure to see what parameters were passed in
+                closure_contents = value.__closure__[0].cell_contents
+                if closure_contents is None:
+                    return "%s.SET_NULL" % (django_db_models_module)
+                # simple function we can perhaps cope with:
+                elif hasattr(closure_contents, '__call__'):
+                    raise ValueError("South does not support on_delete with SET(function) as values.")
+                else:
+                    # Attempt to serialise the value
+                    return "%s.SET(%s)" % (django_db_models_module, value_clean(closure_contents))
+        raise ValueError("%s was not recognized as a valid model deletion handler. Possible values: %s." % (value, ', '.join(f.__name__ for f in (models.CASCADE, models.PROTECT, models.SET, models.SET_NULL, models.SET_DEFAULT, models.DO_NOTHING))))
+    else:
+        raise ValueError("on_delete argument encountered in Django version that does not support it")
+
 # Gives information about how to introspect certain fields.
 # This is a list of triples; the first item is a list of fields it applies to,
 # (note that isinstance is used, so superclasses are perfectly valid here)
@@ -30,6 +66,7 @@ NOISY = False
 # is an optional dict.
 #
 # The introspector uses the combination of all matching entries, in order.
+                                     
 introspection_details = [
     (
         (models.Field, ),
@@ -49,12 +86,13 @@ introspection_details = [
     (
         (models.ForeignKey, models.OneToOneField),
         [],
-        {
-            "to": ["rel.to", {}],
-            "to_field": ["rel.field_name", {"default_attr": "rel.to._meta.pk.name"}],
-            "related_name": ["rel.related_name", {"default": None}],
-            "db_index": ["db_index", {"default": True}],
-        },
+        dict([
+            ("to", ["rel.to", {}]),
+            ("to_field", ["rel.field_name", {"default_attr": "rel.to._meta.pk.name"}]),
+            ("related_name", ["rel.related_name", {"default": None}]),
+            ("db_index", ["db_index", {"default": True}]),
+            ("on_delete", ["rel.on_delete", {"default": getattr(models, "CASCADE", None), "is_django_function": True, "converter": convert_on_delete_handler, "ignore_missing": True}])
+        ])
     ),
     (
         (models.ManyToManyField,),
@@ -84,11 +122,19 @@ introspection_details = [
             "decimal_places": ["decimal_places", {"default": None}],
         },
     ),
+    (
+        (models.SlugField, ),
+        [],
+        {
+            "db_index": ["db_index", {"default": True}],
+        },
+    ),
     (
         (models.BooleanField, ),
         [],
         {
             "default": ["default", {"default": NOT_PROVIDED, "converter": bool}],
+            "blank": ["blank", {"default": True, "ignore_if":"primary_key"}],
         },
     ),
     (
@@ -131,11 +177,11 @@ meta_details = {
     "db_table": ["db_table", {"default_attr_concat": ["%s_%s", "app_label", "module_name"]}],
     "db_tablespace": ["db_tablespace", {"default": settings.DEFAULT_TABLESPACE}],
     "unique_together": ["unique_together", {"default": []}],
+    "index_together": ["index_together", {"default": [], "ignore_missing": True}],
+    "ordering": ["ordering", {"default": []}],
+    "proxy": ["proxy", {"default": False, "ignore_missing": True}],
 }
 
-# 2.4 compatability
-any = lambda x: reduce(lambda y, z: y or z, x, False)
-
 
 def add_introspection_rules(rules=[], patterns=[]):
     "Allows you to add some introspection rules at runtime, e.g. for 3rd party apps."
@@ -144,11 +190,13 @@ def add_introspection_rules(rules=[], patterns=[]):
     allowed_fields.extend(patterns)
     introspection_details.extend(rules)
 
+
 def add_ignored_fields(patterns):
     "Allows you to add some ignore field patterns."
     assert isinstance(patterns, (list, tuple))
     ignored_fields.extend(patterns)
     
+
 def can_ignore(field):
     """
     Returns True if we know for certain that we can ignore this field, False
@@ -160,6 +208,7 @@ def can_ignore(field):
             return True
     return False
 
+
 def can_introspect(field):
     """
     Returns True if we are allowed to introspect this field, False otherwise.
@@ -205,10 +254,17 @@ def get_value(field, descriptor):
     if options.get('is_value', False):
         value = attrname
     else:
-        value = get_attribute(field, attrname)
+        try:
+            value = get_attribute(field, attrname)
+        except AttributeError:
+            if options.get("ignore_missing", False):
+                raise IsDefault
+            else:
+                raise
+            
     # Lazy-eval functions get eval'd.
     if isinstance(value, Promise):
-        value = unicode(value)
+        value = text_type(value)
     # If the value is the same as the default, omit it for clarity
     if "default" in options and value == options['default']:
         raise IsDefault
@@ -231,16 +287,29 @@ def get_value(field, descriptor):
         default_value = format % tuple(map(lambda x: get_attribute(field, x), attrs))
         if value == default_value:
             raise IsDefault
+    # Clean and return the value
+    return value_clean(value, options)
+
+
+def value_clean(value, options={}):
+    "Takes a value and cleans it up (so e.g. it has timezone working right)"
+    # Lazy-eval functions get eval'd.
+    if isinstance(value, Promise):
+        value = text_type(value)
     # Callables get called.
-    if callable(value) and not isinstance(value, ModelBase):
+    if not options.get('is_django_function', False) and callable(value) and not isinstance(value, ModelBase):
         # Datetime.datetime.now is special, as we can access it from the eval
         # context (and because it changes all the time; people will file bugs otherwise).
         if value == datetime.datetime.now:
             return "datetime.datetime.now"
-        if value == datetime.datetime.utcnow:
+        elif value == datetime.datetime.utcnow:
             return "datetime.datetime.utcnow"
-        if value == datetime.date.today:
+        elif value == datetime.date.today:
             return "datetime.date.today"
+        # In case we use Django's own now function, revert to datetime's
+        # original one since we'll deal with timezones on our own.
+        elif timezone and value == timezone.now:
+            return "datetime.datetime.now"
         # All other callables get called.
         value = value()
     # Models get their own special repr()
@@ -257,11 +326,32 @@ def get_value(field, descriptor):
     # Make sure Decimal is converted down into a string
     if isinstance(value, decimal.Decimal):
         value = str(value)
+    # in case the value is timezone aware
+    datetime_types = (
+        datetime.datetime,
+        datetime.time,
+        datetime_safe.datetime,
+    )
+    if (timezone and isinstance(value, datetime_types) and
+            getattr(settings, 'USE_TZ', False) and
+            value is not None and timezone.is_aware(value)):
+        default_timezone = timezone.get_default_timezone()
+        value = timezone.make_naive(value, default_timezone)
+    # datetime_safe has an improper repr value
+    if isinstance(value, datetime_safe.datetime):
+        value = datetime.datetime(*value.utctimetuple()[:7])
+    # converting a date value to a datetime to be able to handle
+    # timezones later gracefully
+    elif isinstance(value, (datetime.date, datetime_safe.date)):
+        value = datetime.datetime(*value.timetuple()[:3])
     # Now, apply the converter func if there is one
     if "converter" in options:
         value = options['converter'](value)
     # Return the final value
-    return repr(value)
+    if options.get('is_django_function', False):
+        return value
+    else:
+        return repr(value)
 
 
 def introspector(field):
@@ -295,7 +385,7 @@ def get_model_fields(model, m2m=False):
     
     # Go through all bases (that are themselves models, but not Model)
     for base in model.__bases__:
-        if base != models.Model and issubclass(base, models.Model):
+        if hasattr(base, '_meta') and issubclass(base, models.Model):
             if not base._meta.abstract:
                 # Looks like we need their fields, Ma.
                 inherited_fields.update(get_model_fields(base))
@@ -312,7 +402,7 @@ def get_model_fields(model, m2m=False):
         # Does it define a south_field_triple method?
         if hasattr(field, "south_field_triple"):
             if NOISY:
-                print " ( Nativing field: %s" % field.name
+                print(" ( Nativing field: %s" % field.name)
             field_defs[field.name] = field.south_field_triple()
         # Can we introspect it?
         elif can_introspect(field):
@@ -320,12 +410,15 @@ def get_model_fields(model, m2m=False):
             field_class = field.__class__.__module__ + "." + field.__class__.__name__
             # Run this field through the introspector
             args, kwargs = introspector(field)
+            # Workaround for Django bug #13987
+            if model._meta.pk.column == field.column and 'primary_key' not in kwargs:
+                kwargs['primary_key'] = True
             # That's our definition!
             field_defs[field.name] = (field_class, args, kwargs)
         # Shucks, no definition!
         else:
             if NOISY:
-                print " ( Nodefing field: %s" % field.name
+                print(" ( Nodefing field: %s" % field.name)
             field_defs[field.name] = None
     
     # If they've used the horrific hack that is order_with_respect_to, deal with
@@ -353,7 +446,7 @@ def get_model_meta(model):
     # This is called _ormbases as the _bases variable was previously used
     # for a list of full class paths to bases, so we can't conflict.
     for base in model.__bases__:
-        if base != models.Model and issubclass(base, models.Model):
+        if hasattr(base, '_meta') and issubclass(base, models.Model):
             if not base._meta.abstract:
                 # OK, that matches our terms.
                 if "_ormbases" not in meta_def:

+ 21 - 15
desktop/core/ext-py/south/south/orm.py → desktop/core/ext-py/South-0.8.2/south/orm.py

@@ -3,17 +3,19 @@ South's fake ORM; lets you not have to write SQL inside migrations.
 Roughly emulates the real Django ORM, to a point.
 """
 
+from __future__ import print_function
+
 import inspect
-import datetime
 
 from django.db import models
 from django.db.models.loading import cache
 from django.core.exceptions import ImproperlyConfigured
 
 from south.db import db
-from south.utils import ask_for_it_by_name
+from south.utils import ask_for_it_by_name, datetime_utils
 from south.hacks import hacks
 from south.exceptions import UnfreezeMeLater, ORMBaseNotIncluded, ImpossibleORMUnfreeze
+from south.utils.py3 import string_types
 
 
 class ModelsLocals(object):
@@ -148,7 +150,7 @@ class _FakeORM(object):
         try:
             return self.models[fullname]
         except KeyError:
-            raise AttributeError("The model '%s' from the app '%s' is not available in this migration." % (key, self.default_app))
+            raise AttributeError("The model '%s' from the app '%s' is not available in this migration. (Did you use orm.ModelName, not orm['app.ModelName']?)" % (key, self.default_app))
     
     
     def __getitem__(self, key):
@@ -179,12 +181,16 @@ class _FakeORM(object):
         "Evaluates the given code in the context of the migration file."
         
         # Drag in the migration module's locals (hopefully including models.py)
-        fake_locals = dict(inspect.getmodule(self.cls).__dict__)
-        
-        # Remove all models from that (i.e. from modern models.py), to stop pollution
-        for key, value in fake_locals.items():
-            if isinstance(value, type) and issubclass(value, models.Model) and hasattr(value, "_meta"):
-                del fake_locals[key]
+        # excluding all models from that (i.e. from modern models.py), to stop pollution
+        fake_locals = dict(
+            (key, value)
+            for key, value in inspect.getmodule(self.cls).__dict__.items()
+            if not (
+                isinstance(value, type)
+                and issubclass(value, models.Model)
+                and hasattr(value, "_meta")
+            )
+        )
         
         # We add our models into the locals for the eval
         fake_locals.update(dict([
@@ -206,7 +212,7 @@ class _FakeORM(object):
         fake_locals['_'] = lambda x: x
         
         # Datetime; there should be no datetime direct accesses
-        fake_locals['datetime'] = datetime
+        fake_locals['datetime'] = datetime_utils
         
         # Now, go through the requested imports and import them.
         for name, value in extra_imports.items():
@@ -228,7 +234,7 @@ class _FakeORM(object):
                 if name == "SouthFieldClass":
                     raise ValueError("Cannot import the required field '%s'" % value)
                 else:
-                    print "WARNING: Cannot import '%s'" % value
+                    print("WARNING: Cannot import '%s'" % value)
         
         # Use ModelsLocals to make lookups work right for CapitalisedModels
         fake_locals = ModelsLocals(fake_locals)
@@ -249,7 +255,7 @@ class _FakeORM(object):
             # OK, add it.
             try:
                 results[key] = self.eval_in_context(code, app)
-            except (NameError, AttributeError), e:
+            except (NameError, AttributeError) as e:
                 raise ValueError("Cannot successfully create meta field '%s' for model '%s.%s': %s." % (
                     key, app, model, e
                 ))
@@ -266,7 +272,7 @@ class _FakeORM(object):
                 key = key.lower()
                 if key not in self.models:
                     raise ORMBaseNotIncluded("Cannot find ORM base %s" % key)
-                elif isinstance(self.models[key], basestring):
+                elif isinstance(self.models[key], string_types):
                     # Then the other model hasn't been unfrozen yet.
                     # We postpone ourselves; the situation will eventually resolve.
                     raise UnfreezeMeLater()
@@ -295,7 +301,7 @@ class _FakeORM(object):
                 continue
             elif not params:
                 raise ValueError("Field '%s' on model '%s.%s' has no definition." % (fname, app, name))
-            elif isinstance(params, (str, unicode)):
+            elif isinstance(params, string_types):
                 # It's a premade definition string! Let's hope it works...
                 code = params
                 extra_imports = {}
@@ -365,7 +371,7 @@ class _FakeORM(object):
                 for fname, (code, extra_imports) in model._failed_fields.items():
                     try:
                         field = self.eval_in_context(code, app, extra_imports)
-                    except (NameError, AttributeError, AssertionError, KeyError), e:
+                    except (NameError, AttributeError, AssertionError, KeyError) as e:
                         # It's failed again. Complain.
                         raise ValueError("Cannot successfully create field '%s' for model '%s': %s." % (
                             fname, modelname, e

+ 24 - 0
desktop/core/ext-py/South-0.8.2/south/signals.py

@@ -0,0 +1,24 @@
+"""
+South-specific signals
+"""
+
+from django.dispatch import Signal
+from django.conf import settings
+
+# Sent at the start of the migration of an app
+pre_migrate = Signal(providing_args=["app"])
+
+# Sent after each successful migration of an app
+post_migrate = Signal(providing_args=["app"])
+
+# Sent after each run of a particular migration in a direction
+ran_migration = Signal(providing_args=["app","migration","method"])
+
+# Compatibility code for django.contrib.auth
+# Is causing strange errors, removing for now (we might need to fix up orm first)
+#if 'django.contrib.auth' in settings.INSTALLED_APPS:
+    #def create_permissions_compat(app, **kwargs):
+        #from django.db.models import get_app
+        #from django.contrib.auth.management import create_permissions
+        #create_permissions(get_app(app), (), 0)
+    #post_migrate.connect(create_permissions_compat)

+ 108 - 0
desktop/core/ext-py/South-0.8.2/south/tests/__init__.py

@@ -0,0 +1,108 @@
+from __future__ import print_function
+
+#import unittest
+import os
+import sys
+from functools import wraps
+from django.conf import settings
+from south.hacks import hacks
+
+# Make sure skipping tests is available.
+try:
+    # easiest and best is unittest included in Django>=1.3
+    from django.utils import unittest
+except ImportError:
+    # earlier django... use unittest from stdlib
+    import unittest
+# however, skipUnless was only added in Python 2.7;
+# if not available, we need to do something else
+try:
+    skipUnless = unittest.skipUnless #@UnusedVariable
+except AttributeError:
+    def skipUnless(condition, message):
+        def decorator(testfunc):
+            @wraps(testfunc)
+            def wrapper(self):
+                if condition:
+                    # Apply method
+                    testfunc(self)
+                else:
+                    # The skip exceptions are not available either...
+                    print("Skipping", testfunc.__name__,"--", message)
+            return wrapper
+        return decorator
+
+# ditto for skipIf
+try:
+    skipIf = unittest.skipIf #@UnusedVariable
+except AttributeError:
+    def skipIf(condition, message):
+        def decorator(testfunc):
+            @wraps(testfunc)
+            def wrapper(self):
+                if condition:
+                    print("Skipping", testfunc.__name__,"--", message)
+                else:
+                    # Apply method
+                    testfunc(self)
+            return wrapper
+        return decorator
+
+# Add the tests directory so fakeapp is on sys.path
+test_root = os.path.dirname(__file__)
+sys.path.append(test_root)
+
+# Note: the individual test files are imported below this.
+
+class Monkeypatcher(unittest.TestCase):
+
+    """
+    Base test class for tests that play with the INSTALLED_APPS setting at runtime.
+    """
+
+    def create_fake_app(self, name):
+        
+        class Fake:
+            pass
+        
+        fake = Fake()
+        fake.__name__ = name
+        try:
+            fake.migrations = __import__(name + ".migrations", {}, {}, ['migrations'])
+        except ImportError:
+            pass
+        return fake
+
+    def setUp(self):
+        """
+        Changes the Django environment so we can run tests against our test apps.
+        """
+        if hasattr(self, 'installed_apps'):
+            hacks.store_app_cache_state()
+            hacks.set_installed_apps(self.installed_apps)
+            # Make sure dependencies are calculated for new apps
+            Migrations._dependencies_done = False
+
+    def tearDown(self):
+        """
+        Undoes what setUp did.
+        """
+        if hasattr(self, 'installed_apps'):
+            hacks.reset_installed_apps()
+            hacks.restore_app_cache_state()
+
+
+# Try importing all tests if asked for (then we can run 'em)
+try:
+    skiptest = settings.SKIP_SOUTH_TESTS
+except:
+    skiptest = True
+
+if not skiptest:
+    from south.tests.db import *
+    from south.tests.db_mysql import *
+    from south.tests.logic import *
+    from south.tests.autodetection import *
+    from south.tests.logger import *
+    from south.tests.inspector import *
+    from south.tests.freezer import *

+ 123 - 3
desktop/core/ext-py/south/south/tests/autodetection.py → desktop/core/ext-py/South-0.8.2/south/tests/autodetection.py

@@ -1,6 +1,11 @@
-import unittest
+from south.tests import unittest
 
-from south.creator.changes import AutoChanges
+from south.creator.changes import AutoChanges, InitialChanges
+from south.migration.base import Migrations
+from south.tests import Monkeypatcher
+from south.creator import freezer
+from south.orm import FakeORM
+from south.v2 import SchemaMigration
 
 class TestComparison(unittest.TestCase):
     
@@ -230,4 +235,119 @@ class TestComparison(unittest.TestCase):
                 ('django.db.models.fields.IntField', [], {'to':'hah'}),
             ),
             True,
-        )
+        )
+
+class TestNonManagedIgnored(Monkeypatcher):
+    
+    installed_apps = ["non_managed"]
+
+    full_defs = {
+        'non_managed.legacy': {
+            'Meta': {'object_name': 'Legacy', 'db_table': "'legacy_table'", 'managed': 'False'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True'}),
+            'size': ('django.db.models.fields.IntegerField', [], {})
+        }
+    } 
+
+    def test_not_added_init(self):
+        
+        migrations = Migrations("non_managed")
+        changes = InitialChanges(migrations)
+        change_list = changes.get_changes()
+        if list(change_list):
+            self.fail("Initial migration creates table for non-managed model")
+
+    def test_not_added_auto(self):
+
+        empty_defs = { }
+        class EmptyMigration(SchemaMigration):
+            "Serves as fake previous migration"
+        
+            def forwards(self, orm):
+                pass
+        
+            def backwards(self, orm):
+                pass
+        
+            models = empty_defs
+
+            complete_apps = ['non_managed']
+                    
+        migrations = Migrations("non_managed")
+        empty_orm = FakeORM(EmptyMigration, "non_managed")
+        changes = AutoChanges(
+            migrations = migrations,
+            old_defs = empty_defs,
+            old_orm = empty_orm,
+            new_defs = self.full_defs,
+        )
+        change_list = changes.get_changes()
+        if list(change_list):
+            self.fail("Auto migration creates table for non-managed model")
+
+    def test_not_deleted_auto(self):
+
+        empty_defs = { }
+        old_defs = freezer.freeze_apps(["non_managed"])
+        class InitialMigration(SchemaMigration):
+            "Serves as fake previous migration"
+        
+            def forwards(self, orm):
+                pass
+        
+            def backwards(self, orm):
+                pass
+        
+            models = self.full_defs
+
+            complete_apps = ['non_managed']
+                    
+        migrations = Migrations("non_managed")
+        initial_orm = FakeORM(InitialMigration, "non_managed")
+        changes = AutoChanges(
+            migrations = migrations,
+            old_defs = self.full_defs,
+            old_orm = initial_orm,
+            new_defs = empty_defs,
+        )
+        change_list = changes.get_changes()
+        if list(change_list):
+            self.fail("Auto migration deletes table for non-managed model")
+
+    def test_not_modified_auto(self):
+
+        fake_defs = {
+            'non_managed.legacy': {
+                'Meta': {'object_name': 'Legacy', 'db_table': "'legacy_table'", 'managed': 'False'},
+                'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+                'name': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True'}),
+                #'size': ('django.db.models.fields.IntegerField', [], {}) # The "change" is the addition of this field
+            }
+        } 
+        class InitialMigration(SchemaMigration):
+            "Serves as fake previous migration"
+        
+            def forwards(self, orm):
+                pass
+        
+            def backwards(self, orm):
+                pass
+        
+            models = fake_defs
+
+            complete_apps = ['non_managed']
+                    
+        from non_managed import models as dummy_import_to_force_loading_models # TODO: Does needing this indicate a bug in MokeyPatcher? 
+        
+        migrations = Migrations("non_managed")
+        initial_orm = FakeORM(InitialMigration, "non_managed")
+        changes = AutoChanges(
+            migrations = migrations,
+            old_defs = fake_defs,
+            old_orm = initial_orm,
+            new_defs = self.full_defs
+        )
+        change_list = changes.get_changes()
+        if list(change_list):
+            self.fail("Auto migration changes table for non-managed model")

+ 0 - 0
desktop/core/ext-py/south/south/management/commands/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/brokenapp/migrations/0001_depends_on_unmigrated.py → desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/migrations/0001_depends_on_unmigrated.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/brokenapp/migrations/0002_depends_on_unknown.py → desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/migrations/0002_depends_on_unknown.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/brokenapp/migrations/0003_depends_on_higher.py → desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/migrations/0003_depends_on_higher.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/brokenapp/migrations/0004_higher.py → desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/migrations/0004_higher.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/brokenapp/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/migrations/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/brokenapp/models.py → desktop/core/ext-py/South-0.8.2/south/tests/brokenapp/models.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/brokenapp/migrations/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/circular_a/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/circular_a/migrations/0001_first.py → desktop/core/ext-py/South-0.8.2/south/tests/circular_a/migrations/0001_first.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/circular_a/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/circular_a/migrations/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/circular_a/models.py → desktop/core/ext-py/South-0.8.2/south/tests/circular_a/models.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/circular_a/migrations/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/circular_b/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/circular_b/migrations/0001_first.py → desktop/core/ext-py/South-0.8.2/south/tests/circular_b/migrations/0001_first.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/circular_b/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/circular_b/migrations/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/circular_b/models.py → desktop/core/ext-py/South-0.8.2/south/tests/circular_b/models.py


+ 1031 - 0
desktop/core/ext-py/South-0.8.2/south/tests/db.py

@@ -0,0 +1,1031 @@
+import datetime
+from warnings import filterwarnings
+
+from south.db import db, generic
+from django.db import connection, models, IntegrityError as DjangoIntegrityError
+
+from south.tests import unittest, skipIf, skipUnless
+from south.utils.py3 import text_type, with_metaclass
+
+# Create a list of error classes from the various database libraries
+errors = []
+try:
+    from psycopg2 import ProgrammingError
+    errors.append(ProgrammingError)
+except ImportError:
+    pass
+errors = tuple(errors)
+
+# On SQL Server, the backend's IntegrityError is not (a subclass of) Django's.
+try:
+    from sql_server.pyodbc.base import IntegrityError as SQLServerIntegrityError
+    IntegrityError = (DjangoIntegrityError, SQLServerIntegrityError)
+except ImportError:
+    IntegrityError = DjangoIntegrityError
+
+try:
+    from south.db import mysql
+except ImportError:
+    mysql = None
+
+
+class TestOperations(unittest.TestCase):
+
+    """
+    Tests if the various DB abstraction calls work.
+    Can only test a limited amount due to DB differences.
+    """
+
+    def setUp(self):
+        db.debug = False
+        try:
+            import MySQLdb
+        except ImportError:
+            pass
+        else:
+            filterwarnings('ignore', category=MySQLdb.Warning)
+        db.clear_deferred_sql()
+        db.start_transaction()
+    
+    def tearDown(self):
+        db.rollback_transaction()
+
+    def test_create(self):
+        """
+        Test creation of tables.
+        """
+        cursor = connection.cursor()
+        # It needs to take at least 2 args
+        self.assertRaises(TypeError, db.create_table)
+        self.assertRaises(TypeError, db.create_table, "test1")
+        # Empty tables (i.e. no columns) are not fine, so make at least 1
+        db.create_table("test1", [('email_confirmed', models.BooleanField(default=False))])
+        # And should exist
+        cursor.execute("SELECT * FROM test1")
+        # Make sure we can't do the same query on an empty table
+        try:
+            cursor.execute("SELECT * FROM nottheretest1")
+        except:
+            pass
+        else:
+            self.fail("Non-existent table could be selected!")
+    
+    @skipUnless(db.raises_default_errors, 'This database does not raise errors on missing defaults.')
+    def test_create_default(self):
+        """
+        Test creation of tables, make sure defaults are not left in the database
+        """
+        db.create_table("test_create_default", [('a', models.IntegerField()),
+                                                ('b', models.IntegerField(default=17))])
+        cursor = connection.cursor()
+        self.assertRaises(IntegrityError, cursor.execute, "INSERT INTO test_create_default(a) VALUES (17)")
+        
+    def test_delete(self):
+        """
+        Test deletion of tables.
+        """
+        cursor = connection.cursor()
+        db.create_table("test_deltable", [('email_confirmed', models.BooleanField(default=False))])
+        db.delete_table("test_deltable")
+        # Make sure it went
+        try:
+            cursor.execute("SELECT * FROM test_deltable")
+        except:
+            pass
+        else:
+            self.fail("Just-deleted table could be selected!")
+    
+    def test_nonexistent_delete(self):
+        """
+        Test deletion of nonexistent tables.
+        """
+        try:
+            db.delete_table("test_nonexistdeltable")
+        except:
+            pass
+        else:
+            self.fail("Non-existent table could be deleted!")
+    
+    def test_foreign_keys(self):
+        """
+        Tests foreign key creation, especially uppercase (see #61)
+        """
+        Test = db.mock_model(model_name='Test', db_table='test5a',
+                             db_tablespace='', pk_field_name='ID',
+                             pk_field_type=models.AutoField, pk_field_args=[])
+        db.create_table("test5a", [('ID', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True))])
+        db.create_table("test5b", [
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('UNIQUE', models.ForeignKey(Test)),
+        ])
+        db.execute_deferred_sql()
+        
+    @skipUnless(db.supports_foreign_keys, 'Foreign keys can only be deleted on '
+                                          'engines that support them.')
+    def test_recursive_foreign_key_delete(self):
+        """
+        Test that recursive foreign keys are deleted correctly (see #1065)
+        """
+        Test = db.mock_model(model_name='Test', db_table='test_rec_fk_del',
+                             db_tablespace='', pk_field_name='id',
+                             pk_field_type=models.AutoField, pk_field_args=[])
+        db.create_table('test_rec_fk_del', [
+            ('id', models.AutoField(primary_key=True, auto_created=True)),
+            ('fk', models.ForeignKey(Test)),
+        ])
+        db.execute_deferred_sql()
+        db.delete_foreign_key('test_rec_fk_del', 'fk_id')
+    
+    def test_rename(self):
+        """
+        Test column renaming
+        """
+        cursor = connection.cursor()
+        db.create_table("test_rn", [('spam', models.BooleanField(default=False))])
+        # Make sure we can select the column
+        cursor.execute("SELECT spam FROM test_rn")
+        # Rename it
+        db.rename_column("test_rn", "spam", "eggs")
+        cursor.execute("SELECT eggs FROM test_rn")
+        db.commit_transaction()
+        db.start_transaction()
+        try:
+            cursor.execute("SELECT spam FROM test_rn")
+        except:
+            pass
+        else:
+            self.fail("Just-renamed column could be selected!")
+        db.rollback_transaction()
+        db.delete_table("test_rn")
+        db.start_transaction()
+    
+    def test_dry_rename(self):
+        """
+        Test column renaming while --dry-run is turned on (should do nothing)
+        See ticket #65
+        """
+        cursor = connection.cursor()
+        db.create_table("test_drn", [('spam', models.BooleanField(default=False))])
+        # Make sure we can select the column
+        cursor.execute("SELECT spam FROM test_drn")
+        # Rename it
+        db.dry_run = True
+        db.rename_column("test_drn", "spam", "eggs")
+        db.dry_run = False
+        cursor.execute("SELECT spam FROM test_drn")
+        db.commit_transaction()
+        db.start_transaction()
+        try:
+            cursor.execute("SELECT eggs FROM test_drn")
+        except:
+            pass
+        else:
+            self.fail("Dry-renamed new column could be selected!")
+        db.rollback_transaction()
+        db.delete_table("test_drn")
+        db.start_transaction()
+    
+    def test_table_rename(self):
+        """
+        Test column renaming
+        """
+        cursor = connection.cursor()
+        db.create_table("testtr", [('spam', models.BooleanField(default=False))])
+        # Make sure we can select the column
+        cursor.execute("SELECT spam FROM testtr")
+        # Rename it
+        db.rename_table("testtr", "testtr2")
+        cursor.execute("SELECT spam FROM testtr2")
+        db.commit_transaction()
+        db.start_transaction()
+        try:
+            cursor.execute("SELECT spam FROM testtr")
+        except:
+            pass
+        else:
+            self.fail("Just-renamed column could be selected!")
+        db.rollback_transaction()
+        db.delete_table("testtr2")
+        db.start_transaction()
+    
+    def test_percents_in_defaults(self):
+        """
+        Test that % in a default gets escaped to %%.
+        """
+        try:
+            db.create_table("testpind", [('cf', models.CharField(max_length=255, default="It should be 2%!"))])
+        except IndexError:
+            self.fail("% was not properly escaped in column SQL.")
+        db.delete_table("testpind")
+    
+    def test_index(self):
+        """
+        Test the index operations
+        """
+        db.create_table("test3", [
+            ('SELECT', models.BooleanField(default=False)),
+            ('eggs', models.IntegerField(unique=True)),
+        ])
+        db.execute_deferred_sql()
+        # Add an index on that column
+        db.create_index("test3", ["SELECT"])
+        # Add another index on two columns
+        db.create_index("test3", ["SELECT", "eggs"])
+        # Delete them both
+        db.delete_index("test3", ["SELECT"])
+        db.delete_index("test3", ["SELECT", "eggs"])
+        # Delete the unique index/constraint
+        if db.backend_name != "sqlite3":
+            db.delete_unique("test3", ["eggs"])
+        db.delete_table("test3")
+    
+    def test_primary_key(self):
+        """
+        Test the primary key operations
+        """
+        
+        db.create_table("test_pk", [
+            ('id', models.IntegerField(primary_key=True)),
+            ('new_pkey', models.IntegerField()),
+            ('eggs', models.IntegerField(unique=True)),
+        ])
+        db.execute_deferred_sql()
+        # Remove the default primary key, and make eggs it
+        db.delete_primary_key("test_pk")
+        db.create_primary_key("test_pk", "new_pkey")
+        # Try inserting a now-valid row pair
+        db.execute("INSERT INTO test_pk (id, new_pkey, eggs) VALUES (1, 2, 3)")
+        db.execute("INSERT INTO test_pk (id, new_pkey, eggs) VALUES (1, 3, 4)")
+        db.delete_table("test_pk")
+    
+    def test_primary_key_implicit(self):
+        """
+        Tests that changing primary key implicitly fails.
+        """
+        db.create_table("test_pki", [
+            ('id', models.IntegerField(primary_key=True)),
+            ('new_pkey', models.IntegerField()),
+            ('eggs', models.IntegerField(unique=True)),
+        ])
+        db.execute_deferred_sql()
+        # Fiddle with alter_column to attempt to make it remove the primary key
+        db.alter_column("test_pki", "id", models.IntegerField())
+        db.alter_column("test_pki", "new_pkey", models.IntegerField(primary_key=True))
+        # Try inserting a should-be-valid row pair
+        db.execute("INSERT INTO test_pki (id, new_pkey, eggs) VALUES (1, 2, 3)")
+        db.execute("INSERT INTO test_pki (id, new_pkey, eggs) VALUES (2, 2, 4)")
+        db.delete_table("test_pki")
+    
+    def test_add_columns(self):
+        """
+        Test adding columns
+        """
+        db.create_table("test_addc", [
+            ('spam', models.BooleanField(default=False)),
+            ('eggs', models.IntegerField()),
+        ])
+        # Add a column
+        db.add_column("test_addc", "add1", models.IntegerField(default=3))
+        User = db.mock_model(model_name='User', db_table='auth_user', db_tablespace='', pk_field_name='id', pk_field_type=models.AutoField, pk_field_args=[], pk_field_kwargs={})
+        # insert some data so we can test the default value of the added fkey
+        db.execute("INSERT INTO test_addc (spam, eggs, add1) VALUES (%s, 1, 2)", [False])
+        db.add_column("test_addc", "user", models.ForeignKey(User, null=True))
+        db.execute_deferred_sql()
+        # try selecting from the user_id column to make sure it was actually created
+        val = db.execute("SELECT user_id FROM test_addc")[0][0]
+        self.assertEquals(val, None)
+        db.delete_column("test_addc", "add1")
+        # make sure adding an indexed field works
+        db.add_column("test_addc", "add2", models.CharField(max_length=15, db_index=True, default='pi'))
+        db.execute_deferred_sql()
+        db.delete_table("test_addc")
+
+    def test_delete_columns(self):
+        """
+        Test deleting columns
+        """
+        db.create_table("test_delc", [
+            ('spam', models.BooleanField(default=False)),
+            ('eggs', models.IntegerField(db_index=True, unique=True)),
+        ])
+        db.delete_column("test_delc", "eggs")
+
+    def test_add_nullbool_column(self):
+        """
+        Test adding NullBoolean columns
+        """
+        db.create_table("test_addnbc", [
+            ('spam', models.BooleanField(default=False)),
+            ('eggs', models.IntegerField()),
+        ])
+        # Add a column
+        db.add_column("test_addnbc", "add1", models.NullBooleanField())
+        # Add a column with a default
+        db.add_column("test_addnbc", "add2", models.NullBooleanField(default=True))
+        # insert some data so we can test the default values of the added column
+        db.execute("INSERT INTO test_addnbc (spam, eggs) VALUES (%s, 1)", [False])
+        # try selecting from the new columns to make sure they were properly created
+        false, null1, null2 = db.execute("SELECT spam,add1,add2 FROM test_addnbc")[0][0:3]
+        self.assertIsNone(null1, "Null boolean field with no value inserted returns non-null")
+        self.assertIsNone(null2, "Null boolean field (added with default) with no value inserted returns non-null")
+        self.assertEquals(false, False)
+        db.delete_table("test_addnbc")
+    
+    def test_alter_columns(self):
+        """
+        Test altering columns
+        """
+        db.create_table("test_alterc", [
+            ('spam', models.BooleanField(default=False)),
+            ('eggs', models.IntegerField()),
+        ])
+        db.execute_deferred_sql()
+        # Change eggs to be a FloatField
+        db.alter_column("test_alterc", "eggs", models.FloatField())
+        db.execute_deferred_sql()
+        db.delete_table("test_alterc")
+        db.execute_deferred_sql()
+    
+    def test_alter_char_default(self):
+        """
+        Test altering column defaults with char fields
+        """
+        db.create_table("test_altercd", [
+            ('spam', models.CharField(max_length=30)),
+            ('eggs', models.IntegerField()),
+        ])
+        # Change spam default
+        db.alter_column("test_altercd", "spam", models.CharField(max_length=30, default="loof", null=True))
+        # Assert the default is not in the database
+        db.execute("INSERT INTO test_altercd (eggs) values (12)")
+        null = db.execute("SELECT spam FROM test_altercd")[0][0]
+        self.assertFalse(null, "Default for char field was installed into database")
+
+        # Change again to a column with default and not null
+        db.alter_column("test_altercd", "spam", models.CharField(max_length=30, default="loof", null=False))
+        # Assert the default is not in the database
+        if 'oracle' in db.backend_name:
+            # Oracle special treatment -- nulls are always allowed in char columns, so 
+            # inserting doesn't raise an integrity error; so we check again as above
+            db.execute("DELETE FROM test_altercd")
+            db.execute("INSERT INTO test_altercd (eggs) values (12)")
+            null = db.execute("SELECT spam FROM test_altercd")[0][0]
+            self.assertFalse(null, "Default for char field was installed into database")
+        else:
+            # For other backends, insert should now just fail
+            self.assertRaises(IntegrityError,
+                              db.execute, "INSERT INTO test_altercd (eggs) values (12)")
+
+    @skipIf('oracle' in db.backend_name, "Oracle does not differentiate empty trings from null")
+    def test_default_empty_string(self):
+        """
+        Test altering column defaults with char fields
+        """
+        db.create_table("test_cd_empty", [
+            ('spam', models.CharField(max_length=30, default='')),
+            ('eggs', models.CharField(max_length=30)),
+        ])
+        # Create a record
+        db.execute("INSERT INTO test_cd_empty (spam, eggs) values ('1','2')")
+        # Add a column
+        db.add_column("test_cd_empty", "ham", models.CharField(max_length=30, default=''))
+        
+        empty = db.execute("SELECT ham FROM test_cd_empty")[0][0]
+        self.assertEquals(empty, "", "Empty Default for char field isn't empty string")
+        
+    @skipUnless('oracle' in db.backend_name, "Oracle does not differentiate empty trings from null")
+    def test_oracle_strings_null(self):
+        """
+        Test that under Oracle, CherFields are created as null even when specified not-null,
+        because otherwise they would not be able to hold empty strings (which Oracle equates
+        with nulls).
+        Verify fix of #1269.
+        """
+        db.create_table("test_ora_char_nulls", [
+            ('spam', models.CharField(max_length=30, null=True)),
+            ('eggs', models.CharField(max_length=30)),
+        ])
+        db.add_column("test_ora_char_nulls", "ham", models.CharField(max_length=30))
+        db.alter_column("test_ora_char_nulls", "spam", models.CharField(max_length=30, null=False))
+        # So, by the look of it, we should now have three not-null columns
+        db.execute("INSERT INTO test_ora_char_nulls VALUES (NULL, NULL, NULL)")
+        
+
+    def test_mysql_defaults(self):
+        """
+        Test MySQL default handling for BLOB and TEXT.
+        """
+        db.create_table("test_altermyd", [
+            ('spam', models.BooleanField(default=False)),
+            ('eggs', models.TextField()),
+        ])
+        # Change eggs to be a FloatField
+        db.alter_column("test_altermyd", "eggs", models.TextField(null=True))
+        db.delete_table("test_altermyd")
+    
+    def test_alter_column_postgres_multiword(self):
+        """
+        Tests altering columns with multiple words in Postgres types (issue #125)
+        e.g. 'datetime with time zone', look at django/db/backends/postgresql/creation.py
+        """
+        db.create_table("test_multiword", [
+            ('col_datetime', models.DateTimeField(null=True)),
+            ('col_integer', models.PositiveIntegerField(null=True)),
+            ('col_smallint', models.PositiveSmallIntegerField(null=True)),
+            ('col_float', models.FloatField(null=True)),
+        ])
+        
+        # test if 'double precision' is preserved
+        db.alter_column('test_multiword', 'col_float', models.FloatField('float', null=True))
+
+        # test if 'CHECK ("%(column)s" >= 0)' is stripped
+        db.alter_column('test_multiword', 'col_integer', models.PositiveIntegerField(null=True))
+        db.alter_column('test_multiword', 'col_smallint', models.PositiveSmallIntegerField(null=True))
+
+        # test if 'with timezone' is preserved
+        if db.backend_name == "postgres":
+            db.execute("INSERT INTO test_multiword (col_datetime) VALUES ('2009-04-24 14:20:55+02')")
+            db.alter_column('test_multiword', 'col_datetime', models.DateTimeField(auto_now=True))
+            assert db.execute("SELECT col_datetime = '2009-04-24 14:20:55+02' FROM test_multiword")[0][0]
+
+        db.delete_table("test_multiword")
+    
+    @skipUnless(db.has_check_constraints, 'Only applies to databases that '
+                                          'support CHECK constraints.')
+    def test_alter_constraints(self):
+        """
+        Tests that going from a PostiveIntegerField to an IntegerField drops
+        the constraint on the database.
+        """
+        # Make the test table
+        db.create_table("test_alterc", [
+            ('num', models.PositiveIntegerField()),
+        ])
+        db.execute_deferred_sql()
+        # Add in some test values
+        db.execute("INSERT INTO test_alterc (num) VALUES (1)")
+        db.execute("INSERT INTO test_alterc (num) VALUES (2)")
+        # Ensure that adding a negative number is bad
+        db.commit_transaction()
+        db.start_transaction()
+        try:
+            db.execute("INSERT INTO test_alterc (num) VALUES (-3)")
+        except:
+            db.rollback_transaction()
+        else:
+            self.fail("Could insert a negative integer into a PositiveIntegerField.")
+        # Alter it to a normal IntegerField
+        db.alter_column("test_alterc", "num", models.IntegerField())
+        db.execute_deferred_sql()
+        # It should now work
+        db.execute("INSERT INTO test_alterc (num) VALUES (-3)")
+        db.delete_table("test_alterc")
+        # We need to match up for tearDown
+        db.start_transaction()
+    
+    @skipIf(db.backend_name == "sqlite3", "SQLite backend doesn't support this "
+                                          "yet.")
+    def test_unique(self):
+        """
+        Tests creating/deleting unique constraints.
+        """
+        db.create_table("test_unique2", [
+            ('id', models.AutoField(primary_key=True)),
+        ])
+        db.create_table("test_unique", [
+            ('spam', models.BooleanField(default=False)),
+            ('eggs', models.IntegerField()),
+            ('ham', models.ForeignKey(db.mock_model('Unique2', 'test_unique2'))),
+        ])
+        db.execute_deferred_sql()
+        # Add a constraint
+        db.create_unique("test_unique", ["spam"])
+        db.execute_deferred_sql()
+        # Shouldn't do anything during dry-run
+        db.dry_run = True
+        db.delete_unique("test_unique", ["spam"])
+        db.dry_run = False
+        db.delete_unique("test_unique", ["spam"])
+        db.create_unique("test_unique", ["spam"])
+        # Special preparations for Sql Server
+        if db.backend_name == "pyodbc":
+            db.execute("SET IDENTITY_INSERT test_unique2 ON;")
+        db.execute("INSERT INTO test_unique2 (id) VALUES (1)")
+        db.execute("INSERT INTO test_unique2 (id) VALUES (2)")
+        db.commit_transaction()
+        db.start_transaction()
+
+        
+        # Test it works
+        TRUE = (True,)
+        FALSE = (False,)
+        db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 0, 1)", TRUE)
+        db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 1, 2)", FALSE)
+        try:
+            db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 2, 1)", FALSE)
+        except:
+            db.rollback_transaction()
+        else:
+            self.fail("Could insert non-unique item.")
+        
+        # Drop that, add one only on eggs
+        db.delete_unique("test_unique", ["spam"])
+        db.execute("DELETE FROM test_unique")
+        db.create_unique("test_unique", ["eggs"])
+        db.start_transaction()
+        
+        # Test similarly
+        db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 0, 1)", TRUE)
+        db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 1, 2)", FALSE)
+        try:
+            db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 1, 1)", TRUE)
+        except:
+            db.rollback_transaction()
+        else:
+            self.fail("Could insert non-unique item.")
+        
+        # Drop those, test combined constraints
+        db.delete_unique("test_unique", ["eggs"])
+        db.execute("DELETE FROM test_unique")
+        db.create_unique("test_unique", ["spam", "eggs", "ham_id"])
+        db.start_transaction()
+        # Test similarly
+        db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 0, 1)", TRUE)
+        db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 1, 1)", FALSE)
+        try:
+            db.execute("INSERT INTO test_unique (spam, eggs, ham_id) VALUES (%s, 0, 1)", TRUE)
+        except:
+            db.rollback_transaction()
+        else:
+            self.fail("Could insert non-unique pair.")
+        db.delete_unique("test_unique", ["spam", "eggs", "ham_id"])
+        db.start_transaction()
+    
+    def test_alter_unique(self):
+        """
+        Tests that unique constraints are not affected when
+        altering columns (that's handled by create_/delete_unique)
+        """
+        db.create_table("test_alter_unique", [
+            ('spam', models.IntegerField()),
+            ('eggs', models.IntegerField(unique=True)),
+        ])
+        db.execute_deferred_sql()
+        
+        # Make sure the unique constraint is created
+        db.execute('INSERT INTO test_alter_unique (spam, eggs) VALUES (0, 42)')
+        db.commit_transaction()
+        db.start_transaction()
+        try:
+            db.execute("INSERT INTO test_alter_unique (spam, eggs) VALUES (1, 42)")
+        except:
+            pass
+        else:
+            self.fail("Could insert the same integer twice into a unique field.")
+        db.rollback_transaction()
+
+        # Alter without unique=True (should not affect anything)
+        db.alter_column("test_alter_unique", "eggs", models.IntegerField())
+
+        # Insertion should still fail
+        db.start_transaction()
+        try:
+            db.execute("INSERT INTO test_alter_unique (spam, eggs) VALUES (1, 42)")
+        except:
+            pass
+        else:
+            self.fail("Could insert the same integer twice into a unique field after alter_column with unique=False.")
+        db.rollback_transaction()
+        
+        # Delete the unique index/constraint
+        if db.backend_name != "sqlite3":
+            db.delete_unique("test_alter_unique", ["eggs"])
+        db.delete_table("test_alter_unique")
+        db.start_transaction()
+
+        # Test multi-field constraint
+        db.create_table("test_alter_unique2", [
+            ('spam', models.IntegerField()),
+            ('eggs', models.IntegerField()),
+        ])
+        db.create_unique('test_alter_unique2', ('spam', 'eggs'))
+        db.execute_deferred_sql()
+        db.execute('INSERT INTO test_alter_unique2 (spam, eggs) VALUES (0, 42)')
+        db.commit_transaction()
+        # Verify that constraint works
+        db.start_transaction()
+        try:
+            db.execute("INSERT INTO test_alter_unique2 (spam, eggs) VALUES (1, 42)")
+        except:
+            self.fail("Looks like multi-field unique constraint applied to only one field.")
+        db.rollback_transaction()
+        db.start_transaction()
+        try:
+            db.execute("INSERT INTO test_alter_unique2 (spam, eggs) VALUES (0, 43)")
+        except:
+            self.fail("Looks like multi-field unique constraint applied to only one field.")
+        db.rollback_transaction()
+        db.start_transaction()
+        try:
+            db.execute("INSERT INTO test_alter_unique2 (spam, eggs) VALUES (0, 42)")
+        except:
+            pass
+        else:
+            self.fail("Could insert the same pair twice into unique-together fields.")
+        db.rollback_transaction()
+        # Altering one column should not drop or modify multi-column constraint
+        db.alter_column("test_alter_unique2", "eggs", models.PositiveIntegerField())
+        db.start_transaction()
+        try:
+            db.execute("INSERT INTO test_alter_unique2 (spam, eggs) VALUES (1, 42)")
+        except:
+            self.fail("Altering one column broken multi-column unique constraint.")
+        db.rollback_transaction()
+        db.start_transaction()
+        try:
+            db.execute("INSERT INTO test_alter_unique2 (spam, eggs) VALUES (0, 43)")
+        except:
+            self.fail("Altering one column broken multi-column unique constraint.")
+        db.rollback_transaction()
+        db.start_transaction()
+        try:
+            db.execute("INSERT INTO test_alter_unique2 (spam, eggs) VALUES (0, 42)")
+        except:
+            pass
+        else:
+            self.fail("Could insert the same pair twice into unique-together fields after alter_column with unique=False.")
+        db.rollback_transaction()
+        db.delete_table("test_alter_unique2")
+        db.start_transaction()
+
+    def test_capitalised_constraints(self):
+        """
+        Under PostgreSQL at least, capitalised constraints must be quoted.
+        """
+        db.create_table("test_capconst", [
+            ('SOMECOL', models.PositiveIntegerField(primary_key=True)),
+        ])
+        # Alter it so it's not got the check constraint
+        db.alter_column("test_capconst", "SOMECOL", models.IntegerField())
+    
+    def test_text_default(self):
+        """
+        MySQL cannot have blank defaults on TEXT columns.
+        """
+        db.create_table("test_textdef", [
+            ('textcol', models.TextField(blank=True)),
+        ])
+
+    def test_text_to_char(self):
+        """
+        On Oracle, you can't simply ALTER TABLE MODIFY a textfield to a charfield
+        """
+        value = "kawabanga"
+        db.create_table("test_text_to_char", [
+            ('textcol', models.TextField()),
+        ])
+        db.execute_deferred_sql()
+        db.execute("INSERT INTO test_text_to_char VALUES (%s)", [value])
+        db.alter_column("test_text_to_char", "textcol", models.CharField(max_length=100))
+        db.execute_deferred_sql()
+        after = db.execute("select * from test_text_to_char")[0][0]
+        self.assertEqual(value, after, "Change from text to char altered value [ %r != %r ]" % (value, after))
+
+    def test_char_to_text(self):
+        """
+        On Oracle, you can't simply ALTER TABLE MODIFY a charfield to a textfield either
+        """
+        value = "agnabawak"
+        db.create_table("test_char_to_text", [
+            ('textcol', models.CharField(max_length=100)),
+        ])
+        db.execute_deferred_sql()
+        db.execute("INSERT INTO test_char_to_text VALUES (%s)", [value])
+        db.alter_column("test_char_to_text", "textcol", models.TextField())
+        db.execute_deferred_sql()
+        after = db.execute("select * from test_char_to_text")[0][0]
+        after = text_type(after) # Oracle text fields return a sort of lazy string -- force evaluation
+        self.assertEqual(value, after, "Change from char to text altered value [ %r != %r ]" % (value, after))
+
+    @skipUnless(db.raises_default_errors, 'This database does not raise errors on missing defaults.')
+    def test_datetime_default(self):
+        """
+        Test that defaults are correctly not created for datetime columns
+        """
+        end_of_world = datetime.datetime(2012, 12, 21, 0, 0, 1)
+
+        try:
+            from django.utils import timezone
+        except ImportError:
+            pass
+        else:
+            from django.conf import settings
+            if getattr(settings, 'USE_TZ', False):
+                end_of_world = end_of_world.replace(tzinfo=timezone.utc)
+
+        db.create_table("test_datetime_def", [
+            ('col0', models.IntegerField(null=True)),
+            ('col1', models.DateTimeField(default=end_of_world)),
+            ('col2', models.DateTimeField(null=True)),
+        ])
+        db.execute_deferred_sql()
+        # insert a row
+        db.execute("INSERT INTO test_datetime_def (col0, col1, col2) values (null,%s,null)", [end_of_world])
+        db.alter_column("test_datetime_def", "col2", models.DateTimeField(default=end_of_world))
+        db.add_column("test_datetime_def", "col3", models.DateTimeField(default=end_of_world))
+        db.execute_deferred_sql()
+        db.commit_transaction()
+        # In the single existing row, we now expect col1=col2=col3=end_of_world...
+        db.start_transaction()
+        ends = db.execute("select col1,col2,col3 from test_datetime_def")[0]
+        self.failUnlessEqual(len(ends), 3)
+        for e in ends:
+            self.failUnlessEqual(e, end_of_world)
+        db.commit_transaction()
+        # ...but there should not be a default in the database for col1 or col3
+        for cols in ["col1,col2", "col2,col3"]:
+            db.start_transaction()
+            statement = "insert into test_datetime_def (col0,%s) values (null,%%s,%%s)" % cols
+            self.assertRaises(
+                IntegrityError,
+                db.execute, statement, [end_of_world, end_of_world]
+            )
+            db.rollback_transaction()
+        
+        db.start_transaction() # To preserve the sanity and semantics of this test class
+        
+    def test_add_unique_fk(self):
+        """
+        Test adding a ForeignKey with unique=True or a OneToOneField
+        """
+        db.create_table("test_add_unique_fk", [
+            ('spam', models.BooleanField(default=False))
+        ])
+        
+        db.add_column("test_add_unique_fk", "mock1", models.ForeignKey(db.mock_model('Mock', 'mock'), null=True, unique=True))
+        db.add_column("test_add_unique_fk", "mock2", models.OneToOneField(db.mock_model('Mock', 'mock'), null=True))
+        
+        db.delete_table("test_add_unique_fk")
+        
+    @skipUnless(db.has_check_constraints, 'Only applies to databases that '
+                                          'support CHECK constraints.')
+    def test_column_constraint(self):
+        """
+        Tests that the value constraint of PositiveIntegerField is enforced on
+        the database level.
+        """
+        db.create_table("test_column_constraint", [
+            ('spam', models.PositiveIntegerField()),
+        ])
+        db.execute_deferred_sql()
+        
+        # Make sure we can't insert negative values
+        db.commit_transaction()
+        db.start_transaction()
+        try:
+            db.execute("INSERT INTO test_column_constraint VALUES (-42)")
+        except:
+            pass
+        else:
+            self.fail("Could insert a negative value into a PositiveIntegerField.")
+        db.rollback_transaction()
+        
+        # remove constraint
+        db.alter_column("test_column_constraint", "spam", models.IntegerField())
+        db.execute_deferred_sql()
+        # make sure the insertion works now
+        db.execute('INSERT INTO test_column_constraint VALUES (-42)')
+        db.execute('DELETE FROM test_column_constraint')
+        
+        # add it back again
+        db.alter_column("test_column_constraint", "spam", models.PositiveIntegerField())
+        db.execute_deferred_sql()
+        # it should fail again
+        db.start_transaction()
+        try:
+            db.execute("INSERT INTO test_column_constraint VALUES (-42)")
+        except:
+            pass
+        else:
+            self.fail("Could insert a negative value after changing an IntegerField to a PositiveIntegerField.")
+        db.rollback_transaction()
+        
+        db.delete_table("test_column_constraint")
+        db.start_transaction()
+
+    def test_sql_defaults(self):
+        """
+        Test that sql default value is correct for non-string field types.
+        Datetimes are handled in test_datetime_default.
+        """
+
+        class CustomField(with_metaclass(models.SubfieldBase, models.CharField)):
+            description = 'CustomField'
+            def get_default(self):
+                if self.has_default():
+                    if callable(self.default):
+                        return self.default()
+                    return self.default
+                return super(CustomField, self).get_default()
+            def get_prep_value(self, value):
+                if not value:
+                    return value
+                return ','.join(map(str, value))
+            def to_python(self, value):
+                if not value or isinstance(value, list):
+                    return value
+                return list(map(int, value.split(',')))
+
+        false_value = db.has_booleans and 'False' or '0'
+        defaults = (
+            (models.CharField(default='sukasuka'), 'DEFAULT \'sukasuka'),
+            (models.BooleanField(default=False), 'DEFAULT %s' % false_value),
+            (models.IntegerField(default=42), 'DEFAULT 42'),
+            (CustomField(default=[2012, 2018, 2021, 2036]), 'DEFAULT \'2012,2018,2021,2036')
+        )
+        for field, sql_test_str in defaults:
+            sql = db.column_sql('fish', 'YAAAAAAZ', field)
+            if sql_test_str not in sql:
+                self.fail("default sql value was not properly generated for field %r.\nSql was %s" % (field, sql))
+
+    def test_make_added_foreign_key_not_null(self):
+        # Table for FK to target
+        User = db.mock_model(model_name='User', db_table='auth_user', db_tablespace='', pk_field_name='id', pk_field_type=models.AutoField, pk_field_args=[], pk_field_kwargs={})
+        # Table with no foreign key
+        db.create_table("test_fk", [
+            ('eggs', models.IntegerField()),
+        ])
+        db.execute_deferred_sql()
+        
+        # Add foreign key
+        db.add_column("test_fk", 'foreik', models.ForeignKey(User, null=True))
+        db.execute_deferred_sql()
+        
+        # Make the FK not null
+        db.alter_column("test_fk", "foreik_id", models.ForeignKey(User))
+        db.execute_deferred_sql()
+
+    def test_make_foreign_key_null(self):
+        # Table for FK to target
+        User = db.mock_model(model_name='User', db_table='auth_user', db_tablespace='', pk_field_name='id', pk_field_type=models.AutoField, pk_field_args=[], pk_field_kwargs={})
+        # Table with no foreign key
+        db.create_table("test_make_fk_null", [
+            ('eggs', models.IntegerField()),
+            ('foreik', models.ForeignKey(User))
+        ])
+        db.execute_deferred_sql()
+        
+        # Make the FK null
+        db.alter_column("test_make_fk_null", "foreik_id", models.ForeignKey(User, null=True))
+        db.execute_deferred_sql()
+
+    def test_alter_double_indexed_column(self):
+        # Table for FK to target
+        User = db.mock_model(model_name='User', db_table='auth_user', db_tablespace='', pk_field_name='id', pk_field_type=models.AutoField, pk_field_args=[], pk_field_kwargs={})
+        # Table with no foreign key
+        db.create_table("test_2indexed", [
+            ('eggs', models.IntegerField()),
+            ('foreik', models.ForeignKey(User))
+        ])
+        db.create_unique("test_2indexed", ["eggs", "foreik_id"])
+        db.execute_deferred_sql()
+        
+        # Make the FK null
+        db.alter_column("test_2indexed", "foreik_id", models.ForeignKey(User, null=True))
+        db.execute_deferred_sql()
+
+class TestCacheGeneric(unittest.TestCase):
+    base_ops_cls = generic.DatabaseOperations
+    def setUp(self):
+        class CacheOps(self.base_ops_cls):
+            def __init__(self):
+                self._constraint_cache = {}
+                self.cache_filled = 0
+                self.settings = {'NAME': 'db'}
+
+            def _fill_constraint_cache(self, db, table):
+                self.cache_filled += 1
+                self._constraint_cache.setdefault(db, {})
+                self._constraint_cache[db].setdefault(table, {})
+
+            @generic.invalidate_table_constraints
+            def clear_con(self, table):
+                pass
+
+            @generic.copy_column_constraints
+            def cp_column(self, table, column_old, column_new):
+                pass
+
+            @generic.delete_column_constraints
+            def rm_column(self, table, column):
+                pass
+
+            @generic.copy_column_constraints
+            @generic.delete_column_constraints
+            def mv_column(self, table, column_old, column_new):
+                pass
+
+            def _get_setting(self, attr):
+                return self.settings[attr]
+        self.CacheOps = CacheOps
+
+    def test_cache(self):
+        ops = self.CacheOps()
+        self.assertEqual(0, ops.cache_filled)
+        self.assertFalse(ops.lookup_constraint('db', 'table'))
+        self.assertEqual(1, ops.cache_filled)
+        self.assertFalse(ops.lookup_constraint('db', 'table'))
+        self.assertEqual(1, ops.cache_filled)
+        ops.clear_con('table')
+        self.assertEqual(1, ops.cache_filled)
+        self.assertFalse(ops.lookup_constraint('db', 'table'))
+        self.assertEqual(2, ops.cache_filled)
+        self.assertFalse(ops.lookup_constraint('db', 'table', 'column'))
+        self.assertEqual(2, ops.cache_filled)
+
+        cache = ops._constraint_cache
+        cache['db']['table']['column'] = 'constraint'
+        self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column'))
+        self.assertEqual([('column', 'constraint')], ops.lookup_constraint('db', 'table'))
+        self.assertEqual(2, ops.cache_filled)
+
+        # invalidate_table_constraints
+        ops.clear_con('new_table')
+        self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column'))
+        self.assertEqual(2, ops.cache_filled)
+
+        self.assertFalse(ops.lookup_constraint('db', 'new_table'))
+        self.assertEqual(3, ops.cache_filled)
+
+        # delete_column_constraints
+        cache['db']['table']['column'] = 'constraint'
+        self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column'))
+        ops.rm_column('table', 'column')
+        self.assertEqual([], ops.lookup_constraint('db', 'table', 'column'))
+        self.assertEqual([], ops.lookup_constraint('db', 'table', 'noexist_column'))
+
+        # copy_column_constraints
+        cache['db']['table']['column'] = 'constraint'
+        self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column'))
+        ops.cp_column('table', 'column', 'column_new')
+        self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column_new'))
+        self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column'))
+
+        # copy + delete
+        cache['db']['table']['column'] = 'constraint'
+        self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column'))
+        ops.mv_column('table', 'column', 'column_new')
+        self.assertEqual('constraint', ops.lookup_constraint('db', 'table', 'column_new'))
+        self.assertEqual([], ops.lookup_constraint('db', 'table', 'column'))
+
+    def test_valid(self):
+        ops = self.CacheOps()
+        # none of these should vivify a table into a valid state
+        self.assertFalse(ops._is_valid_cache('db', 'table'))
+        self.assertFalse(ops._is_valid_cache('db', 'table'))
+        ops.clear_con('table')
+        self.assertFalse(ops._is_valid_cache('db', 'table'))
+        ops.rm_column('table', 'column')
+        self.assertFalse(ops._is_valid_cache('db', 'table'))
+
+        # these should change the cache state
+        ops.lookup_constraint('db', 'table')
+        self.assertTrue(ops._is_valid_cache('db', 'table'))
+        ops.lookup_constraint('db', 'table', 'column')
+        self.assertTrue(ops._is_valid_cache('db', 'table'))
+        ops.clear_con('table')
+        self.assertFalse(ops._is_valid_cache('db', 'table'))
+
+    def test_valid_implementation(self):
+        # generic fills the cache on a per-table basis
+        ops = self.CacheOps()
+        self.assertFalse(ops._is_valid_cache('db', 'table'))
+        self.assertFalse(ops._is_valid_cache('db', 'other_table'))
+        ops.lookup_constraint('db', 'table')
+        self.assertTrue(ops._is_valid_cache('db', 'table'))
+        self.assertFalse(ops._is_valid_cache('db', 'other_table'))
+        ops.lookup_constraint('db', 'other_table')
+        self.assertTrue(ops._is_valid_cache('db', 'table'))
+        self.assertTrue(ops._is_valid_cache('db', 'other_table'))
+        ops.clear_con('table')
+        self.assertFalse(ops._is_valid_cache('db', 'table'))
+        self.assertTrue(ops._is_valid_cache('db', 'other_table'))
+
+if mysql:
+    class TestCacheMysql(TestCacheGeneric):
+        base_ops_cls = mysql.DatabaseOperations
+
+        def test_valid_implementation(self):
+            # mysql fills the cache on a per-db basis
+            ops = self.CacheOps()
+            self.assertFalse(ops._is_valid_cache('db', 'table'))
+            self.assertFalse(ops._is_valid_cache('db', 'other_table'))
+            ops.lookup_constraint('db', 'table')
+            self.assertTrue(ops._is_valid_cache('db', 'table'))
+            self.assertTrue(ops._is_valid_cache('db', 'other_table'))
+            ops.lookup_constraint('db', 'other_table')
+            self.assertTrue(ops._is_valid_cache('db', 'table'))
+            self.assertTrue(ops._is_valid_cache('db', 'other_table'))
+            ops.clear_con('table')
+            self.assertFalse(ops._is_valid_cache('db', 'table'))
+            self.assertTrue(ops._is_valid_cache('db', 'other_table'))

+ 164 - 0
desktop/core/ext-py/South-0.8.2/south/tests/db_mysql.py

@@ -0,0 +1,164 @@
+# Additional MySQL-specific tests
+# Written by: F. Gabriel Gosselin <gabrielNOSPAM@evidens.ca>
+# Based on tests by: aarranz
+from south.tests import unittest, skipUnless
+
+
+from south.db import db, generic, mysql
+from django.db import connection, models
+
+from south.utils.py3 import with_metaclass
+
+
+# A class decoration may be used in lieu of this when Python 2.5 is the
+# minimum.
+class TestMySQLOperationsMeta(type):
+
+    def __new__(mcs, name, bases, dict_):
+        decorator = skipUnless(db.backend_name == "mysql", 'MySQL-specific tests')
+
+        for key, method in dict_.items():
+            if key.startswith('test'):
+                dict_[key] = decorator(method)
+
+        return type.__new__(mcs, name, bases, dict_)
+
+class TestMySQLOperations(with_metaclass(TestMySQLOperationsMeta, unittest.TestCase)):
+    """MySQL-specific tests"""
+
+    def setUp(self):
+        db.debug = False
+        db.clear_deferred_sql()
+
+    def tearDown(self):
+        pass
+
+    def _create_foreign_tables(self, main_name, reference_name):
+        # Create foreign table and model
+        Foreign = db.mock_model(model_name='Foreign', db_table=reference_name,
+                                db_tablespace='', pk_field_name='id',
+                                pk_field_type=models.AutoField,
+                                pk_field_args=[])
+        db.create_table(reference_name, [
+                ('id', models.AutoField(primary_key=True)),
+            ])
+        # Create table with foreign key
+        db.create_table(main_name, [
+                ('id', models.AutoField(primary_key=True)),
+                ('foreign', models.ForeignKey(Foreign)),
+            ])
+        return Foreign
+
+    def test_constraint_references(self):
+        """Tests that referred table is reported accurately"""
+        main_table = 'test_cns_ref'
+        reference_table = 'test_cr_foreign'
+        db.start_transaction()
+        self._create_foreign_tables(main_table, reference_table)
+        db.execute_deferred_sql()
+        constraint = db._find_foreign_constraints(main_table, 'foreign_id')[0]
+        references = db._lookup_constraint_references(main_table, constraint)
+        self.assertEquals((reference_table, 'id'), references)
+        db.delete_table(main_table)
+        db.delete_table(reference_table)
+
+    def test_reverse_column_constraint(self):
+        """Tests that referred column in a foreign key (ex. id) is found"""
+        main_table = 'test_reverse_ref'
+        reference_table = 'test_rr_foreign'
+        db.start_transaction()
+        self._create_foreign_tables(main_table, reference_table)
+        db.execute_deferred_sql()
+        inverse = db._lookup_reverse_constraint(reference_table, 'id')
+        (cname, rev_table, rev_column) = inverse[0]
+        self.assertEquals(main_table, rev_table)
+        self.assertEquals('foreign_id', rev_column)
+        db.delete_table(main_table)
+        db.delete_table(reference_table)
+
+    def test_delete_fk_column(self):
+        main_table = 'test_drop_foreign'
+        ref_table = 'test_df_ref'
+        self._create_foreign_tables(main_table, ref_table)
+        db.execute_deferred_sql()
+        constraints = db._find_foreign_constraints(main_table, 'foreign_id')
+        self.assertEquals(len(constraints), 1)
+        db.delete_column(main_table, 'foreign_id')
+        constraints = db._find_foreign_constraints(main_table, 'foreign_id')
+        self.assertEquals(len(constraints), 0)
+        db.delete_table(main_table)
+        db.delete_table(ref_table)
+
+    def test_rename_fk_column(self):
+        main_table = 'test_rename_foreign'
+        ref_table = 'test_rf_ref'
+        self._create_foreign_tables(main_table, ref_table)
+        db.execute_deferred_sql()
+        constraints = db._find_foreign_constraints(main_table, 'foreign_id')
+        self.assertEquals(len(constraints), 1)
+        db.rename_column(main_table, 'foreign_id', 'reference_id')
+        db.execute_deferred_sql()  #Create constraints
+        constraints = db._find_foreign_constraints(main_table, 'reference_id')
+        self.assertEquals(len(constraints), 1)
+        db.delete_table(main_table)
+        db.delete_table(ref_table)
+
+    def test_rename_fk_inbound(self):
+        """
+        Tests that the column referred to by an external column can be renamed.
+        Edge case, but also useful as stepping stone to renaming tables.
+        """
+        main_table = 'test_rename_fk_inbound'
+        ref_table = 'test_rfi_ref'
+        self._create_foreign_tables(main_table, ref_table)
+        db.execute_deferred_sql()
+        constraints = db._lookup_reverse_constraint(ref_table, 'id')
+        self.assertEquals(len(constraints), 1)
+        db.rename_column(ref_table, 'id', 'rfi_id')
+        db.execute_deferred_sql()  #Create constraints
+        constraints = db._lookup_reverse_constraint(ref_table, 'rfi_id')
+        self.assertEquals(len(constraints), 1)
+        cname = db._find_foreign_constraints(main_table, 'foreign_id')[0]
+        (rtable, rcolumn) = db._lookup_constraint_references(main_table, cname)
+        self.assertEquals(rcolumn, 'rfi_id')
+        db.delete_table(main_table)
+        db.delete_table(ref_table)
+
+    def test_rename_constrained_table(self):
+        """Renames a table with a foreign key column (towards another table)"""
+        main_table = 'test_rn_table'
+        ref_table = 'test_rt_ref'
+        renamed_table = 'test_renamed_table'
+        self._create_foreign_tables(main_table, ref_table)
+        db.execute_deferred_sql()
+        constraints = db._find_foreign_constraints(main_table, 'foreign_id')
+        self.assertEquals(len(constraints), 1)
+        db.rename_table(main_table, renamed_table)
+        db.execute_deferred_sql()  #Create constraints
+        constraints = db._find_foreign_constraints(renamed_table, 'foreign_id')
+        self.assertEquals(len(constraints), 1)
+        (rtable, rcolumn) = db._lookup_constraint_references(
+                renamed_table, constraints[0])
+        self.assertEquals(rcolumn, 'id')
+        db.delete_table(renamed_table)
+        db.delete_table(ref_table)
+
+    def test_renamed_referenced_table(self):
+        """Rename a table referred to in a foreign key"""
+        main_table = 'test_rn_refd_table'
+        ref_table = 'test_rrt_ref'
+        renamed_table = 'test_renamed_ref'
+        self._create_foreign_tables(main_table, ref_table)
+        db.execute_deferred_sql()
+        constraints = db._lookup_reverse_constraint(ref_table)
+        self.assertEquals(len(constraints), 1)
+        db.rename_table(ref_table, renamed_table)
+        db.execute_deferred_sql()  #Create constraints
+        constraints = db._find_foreign_constraints(main_table, 'foreign_id')
+        self.assertEquals(len(constraints), 1)
+        (rtable, rcolumn) = db._lookup_constraint_references(
+                main_table, constraints[0])
+        self.assertEquals(renamed_table, rtable)
+        db.delete_table(main_table)
+        db.delete_table(renamed_table)
+

+ 0 - 0
desktop/core/ext-py/south/south/tests/circular_b/migrations/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_a/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_a/migrations/0001_a.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/0001_a.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_a/migrations/0002_a.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/0002_a.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_a/migrations/0003_a.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/0003_a.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_a/migrations/0004_a.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/0004_a.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_a/migrations/0005_a.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/0005_a.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_a/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_a/migrations/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_a/models.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_a/models.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_a/migrations/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_b/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_b/migrations/0001_b.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/0001_b.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_b/migrations/0002_b.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/0002_b.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_b/migrations/0003_b.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/0003_b.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_b/migrations/0004_b.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/0004_b.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_b/migrations/0005_b.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/0005_b.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_b/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_b/migrations/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_b/models.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_b/models.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_b/migrations/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_c/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_c/migrations/0001_c.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/0001_c.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_c/migrations/0002_c.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/0002_c.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_c/migrations/0003_c.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/0003_c.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_c/migrations/0004_c.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/0004_c.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_c/migrations/0005_c.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/0005_c.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_c/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_c/migrations/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_c/models.py → desktop/core/ext-py/South-0.8.2/south/tests/deps_c/models.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/deps_c/migrations/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/emptyapp/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/emptyapp/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/emptyapp/migrations/__init__.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/emptyapp/models.py → desktop/core/ext-py/South-0.8.2/south/tests/emptyapp/models.py


+ 0 - 0
desktop/core/ext-py/south/south/tests/emptyapp/migrations/__init__.py → desktop/core/ext-py/South-0.8.2/south/tests/fakeapp/__init__.py


Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff