Fixed #23822 -- Added support for serializing model managers in migration

Thanks to Shai Berger, Loïc Bistuer, Simon Charette, Andrew Godwin,
Tim Graham, Carl Meyer, and others for their review and input.
This commit is contained in:
Markus Holtermann 2014-12-12 23:19:58 +01:00 committed by Tim Graham
parent e37ab311fc
commit aa5ef0d4fc
28 changed files with 608 additions and 48 deletions

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import models, migrations from django.db import models, migrations
from django.conf import settings from django.conf import settings
import django.contrib.admin.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -32,5 +33,8 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'log entries', 'verbose_name_plural': 'log entries',
}, },
bases=(models.Model,), bases=(models.Model,),
managers=[
('objects', django.contrib.admin.models.LogEntryManager()),
],
), ),
] ]

View File

@ -15,6 +15,8 @@ DELETION = 3
class LogEntryManager(models.Manager): class LogEntryManager(models.Manager):
use_in_migrations = True
def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''): def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''):
e = self.model( e = self.model(
None, None, user_id, content_type_id, smart_text(object_id), None, None, user_id, content_type_id, smart_text(object_id),

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
from django.core import validators from django.core import validators
from django.db import models, migrations from django.db import models, migrations
from django.utils import timezone from django.utils import timezone
import django.contrib.auth.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -27,6 +28,9 @@ class Migration(migrations.Migration):
'verbose_name': 'permission', 'verbose_name': 'permission',
'verbose_name_plural': 'permissions', 'verbose_name_plural': 'permissions',
}, },
managers=[
('objects', django.contrib.auth.models.PermissionManager()),
],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Group', name='Group',
@ -39,6 +43,9 @@ class Migration(migrations.Migration):
'verbose_name': 'group', 'verbose_name': 'group',
'verbose_name_plural': 'groups', 'verbose_name_plural': 'groups',
}, },
managers=[
('objects', django.contrib.auth.models.GroupManager()),
],
), ),
migrations.CreateModel( migrations.CreateModel(
name='User', name='User',
@ -62,5 +69,8 @@ class Migration(migrations.Migration):
'verbose_name': 'user', 'verbose_name': 'user',
'verbose_name_plural': 'users', 'verbose_name_plural': 'users',
}, },
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
), ),
] ]

View File

@ -29,6 +29,8 @@ user_logged_in.connect(update_last_login)
class PermissionManager(models.Manager): class PermissionManager(models.Manager):
use_in_migrations = True
def get_by_natural_key(self, codename, app_label, model): def get_by_natural_key(self, codename, app_label, model):
return self.get( return self.get(
codename=codename, codename=codename,
@ -87,6 +89,8 @@ class GroupManager(models.Manager):
""" """
The manager for the auth's Group model. The manager for the auth's Group model.
""" """
use_in_migrations = True
def get_by_natural_key(self, name): def get_by_natural_key(self, name):
return self.get(name=name) return self.get(name=name)
@ -160,6 +164,7 @@ class BaseUserManager(models.Manager):
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, username, email, password, def _create_user(self, username, email, password,
is_staff, is_superuser, **extra_fields): is_staff, is_superuser, **extra_fields):

View File

@ -2,6 +2,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models, migrations from django.db import models, migrations
import django.contrib.contenttypes.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -25,6 +26,9 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'content types', 'verbose_name_plural': 'content types',
}, },
bases=(models.Model,), bases=(models.Model,),
managers=[
('objects', django.contrib.contenttypes.models.ContentTypeManager()),
],
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='contenttype', name='contenttype',

View File

@ -9,6 +9,7 @@ from django.utils.encoding import python_2_unicode_compatible
class ContentTypeManager(models.Manager): class ContentTypeManager(models.Manager):
use_in_migrations = True
# Cache to avoid re-looking up ContentType objects all over the place. # Cache to avoid re-looking up ContentType objects all over the place.
# This cache is shared by all the get_for_* methods. # This cache is shared by all the get_for_* methods.

View File

@ -2,6 +2,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models, migrations from django.db import models, migrations
import django.contrib.sessions.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -23,5 +24,8 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'sessions', 'verbose_name_plural': 'sessions',
}, },
bases=(models.Model,), bases=(models.Model,),
managers=[
('objects', django.contrib.sessions.models.SessionManager()),
],
), ),
] ]

View File

@ -6,6 +6,8 @@ from django.utils.translation import ugettext_lazy as _
class SessionManager(models.Manager): class SessionManager(models.Manager):
use_in_migrations = True
def encode(self, session_dict): def encode(self, session_dict):
""" """
Returns the given session dictionary serialized and encoded as a string. Returns the given session dictionary serialized and encoded as a string.

View File

@ -10,6 +10,8 @@ from django.db.models.fields import FieldDoesNotExist
class CurrentSiteManager(models.Manager): class CurrentSiteManager(models.Manager):
"Use this to limit objects to those associated with the current site." "Use this to limit objects to those associated with the current site."
use_in_migrations = True
def __init__(self, field_name=None): def __init__(self, field_name=None):
super(CurrentSiteManager, self).__init__() super(CurrentSiteManager, self).__init__()
self.__field_name = field_name self.__field_name = field_name

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import models, migrations from django.db import models, migrations
from django.contrib.sites.models import _simple_domain_name_validator from django.contrib.sites.models import _simple_domain_name_validator
import django.contrib.sites.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -24,5 +25,8 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'sites', 'verbose_name_plural': 'sites',
}, },
bases=(models.Model,), bases=(models.Model,),
managers=[
('objects', django.contrib.sites.models.SiteManager()),
],
), ),
] ]

View File

@ -33,6 +33,7 @@ def _simple_domain_name_validator(value):
class SiteManager(models.Manager): class SiteManager(models.Manager):
use_in_migrations = True
def _get_site_by_id(self, site_id): def _get_site_by_id(self, site_id):
if site_id not in SITE_CACHE: if site_id not in SITE_CACHE:

View File

