Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Add unique_together altering operation

  • Loading branch information...
commit 67dcea711e92025d0e8676b869b7ef15dbc6db73 1 parent 310cdf4
Andrew Godwin authored July 02, 2013
2  django/db/migrations/operations/__init__.py
... ...
@@ -1,2 +1,2 @@
1  
-from .models import CreateModel, DeleteModel, AlterModelTable
  1
+from .models import CreateModel, DeleteModel, AlterModelTable, AlterUniqueTogether
2 2
 from .fields import AddField, RemoveField, AlterField, RenameField
8  django/db/migrations/operations/fields.py
@@ -7,7 +7,7 @@ class AddField(Operation):
7 7
     """
8 8
 
9 9
     def __init__(self, model_name, name, field):
10  
-        self.model_name = model_name
  10
+        self.model_name = model_name.lower()
11 11
         self.name = name
12 12
         self.field = field
13 13
 
@@ -33,7 +33,7 @@ class RemoveField(Operation):
33 33
     """
34 34
 
35 35
     def __init__(self, model_name, name):
36  
-        self.model_name = model_name
  36
+        self.model_name = model_name.lower()
37 37
         self.name = name
38 38
 
39 39
     def state_forwards(self, app_label, state):
@@ -62,7 +62,7 @@ class AlterField(Operation):
62 62
     """
63 63
 
64 64
     def __init__(self, model_name, name, field):
65  
-        self.model_name = model_name
  65
+        self.model_name = model_name.lower()
66 66
         self.name = name
67 67
         self.field = field
68 68
 
@@ -93,7 +93,7 @@ class RenameField(Operation):
93 93
     """
94 94
 
95 95
     def __init__(self, model_name, old_name, new_name):
96  
-        self.model_name = model_name
  96
+        self.model_name = model_name.lower()
97 97
         self.old_name = old_name
98 98
         self.new_name = new_name
99 99
 
36  django/db/migrations/operations/models.py
@@ -9,7 +9,7 @@ class CreateModel(Operation):
9 9
     """
10 10
 
11 11
     def __init__(self, name, fields, options=None, bases=None):
12  
-        self.name = name
  12
+        self.name = name.lower()
13 13
         self.fields = fields
14 14
         self.options = options or {}
15 15
         self.bases = bases or (models.Model,)
@@ -35,7 +35,7 @@ class DeleteModel(Operation):
35 35
     """
36 36
 
37 37
     def __init__(self, name):
38  
-        self.name = name
  38
+        self.name = name.lower()
39 39
 
40 40
     def state_forwards(self, app_label, state):
41 41
         del state.models[app_label, self.name.lower()]
@@ -58,7 +58,7 @@ class AlterModelTable(Operation):
58 58
     """
59 59
 
60 60
     def __init__(self, name, table):
61  
-        self.name = name
  61
+        self.name = name.lower()
62 62
         self.table = table
63 63
 
64 64
     def state_forwards(self, app_label, state):
@@ -78,3 +78,33 @@ def database_backwards(self, app_label, schema_editor, from_state, to_state):
78 78
 
79 79
     def describe(self):
80 80
         return "Rename table for %s to %s" % (self.name, self.table)
  81
+
  82
+
  83
+class AlterUniqueTogether(Operation):
  84
+    """
  85
+    Changes the value of unique_together to the target one.
  86
+    Input value of unique_together must be a set of tuples.
  87
