Implement swappable model support for migrations

This commit is contained in:
Andrew Godwin 2014-01-15 14:20:47 +00:00
parent 5c7ac7494a
commit c9de1b4a55
10 changed files with 216 additions and 13 deletions

View File

@ -1,2 +1,2 @@
from .migration import Migration # NOQA from .migration import Migration, swappable_dependency # NOQA
from .operations import * # NOQA from .operations import * # NOQA

View File

@ -105,7 +105,11 @@ class MigrationAutodetector(object):
) )
) )
for field_name, other_app_label, other_model_name in related_fields: for field_name, other_app_label, other_model_name in related_fields:
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) self.add_dependency(app_label, other_app_label)
del pending_add[app_label, model_name] del pending_add[app_label, model_name]
# Ah well, we'll need to split one. Pick deterministically. # Ah well, we'll need to split one. Pick deterministically.
@ -140,7 +144,11 @@ class MigrationAutodetector(object):
), ),
new=True, 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) self.add_dependency(app_label, other_app_label)
# Removing models # Removing models
removed_models = set(old_model_keys) - set(new_model_keys) removed_models = set(old_model_keys) - set(new_model_keys)
@ -276,6 +284,13 @@ class MigrationAutodetector(object):
dependency = (other_app_label, "__first__") dependency = (other_app_label, "__first__")
self.migrations[app_label][-1].dependencies.append(dependency) 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): def _arrange_for_graph(self, changes, graph):
""" """
Takes in a result from changes() and a MigrationGraph, Takes in a result from changes() and a MigrationGraph,

View File

@ -223,7 +223,7 @@ class MigrationLoader(object):
self.graph.add_node(parent, new_migration) self.graph.add_node(parent, new_migration)
self.applied_migrations.add(parent) self.applied_migrations.add(parent)
elif parent[0] in self.migrated_apps: 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: else:
raise ValueError("Dependency on unknown app %s" % parent[0]) raise ValueError("Dependency on unknown app %s" % parent[0])
self.graph.add_dependency(key, parent) self.graph.add_dependency(key, parent)

View File

@ -127,3 +127,10 @@ class Migration(object):
to_run.reverse() to_run.reverse()
for operation, to_state, from_state in to_run: for operation, to_state, from_state in to_run:
operation.database_backwards(self.app_label, schema_editor, from_state, to_state) 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__")

View File

@ -13,6 +13,20 @@ from django.utils.functional import Promise
from django.utils import six 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): class MigrationWriter(object):
""" """
Takes a Migration instance and is able to produce the contents 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. Returns a string of the file contents.
""" """
items = { items = {
"dependencies": repr(self.migration.dependencies),
"replaces_str": "", "replaces_str": "",
} }
imports = set() imports = set()
@ -46,6 +59,15 @@ class MigrationWriter(object):
arg_strings.append("%s = %s" % (kw, arg_string)) 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))) 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) 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 # Format imports nicely
imports.discard("from django.db import models") imports.discard("from django.db import models")
if not imports: if not imports:
@ -136,6 +158,9 @@ class MigrationWriter(object):
# Datetimes # Datetimes
elif isinstance(value, (datetime.datetime, datetime.date)): elif isinstance(value, (datetime.datetime, datetime.date)):
return repr(value), set(["import datetime"]) 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 # Simple types
elif isinstance(value, six.integer_types + (float, six.binary_type, six.text_type, bool, type(None))): elif isinstance(value, six.integer_types + (float, six.binary_type, six.text_type, bool, type(None))):
return repr(value), set() return repr(value), set()

View File