@ -178,6 +178,7 @@ class MigrationAutodetector(object):
self.generate_deleted_proxies() self.generate_deleted_proxies()
self.generate_created_proxies() self.generate_created_proxies()
self.generate_altered_options() self.generate_altered_options()
self.generate_altered_managers()
# Generate field operations # Generate field operations
self.generate_renamed_fields() self.generate_renamed_fields()
@ -503,6 +504,7 @@ class MigrationAutodetector(object):
fields=[d for d in model_state.fields if d[0] not in related_fields], fields=[d for d in model_state.fields if d[0] not in related_fields],
options=model_state.options, options=model_state.options,
bases=model_state.bases, bases=model_state.bases,
managers=model_state.managers,
), ),
dependencies=dependencies, dependencies=dependencies,
beginning=True, beginning=True,
@ -607,6 +609,7 @@ class MigrationAutodetector(object):
fields=[], fields=[],
options=model_state.options, options=model_state.options,
bases=model_state.bases, bases=model_state.bases,
managers=model_state.managers,
), ),
# Depend on the deletion of any possible non-proxy version of us # Depend on the deletion of any possible non-proxy version of us
dependencies=dependencies, dependencies=dependencies,
@ -990,6 +993,20 @@ class MigrationAutodetector(object):
dependencies=dependencies, dependencies=dependencies,
) )
def generate_altered_managers(self):
for app_label, model_name in sorted(self.kept_model_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]
if old_model_state.managers != new_model_state.managers:
self.add_operation(
app_label,
operations.AlterModelManagers(
name=model_name,
managers=new_model_state.managers,
)
)
def arrange_for_graph(self, changes, graph, migration_name=None): def arrange_for_graph(self, changes, graph, migration_name=None):
""" """
Takes in a result from changes() and a MigrationGraph, Takes in a result from changes() and a MigrationGraph,

View File

@ -1,6 +1,6 @@
from .models import (CreateModel, DeleteModel, AlterModelTable, from .models import (CreateModel, DeleteModel, AlterModelTable,
AlterUniqueTogether, AlterIndexTogether, RenameModel, AlterModelOptions, AlterUniqueTogether, AlterIndexTogether, RenameModel, AlterModelOptions,
AlterOrderWithRespectTo) AlterOrderWithRespectTo, AlterModelManagers)
from .fields import AddField, RemoveField, AlterField, RenameField from .fields import AddField, RemoveField, AlterField, RenameField
from .special import SeparateDatabaseAndState, RunSQL, RunPython from .special import SeparateDatabaseAndState, RunSQL, RunPython
@ -9,5 +9,5 @@ __all__ = [
'RenameModel', 'AlterIndexTogether', 'AlterModelOptions', 'RenameModel', 'AlterIndexTogether', 'AlterModelOptions',
'AddField', 'RemoveField', 'AlterField', 'RenameField', 'AddField', 'RemoveField', 'AlterField', 'RenameField',
'SeparateDatabaseAndState', 'RunSQL', 'RunPython', 'SeparateDatabaseAndState', 'RunSQL', 'RunPython',
'AlterOrderWithRespectTo', 'AlterOrderWithRespectTo', 'AlterModelManagers',
] ]

View File

@ -12,13 +12,14 @@ class CreateModel(Operation):
Create a model's table. Create a model's table.
""" """
serialization_expand_args = ['fields', 'options'] serialization_expand_args = ['fields', 'options', 'managers']
def __init__(self, name, fields, options=None, bases=None): def __init__(self, name, fields, options=None, bases=None, managers=None):
self.name = name self.name = name
self.fields = fields self.fields = fields
self.options = options or {} self.options = options or {}
self.bases = bases or (models.Model,) self.bases = bases or (models.Model,)
self.managers = managers or []
def deconstruct(self): def deconstruct(self):
kwargs = { kwargs = {
@ -29,6 +30,8 @@ class CreateModel(Operation):
kwargs['options'] = self.options kwargs['options'] = self.options
if self.bases and self.bases != (models.Model,): if self.bases and self.bases != (models.Model,):
kwargs['bases'] = self.bases kwargs['bases'] = self.bases
if self.managers and self.managers != [('objects', models.Manager())]:
kwargs['managers'] = self.managers
return ( return (
self.__class__.__name__, self.__class__.__name__,
[], [],
@ -42,6 +45,7 @@ class CreateModel(Operation):
list(self.fields), list(self.fields),
dict(self.options), dict(self.options),
tuple(self.bases), tuple(self.bases),
list(self.managers),
) )
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
@ -467,3 +471,38 @@ class AlterModelOptions(Operation):
def describe(self): def describe(self):
return "Change Meta options on %s" % (self.name, ) return "Change Meta options on %s" % (self.name, )
class AlterModelManagers(Operation):
"""
Alters the model's managers
"""
serialization_expand_args = ['managers']
def __init__(self, name, managers):
self.name = name
self.managers = managers
def deconstruct(self):
return (
self.__class__.__name__,
[self.name, self.managers],
{}
)
def state_forwards(self, app_label, state):
model_state = state.models[app_label, self.name.lower()]
model_state.managers = list(self.managers)
def database_forwards(self, app_label, schema_editor, from_state, to_state):
pass
def database_backwards(self, app_label, schema_editor, from_state, to_state):
pass
def references_model(self, name, app_label=None):
return name.lower() == self.name.lower()
def describe(self):
return "Change managers on %s" % (self.name, )

View File

@ -149,12 +149,13 @@ class ModelState(object):
assign new ones, as these are not detached during a clone. assign new ones, as these are not detached during a clone.
""" """
def __init__(self, app_label, name, fields, options=None, bases=None): def __init__(self, app_label, name, fields, options=None, bases=None, managers=None):
self.app_label = app_label self.app_label = app_label
self.name = force_text(name) self.name = force_text(name)
self.fields = fields self.fields = fields
self.options = options or {} self.options = options or {}
self.bases = bases or (models.Model, ) self.bases = bases or (models.Model, )
self.managers = managers or []
# Sanity-check that fields is NOT a dict. It must be ordered. # Sanity-check that fields is NOT a dict. It must be ordered.
if isinstance(self.fields, dict): if isinstance(self.fields, dict):
raise ValueError("ModelState.fields cannot be a dict - it must be a list of 2-tuples.") raise ValueError("ModelState.fields cannot be a dict - it must be a list of 2-tuples.")
@ -252,12 +253,51 @@ class ModelState(object):
# Ensure at least one base inherits from models.Model # Ensure at least one base inherits from models.Model
if not any((isinstance(base, six.string_types) or issubclass(base, models.Model)) for base in bases): if not any((isinstance(base, six.string_types) or issubclass(base, models.Model)) for base in bases):
bases = (models.Model,) bases = (models.Model,)
# Constructs all managers on the model
managers = {}
def reconstruct_manager(mgr):
as_manager, manager_path, qs_path, args, kwargs = mgr.deconstruct()
if as_manager:
qs_class = import_string(qs_path)
instance = qs_class.as_manager()
else:
manager_class = import_string(manager_path)
instance = manager_class(*args, **kwargs)
# We rely on the ordering of the creation_counter of the original
# instance
managers[mgr.name] = (mgr.creation_counter, instance)
default_manager_name = model._default_manager.name
# Make sure the default manager is always the first
if model._default_manager.use_in_migrations:
reconstruct_manager(model._default_manager)
else:
# Force this manager to be the first and thus default
managers[default_manager_name] = (0, models.Manager())
# Sort all managers by their creation counter
for _, manager, _ in sorted(model._meta.managers):
if manager.name == '_base_manager' or not manager.use_in_migrations:
continue
reconstruct_manager(manager)
# Sort all managers by their creation counter but take only name and
# instance for further processing
managers = [
(name, instance) for name, (cc, instance) in
sorted(managers.items(), key=lambda v: v[1])
]
if managers == [(default_manager_name, models.Manager())]:
managers = []
# Construct the new ModelState
return cls( return cls(
model._meta.app_label, model._meta.app_label,
model._meta.object_name, model._meta.object_name,
fields, fields,
options, options,
bases, bases,
managers,
) )
@classmethod @classmethod
@ -292,6 +332,7 @@ class ModelState(object):
fields=list(self.construct_fields()), fields=list(self.construct_fields()),
options=dict(self.options), options=dict(self.options),
bases=self.bases, bases=self.bases,
managers=self.managers,
) )
def render(self, apps): def render(self, apps):
@ -312,6 +353,11 @@ class ModelState(object):
body = dict(self.construct_fields()) body = dict(self.construct_fields())
body['Meta'] = meta body['Meta'] = meta
body['__module__'] = "__fake__" body['__module__'] = "__fake__"
# Restore managers
for mgr_name, manager in self.managers:
body[mgr_name] = manager
# Then, make a Model object # Then, make a Model object
return type( return type(
str(self.name), str(self.name),
@ -336,7 +382,8 @@ class ModelState(object):
all((k1 == k2 and (f1.deconstruct()[1:] == f2.deconstruct()[1:])) all((k1 == k2 and (f1.deconstruct()[1:] == f2.deconstruct()[1:]))
for (k1, f1), (k2, f2) in zip(self.fields, other.fields)) and for (k1, f1), (k2, f2) in zip(self.fields, other.fields)) and
(self.options == other.options) and (self.options == other.options) and
(self.bases == other.bases) (self.bases == other.bases) and
(self.managers == other.managers)
) )
def __ne__(self, other): def __ne__(self, other):

