示例#1
0
 def generate_added_fields(self):
     # New fields
     self.renamed_fields = {}
     for app_label, model_name, field_name in sorted(self.new_field_keys - self.old_field_keys):
         old_model_name = self.renamed_models.get((app_label, model_name), model_name)
         old_model_state = self.from_state.models[app_label, old_model_name]
         new_model_state = self.to_state.models[app_label, model_name]
         field = new_model_state.get_field_by_name(field_name)
         # Scan to see if this is actually a rename!
         field_dec = self.deep_deconstruct(field)
         found_rename = False
         for rem_app_label, rem_model_name, rem_field_name in sorted(self.old_field_keys - self.new_field_keys):
             if rem_app_label == app_label and rem_model_name == model_name:
                 old_field_dec = self.deep_deconstruct(old_model_state.get_field_by_name(rem_field_name))
                 if field.rel and field.rel.to and 'to' in old_field_dec[2]:
                     old_rel_to = old_field_dec[2]['to']
                     if old_rel_to in self.renamed_models_rel:
                         old_field_dec[2]['to'] = self.renamed_models_rel[old_rel_to]
                 if old_field_dec == field_dec:
                     if self.questioner.ask_rename(model_name, rem_field_name, field_name, field):
                         self.add_operation(
                             app_label,
                             operations.RenameField(
                                 model_name=model_name,
                                 old_name=rem_field_name,
                                 new_name=field_name,
                             )
                         )
                         self.old_field_keys.remove((rem_app_label, rem_model_name, rem_field_name))
                         self.old_field_keys.add((app_label, model_name, field_name))
                         self.renamed_fields[app_label, model_name, field_name] = rem_field_name
                         found_rename = True
                         break
         if found_rename:
             continue
         # You can't just add NOT NULL fields with no default
         if not field.null and not field.has_default() and not isinstance(field, models.ManyToManyField):
             field = field.clone()
             field.default = self.questioner.ask_not_null_addition(field_name, model_name)
             self.add_operation(
                 app_label,
                 operations.AddField(
                     model_name=model_name,
                     name=field_name,
                     field=field,
                     preserve_default=False,
                 )
             )
         else:
             self.add_operation(
                 app_label,
                 operations.AddField(
                     model_name=model_name,
                     name=field_name,
                     field=field,
                 )
             )
    def test_operation_is_safe(self):
        assert migration_utils.operation_is_safe(
            operations.AddField(
                model_name="unsafemodel",
                name="safe_field",
                field=models.IntegerField(null=True),
            )
        )

        assert (
            migration_utils.operation_is_safe(
                operations.AddField(
                    model_name="unsafemodel",
                    name="field_added",
                    field=models.IntegerField(default=1),
                    preserve_default=False,
                )
            )
            is False
        )

        assert (
            migration_utils.operation_is_safe(
                operations.AlterField(
                    model_name="unsafemodel",
                    name="field_added",
                    field=models.PositiveIntegerField(default=10),
                    preserve_default=False,
                )
            )
            is False
        )

        assert (
            migration_utils.operation_is_safe(
                operations.RenameField(
                    model_name="unsafemodel", old_name="field_added", new_name="field_renamed"
                )
            )
            is False
        )

        assert (
            migration_utils.operation_is_safe(
                operations.RemoveField(
                    model_name="unsafemodel",
                    name="field_renamed",
                )
            )
            is False
        )

        assert migration_utils.operation_is_safe(operations.DeleteModel("unsafemodel")) is False

        assert migration_utils.operation_is_safe(operations.RunSQL("")) is False
        assert migration_utils.operation_is_safe(operations.RunPython(lambda: True)) is False
