Permalink
Browse files

Fixed #10506, #13793, #14891, #25201 -- Introduced new APIs to specif…

…y models' default and base managers.

This deprecates use_for_related_fields.

Old API:

class CustomManager(models.Model):
    use_for_related_fields = True

class Model(models.Model):
    custom_manager = CustomManager()

New API:

class Model(models.Model):
    custom_manager = CustomManager()

    class Meta:
        base_manager_name = 'custom_manager'

Refs #20932, #25897.

Thanks Carl Meyer for the guidance throughout this work.
Thanks Tim Graham for writing the docs.
  • Loading branch information...
1 parent 3a47d42 commit ed0ff913c648b16c4471fc9a9441d1ee48cb5420 @loic loic committed Apr 17, 2016
@@ -13,6 +13,10 @@ class GeoManager(Manager.from_queryset(GeoQuerySet)):
# properly.
use_for_related_fields = True
+ # No need to bother users with the use_for_related_fields
+ # deprecation for this manager which is itself deprecated.
+ silence_use_for_related_fields_deprecation = True
+
def __init__(self, *args, **kwargs):
warnings.warn(
"The GeoManager class is deprecated. Simply use a normal manager "
@@ -476,47 +476,29 @@ def flatten_bases(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_mapping = {}
+ 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
- name = force_text(mgr.name)
- managers_mapping[name] = (mgr.creation_counter, instance)
-
- if hasattr(model, "_default_manager"):
- default_manager_name = force_text(model._default_manager.name)
- # Make sure the default manager is always the first
+ # Make sure the default manager is always first since ordering chooses
+ # the default manager.
+ if not model._default_manager.auto_created:
if model._default_manager.use_in_migrations:
- reconstruct_manager(model._default_manager)
+ default_manager = copy.copy(model._default_manager)
+ default_manager._set_creation_counter()
+
+ # If the default manager doesn't have `use_in_migrations = True`,
+ # shim a default manager so another manager isn't promoted in its
+ # place.
else:
- # Force this manager to be the first and thus default
- managers_mapping[default_manager_name] = (0, models.Manager())
- for manager in 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_mapping.items(), key=lambda v: v[1])
- ]
- # If the only manager on the model is the default manager defined
- # by Django (`objects = models.Manager()`), this manager will not
- # be added to the model state.
- if managers == [('objects', models.Manager())]:
- managers = []
- else:
- managers = []
+ default_manager = models.Manager()
+ default_manager.model = model
+ default_manager.name = model._default_manager.name
+ managers.append((force_text(default_manager.name), default_manager))
+
+ for manager in model._meta.managers:
+ if manager.use_in_migrations and manager is not model._default_manager:
+ manager = copy.copy(manager)
+ manager._set_creation_counter()
+ managers.append((force_text(manager.name), manager))
# Construct the new ModelState
return cls(
View
@@ -24,11 +24,12 @@
ForeignObjectRel, ManyToOneRel, OneToOneField, lazy_related_operation,
resolve_relation,
)
-from django.db.models.manager import ensure_default_manager
+from django.db.models.manager import Manager
from django.db.models.options import Options
from django.db.models.query import Q
from django.db.models.utils import make_model_tuple
from django.utils import six
+from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import (
force_str, force_text, python_2_unicode_compatible,
)
@@ -345,10 +346,99 @@ def _prepare(cls):
if get_absolute_url_override:
setattr(cls, 'get_absolute_url', get_absolute_url_override)
- ensure_default_manager(cls)
+ if not opts.managers or cls._requires_legacy_default_manager():
+ if any(f.name == 'objects' for f in opts.fields):
+ raise ValueError(
+ "Model %s must specify a custom Manager, because it has a "
+ "field named 'objects'." % cls.__name__
+ )
+ manager = Manager()
+ manager.auto_created = True
+ cls.add_to_class('objects', manager)
signals.class_prepared.send(sender=cls)
+ def _requires_legacy_default_manager(cls): # RemovedInDjango20Warning
+ opts = cls._meta
+
+ if opts.manager_inheritance_from_future:
+ return False
+
+ future_default_manager = opts.default_manager
+
+ # Step 1: Locate a manager that would have been promoted
+ # to default manager with the legacy system.
+ for manager in opts.managers:
+ originating_model = manager._originating_model
+ if (cls is originating_model or cls._meta.proxy or
+ originating_model._meta.abstract):
+
+ if manager is not cls._default_manager and not opts.default_manager_name:
+ warnings.warn(
+ "Managers from concrete parents will soon qualify as default "
+ "managers if they appear before any other managers in the "
+ "MRO. As a result, '{legacy_default_manager}' declared on "
+ "'{legacy_default_manager_model}' will no longer be the "
+ "default manager for '{model}' in favor of "
+ "'{future_default_manager}' declared on "
+ "'{future_default_manager_model}'. "
+ "You can redeclare '{legacy_default_manager}' on '{cls}' "
+ "to keep things the way they are or you can switch to the new "
+ "behavior right away by setting "
+ "`Meta.manager_inheritance_from_future` to `True`.".format(
+ cls=cls.__name__,
+ model=opts.label,
+ legacy_default_manager=manager.name,
+ legacy_default_manager_model=manager._originating_model._meta.label,
+ future_default_manager=future_default_manager.name,
+ future_default_manager_model=future_default_manager._originating_model._meta.label,
+ ),
+ RemovedInDjango20Warning, 2
+ )
+
+ opts.default_manager_name = manager.name
+ opts._expire_cache()
+
+ break
+
+ # Step 2: Since there are managers but none of them qualified as
+ # default managers under the legacy system (meaning that there are
+ # managers from concrete parents that would be promoted under the
+ # new system), we need to create a new Manager instance for the
+ # 'objects' attribute as a deprecation shim.
+ else:
+ # If the "future" default manager was auto created there is no
+ # point warning the user since it's basically the same manager.
+ if not future_default_manager.auto_created:
+ warnings.warn(
+ "Managers from concrete parents will soon qualify as "
+ "default managers. As a result, the 'objects' manager "
+ "won't be created (or recreated) automatically "
+ "anymore on '{model}' and '{future_default_manager}' "
+ "declared on '{future_default_manager_model}' will be "
+ "promoted to default manager. You can declare "
+ "explicitly `objects = models.Manager()` on '{cls}' "
+ "to keep things the way they are or you can switch "
+ "to the new behavior right away by setting "
+ "`Meta.manager_inheritance_from_future` to `True`.".format(
+ cls=cls.__name__,
+ model=opts.label,
+ future_default_manager=future_default_manager.name,
+ future_default_manager_model=future_default_manager._originating_model._meta.label,
+ ),
+ RemovedInDjango20Warning, 2
+ )
+
+ return True
+
+ @property
+ def _base_manager(cls):
+ return cls._meta.base_manager
+
+ @property
+ def _default_manager(cls):
+ return cls._meta.default_manager
+
class ModelState(object):
"""
@@ -896,8 +986,8 @@ def _get_next_or_previous_in_order(self, is_next):
order = '_order' if is_next else '-_order'
order_field = self._meta.order_with_respect_to
filter_args = order_field.get_filter_kwargs_for_object(self)
- obj = self._default_manager.filter(**filter_args).filter(**{
- '_order__%s' % op: self._default_manager.values('_order').filter(**{
+ obj = self.__class__._default_manager.filter(**filter_args).filter(**{
+ '_order__%s' % op: self.__class__._default_manager.values('_order').filter(**{
self._meta.pk.name: self.pk
})
}).order_by(order)[:1].get()
@@ -104,11 +104,20 @@ def is_cached(self, instance):
return hasattr(instance, self.cache_name)
def get_queryset(self, **hints):
- manager = self.field.remote_field.model._default_manager
- # If the related manager indicates that it should be used for
- # related fields, respect that.
- if not getattr(manager, 'use_for_related_fields', False):
- manager = self.field.remote_field.model._base_manager
+ related_model = self.field.remote_field.model
+
+ if (not related_model._meta.base_manager_name and
+ getattr(related_model._default_manager, 'use_for_related_fields', False)):
+ if not getattr(related_model._default_manager, 'silence_use_for_related_fields_deprecation', False):
+ warnings.warn(
+ "use_for_related_fields is deprecated, instead "
+ "set Meta.base_manager_name on '{}'.".format(related_model._meta.label),
+ RemovedInDjango20Warning, 2
+ )
+ manager = related_model._default_manager
+ else:
+ manager = related_model._base_manager
+
return manager.db_manager(hints=hints).all()
def get_prefetch_queryset(self, instances, queryset=None):
@@ -281,11 +290,20 @@ def is_cached(self, instance):
return hasattr(instance, self.cache_name)
def get_queryset(self, **hints):
- manager = self.related.related_model._default_manager
- # If the related manager indicates that it should be used for
- # related fields, respect that.
- if not getattr(manager, 'use_for_related_fields', False):
- manager = self.related.related_model._base_manager
+ related_model = self.related.related_model
+
+ if (not related_model._meta.base_manager_name and
+ getattr(related_model._default_manager, 'use_for_related_fields', False)):
+ if not getattr(related_model._default_manager, 'silence_use_for_related_fields_deprecation', False):
+ warnings.warn(
+ "use_for_related_fields is deprecated, instead "
+ "set Meta.base_manager_name on '{}'.".format(related_model._meta.label),
+ RemovedInDjango20Warning, 2
+ )
+ manager = related_model._default_manager
+ else:
+ manager = related_model._base_manager
+
return manager.db_manager(hints=hints).all()
def get_prefetch_queryset(self, instances, queryset=None):
@@ -437,8 +455,10 @@ def __init__(self, rel):
@cached_property
def related_manager_cls(self):
+ related_model = self.rel.related_model
+
return create_reverse_many_to_one_manager(
- self.rel.related_model._default_manager.__class__,
+ related_model._default_manager.__class__,
self.rel,
)
@@ -697,9 +717,10 @@ def through(self):
@cached_property
def related_manager_cls(self):
- model = self.rel.related_model if self.reverse else self.rel.model
+ related_model = self.rel.related_model if self.reverse else self.rel.model
+
return create_forward_many_to_many_manager(
- model._default_manager.__class__,
+ related_model._default_manager.__class__,
self.rel,
reverse=self.reverse,
)
@@ -8,47 +8,14 @@
from django.utils.encoding import python_2_unicode_compatible
-def can_use_for_related_field(manager_class):
- return manager_class is Manager or getattr(manager_class, 'use_for_related_fields', False)
-
-
-def ensure_default_manager(model):
- """
- Ensures that a Model subclass contains a default manager and sets the
- _default_manager and _base_manager attributes on the class.
- """
-
- if not model._meta.managers:
- if any(f.name == 'objects' for f in model._meta.fields):
- raise ValueError(
- "Model %s must specify a custom Manager, because it has a "
- "field named 'objects'" % model.__name__
- )
- model.add_to_class('objects', Manager())
-
- model._default_manager = model._meta.managers[0]
-
- # Just alias _base_manager if default manager is suitable.
- if can_use_for_related_field(model._default_manager.__class__):
- model._base_manager = model._default_manager
-
- # Otherwise search for a suitable manager type in the default manager MRO.
- else:
- for base_manager_class in model._default_manager.__class__.mro()[1:]:
- if can_use_for_related_field(base_manager_class):
- model._base_manager = base_manager_class()
- model._base_manager.name = '_base_manager'
- model._base_manager.model = model
- break
- else:
- raise ValueError("Could not find a suitable base manager.")
-
-
@python_2_unicode_compatible
class BaseManager(object):
# Tracks each time a Manager instance is created. Used to retain order.
creation_counter = 0
+ # Set to True for the 'objects' managers that are automatically created.
+ auto_created = False
+
#: 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
Oops, something went wrong.

0 comments on commit ed0ff91

Please sign in to comment.