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