View File

@ -223,13 +223,7 @@ class MigrationWriter(object):
@classmethod @classmethod
def serialize_deconstructed(cls, path, args, kwargs): def serialize_deconstructed(cls, path, args, kwargs):
module, name = path.rsplit(".", 1) name, imports = cls._serialize_path(path)
if module == "django.db.models":
imports = {"from django.db import models"}
name = "models.%s" % name
else:
imports = {"import %s" % module}
name = path
strings = [] strings = []
for arg in args: for arg in args:
arg_string, arg_imports = cls.serialize(arg) arg_string, arg_imports = cls.serialize(arg)
@ -241,6 +235,17 @@ class MigrationWriter(object):
strings.append("%s=%s" % (kw, arg_string)) strings.append("%s=%s" % (kw, arg_string))
return "%s(%s)" % (name, ", ".join(strings)), imports return "%s(%s)" % (name, ", ".join(strings)), imports
@classmethod
def _serialize_path(cls, path):
module, name = path.rsplit(".", 1)
if module == "django.db.models":
imports = {"from django.db import models"}
name = "models.%s" % name
else:
imports = {"import %s" % module}
name = path
return name, imports
@classmethod @classmethod
def serialize(cls, value): def serialize(cls, value):
""" """
@ -344,6 +349,13 @@ class MigrationWriter(object):
return value.__name__, set() return value.__name__, set()
else: else:
return "%s.%s" % (module, value.__name__), {"import %s" % module} return "%s.%s" % (module, value.__name__), {"import %s" % module}
elif isinstance(value, models.manager.BaseManager):
as_manager, manager_path, qs_path, args, kwargs = value.deconstruct()
if as_manager:
name, imports = cls._serialize_path(qs_path)
return "%s.as_manager()" % name, imports
else:
return cls.serialize_deconstructed(manager_path, args, kwargs)
# Anything that knows how to deconstruct itself. # Anything that knows how to deconstruct itself.
elif hasattr(value, 'deconstruct'): elif hasattr(value, 'deconstruct'):
return cls.serialize_deconstructed(*value.deconstruct()) return cls.serialize_deconstructed(*value.deconstruct())

View File