+    """
  88
+
  89
+    def __init__(self, name, unique_together):
  90
+        self.name = name.lower()
  91
+        self.unique_together = set(tuple(cons) for cons in unique_together)
  92
+
  93
+    def state_forwards(self, app_label, state):
  94
+        model_state = state.models[app_label, self.name.lower()]
  95
+        model_state.options["unique_together"] = self.unique_together
  96
+
  97
+    def database_forwards(self, app_label, schema_editor, from_state, to_state):
  98
+        old_app_cache = from_state.render()
  99
+        new_app_cache = to_state.render()
  100
+        schema_editor.alter_unique_together(
  101
+            new_app_cache.get_model(app_label, self.name),
  102
+            getattr(old_app_cache.get_model(app_label, self.name)._meta, "unique_together", set()),
  103
+            getattr(new_app_cache.get_model(app_label, self.name)._meta, "unique_together", set()),
  104
+        )
  105
+
  106
+    def database_backwards(self, app_label, schema_editor, from_state, to_state):
  107
+        return self.database_forwards(app_label, schema_editor, from_state, to_state)
  108
+
  109
+    def describe(self):
  110
+        return "Alter unique_together for %s (%s constraints)" % (self.name, len(self.unique_together))
9  django/db/migrations/state.py
@@ -80,8 +80,11 @@ def from_model(cls, model):
80 80
             # Ignore some special options
81 81
             if name in ["app_cache", "app_label"]:
82 82
                 continue
83  
-            if name in model._meta.original_attrs:
84  
-                options[name] = model._meta.original_attrs[name]
  83
+            elif name in model._meta.original_attrs:
  84
+                if name == "unique_together":
  85
+                    options[name] = set(model._meta.original_attrs["unique_together"])
  86
+                else:
  87
+                    options[name] = model._meta.original_attrs[name]
85 88
         # Make our record
86 89
         bases = tuple(model for model in model.__bases__ if (not hasattr(model, "_meta") or not model._meta.abstract))
87 90
         if not bases:
@@ -116,6 +119,8 @@ def render(self, app_cache):
116 119
         # First, make a Meta object
117 120
         meta_contents = {'app_label': self.app_label, "app_cache": app_cache}
118 121
         meta_contents.update(self.options)
  122
+        if "unique_together" in meta_contents:
  123
+            meta_contents["unique_together"] = list(meta_contents["unique_together"])
119 124
         meta = type("Meta", tuple(), meta_contents)
120 125
         # Then, work out our bases
121 126
         # TODO: Use the actual bases
4  tests/migrations/test_autodetector.py
@@ -83,7 +83,7 @@ def test_new_model(self):
83 83
         # Right action?
84 84
         action = migration.operations[0]
85 85
         self.assertEqual(action.__class__.__name__, "CreateModel")
86  
-        self.assertEqual(action.name, "Author")
  86
+        self.assertEqual(action.name, "author")
87 87
 
88 88
     def test_old_model(self):
89 89
         "Tests deletion of old models"
@@ -100,7 +100,7 @@ def test_old_model(self):
100 100
         # Right action?
101 101
         action = migration.operations[0]
102 102
         self.assertEqual(action.__class__.__name__, "DeleteModel")
103  
-        self.assertEqual(action.name, "Author")
  103
+        self.assertEqual(action.name, "author")
104 104
 
105 105
     def test_add_field(self):
106 106
         "Tests autodetection of new fields"
40  tests/migrations/test_operations.py
... ...
@@ -1,5 +1,6 @@
1 1
 from django.test import TestCase
2 2
 from django.db import connection, models, migrations
  3
+from django.db.utils import IntegrityError
3 4
 from django.db.migrations.state import ProjectState
4 5
 
5 6
 
@@ -38,6 +39,7 @@ def set_up_test_model(self, app_label):
38 39
             [
39 40
                 ("id", models.AutoField(primary_key=True)),
40 41
                 ("pink", models.BooleanField(default=True)),
  42
+                ("weight", models.FloatField()),
41 43
             ],
42 44
         )
43 45
         project_state = ProjectState()
@@ -50,7 +52,7 @@ def set_up_test_model(self, app_label):
50 52
     def test_create_model(self):
51 53
         """
52 54
         Tests the CreateModel operation.
53  
-        Most other tests use this as part of setup, so check failures here first.
  55
