Project

General

Profile

Statistics
| Branch: | Revision:

root / env / lib / python2.7 / site-packages / south / creator / actions.py @ d1a4905f

History | View | Annotate | Download (18.7 KB)

1
"""
2
Actions - things like 'a model was removed' or 'a field was changed'.
3
Each one has a class, which can take the action description and insert code
4
blocks into the forwards() and backwards() methods, in the right place.
5
"""
6

    
7
import sys
8

    
9
from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
10
from django.db.models.fields import FieldDoesNotExist, NOT_PROVIDED, CharField, TextField
11

    
12
from south.modelsinspector import value_clean
13
from south.creator.freezer import remove_useless_attributes, model_key
14
from south.utils import datetime_utils
15

    
16

    
17
class Action(object):
18
    """
19
    Generic base Action class. Contains utility methods for inserting into
20
    the forwards() and backwards() method lists.
21
    """
22
    
23
    prepend_forwards = False
24
    prepend_backwards = False
25
    
26
    def forwards_code(self):
27
        raise NotImplementedError
28
    
29
    def backwards_code(self):
30
        raise NotImplementedError
31
    
32
    def add_forwards(self, forwards):
33
        if self.prepend_forwards:
34
            forwards.insert(0, self.forwards_code())
35
        else:
36
            forwards.append(self.forwards_code())
37
    
38
    def add_backwards(self, backwards):
39
        if self.prepend_backwards:
40
            backwards.insert(0, self.backwards_code())
41
        else:
42
            backwards.append(self.backwards_code())
43
    
44
    def console_line(self):
45
        "Returns the string to print on the console, e.g. ' + Added field foo'"
46
        raise NotImplementedError
47
    
48
    @classmethod
49
    def triples_to_defs(cls, fields):
50
        # Turn the (class, args, kwargs) format into a string
51
        for field, triple in fields.items():
52
            fields[field] = cls.triple_to_def(triple)
53
        return fields
54
    
55
    @classmethod
56
    def triple_to_def(cls, triple):
57
        "Turns a single triple into a definition."
58
        return "self.gf(%r)(%s)" % (
59
            triple[0], # Field full path
60
            ", ".join(triple[1] + ["%s=%s" % (kwd, val) for kwd, val in triple[2].items()]), # args and kwds
61
        )
62
    
63
    
64
class AddModel(Action):
65
    """
66
    Addition of a model. Takes the Model subclass that is being created.
67
    """
68
    
69
    FORWARDS_TEMPLATE = '''
70
        # Adding model '%(model_name)s'
71
        db.create_table(%(table_name)r, (
72
            %(field_defs)s
73
        ))
74
        db.send_create_signal(%(app_label)r, [%(model_name)r])'''[1:] + "\n"
75
    
76
    BACKWARDS_TEMPLATE = '''
77
        # Deleting model '%(model_name)s'
78
        db.delete_table(%(table_name)r)'''[1:] + "\n"
79

    
80
    def __init__(self, model, model_def):
81
        self.model = model
82
        self.model_def = model_def
83
    
84
    def console_line(self):
85
        "Returns the string to print on the console, e.g. ' + Added field foo'"
86
        return " + Added model %s.%s" % (
87
            self.model._meta.app_label, 
88
            self.model._meta.object_name,
89
        )
90

    
91
    def forwards_code(self):
92
        "Produces the code snippet that gets put into forwards()"
93
        field_defs = ",\n            ".join([
94
            "(%r, %s)" % (name, defn) for name, defn
95
            in self.triples_to_defs(self.model_def).items()
96
        ]) + ","
97
        
98
        return self.FORWARDS_TEMPLATE % {
99
            "model_name": self.model._meta.object_name,
100
            "table_name": self.model._meta.db_table,
101
            "app_label": self.model._meta.app_label,
102
            "field_defs": field_defs,
103
        }
104

    
105
    def backwards_code(self):
106
        "Produces the code snippet that gets put into backwards()"
107
        return self.BACKWARDS_TEMPLATE % {
108
            "model_name": self.model._meta.object_name,
109
            "table_name": self.model._meta.db_table,
110
        }