@ -1,4 +1,5 @@
import copy import copy
from importlib import import_module
import inspect import inspect
from django.db import router from django.db import router
@ -58,6 +59,16 @@ class BaseManager(object):
# Tracks each time a Manager instance is created. Used to retain order. # Tracks each time a Manager instance is created. Used to retain order.
creation_counter = 0 creation_counter = 0
#: If set to True the manager will be serialized into migrations and will
#: thus be available in e.g. RunPython operations
use_in_migrations = False
def __new__(cls, *args, **kwargs):
# We capture the arguments to make returning them trivial
obj = super(BaseManager, cls).__new__(cls)
obj._constructor_args = (args, kwargs)
return obj
def __init__(self): def __init__(self):
super(BaseManager, self).__init__() super(BaseManager, self).__init__()
self._set_creation_counter() self._set_creation_counter()
@ -73,6 +84,43 @@ class BaseManager(object):
app = model._meta.app_label app = model._meta.app_label
return '%s.%s.%s' % (app, model._meta.object_name, self.name) return '%s.%s.%s' % (app, model._meta.object_name, self.name)
def deconstruct(self):
"""
Returns a 5-tuple of the form (as_manager (True), manager_class,
queryset_class, args, kwargs).
Raises a ValueError if the manager is dynamically generated.
"""
qs_class = self._queryset_class
if getattr(self, '_built_as_manager', False):
# using MyQuerySet.as_manager()
return (
True, # as_manager
None, # manager_class
'%s.%s' % (qs_class.__module__, qs_class.__name__), # qs_class
None, # args
None, # kwargs
)
else:
module_name = self.__module__
name = self.__class__.__name__
# Make sure it's actually there and not an inner class
module = import_module(module_name)
if not hasattr(module, name):
raise ValueError(
"Could not find manager %s in %s.\n"
"Please note that you need to inherit from managers you "
"dynamically generated with 'from_queryset()'."
% (name, module_name)
)
return (
False, # as_manager
'%s.%s' % (module_name, name), # manager_class
None, # qs_class
self._constructor_args[0], # args
self._constructor_args[1], # kwargs
)
def check(self, **kwargs): def check(self, **kwargs):
return [] return []
@ -183,6 +231,15 @@ class BaseManager(object):
# understanding of how this comes into play. # understanding of how this comes into play.
return self.get_queryset() return self.get_queryset()
def __eq__(self, other):
return (
isinstance(other, self.__class__) and
self._constructor_args == other._constructor_args
)
def __ne__(self, other):
return not (self == other)
class Manager(BaseManager.from_queryset(QuerySet)): class Manager(BaseManager.from_queryset(QuerySet)):
pass pass

View File

@ -67,7 +67,9 @@ class QuerySet(object):
def as_manager(cls): def as_manager(cls):
# Address the circular dependency between `Queryset` and `Manager`. # Address the circular dependency between `Queryset` and `Manager`.
from django.db.models.manager import Manager from django.db.models.manager import Manager
return Manager.from_queryset(cls)() manager = Manager.from_queryset(cls)()
manager._built_as_manager = True
return manager
as_manager.queryset_only = True as_manager.queryset_only = True
as_manager = classmethod(as_manager) as_manager = classmethod(as_manager)

View File

@ -37,7 +37,7 @@ Schema Operations
CreateModel CreateModel
----------- -----------
.. class:: CreateModel(name, fields, options=None, bases=None) .. class:: CreateModel(name, fields, options=None, bases=None, managers=None)
Creates a new model in the project history and a corresponding table in the Creates a new model in the project history and a corresponding table in the
database to match it. database to match it.
@ -56,6 +56,14 @@ it can contain both class objects as well as strings in the format
from the historical version). If it's not supplied, it defaults to just from the historical version). If it's not supplied, it defaults to just
inheriting from the standard ``models.Model``. inheriting from the standard ``models.Model``.
``managers`` takes a list of 2-tuples of ``(manager_name, manager_instance)``.
The first manager in the list will be the default manager for this model during
migrations.
.. versionchanged:: 1.8
The ``managers`` argument was added.
DeleteModel DeleteModel
----------- -----------
@ -121,6 +129,15 @@ like ``permissions`` and ``verbose_name``. Does not affect the database, but
persists these changes for :class:`RunPython` instances to use. ``options`` persists these changes for :class:`RunPython` instances to use. ``options``
should be a dictionary mapping option names to values. should be a dictionary mapping option names to values.
AlterModelManagers
------------------
.. versionadded:: 1.8
.. class:: AlterModelManagers(name, managers)
Alters the managers that are available during migrations.
AddField AddField
-------- --------

View File

@ -387,6 +387,9 @@ Migrations
* It is now possible to have migrations (most probably :ref:`data migrations * It is now possible to have migrations (most probably :ref:`data migrations
<data-migrations>`) for applications without models. <data-migrations>`) for applications without models.
* Migrations can now :ref:`serialize model managers
<using-managers-in-migrations>` as part of the model state.
Models Models
^^^^^^ ^^^^^^

66
docs/topics/migrations.txt Executable file → Normal file
View File

@ -286,6 +286,36 @@ modified ``__init__`` method with the old signature. So if you need a new
argument, please create a keyword argument and add something like argument, please create a keyword argument and add something like
``assert kwargs.get('argument_name') is not None`` in the constructor. ``assert kwargs.get('argument_name') is not None`` in the constructor.
.. _using-managers-in-migrations:
Model managers
~~~~~~~~~~~~~~
.. versionadded:: 1.8
You can optionally serialize managers into migrations and have them available
in :class:`~django.db.migrations.operations.RunPython` operations. This is done
by defining a ``use_in_migrations`` attribute on the manager class::
class MyManager(models.Manager):
use_in_migrations = True
class MyModel(models.Model):
objects = MyManager()
If you are using the :meth:`~django.db.models.from_queryset` function to
dynamically generate a manager class, you need to inherit from the generated
class to make it importable::
class MyManager(MyBaseManager.from_queryset(CustomQuerySet)):
use_in_migrations = True
class MyModel(models.Model):
objects = MyManager()
Please refer to the notes about :ref:`historical-models` in migrations to see
the implications that come along.
Adding migrations to apps Adding migrations to apps
------------------------- -------------------------
@ -326,16 +356,17 @@ you can use the :djadminopt:`--name` option::
Historical models Historical models
----------------- -----------------
When you run migrations, Django is working from historical versions of When you run migrations, Django is working from historical versions of your
your models stored in the migration files. If you write Python code models stored in the migration files. If you write Python code using the
using the :class:`~django.db.migrations.operations.RunPython` operation, or if :class:`~django.db.migrations.operations.RunPython` operation, or if you have
you have ``allow_migrate`` methods on your database routers, you will be ``allow_migrate`` methods on your database routers, you will be exposed to
exposed to these versions of your models. these versions of your models.
Because it's impossible to serialize arbitrary Python code, these historical Because it's impossible to serialize arbitrary Python code, these historical
models will not have any custom methods or managers that you have defined. models will not have any custom methods that you have defined. They will,
They will, however, have the same fields, relationships and ``Meta`` options however, have the same fields, relationships, managers (limited to those with
(also versioned, so they may be different from your current ones). ``use_in_migrations = True``) and ``Meta`` options (also versioned, so they may
be different from your current ones).
.. warning:: .. warning::
@ -344,16 +375,17 @@ They will, however, have the same fields, relationships and ``Meta`` options
constructors or instance methods. Plan appropriately! constructors or instance methods. Plan appropriately!
References to functions in field options such as ``upload_to`` and References to functions in field options such as ``upload_to`` and
``limit_choices_to`` are serialized in migrations, so the functions will need ``limit_choices_to`` and model manager declarations with managers having
to be kept around for as long as there is a migration referencing them. Any ``use_in_migrations = True`` are serialized in migrations, so the functions and
:doc:`custom model fields </howto/custom-model-fields>` will also need to be classes will need to be kept around for as long as there is a migration
kept, since these are imported directly by migrations. referencing them. Any :doc:`custom model fields </howto/custom-model-fields>`
will also need to be kept, since these are imported directly by migrations.
In addition, the base classes of the model are just stored as pointers, In addition, the base classes of the model are just stored as pointers, so you
so you must always keep base classes around for as long as there is a migration must always keep base classes around for as long as there is a migration that
that contains a reference to them. On the plus side, methods and managers contains a reference to them. On the plus side, methods and managers from these
from these base classes inherit normally, so if you absolutely need access base classes inherit normally, so if you absolutely need access to these you
to these you can opt to move them into a superclass. can opt to move them into a superclass.
.. _data-migrations: .. _data-migrations:

