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 |
|