+        Most other tests use this operation as part of setup, so check failures here first.
54 56
         """
55 57
         operation = migrations.CreateModel(
56 58
             "Pony",
@@ -63,7 +65,7 @@ def test_create_model(self):
63 65
         project_state = ProjectState()
64 66
         new_state = project_state.clone()
65 67
         operation.state_forwards("test_crmo", new_state)
66  
-        self.assertEqual(new_state.models["test_crmo", "pony"].name, "Pony")
  68
+        self.assertEqual(new_state.models["test_crmo", "pony"].name, "pony")
67 69
         self.assertEqual(len(new_state.models["test_crmo", "pony"].fields), 2)
68 70
         # Test the database alteration
69 71
         self.assertTableNotExists("test_crmo_pony")
@@ -110,7 +112,7 @@ def test_add_field(self):
110 112
         operation = migrations.AddField("Pony", "height", models.FloatField(null=True))
111 113
         new_state = project_state.clone()
112 114
         operation.state_forwards("test_adfl", new_state)
113  
-        self.assertEqual(len(new_state.models["test_adfl", "pony"].fields), 3)
  115
+        self.assertEqual(len(new_state.models["test_adfl", "pony"].fields), 4)
114 116
         # Test the database alteration
115 117
         self.assertColumnNotExists("test_adfl_pony", "height")
116 118
         with connection.schema_editor() as editor:
@@ -130,7 +132,7 @@ def test_remove_field(self):
130 132
         operation = migrations.RemoveField("Pony", "pink")
131 133
         new_state = project_state.clone()
132 134
         operation.state_forwards("test_rmfl", new_state)
133  
-        self.assertEqual(len(new_state.models["test_rmfl", "pony"].fields), 1)
  135
+        self.assertEqual(len(new_state.models["test_rmfl", "pony"].fields), 2)
134 136
         # Test the database alteration
135 137
         self.assertColumnExists("test_rmfl_pony", "pink")
136 138
         with connection.schema_editor() as editor:
@@ -208,3 +210,33 @@ def test_rename_field(self):
208 210
             operation.database_backwards("test_rnfl", editor, new_state, project_state)
209 211
         self.assertColumnExists("test_rnfl_pony", "pink")
210 212
         self.assertColumnNotExists("test_rnfl_pony", "blue")
  213
+
  214
+    def test_alter_unique_together(self):
  215
+        """
  216
+        Tests the AlterUniqueTogether operation.
  217
+        """
  218
+        project_state = self.set_up_test_model("test_alunto")
  219
+        # Test the state alteration
  220
+        operation = migrations.AlterUniqueTogether("Pony", [("pink", "weight")])
  221
+        new_state = project_state.clone()
  222
+        operation.state_forwards("test_alunto", new_state)
  223
+        self.assertEqual(len(project_state.models["test_alunto", "pony"].options.get("unique_together", set())), 0)
  224
+        self.assertEqual(len(new_state.models["test_alunto", "pony"].options.get("unique_together", set())), 1)
  225
+        # Make sure we can insert duplicate rows
  226
+        cursor = connection.cursor()
  227
+        cursor.execute("INSERT INTO test_alunto_pony (id, pink, weight) VALUES (1, 1, 1)")
  228
+        cursor.execute("INSERT INTO test_alunto_pony (id, pink, weight) VALUES (2, 1, 1)")
  229
+        cursor.execute("DELETE FROM test_alunto_pony")
  230
+        # Test the database alteration
  231
+        with connection.schema_editor() as editor:
  232
+            operation.database_forwards("test_alunto", editor, project_state, new_state)
  233
+        cursor.execute("INSERT INTO test_alunto_pony (id, pink, weight) VALUES (1, 1, 1)")
  234
+        with self.assertRaises(IntegrityError):
  235
+            cursor.execute("INSERT INTO test_alunto_pony (id, pink, weight) VALUES (2, 1, 1)")
  236
+        cursor.execute("DELETE FROM test_alunto_pony")
  237
+        # And test reversal
  238
+        with connection.schema_editor() as editor:
  239
+            operation.database_backwards("test_alunto", editor, new_state, project_state)
  240
+        cursor.execute("INSERT INTO test_alunto_pony (id, pink, weight) VALUES (1, 1, 1)")
  241
+        cursor.execute("INSERT INTO test_alunto_pony (id, pink, weight) VALUES (2, 1, 1)")
  242
+        cursor.execute("DELETE FROM test_alunto_pony")
2  tests/migrations/test_state.py
@@ -44,7 +44,7 @@ class Meta:
44 44
         self.assertEqual(author_state.fields[1][1].max_length, 255)
45 45
         self.assertEqual(author_state.fields[2][1].null, False)
46 46
         self.assertEqual(author_state.fields[3][1].null, True)
47  
-        self.assertEqual(author_state.options, {"unique_together": ["name", "bio"]})
  47
+        self.assertEqual(author_state.options, {"unique_together": set(("name", "bio"))})
48 48
         self.assertEqual(author_state.bases, (models.Model, ))
49 49
         
50 50
         self.assertEqual(book_state.app_label, "migrations")

0 notes on commit 67dcea7

Please sign in to comment.
Something went wrong with that request. Please try again.