View File

@ -60,9 +60,16 @@ class BaseCustomManager(models.Manager):
def manager_only(self): def manager_only(self):
return self.all() return self.all()
CustomManager = BaseCustomManager.from_queryset(CustomQuerySet) CustomManager = BaseCustomManager.from_queryset(CustomQuerySet)
class DeconstructibleCustomManager(BaseCustomManager.from_queryset(CustomQuerySet)):
def __init__(self, a, b, c=1, d=2):
super(DeconstructibleCustomManager, self).__init__(a)
class FunPeopleManager(models.Manager): class FunPeopleManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return super(FunPeopleManager, self).get_queryset().filter(fun=True) return super(FunPeopleManager, self).get_queryset().filter(fun=True)

View File

@ -1,9 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import six from django.utils import six
from .models import (Book, Car, FunPerson, OneToOneRestrictedModel, Person, from .models import (Book, Car, CustomManager, CustomQuerySet,
DeconstructibleCustomManager, FunPerson, OneToOneRestrictedModel, Person,
PersonManager, PublishedBookManager, RelatedModel, RestrictedModel) PersonManager, PublishedBookManager, RelatedModel, RestrictedModel)
@ -470,6 +472,44 @@ class CustomManagerTests(TestCase):
ordered=False, ordered=False,
) )
def test_deconstruct_default(self):
mgr = models.Manager()
as_manager, mgr_path, qs_path, args, kwargs = mgr.deconstruct()
self.assertFalse(as_manager)
self.assertEqual(mgr_path, 'django.db.models.manager.Manager')
self.assertEqual(args, ())
self.assertEqual(kwargs, {})
def test_deconstruct_as_manager(self):
mgr = CustomQuerySet.as_manager()
as_manager, mgr_path, qs_path, args, kwargs = mgr.deconstruct()
self.assertTrue(as_manager)
self.assertEqual(qs_path, 'custom_managers.models.CustomQuerySet')
def test_deconstruct_from_queryset(self):
mgr = DeconstructibleCustomManager('a', 'b')
as_manager, mgr_path, qs_path, args, kwargs = mgr.deconstruct()
self.assertFalse(as_manager)
self.assertEqual(mgr_path, 'custom_managers.models.DeconstructibleCustomManager')
self.assertEqual(args, ('a', 'b',))
self.assertEqual(kwargs, {})
mgr = DeconstructibleCustomManager('x', 'y', c=3, d=4)
as_manager, mgr_path, qs_path, args, kwargs = mgr.deconstruct()
self.assertFalse(as_manager)
self.assertEqual(mgr_path, 'custom_managers.models.DeconstructibleCustomManager')
self.assertEqual(args, ('x', 'y',))
self.assertEqual(kwargs, {'c': 3, 'd': 4})
def test_deconstruct_from_queryset_failing(self):
mgr = CustomManager('arg')
msg = ("Could not find manager BaseCustomManagerFromCustomQuerySet in "
"django.db.models.manager.\n"
"Please note that you need to inherit from managers you "
"dynamically generated with 'from_queryset()'.")
with self.assertRaisesMessage(ValueError, msg):
mgr.deconstruct()
class TestCars(TestCase): class TestCars(TestCase):

View File

@ -50,3 +50,21 @@ class UnmigratedModel(models.Model):
if its migrations directory has not been repointed) if its migrations directory has not been repointed)
""" """
pass pass
class FoodQuerySet(models.query.QuerySet):
pass
class BaseFoodManager(models.Manager):
def __init__(self, a, b, c=1, d=2):
super(BaseFoodManager, self).__init__()
self.args = (a, b, c, d)
class FoodManager(BaseFoodManager.from_queryset(FoodQuerySet)):
use_in_migrations = True
class NoMigrationFoodManager(BaseFoodManager.from_queryset(FoodQuerySet)):
pass

View File

