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) @@ -241,6 +235,17 @@ class MigrationWriter(object): strings.append("%s=%s" % (kw, arg_string)) 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): """ @@ -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 `) for applications without models. +* Migrations can now :ref:`serialize model managers + ` as part of the model state. + Models ^^^^^^ diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt old mode 100755 new mode 100644 index 41ad9e3db4..24cc36650e --- 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 ` 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 ` +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))