diff options
| author | Markus Holtermann <info@markusholtermann.eu> | 2014-12-12 23:19:58 +0100 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2014-12-15 08:34:15 -0500 |
| commit | aa5ef0d4fc67a95ac2a5103810d0c87d8c547bac (patch) | |
| tree | 74bd11b3ef733268da4d59019e80c47752fc37c8 | |
| parent | e37ab311fc52ff5c9ea0951ce3a8d8b38f285900 (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.
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)) |