示例#3
0
    def _detect_changes(self):
        """
        Returns a dict of migration plans which will achieve the
        change from from_state to to_state. The dict has app labels
        as keys and a list of migrations as values.

        The resulting migrations aren't specially named, but the names
        do matter for dependencies inside the set.
        """
        # We'll store migrations as lists by app names for now
        self.migrations = {}
        old_app_cache = self.from_state.render()
        new_app_cache = self.to_state.render()
        # Prepare lists of old/new model keys that we care about
        # (i.e. ignoring proxy ones)
        old_model_keys = [
            (al, mn)
            for al, mn in self.from_state.models.keys()
            if not old_app_cache.get_model(al, mn)._meta.proxy
        ]
        new_model_keys = [
            (al, mn)
            for al, mn in self.to_state.models.keys()
            if not new_app_cache.get_model(al, mn)._meta.proxy
        ]
        # Adding models. Phase 1 is adding models with no outward relationships.
        added_models = set(new_model_keys) - set(old_model_keys)
        pending_add = {}
        for app_label, model_name in added_models:
            model_state = self.to_state.models[app_label, model_name]
            # Are there any relationships out from this model? if so, punt it to the next phase.
            related_fields = []
            for field in new_app_cache.get_model(app_label, model_name)._meta.local_fields:
                if field.rel:
                    if field.rel.to:
                        related_fields.append((field.name, field.rel.to._meta.app_label.lower(), field.rel.to._meta.object_name.lower()))
                    if hasattr(field.rel, "through") and not field.rel.though._meta.auto_created:
                        related_fields.append((field.name, field.rel.through._meta.app_label.lower(), field.rel.through._meta.object_name.lower()))
            if related_fields:
                pending_add[app_label, model_name] = related_fields
            else:
                self.add_to_migration(
                    app_label,
                    operations.CreateModel(
                        name=model_state.name,
                        fields=model_state.fields,
                        options=model_state.options,
                        bases=model_state.bases,
                    )
                )
        # Phase 2 is progressively adding pending models, splitting up into two
        # migrations if required.
        pending_new_fks = []
        while pending_add:
            # Is there one we can add that has all dependencies satisfied?
            satisfied = [(m, rf) for m, rf in pending_add.items() if all((al, mn) not in pending_add for f, al, mn in rf)]
            if satisfied:
                (app_label, model_name), related_fields = sorted(satisfied)[0]
                model_state = self.to_state.models[app_label, model_name]
                self.add_to_migration(
                    app_label,
                    operations.CreateModel(
                        name=model_state.name,
                        fields=model_state.fields,
                        options=model_state.options,
                        bases=model_state.bases,
                    )
                )
                for field_name, other_app_label, other_model_name in related_fields:
                    if app_label != other_app_label:
                        self.add_dependency(app_label, other_app_label)
                del pending_add[app_label, model_name]
            # Ah well, we'll need to split one. Pick deterministically.
            else:
                (app_label, model_name), related_fields = sorted(pending_add.items())[0]
                model_state = self.to_state.models[app_label, model_name]
                # Work out the fields that need splitting out
                bad_fields = dict((f, (al, mn)) for f, al, mn in related_fields if (al, mn) in pending_add)
                # Create the model, without those
                self.add_to_migration(
                    app_label,
                    operations.CreateModel(
                        name=model_state.name,
                        fields=[(n, f) for n, f in model_state.fields if n not in bad_fields],
                        options=model_state.options,
                        bases=model_state.bases,
                    )
                )
                # Add the bad fields to be made in a phase 3
                for field_name, (other_app_label, other_model_name) in bad_fields.items():
                    pending_new_fks.append((app_label, model_name, field_name, other_app_label))
                del pending_add[app_label, model_name]
        # Phase 3 is adding the final set of FKs as separate new migrations
        for app_label, model_name, field_name, other_app_label in pending_new_fks:
            model_state = self.to_state.models[app_label, model_name]
            self.add_to_migration(
                app_label,
                operations.AddField(
                    model_name=model_name,
                    name=field_name,
                    field=model_state.get_field_by_name(field_name),
                ),
                new=True,
            )
            if app_label != other_app_label:
                self.add_dependency(app_label, other_app_label)
        # Removing models
        removed_models = set(old_model_keys) - set(new_model_keys)
        for app_label, model_name in removed_models:
            model_state = self.from_state.models[app_label, model_name]
            self.add_to_migration(
                app_label,
                operations.DeleteModel(
                    model_state.name,
                )
            )
        # Changes within models
        kept_models = set(old_model_keys).intersection(new_model_keys)
        old_fields = set()
        new_fields = set()
        for app_label, model_name in kept_models:
            old_model_state = self.from_state.models[app_label, model_name]
            new_model_state = self.to_state.models[app_label, model_name]
            # Collect field changes for later global dealing with (so AddFields
            # always come before AlterFields even on separate models)
            old_fields.update((app_label, model_name, x) for x, y in old_model_state.fields)
            new_fields.update((app_label, model_name, x) for x, y in new_model_state.fields)
            # Unique_together changes
            if old_model_state.options.get("unique_together", set()) != new_model_state.options.get("unique_together", set()):
                self.add_to_migration(
                    app_label,
                    operations.AlterUniqueTogether(
                        name=model_name,
                        unique_together=new_model_state.options.get("unique_together", set()),
                    )
                )
        # New fields
        for app_label, model_name, field_name in new_fields - old_fields:
            old_model_state = self.from_state.models[app_label, model_name]
            new_model_state = self.to_state.models[app_label, model_name]
            field = new_model_state.get_field_by_name(field_name)
            # Scan to see if this is actually a rename!
            field_dec = field.deconstruct()[1:]
            found_rename = False
            for rem_app_label, rem_model_name, rem_field_name in (old_fields - new_fields):
                if rem_app_label == app_label and rem_model_name == model_name:
                    if old_model_state.get_field_by_name(rem_field_name).deconstruct()[1:] == field_dec:
                        if self.questioner.ask_rename(model_name, rem_field_name, field_name, field):
                            self.add_to_migration(
                                app_label,
                                operations.RenameField(
                                    model_name=model_name,
                                    old_name=rem_field_name,
                                    new_name=field_name,
                                )
                            )
                            old_fields.remove((rem_app_label, rem_model_name, rem_field_name))
                            new_fields.remove((app_label, model_name, field_name))
                            found_rename = True
                            break
            if found_rename:
                continue
            # You can't just add NOT NULL fields with no default
            if not field.null and not field.has_default():
                field = field.clone()
                field.default = self.questioner.ask_not_null_addition(field_name, model_name)
                self.add_to_migration(
                    app_label,
                    operations.AddField(
                        model_name=model_name,
                        name=field_name,
                        field=field,
                        preserve_default=False,
                    )
                )
            else:
                self.add_to_migration(
                    app_label,
                    operations.AddField(
                        model_name=model_name,
                        name=field_name,
                        field=field,
                    )
                )
        # Old fields
        for app_label, model_name, field_name in old_fields - new_fields:
            old_model_state = self.from_state.models[app_label, model_name]
            new_model_state = self.to_state.models[app_label, model_name]
            self.add_to_migration(
                app_label,
                operations.RemoveField(
                    model_name=model_name,
                    name=field_name,
                )
            )
        # The same fields
        for app_label, model_name, field_name in old_fields.intersection(new_fields):
            # Did the field change?
            old_model_state = self.from_state.models[app_label, model_name]
            new_model_state = self.to_state.models[app_label, model_name]
            old_field_dec = old_model_state.get_field_by_name(field_name).deconstruct()
            new_field_dec = new_model_state.get_field_by_name(field_name).deconstruct()
            if old_field_dec != new_field_dec:
                self.add_to_migration(
                    app_label,
                    operations.AlterField(
                        model_name=model_name,
                        name=field_name,
                        field=new_model_state.get_field_by_name(field_name),
                    )
                )
        # Alright, now add internal dependencies
        for app_label, migrations in self.migrations.items():
            for m1, m2 in zip(migrations, migrations[1:]):
                m2.dependencies.append((app_label, m1.name))
        # Clean up dependencies
        for app_label, migrations in self.migrations.items():
            for migration in migrations:
                migration.dependencies = list(set(migration.dependencies))
        return self.migrations