@ -9,6 +9,8 @@ from django.db.migrations.loader import MigrationLoader
from django.db import models, connection from django.db import models, connection
from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.models import AbstractBaseUser
from .models import FoodManager, FoodQuerySet
class DeconstructableObject(object): class DeconstructableObject(object):
""" """
@ -159,6 +161,13 @@ class AutodetectorTests(TestCase):
other_pony = ModelState("otherapp", "Pony", [ other_pony = ModelState("otherapp", "Pony", [
("id", models.AutoField(primary_key=True)), ("id", models.AutoField(primary_key=True)),
]) ])
other_pony_food = ModelState("otherapp", "Pony", [
("id", models.AutoField(primary_key=True)),
], managers=[
('food_qs', FoodQuerySet.as_manager()),
('food_mgr', FoodManager('a', 'b')),
('food_mgr_kwargs', FoodManager('x', 'y', 3, 4)),
])
other_stable = ModelState("otherapp", "Stable", [("id", models.AutoField(primary_key=True))]) other_stable = ModelState("otherapp", "Stable", [("id", models.AutoField(primary_key=True))])
third_thing = ModelState("thirdapp", "Thing", [("id", models.AutoField(primary_key=True))]) third_thing = ModelState("thirdapp", "Thing", [("id", models.AutoField(primary_key=True))])
book = ModelState("otherapp", "Book", [ book = ModelState("otherapp", "Book", [
@ -456,13 +465,15 @@ class AutodetectorTests(TestCase):
"""Tests autodetection of new models.""" """Tests autodetection of new models."""
# Make state # Make state
before = self.make_project_state([]) before = self.make_project_state([])
after = self.make_project_state([self.author_empty]) after = self.make_project_state([self.other_pony_food])
autodetector = MigrationAutodetector(before, after) autodetector = MigrationAutodetector(before, after)
changes = autodetector._detect_changes() changes = autodetector._detect_changes()
# Right number/type of migrations? # Right number/type of migrations?
self.assertNumberMigrations(changes, 'testapp', 1) self.assertNumberMigrations(changes, 'otherapp', 1)
self.assertOperationTypes(changes, 'testapp', 0, ["CreateModel"]) self.assertOperationTypes(changes, 'otherapp', 0, ["CreateModel"])
self.assertOperationAttributes(changes, "testapp", 0, 0, name="Author") self.assertOperationAttributes(changes, "otherapp", 0, 0, name="Pony")
self.assertEqual([name for name, mgr in changes['otherapp'][0].operations[0].managers],
['food_qs', 'food_mgr', 'food_mgr_kwargs'])
def test_old_model(self): def test_old_model(self):
"""Tests deletion of old models.""" """Tests deletion of old models."""
@ -1406,6 +1417,24 @@ class AutodetectorTests(TestCase):
self.assertOperationAttributes(changes, 'testapp', 0, 1, name="author", order_with_respect_to="book") self.assertOperationAttributes(changes, 'testapp', 0, 1, name="author", order_with_respect_to="book")
self.assertNotIn("_order", [name for name, field in changes['testapp'][0].operations[0].fields]) self.assertNotIn("_order", [name for name, field in changes['testapp'][0].operations[0].fields])
def test_alter_model_managers(self):
"""
Tests that changing the model managers adds a new operation.
"""
# Make state
before = self.make_project_state([self.other_pony])
after = self.make_project_state([self.other_pony_food])
autodetector = MigrationAutodetector(before, after)
changes = autodetector._detect_changes()
# Right number/type of migrations?
self.assertNumberMigrations(changes, 'otherapp', 1)
self.assertOperationTypes(changes, 'otherapp', 0, ["AlterModelManagers"])
self.assertOperationAttributes(changes, 'otherapp', 0, 0, name="pony")
self.assertEqual([name for name, mgr in changes['otherapp'][0].operations[0].managers],
['food_qs', 'food_mgr', 'food_mgr_kwargs'])
self.assertEqual(changes['otherapp'][0].operations[0].managers[1][1].args, ('a', 'b', 1, 2))
self.assertEqual(changes['otherapp'][0].operations[0].managers[2][1].args, ('x', 'y', 3, 4))
def test_swappable_first_inheritance(self): def test_swappable_first_inheritance(self):
"""Tests that swappable models get their CreateModel first.""" """Tests that swappable models get their CreateModel first."""
# Make state # Make state

View File

@ -17,6 +17,7 @@ from django.db.utils import IntegrityError, DatabaseError
from django.test import override_settings from django.test import override_settings
from django.utils import six from django.utils import six
from .models import FoodManager, FoodQuerySet
from .test_base import MigrationTestBase from .test_base import MigrationTestBase
@ -48,7 +49,7 @@ class OperationTestBase(MigrationTestBase):
return project_state, new_state return project_state, new_state
def set_up_test_model(self, app_label, second_model=False, third_model=False, def set_up_test_model(self, app_label, second_model=False, third_model=False,
related_model=False, mti_model=False, proxy_model=False, related_model=False, mti_model=False, proxy_model=False, manager_model=False,
unique_together=False, options=False, db_table=None, index_together=False): unique_together=False, options=False, db_table=None, index_together=False):
""" """
Creates a test model state and database table. Creates a test model state and database table.
@ -142,6 +143,18 @@ class OperationTestBase(MigrationTestBase):
options={"proxy": True}, options={"proxy": True},
bases=['%s.Pony' % app_label], bases=['%s.Pony' % app_label],
)) ))
if manager_model:
operations.append(migrations.CreateModel(
"Food",
fields=[
("id", models.AutoField(primary_key=True)),
],
managers=[
("food_qs", FoodQuerySet.as_manager()),
("food_mgr", FoodManager("a", "b")),
("food_mgr_kwargs", FoodManager("x", "y", 3, 4)),
]
))
return self.apply_operations(app_label, ProjectState(), operations) return self.apply_operations(app_label, ProjectState(), operations)
@ -186,6 +199,10 @@ class OperationTests(OperationTestBase):
self.assertEqual(definition[0], "CreateModel") self.assertEqual(definition[0], "CreateModel")
self.assertEqual(definition[1], []) self.assertEqual(definition[1], [])
self.assertEqual(sorted(definition[2].keys()), ["fields", "name"]) self.assertEqual(sorted(definition[2].keys()), ["fields", "name"])
# And default manager not in set
operation = migrations.CreateModel("Foo", fields=[], managers=[("objects", models.Manager())])
definition = operation.deconstruct()
self.assertNotIn('managers', definition[2])
def test_create_model_with_unique_after(self): def test_create_model_with_unique_after(self):
""" """
@ -365,6 +382,37 @@ class OperationTests(OperationTestBase):
self.assertTableNotExists("test_crummo_unmanagedpony") self.assertTableNotExists("test_crummo_unmanagedpony")
self.assertTableExists("test_crummo_pony") self.assertTableExists("test_crummo_pony")
def test_create_model_managers(self):
"""
Tests that the managers on a model are set.
"""
project_state = self.set_up_test_model("test_cmoma")
# Test the state alteration
operation = migrations.CreateModel(
"Food",
fields=[
("id", models.AutoField(primary_key=True)),
],
managers=[
("food_qs", FoodQuerySet.as_manager()),
("food_mgr", FoodManager("a", "b")),
("food_mgr_kwargs", FoodManager("x", "y", 3, 4)),
]
)
self.assertEqual(operation.describe(), "Create model Food")
new_state = project_state.clone()
operation.state_forwards("test_cmoma", new_state)
self.assertIn(("test_cmoma", "food"), new_state.models)
managers = new_state.models["test_cmoma", "food"].managers
self.assertEqual(managers[0][0], "food_qs")
self.assertIsInstance(managers[0][1], models.Manager)
self.assertEqual(managers[1][0], "food_mgr")
self.assertIsInstance(managers[1][1], FoodManager)
self.assertEqual(managers[1][1].args, ("a", "b", 1, 2))
self.assertEqual(managers[2][0], "food_mgr_kwargs")
self.assertIsInstance(managers[2][1], FoodManager)
self.assertEqual(managers[2][1].args, ("x", "y", 3, 4))
def test_delete_model(self): def test_delete_model(self):
""" """
Tests the DeleteModel operation. Tests the DeleteModel operation.
@ -1208,6 +1256,61 @@ class OperationTests(OperationTestBase):
self.assertEqual(definition[1], []) self.assertEqual(definition[1], [])
self.assertEqual(definition[2], {'name': "Rider", 'order_with_respect_to': "pony"}) self.assertEqual(definition[2], {'name': "Rider", 'order_with_respect_to': "pony"})
def test_alter_model_managers(self):
"""
Tests that the managers on a model are set.
"""
project_state = self.set_up_test_model("test_almoma")
# Test the state alteration
operation = migrations.AlterModelManagers(
"Pony",
managers=[
("food_qs", FoodQuerySet.as_manager()),
("food_mgr", FoodManager("a", "b")),
("food_mgr_kwargs", FoodManager("x", "y", 3, 4)),
]
)
self.assertEqual(operation.describe(), "Change managers on Pony")
managers = project_state.models["test_almoma", "pony"].managers
self.assertEqual(managers, [])
new_state = project_state.clone()
operation.state_forwards("test_almoma", new_state)
self.assertIn(("test_almoma", "pony"), new_state.models)
managers = new_state.models["test_almoma", "pony"].managers
self.assertEqual(managers[0][0], "food_qs")
self.assertIsInstance(managers[0][1], models.Manager)
self.assertEqual(managers[1][0], "food_mgr")
self.assertIsInstance(managers[1][1], FoodManager)
self.assertEqual(managers[1][1].args, ("a", "b", 1, 2))
self.assertEqual(managers[2][0], "food_mgr_kwargs")
self.assertIsInstance(managers[2][1], FoodManager)
self.assertEqual(managers[2][1].args, ("x", "y", 3, 4))
def test_alter_model_managers_emptying(self):
"""
Tests that the managers on a model are set.
"""
project_state = self.set_up_test_model("test_almomae", manager_model=True)
# Test the state alteration
operation = migrations.AlterModelManagers("Food", managers=[])
self.assertEqual(operation.describe(), "Change managers on Food")
self.assertIn(("test_almomae", "food"), project_state.models)
managers = project_state.models["test_almomae", "food"].managers
self.assertEqual(managers[0][0], "food_qs")
self.assertIsInstance(managers[0][1], models.Manager)
self.assertEqual(managers[1][0], "food_mgr")
self.assertIsInstance(managers[1][1], FoodManager)
self.assertEqual(managers[1][1].args, ("a", "b", 1, 2))
self.assertEqual(managers[2][0], "food_mgr_kwargs")
self.assertIsInstance(managers[2][1], FoodManager)
self.assertEqual(managers[2][1].args, ("x", "y", 3, 4))
new_state = project_state.clone()
operation.state_forwards("test_almomae", new_state)
managers = new_state.models["test_almomae", "food"].managers
self.assertEqual(managers, [])
def test_alter_fk(self): def test_alter_fk(self):
""" """
Tests that creating and then altering an FK works correctly Tests that creating and then altering an FK works correctly