111
    
112
    
113
class DeleteModel(AddModel):
114
    """
115
    Deletion of a model. Takes the Model subclass that is being created.
116
    """
117
    
118
    def console_line(self):
119
        "Returns the string to print on the console, e.g. ' + Added field foo'"
120
        return " - Deleted model %s.%s" % (
121
            self.model._meta.app_label, 
122
            self.model._meta.object_name,
123
        )
124

    
125
    def forwards_code(self):
126
        return AddModel.backwards_code(self)
127

    
128
    def backwards_code(self):
129
        return AddModel.forwards_code(self)
130

    
131

    
132
class _NullIssuesField(object):
133
    """
134
    A field that might need to ask a question about rogue NULL values.
135
    """
136

    
137
    allow_third_null_option = False
138
    irreversible = False
139

    
140
    IRREVERSIBLE_TEMPLATE = '''
141
        # User chose to not deal with backwards NULL issues for '%(model_name)s.%(field_name)s'
142
        raise RuntimeError("Cannot reverse this migration. '%(model_name)s.%(field_name)s' and its values cannot be restored.")'''
143

    
144
    def deal_with_not_null_no_default(self, field, field_def):
145
        # If it's a CharField or TextField that's blank, skip this step.
146
        if isinstance(field, (CharField, TextField)) and field.blank:
147
            field_def[2]['default'] = repr("")
148
            return
149
        # Oh dear. Ask them what to do.
150
        print " ? The field '%s.%s' does not have a default specified, yet is NOT NULL." % (
151
            self.model._meta.object_name,
152
            field.name,
153
        )
154
        print " ? Since you are %s, you MUST specify a default" % self.null_reason
155
        print " ? value to use for existing rows. Would you like to:"
156
        print " ?  1. Quit now, and add a default to the field in models.py"
157
        print " ?  2. Specify a one-off value to use for existing columns now"
158
        if self.allow_third_null_option:
159
            print " ?  3. Disable the backwards migration by raising an exception."
160
        while True:
161
            choice = raw_input(" ? Please select a choice: ")
162
            if choice == "1":
163
                sys.exit(1)
164
            elif choice == "2":
165
                break
166
            elif choice == "3" and self.allow_third_null_option:
167
                break
168
            else:
169
                print " ! Invalid choice."
170
        if choice == "2":
171
            self.add_one_time_default(field, field_def)
172
        elif choice == "3":
173
            self.irreversible = True
174

    
175
    def add_one_time_default(self, field, field_def):
176
        # OK, they want to pick their own one-time default. Who are we to refuse?
177
        print " ? Please enter Python code for your one-off default value."
178
        print " ? The datetime module is available, so you can do e.g. datetime.date.today()"
179
        while True:
180
            code = raw_input(" >>> ")
181
            if not code:
182
                print " ! Please enter some code, or 'exit' (with no quotes) to exit."
183
            elif code == "exit":
184
                sys.exit(1)
185
            else:
186
                try:
187
                    result = eval(code, {}, {"datetime": datetime_utils})
188
                except (SyntaxError, NameError), e:
189
                    print " ! Invalid input: %s" % e
190
                else:
191
                    break
192
        # Right, add the default in.
193
        field_def[2]['default'] = value_clean(result)
194

    
195
    def irreversable_code(self, field):
196
        return self.IRREVERSIBLE_TEMPLATE % {
197
            "model_name": self.model._meta.object_name,
198
            "table_name": self.model._meta.db_table,
199
            "field_name": field.name,
200
            "field_column": field.column,
201
        }
202
    
203
    
204
class AddField(Action, _NullIssuesField):
205
    """
206
    Adds a field to a model. Takes a Model class and the field name.
207
    """
208

    
209
    null_reason = "adding this field"
210
    
211
    FORWARDS_TEMPLATE = '''
212
        # Adding field '%(model_name)s.%(field_name)s'
213
        db.add_column(%(table_name)r, %(field_name)r,
214
                      %(field_def)s,
215
                      keep_default=False)'''[1:] + "\n"
216
    
