Commit ed0ff913 authored by Loïc Bistuer's avatar Loïc Bistuer
Browse files

Fixed #10506, #13793, #14891, #25201 -- Introduced new APIs to specify models'...

Fixed #10506, #13793, #14891, #25201 -- Introduced new APIs to specify 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.
parent 3a47d42f
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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 "
+20 −38
Original line number Diff line number Diff line
@@ -476,47 +476,29 @@ class ModelState(object):
        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())
                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.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 = []
            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(
+94 −4
Original line number Diff line number Diff line
@@ -24,11 +24,12 @@ from django.db.models.fields.related import (
    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 @@ class ModelBase(type):
        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 @@ class Model(six.with_metaclass(ModelBase)):
            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()
+34 −13
Original line number Diff line number Diff line
@@ -104,11 +104,20 @@ class ForwardManyToOneDescriptor(object):
        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 @@ class ReverseOneToOneDescriptor(object):
        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 @@ class ReverseManyToOneDescriptor(object):

    @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 @@ class ManyToManyDescriptor(ReverseManyToOneDescriptor):

    @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,
        )
+3 −36
Original line number Diff line number Diff line
@@ -8,47 +8,14 @@ from django.utils import six
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
Loading