Selaa lähdekoodia

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 12 vuotta sitten
vanhempi
commit
8a9946c3c3
100 muutettua tiedostoa jossa 5265 lisäystä ja 590 poistoa
  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),
+        ))

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 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


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä