1""" 2Contains things to detect changes - either using options passed in on the 3commandline, or by using autodetection, etc. 4""" 5 6from __future__ import print_function 7 8from django.db import models 9from django.contrib.contenttypes.generic import GenericRelation 10from django.utils.datastructures import SortedDict 11 12from south.creator.freezer import remove_useless_attributes, freeze_apps, model_key 13from south.utils import auto_through 14from south.utils.py3 import string_types 15 16class BaseChanges(object): 17 """ 18 Base changes class. 19 """ 20 def suggest_name(self): 21 return '' 22 23 def split_model_def(self, model, model_def): 24 """ 25 Given a model and its model def (a dict of field: triple), returns three 26 items: the real fields dict, the Meta dict, and the M2M fields dict. 27 """ 28 real_fields = SortedDict() 29 meta = SortedDict() 30 m2m_fields = SortedDict() 31 for name, triple in model_def.items(): 32 if name == "Meta": 33 meta = triple 34 elif isinstance(model._meta.get_field_by_name(name)[0], models.ManyToManyField): 35 m2m_fields[name] = triple 36 else: 37 real_fields[name] = triple 38 return real_fields, meta, m2m_fields 39 40 def current_model_from_key(self, key): 41 app_label, model_name = key.split(".") 42 return models.get_model(app_label, model_name) 43 44 def current_field_from_key(self, key, fieldname): 45 app_label, model_name = key.split(".") 46 # Special, for the magical field from order_with_respect_to 47 if fieldname == "_order": 48 field = models.IntegerField() 49 field.name = "_order" 50 field.attname = "_order" 51 field.column = "_order" 52 field.default = 0 53 return field 54 # Otherwise, normal. 55 return models.get_model(app_label, model_name)._meta.get_field_by_name(fieldname)[0] 56 57 58class AutoChanges(BaseChanges): 59 """ 60 Detects changes by 'diffing' two sets of frozen model definitions. 61 """ 62 63 # Field types we don't generate add/remove field changes for. 64 IGNORED_FIELD_TYPES = [ 65 GenericRelation, 66 ] 67 68 def __init__(self, migrations, old_defs, old_orm, new_defs): 69 self.migrations = migrations 70 self.old_defs = old_defs 71 self.old_orm = old_orm 72 self.new_defs = new_defs 73 74 def suggest_name(self): 75 parts = ["auto"] 76 for change_name, params in self.get_changes(): 77 if change_name == "AddModel": 78 parts.append("add_%s" % params['model']._meta.object_name.lower()) 79 elif change_name == "DeleteModel": 80 parts.append("del_%s" % params['model']._meta.object_name.lower()) 81 elif change_name == "AddField": 82 parts.append("add_field_%s_%s" % ( 83 params['model']._meta.object_name.lower(), 84 params['field'].name, 85 )) 86 elif change_name == "DeleteField": 87 parts.append("del_field_%s_%s" % ( 88 params['model']._meta.object_name.lower(), 89 params['field'].name, 90 )) 91 elif change_name == "ChangeField": 92 parts.append("chg_field_%s_%s" % ( 93 params['model']._meta.object_name.lower(), 94 params['new_field'].name, 95 )) 96 elif change_name == "AddUnique": 97 parts.append("add_unique_%s_%s" % ( 98 params['model']._meta.object_name.lower(), 99 "_".join([x.name for x in params['fields']]), 100 )) 101 elif change_name == "DeleteUnique": 102 parts.append("del_unique_%s_%s" % ( 103 params['model']._meta.object_name.lower(), 104 "_".join([x.name for x in params['fields']]), 105 )) 106 elif change_name == "AddIndex": 107 parts.append("add_index_%s_%s" % ( 108 params['model']._meta.object_name.lower(), 109 "_".join([x.name for x in params['fields']]), 110 )) 111 elif change_name == "DeleteIndex": 112 parts.append("del_index_%s_%s" % ( 113 params['model']._meta.object_name.lower(), 114 "_".join([x.name for x in params['fields']]), 115 )) 116 return ("__".join(parts))[:70] 117 118 def get_changes(self): 119 """ 120 Returns the difference between the old and new sets of models as a 5-tuple: 121 added_models, deleted_models, added_fields, deleted_fields, changed_fields 122 """ 123 124 deleted_models = set() 125 126 # See if anything's vanished 127 for key in self.old_defs: 128 if key not in self.new_defs: 129 # We shouldn't delete it if it was managed=False 130 old_fields, old_meta, old_m2ms = self.split_model_def(self.old_orm[key], self.old_defs[key]) 131 if old_meta.get("managed", "True") != "False": 132 # Alright, delete it. 133 yield ("DeleteModel", { 134 "model": self.old_orm[key], 135 "model_def": old_fields, 136 }) 137 # Also make sure we delete any M2Ms it had. 138 for fieldname in old_m2ms: 139 # Only delete its stuff if it wasn't a through=. 140 field = self.old_orm[key + ":" + fieldname] 141 if auto_through(field): 142 yield ("DeleteM2M", {"model": self.old_orm[key], "field": field}) 143 # And any index/uniqueness constraints it had 144 for attr, operation in (("unique_together", "DeleteUnique"), ("index_together", "DeleteIndex")): 145 together = eval(old_meta.get(attr, "[]")) 146 if together: 147 # If it's only a single tuple, make it into the longer one 148 if isinstance(together[0], string_types): 149 together = [together] 150 # For each combination, make an action for it 151 for fields in together: 152 yield (operation, { 153 "model": self.old_orm[key], 154 "fields": [self.old_orm[key]._meta.get_field_by_name(x)[0] for x in fields], 155 }) 156 # We always add it in here so we ignore it later 157 deleted_models.add(key) 158 159 # Or appeared 160 for key in self.new_defs: 161 if key not in self.old_defs: 162 # We shouldn't add it if it's managed=False 163 new_fields, new_meta, new_m2ms = self.split_model_def(self.current_model_from_key(key), self.new_defs[key]) 164 if new_meta.get("managed", "True") != "False": 165 yield ("AddModel", { 166 "model": self.current_model_from_key(key), 167 "model_def": new_fields, 168 }) 169 # Also make sure we add any M2Ms it has. 170 for fieldname in new_m2ms: 171 # Only create its stuff if it wasn't a through=. 172 field = self.current_field_from_key(key, fieldname) 173 if auto_through(field): 174 yield ("AddM2M", {"model": self.current_model_from_key(key), "field": field}) 175 # And any index/uniqueness constraints it has 176 for attr, operation in (("unique_together", "AddUnique"), ("index_together", "AddIndex")): 177 together = eval(new_meta.get(attr, "[]")) 178 if together: 179 # If it's only a single tuple, make it into the longer one 180 if isinstance(together[0], string_types): 181 together = [together] 182 # For each combination, make an action for it 183 for fields in together: 184 yield (operation, { 185 "model": self.current_model_from_key(key), 186 "fields": [self.current_model_from_key(key)._meta.get_field_by_name(x)[0] for x in fields], 187 }) 188 189 # Now, for every model that's stayed the same, check its fields. 190 for key in self.old_defs: 191 if key not in deleted_models: 192 193 old_fields, old_meta, old_m2ms = self.split_model_def(self.old_orm[key], self.old_defs[key]) 194 new_fields, new_meta, new_m2ms = self.split_model_def(self.current_model_from_key(key), self.new_defs[key]) 195 196 # Do nothing for models which are now not managed. 197 if new_meta.get("managed", "True") == "False": 198 continue 199 200 # Find fields that have vanished. 201 for fieldname in old_fields: 202 if fieldname not in new_fields: 203 # Don't do it for any fields we're ignoring 204 field = self.old_orm[key + ":" + fieldname] 205 field_allowed = True 206 for field_type in self.IGNORED_FIELD_TYPES: 207 if isinstance(field, field_type): 208 field_allowed = False 209 if field_allowed: 210 # Looks alright. 211 yield ("DeleteField", { 212 "model": self.old_orm[key], 213 "field": field, 214 "field_def": old_fields[fieldname], 215 }) 216 217 # And ones that have appeared 218 for fieldname in new_fields: 219 if fieldname not in old_fields: 220 # Don't do it for any fields we're ignoring 221 field = self.current_field_from_key(key, fieldname) 222 field_allowed = True 223 for field_type in self.IGNORED_FIELD_TYPES: 224 if isinstance(field, field_type): 225 field_allowed = False 226 if field_allowed: 227 # Looks alright. 228 yield ("AddField", { 229 "model": self.current_model_from_key(key), 230 "field": field, 231 "field_def": new_fields[fieldname], 232 }) 233 234 # Find M2Ms that have vanished 235 for fieldname in old_m2ms: 236 if fieldname not in new_m2ms: 237 # Only delete its stuff if it wasn't a through=. 238 field = self.old_orm[key + ":" + fieldname] 239 if auto_through(field): 240 yield ("DeleteM2M", {"model": self.old_orm[key], "field": field}) 241 242 # Find M2Ms that have appeared 243 for fieldname in new_m2ms: 244 if fieldname not in old_m2ms: 245 # Only create its stuff if it wasn't a through=. 246 field = self.current_field_from_key(key, fieldname) 247 if auto_through(field): 248 yield ("AddM2M", {"model": self.current_model_from_key(key), "field": field}) 249 250 # For the ones that exist in both models, see if they were changed 251 for fieldname in set(old_fields).intersection(set(new_fields)): 252 # Non-index changes 253 if self.different_attributes( 254 remove_useless_attributes(old_fields[fieldname], True, True), 255 remove_useless_attributes(new_fields[fieldname], True, True)): 256 yield ("ChangeField", { 257 "model": self.current_model_from_key(key), 258 "old_field": self.old_orm[key + ":" + fieldname], 259 "new_field": self.current_field_from_key(key, fieldname), 260 "old_def": old_fields[fieldname], 261 "new_def": new_fields[fieldname], 262 }) 263 # Index changes 264 old_field = self.old_orm[key + ":" + fieldname] 265 new_field = self.current_field_from_key(key, fieldname) 266 if not old_field.db_index and new_field.db_index: 267 # They've added an index. 268 yield ("AddIndex", { 269 "model": self.current_model_from_key(key), 270 "fields": [new_field], 271 }) 272 if old_field.db_index and not new_field.db_index: 273 # They've removed an index. 274 yield ("DeleteIndex", { 275 "model": self.old_orm[key], 276 "fields": [old_field], 277 }) 278 # See if their uniques have changed 279 if old_field.unique != new_field.unique: 280 # Make sure we look at the one explicitly given to see what happened 281 if new_field.unique: 282 yield ("AddUnique", { 283 "model": self.current_model_from_key(key), 284 "fields": [new_field], 285 }) 286 else: 287 yield ("DeleteUnique", { 288 "model": self.old_orm[key], 289 "fields": [old_field], 290 }) 291 292 # See if there's any M2Ms that have changed. 293 for fieldname in set(old_m2ms).intersection(set(new_m2ms)): 294 old_field = self.old_orm[key + ":" + fieldname] 295 new_field = self.current_field_from_key(key, fieldname) 296 # Have they _added_ a through= ? 297 if auto_through(old_field) and not auto_through(new_field): 298 yield ("DeleteM2M", {"model": self.old_orm[key], "field": old_field}) 299 # Have they _removed_ a through= ? 300 if not auto_through(old_field) and auto_through(new_field): 301 yield ("AddM2M", {"model": self.current_model_from_key(key), "field": new_field}) 302 303 ## See if the {index,unique}_togethers have changed 304 for attr, add_operation, del_operation in (("unique_together", "AddUnique", "DeleteUnique"), ("index_together", "AddIndex", "DeleteIndex")): 305 # First, normalise them into lists of sets. 306 old_together = eval(old_meta.get(attr, "[]")) 307 new_together = eval(new_meta.get(attr, "[]")) 308 if old_together and isinstance(old_together[0], string_types): 309 old_together = [old_together] 310 if new_together and isinstance(new_together[0], string_types): 311 new_together = [new_together] 312 old_together = frozenset(tuple(o) for o in old_together) 313 new_together = frozenset(tuple(n) for n in new_together) 314 # See if any appeared or disappeared 315 disappeared = old_together.difference(new_together) 316 appeared = new_together.difference(old_together) 317 for item in disappeared: 318 yield (del_operation, { 319 "model": self.old_orm[key], 320 "fields": [self.old_orm[key + ":" + x] for x in item], 321 }) 322 for item in appeared: 323 yield (add_operation, { 324 "model": self.current_model_from_key(key), 325 "fields": [self.current_field_from_key(key, x) for x in item], 326 }) 327 328 @classmethod 329 def is_triple(cls, triple): 330 "Returns whether the argument is a triple." 331 return isinstance(triple, (list, tuple)) and len(triple) == 3 and \ 332 isinstance(triple[0], string_types) and \ 333 isinstance(triple[1], (list, tuple)) and \ 334 isinstance(triple[2], dict) 335 336 @classmethod 337 def different_attributes(cls, old, new): 338 """ 339 Backwards-compat comparison that ignores orm. on the RHS and not the left 340 and which knows django.db.models.fields.CharField = models.CharField. 341 Has a whole load of tests in tests/autodetection.py. 342 """ 343 344 # If they're not triples, just do normal comparison 345 if not cls.is_triple(old) or not cls.is_triple(new): 346 return old != new 347 348 # Expand them out into parts 349 old_field, old_pos, old_kwd = old 350 new_field, new_pos, new_kwd = new 351 352 # Copy the positional and keyword arguments so we can compare them and pop off things 353 old_pos, new_pos = old_pos[:], new_pos[:] 354 old_kwd = dict(old_kwd.items()) 355 new_kwd = dict(new_kwd.items()) 356 357 # Remove comparison of the existence of 'unique', that's done elsewhere. 358 # TODO: Make this work for custom fields where unique= means something else? 359 if "unique" in old_kwd: 360 del old_kwd['unique'] 361 if "unique" in new_kwd: 362 del new_kwd['unique'] 363 364 # If the first bit is different, check it's not by dj.db.models... 365 if old_field != new_field: 366 if old_field.startswith("models.") and (new_field.startswith("django.db.models") \ 367 or new_field.startswith("django.contrib.gis")): 368 if old_field.split(".")[-1] != new_field.split(".")[-1]: 369 return True 370 else: 371 # Remove those fields from the final comparison 372 old_field = new_field = "" 373 374 # If there's a positional argument in the first, and a 'to' in the second, 375 # see if they're actually comparable. 376 if (old_pos and "to" in new_kwd) and ("orm" in new_kwd['to'] and "orm" not in old_pos[0]): 377 # Do special comparison to fix #153 378 try: 379 if old_pos[0] != new_kwd['to'].split("'")[1].split(".")[1]: 380 return True 381 except IndexError: 382 pass # Fall back to next comparison 383 # Remove those attrs from the final comparison 384 old_pos = old_pos[1:] 385 del new_kwd['to'] 386 387 return old_field != new_field or old_pos != new_pos or old_kwd != new_kwd 388 389 390class ManualChanges(BaseChanges): 391 """ 392 Detects changes by reading the command line. 393 """ 394 395 def __init__(self, migrations, added_models, added_fields, added_indexes): 396 self.migrations = migrations 397 self.added_models = added_models 398 self.added_fields = added_fields 399 self.added_indexes = added_indexes 400 401 def suggest_name(self): 402 bits = [] 403 for model_name in self.added_models: 404 bits.append('add_model_%s' % model_name) 405 for field_name in self.added_fields: 406 bits.append('add_field_%s' % field_name) 407 for index_name in self.added_indexes: 408 bits.append('add_index_%s' % index_name) 409 return '_'.join(bits).replace('.', '_') 410 411 def get_changes(self): 412 # Get the model defs so we can use them for the yield later 413 model_defs = freeze_apps([self.migrations.app_label()]) 414 # Make the model changes 415 for model_name in self.added_models: 416 model = models.get_model(self.migrations.app_label(), model_name) 417 real_fields, meta, m2m_fields = self.split_model_def(model, model_defs[model_key(model)]) 418 yield ("AddModel", { 419 "model": model, 420 "model_def": real_fields, 421 }) 422 # And the field changes 423 for field_desc in self.added_fields: 424 try: 425 model_name, field_name = field_desc.split(".") 426 except (TypeError, ValueError): 427 raise ValueError("%r is not a valid field description." % field_desc) 428 model = models.get_model(self.migrations.app_label(), model_name) 429 real_fields, meta, m2m_fields = self.split_model_def(model, model_defs[model_key(model)]) 430 yield ("AddField", { 431 "model": model, 432 "field": model._meta.get_field_by_name(field_name)[0], 433 "field_def": real_fields[field_name], 434 }) 435 # And the indexes 436 for field_desc in self.added_indexes: 437 try: 438 model_name, field_name = field_desc.split(".") 439 except (TypeError, ValueError): 440 print("%r is not a valid field description." % field_desc) 441 model = models.get_model(self.migrations.app_label(), model_name) 442 yield ("AddIndex", { 443 "model": model, 444 "fields": [model._meta.get_field_by_name(field_name)[0]], 445 }) 446 447 448class InitialChanges(BaseChanges): 449 """ 450 Creates all models; handles --initial. 451 """ 452 def suggest_name(self): 453 return 'initial' 454 455 def __init__(self, migrations): 456 self.migrations = migrations 457 458 def get_changes(self): 459 # Get the frozen models for this app 460 model_defs = freeze_apps([self.migrations.app_label()]) 461 462 for model in models.get_models(models.get_app(self.migrations.app_label())): 463 464 # Don't do anything for unmanaged, abstract or proxy models 465 if model._meta.abstract or getattr(model._meta, "proxy", False) or not getattr(model._meta, "managed", True): 466 continue 467 468 real_fields, meta, m2m_fields = self.split_model_def(model, model_defs[model_key(model)]) 469 470 # Firstly, add the main table and fields 471 yield ("AddModel", { 472 "model": model, 473 "model_def": real_fields, 474 }) 475 476 # Then, add any indexing/uniqueness that's around 477 if meta: 478 for attr, operation in (("unique_together", "AddUnique"), ("index_together", "AddIndex")): 479 together = eval(meta.get(attr, "[]")) 480 if together: 481 # If it's only a single tuple, make it into the longer one 482 if isinstance(together[0], string_types): 483 together = [together] 484 # For each combination, make an action for it 485 for fields in together: 486 yield (operation, { 487 "model": model, 488 "fields": [model._meta.get_field_by_name(x)[0] for x in fields], 489 }) 490 491 # Finally, see if there's some M2M action 492 for name, triple in m2m_fields.items(): 493 field = model._meta.get_field_by_name(name)[0] 494 # But only if it's not through=foo (#120) 495 if field.rel.through: 496 try: 497 # Django 1.1 and below 498 through_model = field.rel.through_model 499 except AttributeError: 500 # Django 1.2 501 through_model = field.rel.through 502 if (not field.rel.through) or getattr(through_model._meta, "auto_created", False): 503 yield ("AddM2M", { 504 "model": model, 505 "field": field, 506 }) 507