217
    BACKWARDS_TEMPLATE = '''
218
        # Deleting field '%(model_name)s.%(field_name)s'
219
        db.delete_column(%(table_name)r, %(field_column)r)'''[1:] + "\n"
220
    
221
    def __init__(self, model, field, field_def):
222
        self.model = model
223
        self.field = field
224
        self.field_def = field_def
225
        
226
        # See if they've made a NOT NULL column but also have no default (far too common)
227
        is_null = self.field.null
228
        default = (self.field.default is not None) and (self.field.default is not NOT_PROVIDED)
229
        
230
        if not is_null and not default:
231
            self.deal_with_not_null_no_default(self.field, self.field_def)
232

    
233
    def console_line(self):
234
        "Returns the string to print on the console, e.g. ' + Added field foo'"
235
        return " + Added field %s on %s.%s" % (
236
            self.field.name,
237
            self.model._meta.app_label,
238
            self.model._meta.object_name,
239
        )
240
    
241
    def forwards_code(self):
242
        
243
        return self.FORWARDS_TEMPLATE % {
244
            "model_name": self.model._meta.object_name,
245
            "table_name": self.model._meta.db_table,
246
            "field_name": self.field.name,
247
            "field_column": self.field.column,
248
            "field_def": self.triple_to_def(self.field_def),
249
        }
250

    
251
    def backwards_code(self):
252
        return self.BACKWARDS_TEMPLATE % {
253
            "model_name": self.model._meta.object_name,
254
            "table_name": self.model._meta.db_table,
255
            "field_name": self.field.name,
256
            "field_column": self.field.column,
257
        }
258
    
259
    
260
class DeleteField(AddField):
261
    """
262
    Removes a field from a model. Takes a Model class and the field name.
263
    """
264

    
265
    null_reason = "removing this field"
266
    allow_third_null_option = True
267

    
268
    def console_line(self):
269
        "Returns the string to print on the console, e.g. ' + Added field foo'"
270
        return " - Deleted field %s on %s.%s" % (
271
            self.field.name,
272
            self.model._meta.app_label, 
273
            self.model._meta.object_name,
274
        )
275
    
276
    def forwards_code(self):
277
        return AddField.backwards_code(self)
278

    
279
    def backwards_code(self):
280
        if not self.irreversible:
281
            return AddField.forwards_code(self)
282
        else:
283
            return self.irreversable_code(self.field)
284

    
285

    
286
class ChangeField(Action, _NullIssuesField):
287
    """
288
    Changes a field's type/options on a model.
289
    """
290

    
291
    null_reason = "making this field non-nullable"
292
    
293
    FORWARDS_TEMPLATE = BACKWARDS_TEMPLATE = '''
294
        # Changing field '%(model_name)s.%(field_name)s'
295
        db.alter_column(%(table_name)r, %(field_column)r, %(field_def)s)'''
296
    
297
    RENAME_TEMPLATE = '''
298
        # Renaming column for '%(model_name)s.%(field_name)s' to match new field type.
299
        db.rename_column(%(table_name)r, %(old_column)r, %(new_column)r)'''
300
    
301
    def __init__(self, model, old_field, new_field, old_def, new_def):
302
        self.model = model
303
        self.old_field = old_field
304
        self.new_field = new_field
305
        self.old_def = old_def
306
        self.new_def = new_def
307

    
308
        # See if they've changed a not-null field to be null
309
        new_default = (self.new_field.default is not None) and (self.new_field.default is not NOT_PROVIDED)
310
        old_default = (self.old_field.default is not None) and (self.old_field.default is not NOT_PROVIDED)
311
        if self.old_field.null and not self.new_field.null and not new_default:
312
            self.deal_with_not_null_no_default(self.new_field, self.new_def)
313
        if not self.old_field.null and self.new_field.null and not old_default:
314
            self.null_reason = "making this field nullable"
315
            self.allow_third_null_option = True
316
            self.deal_with_not_null_no_default(self.old_field, self.old_def)
317
    
318
    def console_line(self):
319
        "Returns the string to print on the console, e.g. ' + Added field foo'"