@ -16,6 +16,7 @@ from django.utils.deprecation import RenameMethodsBase
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.functional import curry, cached_property from django.utils.functional import curry, cached_property
from django.core import exceptions from django.core import exceptions
from django.apps import apps
from django import forms from django import forms
RECURSIVE_RELATIONSHIP_CONSTANT = 'self' RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
@ -121,6 +122,30 @@ class RelatedField(Field):
else: else:
self.do_related_class(other, cls) 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): def set_attributes_from_rel(self):
self.name = self.name or (self.rel.to._meta.model_name + '_' + self.rel.to._meta.pk.name) self.name = self.name or (self.rel.to._meta.model_name + '_' + self.rel.to._meta.pk.name)
if self.verbose_name is None: if self.verbose_name is None:
@ -1061,9 +1086,10 @@ class ForeignObject(RelatedField):
generate_reverse_relation = True generate_reverse_relation = True
related_accessor_class = ForeignRelatedObjectsDescriptor 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.from_fields = from_fields
self.to_fields = to_fields self.to_fields = to_fields
self.swappable = swappable
if 'rel' not in kwargs: if 'rel' not in kwargs:
kwargs['rel'] = ForeignObjectRel( kwargs['rel'] = ForeignObjectRel(
@ -1082,10 +1108,25 @@ class ForeignObject(RelatedField):
name, path, args, kwargs = super(ForeignObject, self).deconstruct() name, path, args, kwargs = super(ForeignObject, self).deconstruct()
kwargs['from_fields'] = self.from_fields kwargs['from_fields'] = self.from_fields
kwargs['to_fields'] = self.to_fields kwargs['to_fields'] = self.to_fields
# Work out string form of "to"
if isinstance(self.rel.to, six.string_types): if isinstance(self.rel.to, six.string_types):
kwargs['to'] = self.rel.to kwargs['to'] = self.rel.to
else: else:
kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name) 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 return name, path, args, kwargs
def resolve_related_fields(self): def resolve_related_fields(self):
@ -1516,7 +1557,7 @@ def create_many_to_many_intermediary_model(field, klass):
class ManyToManyField(RelatedField): class ManyToManyField(RelatedField):
description = _("Many-to-many relationship") description = _("Many-to-many relationship")
def __init__(self, to, db_constraint=True, **kwargs): def __init__(self, to, db_constraint=True, swappable=True, **kwargs):
try: try:
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) 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 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, db_constraint=db_constraint,
) )
self.swappable = swappable
self.db_table = kwargs.pop('db_table', None) self.db_table = kwargs.pop('db_table', None)
if kwargs['rel'].through is not 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." 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 kwargs['to'] = self.rel.to
else: else:
kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name) 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 return name, path, args, kwargs
def _get_path_info(self, direct=False): def _get_path_info(self, direct=False):

View File

@ -1201,6 +1201,23 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in
you manually add an SQL ``ON DELETE`` constraint to the database field you manually add an SQL ``ON DELETE`` constraint to the database field
(perhaps using :ref:`initial sql<initial-sql>`). (perhaps using :ref:`initial sql<initial-sql>`).
.. attribute:: ForeignKey.swappable
.. versionadded:: 1.7
Controls the migration framework's reaction if this :class:`ForeignKey`
is pointing at a swappable model. If it is ``True`` - the default -
then if the :class:`ForeignKey` is pointing at a model which matches
the current value of ``settings.AUTH_USER_MODEL`` (or another swappable
model setting) the relationship will be stored in the migration using
a reference to the setting, not to the model directly.
You only want to override this to be ``False`` if you are sure your
model should always point towards the swapped-in model - for example,
if it is a profile model designed specifically for your custom user model.
If in doubt, leave it to its default of ``True``.
.. _ref-manytomany: .. _ref-manytomany:
``ManyToManyField`` ``ManyToManyField``
@ -1309,6 +1326,23 @@ that control how the relationship functions.
It is an error to pass both ``db_constraint`` and ``through``. It is an error to pass both ``db_constraint`` and ``through``.
.. attribute:: ManyToManyField.swappable
.. versionadded:: 1.7
Controls the migration framework's reaction if this :class:`ManyToManyField`
is pointing at a swappable model. If it is ``True`` - the default -
then if the :class:`ManyToManyField` is pointing at a model which matches
the current value of ``settings.AUTH_USER_MODEL`` (or another swappable
model setting) the relationship will be stored in the migration using
a reference to the setting, not to the model directly.
You only want to override this to be ``False`` if you are sure your
model should always point towards the swapped-in model - for example,
if it is a profile model designed specifically for your custom user model.
If in doubt, leave it to its default of ``True``.
.. _ref-onetoone: .. _ref-onetoone:

View File

