summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarkus Holtermann <info@markusholtermann.eu>2014-12-12 23:19:58 +0100
committerTim Graham <timograham@gmail.com>2014-12-15 08:34:15 -0500
commitaa5ef0d4fc67a95ac2a5103810d0c87d8c547bac (patch)
tree74bd11b3ef733268da4d59019e80c47752fc37c8
parente37ab311fc52ff5c9ea0951ce3a8d8b38f285900 (diff)
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.
-rw-r--r--django/contrib/admin/migrations/0001_initial.py4
-rw-r--r--django/contrib/admin/models.py2
-rw-r--r--django/contrib/auth/migrations/0001_initial.py10
-rw-r--r--django/contrib/auth/models.py5
-rw-r--r--django/contrib/contenttypes/migrations/0001_initial.py4
-rw-r--r--django/contrib/contenttypes/models.py1
-rw-r--r--django/contrib/sessions/migrations/0001_initial.py4
-rw-r--r--django/contrib/sessions/models.py2
-rw-r--r--django/contrib/sites/managers.py2
-rw-r--r--django/contrib/sites/migrations/0001_initial.py4
-rw-r--r--django/contrib/sites/models.py1
-rw-r--r--django/db/migrations/autodetector.py17
-rw-r--r--django/db/migrations/operations/__init__.py4
-rw-r--r--django/db/migrations/operations/models.py43
-rw-r--r--django/db/migrations/state.py51
-rw-r--r--django/db/migrations/writer.py26
-rw-r--r--django/db/models/manager.py57
-rw-r--r--django/db/models/query.py4
-rw-r--r--docs/ref/migration-operations.txt19
-rw-r--r--docs/releases/1.8.txt3
-rw-r--r--[-rwxr-xr-x]docs/topics/migrations.txt66
-rw-r--r--tests/custom_managers/models.py7
-rw-r--r--tests/custom_managers/tests.py42
-rw-r--r--tests/migrations/models.py18
-rw-r--r--tests/migrations/test_autodetector.py37
-rw-r--r--tests/migrations/test_operations.py105
-rw-r--r--tests/migrations/test_state.py108
-rw-r--r--tests/migrations/test_writer.py10
28 files changed, 608 insertions, 48 deletions
diff --git a/django/contrib/admin/migrations/0001_initial.py b/django/contrib/admin/migrations/0001_initial.py
index 8d46ab1572..b9872dcc44 100644
--- a/django/contrib/admin/migrations/0001_initial.py
+++ b/django/contrib/admin/migrations/0001_initial.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
+import django.contrib.admin.models
class Migration(migrations.Migration):
@@ -32,5 +33,8 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'log entries',
},
bases=(models.Model,),
+ managers=[
+ ('objects', django.contrib.admin.models.LogEntryManager()),
+ ],
),
]
diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py
index 572de810f7..9ecb367e05 100644
--- a/django/contrib/admin/models.py
+++ b/django/contrib/admin/models.py
@@ -15,6 +15,8 @@ DELETION = 3
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=''):
e = self.model(
None, None, user_id, content_type_id, smart_text(object_id),
diff --git a/django/contrib/auth/migrations/0001_initial.py b/django/contrib/auth/migrations/0001_initial.py
index f66ab0d468..33c8e00153 100644
--- a/django/contrib/auth/migrations/0001_initial.py
+++ b/django/contrib/auth/migrations/0001_initial.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
from django.core import validators
from django.db import models, migrations
from django.utils import timezone
+import django.contrib.auth.models
class Migration(migrations.Migration):
@@ -27,6 +28,9 @@ class Migration(migrations.Migration):
'verbose_name': 'permission',
'verbose_name_plural': 'permissions',
},
+ managers=[
+ ('objects', django.contrib.auth.models.PermissionManager()),
+ ],
),
migrations.CreateModel(
name='Group',
@@ -39,6 +43,9 @@ class Migration(migrations.Migration):
'verbose_name': 'group',
'verbose_name_plural': 'groups',
},
+ managers=[
+ ('objects', django.contrib.auth.models.GroupManager()),
+ ],
),
migrations.CreateModel(
name='User',
@@ -62,5 +69,8 @@ class Migration(migrations.Migration):
'verbose_name': 'user',
'verbose_name_plural': 'users',
},
+ managers=[
+ ('objects', django.contrib.auth.models.UserManager()),
+ ],
),
]
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
index 737d6d3aea..89b3054b70 100644
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -29,6 +29,8 @@ user_logged_in.connect(update_last_login)
class PermissionManager(models.Manager):
+ use_in_migrations = True
+
def get_by_natural_key(self, codename, app_label, model):
return self.get(
codename=codename,
@@ -87,6 +89,8 @@ class GroupManager(models.Manager):
"""
The manager for the auth's Group model.
"""
+ use_in_migrations = True
+
def get_by_natural_key(self, name):
return self.get(name=name)
@@ -160,6 +164,7 @@ class BaseUserManager(models.Manager):
class UserManager(BaseUserManager):
+ use_in_migrations = True
def _create_user(self, username, email, password,
is_staff, is_superuser, **extra_fields):
diff --git a/django/contrib/contenttypes/migrations/0001_initial.py b/django/contrib/contenttypes/migrations/0001_initial.py
index 08e1119376..09519f1d75 100644
--- a/django/contrib/contenttypes/migrations/0001_initial.py
+++ b/django/contrib/contenttypes/migrations/0001_initial.py
@@ -2,6 +2,7 @@
from __future__ import unicode_literals
from django.db import models, migrations
+import django.contrib.contenttypes.models
class Migration(migrations.Migration):
@@ -25,6 +26,9 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'content types',
},
bases=(models.Model,),
+ managers=[
+ ('objects', django.contrib.contenttypes.models.ContentTypeManager()),
+ ],
),
migrations.AlterUniqueTogether(
name='contenttype',
diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py
index f5a1ff2ec8..3617389058 100644
--- a/django/contrib/contenttypes/models.py
+++ b/django/contrib/contenttypes/models.py
@@ -9,6 +9,7 @@ from django.utils.encoding import python_2_unicode_compatible
class ContentTypeManager(models.Manager):
+ use_in_migrations = True
# Cache to avoid re-looking up ContentType objects all over the place.
# This cache is shared by all the get_for_* methods.
diff --git a/django/contrib/sessions/migrations/0001_initial.py b/django/contrib/sessions/migrations/0001_initial.py
index 652b128d85..ade7ac6043 100644
--- a/django/contrib/sessions/migrations/0001_initial.py
+++ b/django/contrib/sessions/migrations/0001_initial.py
@@ -2,6 +2,7 @@
from __future__ import unicode_literals
from django.db import models, migrations
+import django.contrib.sessions.models
class Migration(migrations.Migration):
@@ -23,5 +24,8 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'sessions',
},
bases=(models.Model,),
+ managers=[
+ ('objects', django.contrib.sessions.models.SessionManager()),
+ ],
),
]
diff --git a/django/contrib/sessions/models.py b/django/contrib/sessions/models.py
index 8aad470cf3..bd2b7dfcf4 100644
--- a/django/contrib/sessions/models.py
+++ b/django/contrib/sessions/models.py
@@ -6,6 +6,8 @@ from django.utils.translation import ugettext_lazy as _
class SessionManager(models.Manager):
+ use_in_migrations = True
+
def encode(self, session_dict):
"""
Returns the given session dictionary serialized and encoded as a string.
diff --git a/django/contrib/sites/managers.py b/django/contrib/sites/managers.py
index d5ed93c791..45dddd9273 100644
--- a/django/contrib/sites/managers.py
+++ b/django/contrib/sites/managers.py
@@ -10,6 +10,8 @@ from django.db.models.fields import FieldDoesNotExist
class CurrentSiteManager(models.Manager):
"Use this to limit objects to those associated with the current site."
+ use_in_migrations = True
+
def __init__(self, field_name=None):
super(CurrentSiteManager, self).__init__()
self.__field_name = field_name
diff --git a/django/contrib/sites/migrations/0001_initial.py b/django/contrib/sites/migrations/0001_initial.py
index 2724d4f575..00ac06de10 100644
--- a/django/contrib/sites/migrations/0001_initial.py
+++ b/django/contrib/sites/migrations/0001_initial.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import models, migrations
from django.contrib.sites.models import _simple_domain_name_validator
+import django.contrib.sites.models
class Migration(migrations.Migration):
@@ -24,5 +25,8 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'sites',
},
bases=(models.Model,),
+ managers=[
+ ('objects', django.contrib.sites.models.SiteManager()),
+ ],
),
]
diff --git a/django/contrib/sites/models.py b/django/contrib/sites/models.py
index 9efba9bd5c..96b89011b0 100644
--- a/django/contrib/sites/models.py
+++ b/django/contrib/sites/models.py
@@ -33,6 +33,7 @@ def _simple_domain_name_validator(value):
class SiteManager(models.Manager):
+ use_in_migrations = True
def _get_site_by_id(self, site_id):
if site_id not in SITE_CACHE:
diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py
index 007ea33666..c8a092cc3a 100644
--- a/django/db/migrations/autodetector.py
+++ b/django/db/migrations/autodetector.py
@@ -178,6 +178,7 @@ class MigrationAutodetector(object):
self.generate_deleted_proxies()
self.generate_created_proxies()
self.generate_altered_options()
+ self.generate_altered_managers()
# Generate field operations
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],
options=model_state.options,
bases=model_state.bases,
+ managers=model_state.managers,
),
dependencies=dependencies,
beginning=True,
@@ -607,6 +609,7 @@ class MigrationAutodetector(object):
fields=[],
options=model_state.options,
bases=model_state.bases,
+ managers=model_state.managers,
),
# Depend on the deletion of any possible non-proxy version of us
dependencies=dependencies,
@@ -990,6 +993,20 @@ class MigrationAutodetector(object):
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):
"""
Takes in a result from changes() and a MigrationGraph,
diff --git a/django/db/migrations/operations/__init__.py b/django/db/migrations/operations/__init__.py
index a6bee2d24a..5e069ffb59 100644
--- a/django/db/migrations/operations/__init__.py
+++ b/django/db/migrations/operations/__init__.py
@@ -1,6 +1,6 @@
from .models import (CreateModel, DeleteModel, AlterModelTable,
AlterUniqueTogether, AlterIndexTogether, RenameModel, AlterModelOptions,
- AlterOrderWithRespectTo)
+ AlterOrderWithRespectTo, AlterModelManagers)
from .fields import AddField, RemoveField, AlterField, RenameField
from .special import SeparateDatabaseAndState, RunSQL, RunPython
@@ -9,5 +9,5 @@ __all__ = [
'RenameModel', 'AlterIndexTogether', 'AlterModelOptions',
'AddField', 'RemoveField', 'AlterField', 'RenameField',
'SeparateDatabaseAndState', 'RunSQL', 'RunPython',
- 'AlterOrderWithRespectTo',
+ 'AlterOrderWithRespectTo', 'AlterModelManagers',
]
diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py
index f47ba53ad8..e92c3d6bd3 100644
--- a/django/db/migrations/operations/models.py
+++ b/django/db/migrations/operations/models.py
@@ -12,13 +12,14 @@ class CreateModel(Operation):
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.fields = fields
self.options = options or {}
self.bases = bases or (models.Model,)
+ self.managers = managers or []
def deconstruct(self):
kwargs = {
@@ -29,6 +30,8 @@ class CreateModel(Operation):
kwargs['options'] = self.options
if self.bases and self.bases != (models.Model,):
kwargs['bases'] = self.bases
+ if self.managers and self.managers != [('objects', models.Manager())]:
+ kwargs['managers'] = self.managers
return (
self.__class__.__name__,
[],
@@ -42,6 +45,7 @@ class CreateModel(Operation):
list(self.fields),
dict(self.options),
tuple(self.bases),
+ list(self.managers),
)
def database_forwards(self, app_label, schema_editor, from_state, to_state):
@@ -467,3 +471,38 @@ class AlterModelOptions(Operation):
def describe(self):
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, )
diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py
index b13e59c221..797afef722 100644
--- a/django/db/migrations/state.py
+++ b/django/db/migrations/state.py
@@ -149,12 +149,13 @@ class ModelState(object):
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.name = force_text(name)
self.fields = fields
self.options = options or {}
self.bases = bases or (models.Model, )
+ self.managers = managers or []
# Sanity-check that fields is NOT a dict. It must be ordered.
if isinstance(self.fields, dict):
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
if not any((isinstance(base, six.string_types) or issubclass(base, models.Model)) for base in bases):
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(
model._meta.app_label,
model._meta.object_name,
fields,
options,
bases,
+ managers,
)
@classmethod
@@ -292,6 +332,7 @@ class ModelState(object):
fields=list(self.construct_fields()),
options=dict(self.options),
bases=self.bases,
+ managers=self.managers,
)
def render(self, apps):
@@ -312,6 +353,11 @@ class ModelState(object):
body = dict(self.construct_fields())
body['Meta'] = meta
body['__module__'] = "__fake__"
+
+ # Restore managers
+ for mgr_name, manager in self.managers:
+ body[mgr_name] = manager
+
# Then, make a Model object
return type(
str(self.name),
@@ -336,7 +382,8 @@ class ModelState(object):
all((k1 == k2 and (f1.deconstruct()[1:] == f2.deconstruct()[1:]))
for (k1, f1), (k2, f2) in zip(self.fields, other.fields)) and
(self.options == other.options) and
- (self.bases == other.bases)
+ (self.bases == other.bases) and
+ (self.managers == other.managers)
)
def __ne__(self, other):
diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py
index 2e7afbd663..9907874bb4 100644
--- a/django/db/migrations/writer.py
+++ b/django/db/migrations/writer.py
@@ -223,13 +223,7 @@ class MigrationWriter(object):
@classmethod
def serialize_deconstructed(cls, path, args, kwargs):
- 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
+ name, imports = cls._serialize_path(path)
strings = []
for arg in args:
arg_string, arg_imports = cls.serialize(arg)
@@ -242,6 +236,17 @@ class MigrationWriter(object):
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
def serialize(cls, value):
"""
Serializes the value to a string that's parsable by Python, along
@@ -344,6 +349,13 @@ class MigrationWriter(object):
return value.__name__, set()
else:
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.
elif hasattr(value, 'deconstruct'):
return cls.serialize_deconstructed(*value.deconstruct())
diff --git a/django/db/models/manager.py b/django/db/models/manager.py
index 3f459faf13..6944a7a2b4 100644
--- a/django/db/models/manager.py
+++ b/django/db/models/manager.py
@@ -1,4 +1,5 @@
import copy
+from importlib import import_module
import inspect
from django.db import router
@@ -58,6 +59,16 @@ class BaseManager(object):
# Tracks each time a Manager instance is created. Used to retain order.
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):
super(BaseManager, self).__init__()
self._set_creation_counter()
@@ -73,6 +84,43 @@ class BaseManager(object):
app = model._meta.app_label
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):
return []
@@ -183,6 +231,15 @@ class BaseManager(object):
# understanding of how this comes into play.
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)):
pass
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 6e6d1abf3f..42dfd47eb2 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -67,7 +67,9 @@ class QuerySet(object):
def as_manager(cls):
# Address the circular dependency between `Queryset` and `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 = classmethod(as_manager)
diff --git a/docs/ref/migration-operations.txt b/docs/ref/migration-operations.txt
index ef5f2a44d9..4055c4f828 100644
--- a/docs/ref/migration-operations.txt
+++ b/docs/ref/migration-operations.txt
@@ -37,7 +37,7 @@ Schema Operations
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
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
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
-----------
@@ -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``
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
--------
diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt
index ddb98aff26..0637bdc206 100644
--- a/docs/releases/1.8.txt
+++ b/docs/releases/1.8.txt
@@ -387,6 +387,9 @@ Migrations
* It is now possible to have migrations (most probably :ref:`data migrations
<data-migrations>`) for applications without models.
+* Migrations can now :ref:`serialize model managers
+ <using-managers-in-migrations>` as part of the model state.
+
Models
^^^^^^
diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt
index 41ad9e3db4..24cc36650e 100755..100644
--- a/docs/topics/migrations.txt
+++ b/docs/topics/migrations.txt
@@ -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
``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
-------------------------
@@ -326,16 +356,17 @@ you can use the :djadminopt:`--name` option::
Historical models
-----------------
-When you run migrations, Django is working from historical versions of
-your models stored in the migration files. If you write Python code
-using the :class:`~django.db.migrations.operations.RunPython` operation, or if
-you have ``allow_migrate`` methods on your database routers, you will be
-exposed to these versions of your models.
+When you run migrations, Django is working from historical versions of your
+models stored in the migration files. If you write Python code using the
+:class:`~django.db.migrations.operations.RunPython` operation, or if you have
+``allow_migrate`` methods on your database routers, you will be exposed to
+these versions of your models.
Because it's impossible to serialize arbitrary Python code, these historical
-models will not have any custom methods or managers that you have defined.
-They will, however, have the same fields, relationships and ``Meta`` options
-(also versioned, so they may be different from your current ones).
+models will not have any custom methods that you have defined. They will,
+however, have the same fields, relationships, managers (limited to those with
+``use_in_migrations = True``) and ``Meta`` options (also versioned, so they may
+be different from your current ones).
.. warning::
@@ -344,16 +375,17 @@ They will, however, have the same fields, relationships and ``Meta`` options
constructors or instance methods. Plan appropriately!
References to functions in field options such as ``upload_to`` and
-``limit_choices_to`` are serialized in migrations, so the functions will need
-to be kept around for as long as there is a migration referencing them. Any
-:doc:`custom model fields </howto/custom-model-fields>` will also need to be
-kept, since these are imported directly by migrations.
+``limit_choices_to`` and model manager declarations with managers having
+``use_in_migrations = True`` are serialized in migrations, so the functions and
+classes will need to be kept around for as long as there is a migration
+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,
-so you must always keep base classes around for as long as there is a migration
-that contains a reference to them. On the plus side, methods and managers
-from these base classes inherit normally, so if you absolutely need access
-to these you can opt to move them into a superclass.
+In addition, the base classes of the model are just stored as pointers, so you
+must always keep base classes around for as long as there is a migration that
+contains a reference to them. On the plus side, methods and managers from these
+base classes inherit normally, so if you absolutely need access to these you
+can opt to move them into a superclass.
.. _data-migrations:
diff --git a/tests/custom_managers/models.py b/tests/custom_managers/models.py
index cecfd2c948..d4ca730629 100644
--- a/tests/custom_managers/models.py
+++ b/tests/custom_managers/models.py
@@ -60,9 +60,16 @@ class BaseCustomManager(models.Manager):
def manager_only(self):
return self.all()
+
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):
def get_queryset(self):
return super(FunPeopleManager, self).get_queryset().filter(fun=True)
diff --git a/tests/custom_managers/tests.py b/tests/custom_managers/tests.py
index bd18e7d8ea..c115482802 100644
--- a/tests/custom_managers/tests.py
+++ b/tests/custom_managers/tests.py
@@ -1,9 +1,11 @@
from __future__ import unicode_literals
+from django.db import models
from django.test import TestCase
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)
@@ -470,6 +472,44 @@ class CustomManagerTests(TestCase):
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):
diff --git a/tests/migrations/models.py b/tests/migrations/models.py
index fc063f5cb0..1e6ab07f75 100644
--- a/tests/migrations/models.py
+++ b/tests/migrations/models.py
@@ -50,3 +50,21 @@ class UnmigratedModel(models.Model):
if its migrations directory has not been repointed)
"""
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
diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py
index 9e7511cc45..46c0102280 100644
--- a/tests/migrations/test_autodetector.py
+++ b/tests/migrations/test_autodetector.py
@@ -9,6 +9,8 @@ from django.db.migrations.loader import MigrationLoader
from django.db import models, connection
from django.contrib.auth.models import AbstractBaseUser
+from .models import FoodManager, FoodQuerySet
+
class DeconstructableObject(object):
"""
@@ -159,6 +161,13 @@ class AutodetectorTests(TestCase):
other_pony = ModelState("otherapp", "Pony", [
("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))])
third_thing = ModelState("thirdapp", "Thing", [("id", models.AutoField(primary_key=True))])
book = ModelState("otherapp", "Book", [
@@ -456,13 +465,15 @@ class AutodetectorTests(TestCase):
"""Tests autodetection of new models."""
# Make 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)
changes = autodetector._detect_changes()
# Right number/type of migrations?
- self.assertNumberMigrations(changes, 'testapp', 1)
- self.assertOperationTypes(changes, 'testapp', 0, ["CreateModel"])
- self.assertOperationAttributes(changes, "testapp", 0, 0, name="Author")
+ self.assertNumberMigrations(changes, 'otherapp', 1)
+ self.assertOperationTypes(changes, 'otherapp', 0, ["CreateModel"])
+ 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):
"""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.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):
"""Tests that swappable models get their CreateModel first."""
# Make state
diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index 0378b86f02..bccfb8e3e2 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -17,6 +17,7 @@ from django.db.utils import IntegrityError, DatabaseError
from django.test import override_settings
from django.utils import six
+from .models import FoodManager, FoodQuerySet
from .test_base import MigrationTestBase
@@ -48,7 +49,7 @@ class OperationTestBase(MigrationTestBase):
return project_state, new_state
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):
"""
Creates a test model state and database table.
@@ -142,6 +143,18 @@ class OperationTestBase(MigrationTestBase):
options={"proxy": True},
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)
@@ -186,6 +199,10 @@ class OperationTests(OperationTestBase):
self.assertEqual(definition[0], "CreateModel")
self.assertEqual(definition[1], [])
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):
"""
@@ -365,6 +382,37 @@ class OperationTests(OperationTestBase):
self.assertTableNotExists("test_crummo_unmanagedpony")
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):
"""
Tests the DeleteModel operation.
@@ -1208,6 +1256,61 @@ class OperationTests(OperationTestBase):
self.assertEqual(definition[1], [])
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):
"""
Tests that creating and then altering an FK works correctly
diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py
index 65ea6dc1fa..512f75ec15 100644
--- a/tests/migrations/test_state.py
+++ b/tests/migrations/test_state.py
@@ -3,7 +3,8 @@ from django.db import models
from django.db.migrations.state import ProjectState, ModelState, InvalidBasesError
from django.test import TestCase
-from .models import ModelWithCustomBase
+from .models import (FoodManager, FoodQuerySet, ModelWithCustomBase,
+ NoMigrationFoodManager)
class StateTests(TestCase):
@@ -54,11 +55,56 @@ class StateTests(TestCase):
verbose_name = "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)
author_state = project_state.models['migrations', 'author']
author_proxy_state = project_state.models['migrations', 'authorproxy']
sub_author_state = project_state.models['migrations', 'subauthor']
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.name, "Author")
@@ -89,26 +135,43 @@ class StateTests(TestCase):
self.assertEqual(len(sub_author_state.fields), 2)
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):
"""
Tests rendering a ProjectState into an Apps.
"""
project_state = ProjectState()
project_state.add_model_state(ModelState(
- "migrations",
- "Tag",
- [
+ app_label="migrations",
+ name="Tag",
+ fields=[
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=100)),
("hidden", models.BooleanField()),
],
- {},
- None,
))
project_state.add_model_state(ModelState(
- "migrations",
- "SubTag",
- [
+ app_label="migrations",
+ name="SubTag",
+ fields=[
('tag_ptr', models.OneToOneField(
auto_created=True,
primary_key=True,
@@ -118,15 +181,40 @@ class StateTests(TestCase):
)),
("awesome", models.BooleanField()),
],
- options={},
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()
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(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):
class Book(models.Model):
title = models.CharField(max_length=1000)
diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py
index 618f8df925..c42008c9d3 100644
--- a/tests/migrations/test_writer.py
+++ b/tests/migrations/test_writer.py
@@ -19,6 +19,7 @@ from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _
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.more_operations
@@ -351,3 +352,12 @@ class WriterTests(TestCase):
string = MigrationWriter.serialize(models.CharField(default=DeconstructableInstances))[0]
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))