320
        return " ~ Changed field %s on %s.%s" % (
321
            self.new_field.name,
322
            self.model._meta.app_label, 
323
            self.model._meta.object_name,
324
        )
325
    
326
    def _code(self, old_field, new_field, new_def):
327
        
328
        output = ""
329
        
330
        if self.old_field.column != self.new_field.column:
331
            output += self.RENAME_TEMPLATE % {
332
                "model_name": self.model._meta.object_name,
333
                "table_name": self.model._meta.db_table,
334
                "field_name": new_field.name,
335
                "old_column": old_field.column,
336
                "new_column": new_field.column,
337
            }
338
        
339
        output += self.FORWARDS_TEMPLATE % {
340
            "model_name": self.model._meta.object_name,
341
            "table_name": self.model._meta.db_table,
342
            "field_name": new_field.name,
343
            "field_column": new_field.column,
344
            "field_def": self.triple_to_def(new_def),
345
        }
346
        
347
        return output
348

    
349
    def forwards_code(self):
350
        return self._code(self.old_field, self.new_field, self.new_def)
351

    
352
    def backwards_code(self):
353
        if not self.irreversible:
354
            return self._code(self.new_field, self.old_field, self.old_def)
355
        else:
356
            return self.irreversable_code(self.old_field)
357

    
358

    
359
class AddUnique(Action):
360
    """
361
    Adds a unique constraint to a model. Takes a Model class and the field names.
362
    """
363
    
364
    FORWARDS_TEMPLATE = '''
365
        # Adding unique constraint on '%(model_name)s', fields %(field_names)s
366
        db.create_unique(%(table_name)r, %(fields)r)'''[1:] + "\n"
367
    
368
    BACKWARDS_TEMPLATE = '''
369
        # Removing unique constraint on '%(model_name)s', fields %(field_names)s
370
        db.delete_unique(%(table_name)r, %(fields)r)'''[1:] + "\n"
371
    
372
    prepend_backwards = True
373
    
374
    def __init__(self, model, fields):
375
        self.model = model
376
        self.fields = fields
377
    
378
    def console_line(self):
379
        "Returns the string to print on the console, e.g. ' + Added field foo'"
380
        return " + Added unique constraint for %s on %s.%s" % (
381
            [x.name for x in self.fields],
382
            self.model._meta.app_label, 
383
            self.model._meta.object_name,
384
        )
385
    
386
    def forwards_code(self):
387
        
388
        return self.FORWARDS_TEMPLATE % {
389
            "model_name": self.model._meta.object_name,
390
            "table_name": self.model._meta.db_table,
391
            "fields":  [field.column for field in self.fields],
392
            "field_names":  [field.name for field in self.fields],
393
        }
394

    
395
    def backwards_code(self):
396
        return self.BACKWARDS_TEMPLATE % {
397
            "model_name": self.model._meta.object_name,
398
            "table_name": self.model._meta.db_table,
399
            "fields": [field.column for field in self.fields],
400
            "field_names":  [field.name for field in self.fields],
401
        }
402

    
403

    
404
class DeleteUnique(AddUnique):
405
    """
406
    Removes a unique constraint from a model. Takes a Model class and the field names.
407
    """
408
    
409
    prepend_forwards = True
410
    prepend_backwards = False
411
    
412
    def console_line(self):
413
        "Returns the string to print on the console, e.g. ' + Added field foo'"
414
        return " - Deleted unique constraint for %s on %s.%s" % (
415
            [x.name for x in self.fields],
416
            self.model._meta.app_label, 
417
            self.model._meta.object_name,
418
        )
419
    
420
    def forwards_code(self):
421
        return AddUnique.backwards_code(self)
422

    
423
    def backwards_code(self):
424
        return AddUnique.forwards_code(self)
425

    
426

    
427
class AddIndex(AddUnique):
428
    """
429
    Adds an index to a model field[s]. Takes a Model class and the field names.
430
    """
431
    
432
    FORWARDS_TEMPLATE = '''
433
        # Adding index on '%(model_name)s', fields %(field_names)s
434
        db.create_index(%(table_name)r, %(fields)r)'''[1:] + "\n"