@ -1,5 +1,5 @@
import warnings import warnings
from django.test import TestCase from django.test import TestCase, override_settings
from django.db import models from django.db import models
@ -149,22 +149,44 @@ class FieldDeconstructionTests(TestCase):
self.assertEqual(kwargs, {}) self.assertEqual(kwargs, {})
def test_foreign_key(self): def test_foreign_key(self):
# Test basic pointing
field = models.ForeignKey("auth.Permission")
name, path, args, kwargs = field.deconstruct()
self.assertEqual(path, "django.db.models.ForeignKey")
self.assertEqual(args, [])
self.assertEqual(kwargs, {"to": "auth.Permission"})
self.assertFalse(hasattr(kwargs['to'], "setting_name"))
# Test swap detection for swappable model
field = models.ForeignKey("auth.User") field = models.ForeignKey("auth.User")
name, path, args, kwargs = field.deconstruct() name, path, args, kwargs = field.deconstruct()
self.assertEqual(path, "django.db.models.ForeignKey") self.assertEqual(path, "django.db.models.ForeignKey")
self.assertEqual(args, []) self.assertEqual(args, [])
self.assertEqual(kwargs, {"to": "auth.User"}) self.assertEqual(kwargs, {"to": "auth.User"})
self.assertEqual(kwargs['to'].setting_name, "AUTH_USER_MODEL")
# Test nonexistent (for now) model
field = models.ForeignKey("something.Else") field = models.ForeignKey("something.Else")
name, path, args, kwargs = field.deconstruct() name, path, args, kwargs = field.deconstruct()
self.assertEqual(path, "django.db.models.ForeignKey") self.assertEqual(path, "django.db.models.ForeignKey")
self.assertEqual(args, []) self.assertEqual(args, [])
self.assertEqual(kwargs, {"to": "something.Else"}) self.assertEqual(kwargs, {"to": "something.Else"})
# Test on_delete
field = models.ForeignKey("auth.User", on_delete=models.SET_NULL) field = models.ForeignKey("auth.User", on_delete=models.SET_NULL)
name, path, args, kwargs = field.deconstruct() name, path, args, kwargs = field.deconstruct()
self.assertEqual(path, "django.db.models.ForeignKey") self.assertEqual(path, "django.db.models.ForeignKey")
self.assertEqual(args, []) self.assertEqual(args, [])
self.assertEqual(kwargs, {"to": "auth.User", "on_delete": models.SET_NULL}) self.assertEqual(kwargs, {"to": "auth.User", "on_delete": models.SET_NULL})
@override_settings(AUTH_USER_MODEL="auth.Permission")
def test_foreign_key_swapped(self):
# It doesn't matter that we swapped out user for permission;
# there's no validation. We just want to check the setting stuff works.
field = models.ForeignKey("auth.Permission")
name, path, args, kwargs = field.deconstruct()
self.assertEqual(path, "django.db.models.ForeignKey")
self.assertEqual(args, [])
self.assertEqual(kwargs, {"to": "auth.Permission"})
self.assertEqual(kwargs['to'].setting_name, "AUTH_USER_MODEL")
def test_image_field(self): def test_image_field(self):
field = models.ImageField(upload_to="foo/barness", width_field="width", height_field="height") field = models.ImageField(upload_to="foo/barness", width_field="width", height_field="height")
name, path, args, kwargs = field.deconstruct() name, path, args, kwargs = field.deconstruct()
@ -201,11 +223,31 @@ class FieldDeconstructionTests(TestCase):
self.assertEqual(kwargs, {"protocol": "IPv6"}) self.assertEqual(kwargs, {"protocol": "IPv6"})
def test_many_to_many_field(self): def test_many_to_many_field(self):
# Test normal
field = models.ManyToManyField("auth.Permission")
name, path, args, kwargs = field.deconstruct()
self.assertEqual(path, "django.db.models.ManyToManyField")
self.assertEqual(args, [])
self.assertEqual(kwargs, {"to": "auth.Permission"})
self.assertFalse(hasattr(kwargs['to'], "setting_name"))
# Test swappable
field = models.ManyToManyField("auth.User") field = models.ManyToManyField("auth.User")
name, path, args, kwargs = field.deconstruct() name, path, args, kwargs = field.deconstruct()
self.assertEqual(path, "django.db.models.ManyToManyField") self.assertEqual(path, "django.db.models.ManyToManyField")
self.assertEqual(args, []) self.assertEqual(args, [])
self.assertEqual(kwargs, {"to": "auth.User"}) self.assertEqual(kwargs, {"to": "auth.User"})
self.assertEqual(kwargs['to'].setting_name, "AUTH_USER_MODEL")
@override_settings(AUTH_USER_MODEL="auth.Permission")
def test_many_to_many_field_swapped(self):
# It doesn't matter that we swapped out user for permission;
# there's no validation. We just want to check the setting stuff works.
field = models.ManyToManyField("auth.Permission")
name, path, args, kwargs = field.deconstruct()
self.assertEqual(path, "django.db.models.ManyToManyField")
self.assertEqual(args, [])
self.assertEqual(kwargs, {"to": "auth.Permission"})
self.assertEqual(kwargs['to'].setting_name, "AUTH_USER_MODEL")
def test_null_boolean_field(self): def test_null_boolean_field(self):
field = models.NullBooleanField() field = models.NullBooleanField()

View File