View File

@ -3,7 +3,8 @@ from django.db import models
from django.db.migrations.state import ProjectState, ModelState, InvalidBasesError from django.db.migrations.state import ProjectState, ModelState, InvalidBasesError
from django.test import TestCase from django.test import TestCase
from .models import ModelWithCustomBase from .models import (FoodManager, FoodQuerySet, ModelWithCustomBase,
NoMigrationFoodManager)
class StateTests(TestCase): class StateTests(TestCase):
@ -54,11 +55,56 @@ class StateTests(TestCase):
verbose_name = "tome" verbose_name = "tome"
db_table = "test_tome" db_table = "test_tome"
class Food(models.Model):
food_mgr = FoodManager('a', 'b')
food_qs = FoodQuerySet.as_manager()
food_no_mgr = NoMigrationFoodManager('x', 'y')
class Meta:
app_label = "migrations"
apps = new_apps
class FoodNoManagers(models.Model):
class Meta:
app_label = "migrations"
apps = new_apps
class FoodNoDefaultManager(models.Model):
food_no_mgr = NoMigrationFoodManager('x', 'y')
food_mgr = FoodManager('a', 'b')
food_qs = FoodQuerySet.as_manager()
class Meta:
app_label = "migrations"
apps = new_apps
mgr1 = FoodManager('a', 'b')
mgr2 = FoodManager('x', 'y', c=3, d=4)
class FoodOrderedManagers(models.Model):
# The managers on this model should be orderd by their creation
# counter and not by the order in model body
food_no_mgr = NoMigrationFoodManager('x', 'y')
food_mgr2 = mgr2
food_mgr1 = mgr1
class Meta:
app_label = "migrations"
apps = new_apps
project_state = ProjectState.from_apps(new_apps) project_state = ProjectState.from_apps(new_apps)
author_state = project_state.models['migrations', 'author'] author_state = project_state.models['migrations', 'author']
author_proxy_state = project_state.models['migrations', 'authorproxy'] author_proxy_state = project_state.models['migrations', 'authorproxy']
sub_author_state = project_state.models['migrations', 'subauthor'] sub_author_state = project_state.models['migrations', 'subauthor']
book_state = project_state.models['migrations', 'book'] book_state = project_state.models['migrations', 'book']
food_state = project_state.models['migrations', 'food']
food_no_managers_state = project_state.models['migrations', 'foodnomanagers']
food_no_default_manager_state = project_state.models['migrations', 'foodnodefaultmanager']
food_order_manager_state = project_state.models['migrations', 'foodorderedmanagers']
self.assertEqual(author_state.app_label, "migrations") self.assertEqual(author_state.app_label, "migrations")
self.assertEqual(author_state.name, "Author") self.assertEqual(author_state.name, "Author")
@ -89,26 +135,43 @@ class StateTests(TestCase):
self.assertEqual(len(sub_author_state.fields), 2) self.assertEqual(len(sub_author_state.fields), 2)
self.assertEqual(sub_author_state.bases, ("migrations.author", )) self.assertEqual(sub_author_state.bases, ("migrations.author", ))
# The default manager is used in migrations
self.assertEqual([name for name, mgr in food_state.managers], ['food_mgr'])
self.assertEqual(food_state.managers[0][1].args, ('a', 'b', 1, 2))
# No explicit managers defined. Migrations will fall back to the default
self.assertEqual(food_no_managers_state.managers, [])
# food_mgr is used in migration but isn't the default mgr, hence add the
# default
self.assertEqual([name for name, mgr in food_no_default_manager_state.managers],
['food_no_mgr', 'food_mgr'])
self.assertEqual(food_no_default_manager_state.managers[0][1].__class__, models.Manager)
self.assertIsInstance(food_no_default_manager_state.managers[1][1], FoodManager)
self.assertEqual([name for name, mgr in food_order_manager_state.managers],
['food_mgr1', 'food_mgr2'])
self.assertEqual([mgr.args for name, mgr in food_order_manager_state.managers],
[('a', 'b', 1, 2), ('x', 'y', 3, 4)])
def test_render(self): def test_render(self):
""" """
Tests rendering a ProjectState into an Apps. Tests rendering a ProjectState into an Apps.
""" """
project_state = ProjectState() project_state = ProjectState()
project_state.add_model_state(ModelState( project_state.add_model_state(ModelState(
"migrations", app_label="migrations",
"Tag", name="Tag",
[ fields=[
("id", models.AutoField(primary_key=True)), ("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=100)), ("name", models.CharField(max_length=100)),
("hidden", models.BooleanField()), ("hidden", models.BooleanField()),
], ],
{},
None,
)) ))
project_state.add_model_state(ModelState( project_state.add_model_state(ModelState(
"migrations", app_label="migrations",
"SubTag", name="SubTag",
[ fields=[
('tag_ptr', models.OneToOneField( ('tag_ptr', models.OneToOneField(
auto_created=True, auto_created=True,
primary_key=True, primary_key=True,
@ -118,15 +181,40 @@ class StateTests(TestCase):
)), )),
("awesome", models.BooleanField()), ("awesome", models.BooleanField()),
], ],
options={},
bases=("migrations.Tag",), bases=("migrations.Tag",),
)) ))
base_mgr = models.Manager()
mgr1 = FoodManager('a', 'b')
mgr2 = FoodManager('x', 'y', c=3, d=4)
project_state.add_model_state(ModelState(
app_label="migrations",
name="Food",
fields=[
("id", models.AutoField(primary_key=True)),
],
managers=[
# The ordering we really want is objects, mgr1, mgr2
('default', base_mgr),
('food_mgr2', mgr2),
('food_mgr1', mgr1),
]
))
new_apps = project_state.render() new_apps = project_state.render()
self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field_by_name("name")[0].max_length, 100) self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field_by_name("name")[0].max_length, 100)
self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field_by_name("hidden")[0].null, False) self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field_by_name("hidden")[0].null, False)
self.assertEqual(len(new_apps.get_model("migrations", "SubTag")._meta.local_fields), 2) self.assertEqual(len(new_apps.get_model("migrations", "SubTag")._meta.local_fields), 2)
Food = new_apps.get_model("migrations", "Food")
managers = sorted(Food._meta.managers)
self.assertEqual([mgr.name for _, mgr, _ in managers],
['default', 'food_mgr1', 'food_mgr2'])
self.assertEqual([mgr.__class__ for _, mgr, _ in managers],
[models.Manager, FoodManager, FoodManager])
self.assertIs(managers[0][1], Food._default_manager)
def test_render_model_inheritance(self): def test_render_model_inheritance(self):
class Book(models.Model): class Book(models.Model):
title = models.CharField(max_length=1000) title = models.CharField(max_length=1000)