435
    
436
    BACKWARDS_TEMPLATE = '''
437
        # Removing index on '%(model_name)s', fields %(field_names)s
438
        db.delete_index(%(table_name)r, %(fields)r)'''[1:] + "\n"
439
    
440
    def console_line(self):
441
        "Returns the string to print on the console, e.g. ' + Added field foo'"
442
        return " + Added index for %s on %s.%s" % (
443
            [x.name for x in self.fields],
444
            self.model._meta.app_label, 
445
            self.model._meta.object_name,
446
        )
447

    
448

    
449
class DeleteIndex(AddIndex):
450
    """
451
    Deletes an index off a model field[s]. Takes a Model class and the field names.
452
    """
453
    
454
    def console_line(self):
455
        "Returns the string to print on the console, e.g. ' + Added field foo'"
456
        return " + Deleted index for %s on %s.%s" % (
457
            [x.name for x in self.fields],
458
            self.model._meta.app_label, 
459
            self.model._meta.object_name,
460
        )
461
    
462
    def forwards_code(self):
463
        return AddIndex.backwards_code(self)
464

    
465
    def backwards_code(self):
466
        return AddIndex.forwards_code(self)
467

    
468

    
469
class AddM2M(Action):
470
    """
471
    Adds a unique constraint to a model. Takes a Model class and the field names.
472
    """
473
    
474
    FORWARDS_TEMPLATE = '''
475
        # Adding M2M table for field %(field_name)s on '%(model_name)s'
476
        db.create_table(%(table_name)r, (
477
            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
478
            (%(left_field)r, models.ForeignKey(orm[%(left_model_key)r], null=False)),
479
            (%(right_field)r, models.ForeignKey(orm[%(right_model_key)r], null=False))
480
        ))
481
        db.create_unique(%(table_name)r, [%(left_column)r, %(right_column)r])'''[1:] + "\n"
482
    
483
    BACKWARDS_TEMPLATE = '''
484
        # Removing M2M table for field %(field_name)s on '%(model_name)s'
485
        db.delete_table('%(table_name)s')'''[1:] + "\n"
486
    
487
    def __init__(self, model, field):
488
        self.model = model
489
        self.field = field
490
    
491
    def console_line(self):
492
        "Returns the string to print on the console, e.g. ' + Added field foo'"
493
        return " + Added M2M table for %s on %s.%s" % (
494
            self.field.name,
495
            self.model._meta.app_label, 
496
            self.model._meta.object_name,
497
        )
498
    
499
    def forwards_code(self):
500
        
501
        return self.FORWARDS_TEMPLATE % {
502
            "model_name": self.model._meta.object_name,
503
            "field_name": self.field.name,
504
            "table_name": self.field.m2m_db_table(),
505
            "left_field": self.field.m2m_column_name()[:-3], # Remove the _id part
506
            "left_column": self.field.m2m_column_name(),
507
            "left_model_key": model_key(self.model),
508
            "right_field": self.field.m2m_reverse_name()[:-3], # Remove the _id part
509
            "right_column": self.field.m2m_reverse_name(),
510
            "right_model_key": model_key(self.field.rel.to),
511
        }
512

    
513
    def backwards_code(self):
514
        
515
        return self.BACKWARDS_TEMPLATE % {
516
            "model_name": self.model._meta.object_name,
517
            "field_name": self.field.name,
518
            "table_name": self.field.m2m_db_table(),
519
        }
520

    
521

    
522
class DeleteM2M(AddM2M):
523
    """
524
    Adds a unique constraint to a model. Takes a Model class and the field names.
525
    """
526
    
527
    def console_line(self):
528
        "Returns the string to print on the console, e.g. ' + Added field foo'"
529
        return " - Deleted M2M table for %s on %s.%s" % (
530
            self.field.name,
531
            self.model._meta.app_label, 
532
            self.model._meta.object_name,
533
        )
534
    
535
    def forwards_code(self):
536
        return AddM2M.backwards_code(self)
537

    
538
    def backwards_code(self):
539
        return AddM2M.forwards_code(self)
540