示例#4
0
    def _detect_changes(self):
        """
        Returns a dict of migration plans which will achieve the
        change from from_state to to_state. The dict has app labels
        as keys and a list of migrations as values.

        The resulting migrations aren't specially named, but the names
        do matter for dependencies inside the set.
        """
        # We'll store migrations as lists by app names for now
        self.migrations = {}
        old_apps = self.from_state.render()
        new_apps = self.to_state.render()
        # Prepare lists of old/new model keys that we care about
        # (i.e. ignoring proxy ones)
        old_model_keys = [(al, mn) for al, mn in self.from_state.models.keys()
                          if not old_apps.get_model(al, mn)._meta.proxy]
        new_model_keys = [(al, mn) for al, mn in self.to_state.models.keys()
                          if not new_apps.get_model(al, mn)._meta.proxy]

        def _rel_agnostic_fields_def(fields):
            """
            Return a definition of the fields that ignores field names and
            what related fields actually relate to.
            """
            fields_def = []
            for name, field in fields:
                deconstruction = field.deconstruct()[1:]
                if field.rel and field.rel.to:
                    del deconstruction[2]['to']
                fields_def.append(deconstruction)
            return fields_def

        # Find any renamed models.
        renamed_models = {}
        renamed_models_rel = {}
        added_models = set(new_model_keys) - set(old_model_keys)
        for app_label, model_name in added_models:
            model_state = self.to_state.models[app_label, model_name]
            model_fields_def = _rel_agnostic_fields_def(model_state.fields)

            removed_models = set(old_model_keys) - set(new_model_keys)
            for rem_app_label, rem_model_name in removed_models:
                if rem_app_label == app_label:
                    rem_model_state = self.from_state.models[rem_app_label,
                                                             rem_model_name]
                    rem_model_fields_def = _rel_agnostic_fields_def(
                        rem_model_state.fields)
                    if model_fields_def == rem_model_fields_def:
                        if self.questioner.ask_rename_model(
                                rem_model_state, model_state):
                            self.add_to_migration(
                                app_label,
                                operations.RenameModel(
                                    old_name=rem_model_state.name,
                                    new_name=model_state.name,
                                ))
                            renamed_models[app_label,
                                           model_name] = rem_model_name
                            renamed_models_rel[
                                '%s.%s' %
                                (rem_model_state.app_label,
                                 rem_model_state.name)] = '%s.%s' % (
                                     model_state.app_label, model_state.name)
                            old_model_keys.remove(
                                (rem_app_label, rem_model_name))
                            old_model_keys.append((app_label, model_name))
                            break

        # Adding models. Phase 1 is adding models with no outward relationships.
        added_models = set(new_model_keys) - set(old_model_keys)
        pending_add = {}
        for app_label, model_name in added_models:
            model_state = self.to_state.models[app_label, model_name]
            # Are there any relationships out from this model? if so, punt it to the next phase.
            related_fields = []
            for field in new_apps.get_model(app_label,
                                            model_name)._meta.local_fields:
                if field.rel:
                    if field.rel.to:
                        related_fields.append(
                            (field.name, field.rel.to._meta.app_label,
                             field.rel.to._meta.model_name))
                    if hasattr(field.rel, "through"
                               ) and not field.rel.through._meta.auto_created:
                        related_fields.append(
                            (field.name, field.rel.through._meta.app_label,
                             field.rel.through._meta.model_name))
            for field in new_apps.get_model(
                    app_label, model_name)._meta.local_many_to_many:
                if field.rel.to:
                    related_fields.append(
                        (field.name, field.rel.to._meta.app_label,
                         field.rel.to._meta.model_name))
                if hasattr(field.rel, "through"
                           ) and not field.rel.through._meta.auto_created:
                    related_fields.append(
                        (field.name, field.rel.through._meta.app_label,
                         field.rel.through._meta.model_name))
            if related_fields:
                pending_add[app_label, model_name] = related_fields
            else:
                self.add_to_migration(
                    app_label,
                    operations.CreateModel(
                        name=model_state.name,
                        fields=model_state.fields,
                        options=model_state.options,
                        bases=model_state.bases,
                    ))
        # Phase 2 is progressively adding pending models, splitting up into two
        # migrations if required.
        pending_new_fks = []
        pending_unique_together = []
        added_phase_2 = set()
        while pending_add:
            # Is there one we can add that has all dependencies satisfied?
            satisfied = [(m, rf) for m, rf in pending_add.items() if all(
                (al, mn) not in pending_add for f, al, mn in rf)]
            if satisfied:
                (app_label, model_name), related_fields = sorted(satisfied)[0]
                model_state = self.to_state.models[app_label, model_name]
                self.add_to_migration(
                    app_label,
                    operations.CreateModel(
                        name=model_state.name,
                        fields=model_state.fields,
                        options=model_state.options,
                        bases=model_state.bases,
                    ),
                    # If it's already been added in phase 2 put it in a new
                    # migration for safety.
                    new=any((al, mn) in added_phase_2
                            for f, al, mn in related_fields),
                )
                for field_name, other_app_label, other_model_name in related_fields:
                    # If it depends on a swappable something, add a dynamic depend'cy
                    swappable_setting = new_apps.get_model(
                        app_label, model_name)._meta.get_field_by_name(
                            field_name)[0].swappable_setting
                    if swappable_setting is not None:
                        self.add_swappable_dependency(app_label,
                                                      swappable_setting)
                    elif app_label != other_app_label:
                        self.add_dependency(app_label, other_app_label)
                del pending_add[app_label, model_name]
                added_phase_2.add((app_label, model_name))
            # Ah well, we'll need to split one. Pick deterministically.
            else:
                (app_label,
                 model_name), related_fields = sorted(pending_add.items())[0]
                model_state = self.to_state.models[app_label, model_name]
                # Defer unique together constraints creation, see ticket #22275
                unique_together_constraints = model_state.options.pop(
                    'unique_together', None)
                if unique_together_constraints:
                    pending_unique_together.append(
                        (app_label, model_name, unique_together_constraints))
                # Work out the fields that need splitting out
                bad_fields = dict((f, (al, mn)) for f, al, mn in related_fields
                                  if (al, mn) in pending_add)
                # Create the model, without those
                self.add_to_migration(
                    app_label,
                    operations.CreateModel(
                        name=model_state.name,
                        fields=[(n, f) for n, f in model_state.fields
                                if n not in bad_fields],
                        options=model_state.options,
                        bases=model_state.bases,
                    ))
                # Add the bad fields to be made in a phase 3
                for field_name, (other_app_label,
                                 other_model_name) in bad_fields.items():
                    pending_new_fks.append(
                        (app_label, model_name, field_name, other_app_label))
                del pending_add[app_label, model_name]
        # Phase 3 is adding the final set of FKs as separate new migrations
        for app_label, model_name, field_name, other_app_label in pending_new_fks:
            model_state = self.to_state.models[app_label, model_name]
            self.add_to_migration(
                app_label,
                operations.AddField(
                    model_name=model_name,
                    name=field_name,
                    field=model_state.get_field_by_name(field_name),
                ),
                new=True,
            )
            # If it depends on a swappable something, add a dynamic depend'cy
            swappable_setting = new_apps.get_model(
                app_label, model_name)._meta.get_field_by_name(
                    field_name)[0].swappable_setting
            if swappable_setting is not None:
                self.add_swappable_dependency(app_label, swappable_setting)
            elif app_label != other_app_label:
                self.add_dependency(app_label, other_app_label)
        # Phase 3.1 - unique together constraints
        for app_label, model_name, unique_together in pending_unique_together:
            self.add_to_migration(
                app_label,
                operations.AlterUniqueTogether(
                    name=model_name, unique_together=unique_together))
        # Removing models
        removed_models = set(old_model_keys) - set(new_model_keys)
        for app_label, model_name in removed_models:
            model_state = self.from_state.models[app_label, model_name]
            self.add_to_migration(app_label,
                                  operations.DeleteModel(model_state.name, ))
        # Changes within models
        kept_models = set(old_model_keys).intersection(new_model_keys)
        old_fields = set()
        new_fields = set()
        unique_together_operations = []
        for app_label, model_name in kept_models:
            old_model_name = renamed_models.get((app_label, model_name),
                                                model_name)
            old_model_state = self.from_state.models[app_label, old_model_name]
            new_model_state = self.to_state.models[app_label, model_name]
            # Collect field changes for later global dealing with (so AddFields
            # always come before AlterFields even on separate models)
            old_fields.update(
                (app_label, model_name, x) for x, y in old_model_state.fields)
            new_fields.update(
                (app_label, model_name, x) for x, y in new_model_state.fields)
            # Unique_together changes. Operations will be added to migration a
            # bit later, after fields creation. See ticket #22035.
            if old_model_state.options.get(
                    "unique_together", set()) != new_model_state.options.get(
                        "unique_together", set()):
                unique_together_operations.append(
                    (app_label,
                     operations.AlterUniqueTogether(
                         name=model_name,
                         unique_together=new_model_state.options.get(
                             "unique_together", set()),
                     )))
        # New fields
        renamed_fields = {}
        for app_label, model_name, field_name in new_fields - old_fields:
            old_model_name = renamed_models.get((app_label, model_name),
                                                model_name)
            old_model_state = self.from_state.models[app_label, old_model_name]
            new_model_state = self.to_state.models[app_label, model_name]
            field = new_model_state.get_field_by_name(field_name)
            # Scan to see if this is actually a rename!
            field_dec = field.deconstruct()[1:]
            found_rename = False
            for rem_app_label, rem_model_name, rem_field_name in (old_fields -
                                                                  new_fields):
                if rem_app_label == app_label and rem_model_name == model_name:
                    old_field_dec = old_model_state.get_field_by_name(
                        rem_field_name).deconstruct()[1:]
                    if field.rel and field.rel.to:
                        old_rel_to = old_field_dec[2]['to']
                        if old_rel_to in renamed_models_rel:
                            old_field_dec[2]['to'] = renamed_models_rel[
                                old_rel_to]
                    if old_field_dec == field_dec:
                        if self.questioner.ask_rename(model_name,
                                                      rem_field_name,
                                                      field_name, field):
                            self.add_to_migration(
                                app_label,
                                operations.RenameField(
                                    model_name=model_name,
                                    old_name=rem_field_name,
                                    new_name=field_name,
                                ))
                            old_fields.remove((rem_app_label, rem_model_name,
                                               rem_field_name))
                            old_fields.add((app_label, model_name, field_name))
                            renamed_fields[app_label, model_name,
                                           field_name] = rem_field_name
                            found_rename = True
                            break
            if found_rename:
                continue
            # You can't just add NOT NULL fields with no default
            if not field.null and not field.has_default():
                field = field.clone()
                field.default = self.questioner.ask_not_null_addition(
                    field_name, model_name)
                self.add_to_migration(
                    app_label,
                    operations.AddField(
                        model_name=model_name,
                        name=field_name,
                        field=field,
                        preserve_default=False,
                    ))
            else:
                self.add_to_migration(
                    app_label,
                    operations.AddField(
                        model_name=model_name,
                        name=field_name,
                        field=field,
                    ))
                new_field = new_apps.get_model(
                    app_label,
                    model_name)._meta.get_field_by_name(field_name)[0]
                swappable_setting = getattr(new_field, 'swappable_setting',
                                            None)
                if swappable_setting is not None:
                    self.add_swappable_dependency(app_label, swappable_setting)
        # Old fields
        for app_label, model_name, field_name in old_fields - new_fields:
            old_model_name = renamed_models.get((app_label, model_name),
                                                model_name)
            old_model_state = self.from_state.models[app_label, old_model_name]
            new_model_state = self.to_state.models[app_label, model_name]
            self.add_to_migration(
                app_label,
                operations.RemoveField(
                    model_name=model_name,
                    name=field_name,
                ))
        # The same fields
        for app_label, model_name, field_name in old_fields.intersection(
                new_fields):
            # Did the field change?
            old_model_name = renamed_models.get((app_label, model_name),
                                                model_name)
            old_model_state = self.from_state.models[app_label, old_model_name]
            new_model_state = self.to_state.models[app_label, model_name]
            old_field_name = renamed_fields.get(
                (app_label, model_name, field_name), field_name)
            old_field_dec = old_model_state.get_field_by_name(
                old_field_name).deconstruct()[1:]
            new_field_dec = new_model_state.get_field_by_name(
                field_name).deconstruct()[1:]
            if old_field_dec != new_field_dec:
                self.add_to_migration(
                    app_label,
                    operations.AlterField(
                        model_name=model_name,
                        name=field_name,
                        field=new_model_state.get_field_by_name(field_name),
                    ))
        for app_label, operation in unique_together_operations:
            self.add_to_migration(app_label, operation)
        # Alright, now add internal dependencies
        for app_label, migrations in self.migrations.items():
            for m1, m2 in zip(migrations, migrations[1:]):
                m2.dependencies.append((app_label, m1.name))
        # Clean up dependencies
        for app_label, migrations in self.migrations.items():
            for migration in migrations:
                migration.dependencies = list(set(migration.dependencies))
        return self.migrations