mirror of
https://github.com/django/django.git
synced 2025-10-24 06:06:09 +00:00
Implement swappable model support for migrations
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
from .migration import Migration # NOQA
|
||||
from .migration import Migration, swappable_dependency # NOQA
|
||||
from .operations import * # NOQA
|
||||
|
||||
@@ -105,8 +105,12 @@ class MigrationAutodetector(object):
|
||||
)
|
||||
)
|
||||
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)
|
||||
# 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]
|
||||
# Ah well, we'll need to split one. Pick deterministically.
|
||||
else:
|
||||
@@ -140,7 +144,11 @@ class MigrationAutodetector(object):
|
||||
),
|
||||
new=True,
|
||||
)
|
||||
if app_label != other_app_label:
|
||||
# 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)
|
||||
# Removing models
|
||||
removed_models = set(old_model_keys) - set(new_model_keys)
|
||||
@@ -276,6 +284,13 @@ class MigrationAutodetector(object):
|
||||
dependency = (other_app_label, "__first__")
|
||||
self.migrations[app_label][-1].dependencies.append(dependency)
|
||||
|
||||
def add_swappable_dependency(self, app_label, setting_name):
|
||||
"""
|
||||
Adds a dependency to the value of a swappable model setting.
|
||||
"""
|
||||
dependency = ("__setting__", setting_name)
|
||||
self.migrations[app_label][-1].dependencies.append(dependency)
|
||||
|
||||
def _arrange_for_graph(self, changes, graph):
|
||||
"""
|
||||
Takes in a result from changes() and a MigrationGraph,
|
||||
|
||||
@@ -223,7 +223,7 @@ class MigrationLoader(object):
|
||||
self.graph.add_node(parent, new_migration)
|
||||
self.applied_migrations.add(parent)
|
||||
elif parent[0] in self.migrated_apps:
|
||||
parent = (parent[0], list(self.graph.root_nodes(parent[0]))[0])
|
||||
parent = list(self.graph.root_nodes(parent[0]))[0]
|
||||
else:
|
||||
raise ValueError("Dependency on unknown app %s" % parent[0])
|
||||
self.graph.add_dependency(key, parent)
|
||||
|
||||
@@ -127,3 +127,10 @@ class Migration(object):
|
||||
to_run.reverse()
|
||||
for operation, to_state, from_state in to_run:
|
||||
operation.database_backwards(self.app_label, schema_editor, from_state, to_state)
|
||||
|
||||
|
||||
def swappable_dependency(value):
|
||||
"""
|
||||
Turns a setting value into a dependency.
|
||||
"""
|
||||
return (value.split(".", 1)[0], "__first__")
|
||||
|
||||
@@ -13,6 +13,20 @@ from django.utils.functional import Promise
|
||||
from django.utils import six
|
||||
|
||||
|
||||
class SettingsReference(str):
|
||||
"""
|
||||
Special subclass of string which actually references a current settings
|
||||
value. It's treated as the value in memory, but serializes out to a
|
||||
settings.NAME attribute reference.
|
||||
"""
|
||||
|
||||
def __new__(self, value, setting_name):
|
||||
return str.__new__(self, value)
|
||||
|
||||
def __init__(self, value, setting_name):
|
||||
self.setting_name = setting_name
|
||||
|
||||
|
||||
class MigrationWriter(object):
|
||||
"""
|
||||
Takes a Migration instance and is able to produce the contents
|
||||
@@ -27,7 +41,6 @@ class MigrationWriter(object):
|
||||
Returns a string of the file contents.
|
||||
"""
|
||||
items = {
|
||||
"dependencies": repr(self.migration.dependencies),
|
||||
"replaces_str": "",
|
||||
}
|
||||
imports = set()
|
||||
@@ -46,6 +59,15 @@ class MigrationWriter(object):
|
||||
arg_strings.append("%s = %s" % (kw, arg_string))
|
||||
operation_strings.append("migrations.%s(%s\n )" % (name, "".join("\n %s," % arg for arg in arg_strings)))
|
||||
items["operations"] = "[%s\n ]" % "".join("\n %s," % s for s in operation_strings)
|
||||
# Format dependencies and write out swappable dependencies right
|
||||
items["dependencies"] = "["
|
||||
for dependency in self.migration.dependencies:
|
||||
if dependency[0] == "__setting__":
|
||||
items["dependencies"] += "\n migrations.swappable_dependency(settings.%s)," % dependency[1]
|
||||
imports.add("from django.conf import settings")
|
||||
else:
|
||||
items["dependencies"] += "\n %s," % repr(dependency)
|
||||
items["dependencies"] += "\n ]"
|
||||
# Format imports nicely
|
||||
imports.discard("from django.db import models")
|
||||
if not imports:
|
||||
@@ -136,6 +158,9 @@ class MigrationWriter(object):
|
||||
# Datetimes
|
||||
elif isinstance(value, (datetime.datetime, datetime.date)):
|
||||
return repr(value), set(["import datetime"])
|
||||
# Settings references
|
||||
elif isinstance(value, SettingsReference):
|
||||
return "settings.%s" % value.setting_name, set(["from django.conf import settings"])
|
||||
# Simple types
|
||||
elif isinstance(value, six.integer_types + (float, six.binary_type, six.text_type, bool, type(None))):
|
||||
return repr(value), set()
|
||||
|
||||
@@ -16,6 +16,7 @@ from django.utils.deprecation import RenameMethodsBase
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.functional import curry, cached_property
|
||||
from django.core import exceptions
|
||||
from django.apps import apps
|
||||
from django import forms
|
||||
|
||||
RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
|
||||
@@ -121,6 +122,30 @@ class RelatedField(Field):
|
||||
else:
|
||||
self.do_related_class(other, cls)
|
||||
|
||||
@property
|
||||
def swappable_setting(self):
|
||||
"""
|
||||
Gets the setting that this is powered from for swapping, or None
|
||||
if it's not swapped in / marked with swappable=False.
|
||||
"""
|
||||
if self.swappable:
|
||||
# Work out string form of "to"
|
||||
if isinstance(self.rel.to, six.string_types):
|
||||
to_string = self.rel.to
|
||||
else:
|
||||
to_string = "%s.%s" % (
|
||||
self.rel.to._meta.app_label,
|
||||
self.rel.to._meta.object_name,
|
||||
)
|
||||
# See if anything swapped/swappable matches
|
||||
for model in apps.get_models(include_swapped=True):
|
||||
if model._meta.swapped:
|
||||
if model._meta.swapped == to_string:
|
||||
return model._meta.swappable
|
||||
if ("%s.%s" % (model._meta.app_label, model._meta.object_name)) == to_string and model._meta.swappable:
|
||||
return model._meta.swappable
|
||||
return None
|
||||
|
||||
def set_attributes_from_rel(self):
|
||||
self.name = self.name or (self.rel.to._meta.model_name + '_' + self.rel.to._meta.pk.name)
|
||||
if self.verbose_name is None:
|
||||
@@ -1061,9 +1086,10 @@ class ForeignObject(RelatedField):
|
||||
generate_reverse_relation = True
|
||||
related_accessor_class = ForeignRelatedObjectsDescriptor
|
||||
|
||||
def __init__(self, to, from_fields, to_fields, **kwargs):
|
||||
def __init__(self, to, from_fields, to_fields, swappable=True, **kwargs):
|
||||
self.from_fields = from_fields
|
||||
self.to_fields = to_fields
|
||||
self.swappable = swappable
|
||||
|
||||
if 'rel' not in kwargs:
|
||||
kwargs['rel'] = ForeignObjectRel(
|
||||
@@ -1082,10 +1108,25 @@ class ForeignObject(RelatedField):
|
||||
name, path, args, kwargs = super(ForeignObject, self).deconstruct()
|
||||
kwargs['from_fields'] = self.from_fields
|
||||
kwargs['to_fields'] = self.to_fields
|
||||
# Work out string form of "to"
|
||||
if isinstance(self.rel.to, six.string_types):
|
||||
kwargs['to'] = self.rel.to
|
||||
else:
|
||||
kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name)
|
||||
# If swappable is True, then see if we're actually pointing to the target
|
||||
# of a swap.
|
||||
swappable_setting = self.swappable_setting
|
||||
if swappable_setting is not None:
|
||||
# If it's already a settings reference, error
|
||||
if hasattr(kwargs['to'], "setting_name"):
|
||||
if kwargs['to'].setting_name != swappable_setting:
|
||||
raise ValueError("Cannot deconstruct a ForeignKey pointing to a model that is swapped in place of more than one model (%s and %s)" % (kwargs['to'].setting_name, swappable_setting))
|
||||
# Set it
|
||||
from django.db.migrations.writer import SettingsReference
|
||||
kwargs['to'] = SettingsReference(
|
||||
kwargs['to'],
|
||||
swappable_setting,
|
||||
)
|
||||
return name, path, args, kwargs
|
||||
|
||||
def resolve_related_fields(self):
|
||||
@@ -1516,7 +1557,7 @@ def create_many_to_many_intermediary_model(field, klass):
|
||||
class ManyToManyField(RelatedField):
|
||||
description = _("Many-to-many relationship")
|
||||
|
||||
def __init__(self, to, db_constraint=True, **kwargs):
|
||||
def __init__(self, to, db_constraint=True, swappable=True, **kwargs):
|
||||
try:
|
||||
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
|
||||
except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
|
||||
@@ -1534,6 +1575,7 @@ class ManyToManyField(RelatedField):
|
||||
db_constraint=db_constraint,
|
||||
)
|
||||
|
||||
self.swappable = swappable
|
||||
self.db_table = kwargs.pop('db_table', None)
|
||||
if kwargs['rel'].through is not None:
|
||||
assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
|
||||
@@ -1552,6 +1594,20 @@ class ManyToManyField(RelatedField):
|
||||
kwargs['to'] = self.rel.to
|
||||
else:
|
||||
kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name)
|
||||
# If swappable is True, then see if we're actually pointing to the target
|
||||
# of a swap.
|
||||
swappable_setting = self.swappable_setting
|
||||
if swappable_setting is not None:
|
||||
# If it's already a settings reference, error
|
||||
if hasattr(kwargs['to'], "setting_name"):
|
||||
if kwargs['to'].setting_name != swappable_setting:
|
||||
raise ValueError("Cannot deconstruct a ManyToManyField pointing to a model that is swapped in place of more than one model (%s and %s)" % (kwargs['to'].setting_name, swappable_setting))
|
||||
# Set it
|
||||
from django.db.migrations.writer import SettingsReference
|
||||
kwargs['to'] = SettingsReference(
|
||||
kwargs['to'],
|
||||
swappable_setting,
|
||||
)
|
||||
return name, path, args, kwargs
|
||||
|
||||
def _get_path_info(self, direct=False):
|
||||
|
||||
Reference in New Issue
Block a user