View File

@ -19,6 +19,7 @@ from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import get_default_timezone, utc, FixedOffset from django.utils.timezone import get_default_timezone, utc, FixedOffset
from custom_managers import models as custom_manager_models
import custom_migration_operations.operations import custom_migration_operations.operations
import custom_migration_operations.more_operations import custom_migration_operations.more_operations
@ -351,3 +352,12 @@ class WriterTests(TestCase):
string = MigrationWriter.serialize(models.CharField(default=DeconstructableInstances))[0] string = MigrationWriter.serialize(models.CharField(default=DeconstructableInstances))[0]
self.assertEqual(string, "models.CharField(default=migrations.test_writer.DeconstructableInstances)") self.assertEqual(string, "models.CharField(default=migrations.test_writer.DeconstructableInstances)")
def test_serialize_managers(self):
self.assertSerializedEqual(models.Manager())
self.assertSerializedResultEqual(
custom_manager_models.CustomQuerySet.as_manager(),
('custom_managers.models.CustomQuerySet.as_manager()', {'import custom_managers.models'})
)
self.assertSerializedEqual(custom_manager_models.DeconstructibleCustomManager('a', 'b'))
self.assertSerializedEqual(custom_manager_models.DeconstructibleCustomManager('x', 'y', c=3, d=4))