@ -1,5 +1,5 @@
# encoding: utf8 # encoding: utf8
from django.test import TestCase from django.test import TestCase, override_settings
from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.autodetector import MigrationAutodetector
from django.db.migrations.questioner import MigrationQuestioner from django.db.migrations.questioner import MigrationQuestioner
from django.db.migrations.state import ProjectState, ModelState from django.db.migrations.state import ProjectState, ModelState
@ -18,6 +18,7 @@ class AutodetectorTests(TestCase):
author_name_renamed = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("names", models.CharField(max_length=200))]) author_name_renamed = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("names", models.CharField(max_length=200))])
author_with_book = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200)), ("book", models.ForeignKey("otherapp.Book"))]) author_with_book = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200)), ("book", models.ForeignKey("otherapp.Book"))])
author_with_publisher = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200)), ("publisher", models.ForeignKey("testapp.Publisher"))]) author_with_publisher = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200)), ("publisher", models.ForeignKey("testapp.Publisher"))])
author_with_custom_user = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200)), ("user", models.ForeignKey("thirdapp.CustomUser"))])
author_proxy = ModelState("testapp", "AuthorProxy", [], {"proxy": True}, ("testapp.author", )) author_proxy = ModelState("testapp", "AuthorProxy", [], {"proxy": True}, ("testapp.author", ))
author_proxy_notproxy = ModelState("testapp", "AuthorProxy", [], {}, ("testapp.author", )) author_proxy_notproxy = ModelState("testapp", "AuthorProxy", [], {}, ("testapp.author", ))
publisher = ModelState("testapp", "Publisher", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=100))]) publisher = ModelState("testapp", "Publisher", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=100))])
@ -29,6 +30,7 @@ class AutodetectorTests(TestCase):
book_unique = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("title", models.CharField(max_length=200))], {"unique_together": [("author", "title")]}) book_unique = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("title", models.CharField(max_length=200))], {"unique_together": [("author", "title")]})
book_unique_2 = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("title", models.CharField(max_length=200))], {"unique_together": [("title", "author")]}) book_unique_2 = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("title", models.CharField(max_length=200))], {"unique_together": [("title", "author")]})
edition = ModelState("thirdapp", "Edition", [("id", models.AutoField(primary_key=True)), ("book", models.ForeignKey("otherapp.Book"))]) edition = ModelState("thirdapp", "Edition", [("id", models.AutoField(primary_key=True)), ("book", models.ForeignKey("otherapp.Book"))])
custom_user = ModelState("thirdapp", "CustomUser", [("id", models.AutoField(primary_key=True)), ("username", models.CharField(max_length=255))])
def make_project_state(self, model_states): def make_project_state(self, model_states):
"Shortcut to make ProjectStates from lists of predefined models" "Shortcut to make ProjectStates from lists of predefined models"
@ -355,3 +357,15 @@ class AutodetectorTests(TestCase):
action = migration.operations[0] action = migration.operations[0]
self.assertEqual(action.__class__.__name__, "CreateModel") self.assertEqual(action.__class__.__name__, "CreateModel")
self.assertEqual(action.name, "AuthorProxy") self.assertEqual(action.name, "AuthorProxy")
@override_settings(AUTH_USER_MODEL="thirdapp.CustomUser")
def test_swappable(self):
before = self.make_project_state([self.custom_user])
after = self.make_project_state([self.custom_user, self.author_with_custom_user])
autodetector = MigrationAutodetector(before, after)
changes = autodetector._detect_changes()
# Right number of migrations?
self.assertEqual(len(changes), 1)
# Check the dependency is correct
migration = changes['testapp'][0]
self.assertEqual(migration.dependencies, [("__setting__", "AUTH_USER_MODEL")])

View File

@ -7,8 +7,9 @@ import os
from django.core.validators import RegexValidator, EmailValidator from django.core.validators import RegexValidator, EmailValidator
from django.db import models, migrations from django.db import models, migrations
from django.db.migrations.writer import MigrationWriter from django.db.migrations.writer import MigrationWriter, SettingsReference
from django.test import TestCase from django.test import TestCase
from django.conf import settings
from django.utils import six from django.utils import six
from django.utils.deconstruct import deconstructible from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -37,8 +38,8 @@ class WriterTests(TestCase):
def assertSerializedEqual(self, value): def assertSerializedEqual(self, value):
self.assertEqual(self.serialize_round_trip(value), value) self.assertEqual(self.serialize_round_trip(value), value)
def assertSerializedIs(self, value): def assertSerializedResultEqual(self, value, target):
self.assertIs(self.serialize_round_trip(value), value) self.assertEqual(MigrationWriter.serialize(value), target)
def assertSerializedFieldEqual(self, value): def assertSerializedFieldEqual(self, value):
new_value = self.serialize_round_trip(value) new_value = self.serialize_round_trip(value)
@ -92,6 +93,15 @@ class WriterTests(TestCase):
# Django fields # Django fields
self.assertSerializedFieldEqual(models.CharField(max_length=255)) self.assertSerializedFieldEqual(models.CharField(max_length=255))
self.assertSerializedFieldEqual(models.TextField(null=True, blank=True)) self.assertSerializedFieldEqual(models.TextField(null=True, blank=True))
# Setting references
self.assertSerializedEqual(SettingsReference(settings.AUTH_USER_MODEL, "AUTH_USER_MODEL"))
self.assertSerializedResultEqual(
SettingsReference("someapp.model", "AUTH_USER_MODEL"),
(
"settings.AUTH_USER_MODEL",
set(["from django.conf import settings"]),
)
)
def test_simple_migration(self): def test_simple_migration(self):
""" """