Plain is headed towards 1.0! Subscribe for development updates →

  1"""
  2Accessors for related objects.
  3
  4When a field defines a relation between two models, each model class provides
  5an attribute to access related instances of the other model class (unless the
  6reverse accessor has been disabled with related_name='+').
  7
  8Accessors are implemented as descriptors in order to customize access and
  9assignment. This module defines the descriptor classes.
 10
 11Forward accessors follow foreign keys. Reverse accessors trace them back. For
 12example, with the following models::
 13
 14    class Parent(Model):
 15        pass
 16
 17    class Child(Model):
 18        parent = ForeignKey(Parent, related_name='children')
 19
 20 ``child.parent`` is a forward many-to-one relation. ``parent.children`` is a
 21reverse many-to-one relation.
 22
 231. Related instance on the forward side of a many-to-one relation:
 24   ``ForwardManyToOneDescriptor``.
 25
 26   Uniqueness of foreign key values is irrelevant to accessing the related
 27   instance, making the many-to-one and one-to-one cases identical as far as
 28   the descriptor is concerned. The constraint is checked upstream (unicity
 29   validation in forms) or downstream (unique indexes in the database).
 30
 312. Related objects manager for related instances on the reverse side of a
 32   many-to-one relation: ``ReverseManyToOneDescriptor``.
 33
 34   Unlike the previous two classes, this one provides access to a collection
 35   of objects. It returns a manager rather than an instance.
 36
 373. Related objects manager for related instances on the forward or reverse
 38   sides of a many-to-many relation: ``ManyToManyDescriptor``.
 39
 40   Many-to-many relations are symmetrical. The syntax of Plain models
 41   requires declaring them on one side but that's an implementation detail.
 42   They could be declared on the other side without any change in behavior.
 43   Therefore the forward and reverse descriptors can be the same.
 44
 45   If you're looking for ``ForwardManyToManyDescriptor`` or
 46   ``ReverseManyToManyDescriptor``, use ``ManyToManyDescriptor`` instead.
 47"""
 48
 49from __future__ import annotations
 50
 51from abc import ABC, abstractmethod
 52from functools import cached_property
 53from typing import Any
 54
 55from plain.models.query import QuerySet
 56from plain.utils.functional import LazyObject
 57
 58from .related_managers import (
 59    ForwardManyToManyManager,
 60    ReverseManyToManyManager,
 61    ReverseManyToOneManager,
 62)
 63
 64
 65class ForwardManyToOneDescriptor:
 66    """
 67    Accessor to the related object on the forward side of a many-to-one relation.
 68
 69    In the example::
 70
 71        class Child(Model):
 72            parent = ForeignKey(Parent, related_name='children')
 73
 74    ``Child.parent`` is a ``ForwardManyToOneDescriptor`` instance.
 75    """
 76
 77    def __init__(self, field_with_rel: Any) -> None:
 78        self.field = field_with_rel
 79
 80    @cached_property
 81    def RelatedObjectDoesNotExist(self) -> type:
 82        # The exception can't be created at initialization time since the
 83        # related model might not be resolved yet; `self.field.model` might
 84        # still be a string model reference.
 85        return type(
 86            "RelatedObjectDoesNotExist",
 87            (self.field.remote_field.model.DoesNotExist, AttributeError),
 88            {
 89                "__module__": self.field.model.__module__,
 90                "__qualname__": f"{self.field.model.__qualname__}.{self.field.name}.RelatedObjectDoesNotExist",
 91            },
 92        )
 93
 94    def is_cached(self, instance: Any) -> bool:
 95        return self.field.is_cached(instance)
 96
 97    def get_queryset(self) -> QuerySet:
 98        qs = self.field.remote_field.model._model_meta.base_queryset
 99        return qs.all()
100
101    def get_prefetch_queryset(
102        self, instances: list[Any], queryset: QuerySet | None = None
103    ) -> tuple[QuerySet, Any, Any, bool, str, bool]:
104        if queryset is None:
105            queryset = self.get_queryset()
106
107        rel_obj_attr = self.field.get_foreign_related_value
108        instance_attr = self.field.get_local_related_value
109        instances_dict = {instance_attr(inst): inst for inst in instances}
110        related_field = self.field.foreign_related_fields[0]
111        remote_field = self.field.remote_field
112
113        # FIXME: This will need to be revisited when we introduce support for
114        # composite fields. In the meantime we take this practical approach to
115        # solve a regression on 1.6 when the reverse manager in hidden
116        # (related_name ends with a '+'). Refs #21410.
117        # The check for len(...) == 1 is a special case that allows the query
118        # to be join-less and smaller. Refs #21760.
119        if remote_field.is_hidden() or len(self.field.foreign_related_fields) == 1:
120            query = {
121                f"{related_field.name}__in": {
122                    instance_attr(inst)[0] for inst in instances
123                }
124            }
125        else:
126            query = {f"{self.field.related_query_name()}__in": instances}
127        queryset = queryset.filter(**query)
128
129        # Since we're going to assign directly in the cache,
130        # we must manage the reverse relation cache manually.
131        if not remote_field.multiple:
132            for rel_obj in queryset:
133                instance = instances_dict[rel_obj_attr(rel_obj)]
134                remote_field.set_cached_value(rel_obj, instance)
135        return (
136            queryset,
137            rel_obj_attr,
138            instance_attr,
139            True,
140            self.field.get_cache_name(),
141            False,
142        )
143
144    def get_object(self, instance: Any) -> Any:
145        qs = self.get_queryset()
146        # Assuming the database enforces foreign keys, this won't fail.
147        return qs.get(self.field.get_reverse_related_filter(instance))
148
149    def __get__(
150        self, instance: Any | None, cls: type | None = None
151    ) -> ForwardManyToOneDescriptor | Any | None:
152        """
153        Get the related instance through the forward relation.
154
155        With the example above, when getting ``child.parent``:
156
157        - ``self`` is the descriptor managing the ``parent`` attribute
158        - ``instance`` is the ``child`` instance
159        - ``cls`` is the ``Child`` class (we don't need it)
160        """
161        if instance is None:
162            return self
163
164        # The related instance is loaded from the database and then cached
165        # by the field on the model instance state. It can also be pre-cached
166        # by the reverse accessor.
167        try:
168            rel_obj = self.field.get_cached_value(instance)
169        except KeyError:
170            has_value = None not in self.field.get_local_related_value(instance)
171            rel_obj = None
172
173            if rel_obj is None and has_value:
174                rel_obj = self.get_object(instance)
175                remote_field = self.field.remote_field
176                # If this is a one-to-one relation, set the reverse accessor
177                # cache on the related object to the current instance to avoid
178                # an extra SQL query if it's accessed later on.
179                if not remote_field.multiple:
180                    remote_field.set_cached_value(rel_obj, instance)
181            self.field.set_cached_value(instance, rel_obj)
182
183        if rel_obj is None and not self.field.allow_null:
184            raise self.RelatedObjectDoesNotExist(
185                f"{self.field.model.__name__} has no {self.field.name}."
186            )
187        else:
188            return rel_obj
189
190    def __set__(self, instance: Any, value: Any) -> None:
191        """
192        Set the related instance through the forward relation.
193
194        With the example above, when setting ``child.parent = parent``:
195
196        - ``self`` is the descriptor managing the ``parent`` attribute
197        - ``instance`` is the ``child`` instance
198        - ``value`` is the ``parent`` instance on the right of the equal sign
199        """
200        # If value is a LazyObject, force its evaluation. For ForeignKey fields,
201        # the value should only be None or a model instance, never a boolean or
202        # other type.
203        if isinstance(value, LazyObject):
204            # This forces evaluation: if it's None, value becomes None;
205            # if it's a User instance, value becomes that instance.
206            value = value if value else None
207
208        # An object must be an instance of the related class.
209        if value is not None and not isinstance(value, self.field.remote_field.model):
210            raise ValueError(
211                f'Cannot assign "{value!r}": "{instance.model_options.object_name}.{self.field.name}" must be a "{self.field.remote_field.model.model_options.object_name}" instance.'
212            )
213        remote_field = self.field.remote_field
214        # If we're setting the value of a OneToOneField to None, we need to clear
215        # out the cache on any old related object. Otherwise, deleting the
216        # previously-related object will also cause this object to be deleted,
217        # which is wrong.
218        if value is None:
219            # Look up the previously-related object, which may still be available
220            # since we've not yet cleared out the related field.
221            # Use the cache directly, instead of the accessor; if we haven't
222            # populated the cache, then we don't care - we're only accessing
223            # the object to invalidate the accessor cache, so there's no
224            # need to populate the cache just to expire it again.
225            related = self.field.get_cached_value(instance, default=None)
226
227            # If we've got an old related object, we need to clear out its
228            # cache. This cache also might not exist if the related object
229            # hasn't been accessed yet.
230            if related is not None:
231                remote_field.set_cached_value(related, None)
232
233            for lh_field, rh_field in self.field.related_fields:
234                setattr(instance, lh_field.attname, None)
235
236        # Set the values of the related field.
237        else:
238            for lh_field, rh_field in self.field.related_fields:
239                setattr(instance, lh_field.attname, getattr(value, rh_field.attname))
240
241        # Set the related instance cache used by __get__ to avoid an SQL query
242        # when accessing the attribute we just set.
243        self.field.set_cached_value(instance, value)
244
245        # If this is a one-to-one relation, set the reverse accessor cache on
246        # the related object to the current instance to avoid an extra SQL
247        # query if it's accessed later on.
248        if value is not None and not remote_field.multiple:
249            remote_field.set_cached_value(value, instance)
250
251    def __reduce__(self) -> tuple[Any, tuple[Any, str]]:
252        """
253        Pickling should return the instance attached by self.field on the
254        model, not a new copy of that descriptor. Use getattr() to retrieve
255        the instance directly from the model.
256        """
257        return getattr, (self.field.model, self.field.name)
258
259
260class RelationDescriptorBase(ABC):
261    """
262    Base class for relation descriptors that don't allow direct assignment.
263
264    This is used for descriptors that manage collections of related objects
265    (reverse FK and M2M relations). Forward FK relations don't inherit from
266    this because they allow direct assignment.
267    """
268
269    def __init__(self, rel: Any) -> None:
270        self.rel = rel
271        self.field = rel.field
272
273    def __get__(
274        self, instance: Any | None, cls: type | None = None
275    ) -> RelationDescriptorBase | Any:
276        """
277        Get the related manager when the descriptor is accessed.
278
279        Subclasses must implement get_related_manager().
280        """
281        if instance is None:
282            return self
283        return self.get_related_manager(instance)
284
285    @abstractmethod
286    def get_related_manager(self, instance: Any) -> Any:
287        """Return the appropriate manager for this relation."""
288        ...
289
290    @abstractmethod
291    def _get_set_deprecation_msg_params(self) -> tuple[str, str]:
292        """Return parameters for the error message when direct assignment is attempted."""
293        ...
294
295    def __set__(self, instance: Any, value: Any) -> None:
296        """Prevent direct assignment to the relation."""
297        raise TypeError(
298            "Direct assignment to the {} is prohibited. Use {}.set() instead.".format(
299                *self._get_set_deprecation_msg_params()
300            ),
301        )
302
303
304class ReverseManyToOneDescriptor(RelationDescriptorBase):
305    """
306    Accessor to the related objects manager on the reverse side of a
307    many-to-one relation.
308
309    In the example::
310
311        class Child(Model):
312            parent = ForeignKey(Parent, related_name='children')
313
314    ``Parent.children`` is a ``ReverseManyToOneDescriptor`` instance.
315
316    Most of the implementation is delegated to the ReverseManyToOneManager class.
317    """
318
319    def get_related_manager(self, instance: Any) -> ReverseManyToOneManager:
320        """Return the ReverseManyToOneManager for this relation."""
321        return ReverseManyToOneManager(instance, self.rel)
322
323    def _get_set_deprecation_msg_params(self) -> tuple[str, str]:
324        return (
325            "reverse side of a related set",
326            self.rel.get_accessor_name(),
327        )
328
329
330class ForwardManyToManyDescriptor(RelationDescriptorBase):
331    """
332    Accessor to the related objects manager on the forward side of a
333    many-to-many relation.
334
335    In the example::
336
337        class Pizza(Model):
338            toppings = ManyToManyField(Topping, related_name='pizzas')
339
340    ``Pizza.toppings`` is a ``ForwardManyToManyDescriptor`` instance.
341    """
342
343    @property
344    def through(self) -> Any:
345        # through is provided so that you have easy access to the through
346        # model (Book.authors.through) for inlines, etc. This is done as
347        # a property to ensure that the fully resolved value is returned.
348        return self.rel.through
349
350    def get_related_manager(self, instance: Any) -> ForwardManyToManyManager:
351        """Return the ForwardManyToManyManager for this relation."""
352        return ForwardManyToManyManager(instance, self.rel)
353
354    def _get_set_deprecation_msg_params(self) -> tuple[str, str]:
355        return (
356            "forward side of a many-to-many set",
357            self.field.name,
358        )
359
360
361class ReverseManyToManyDescriptor(RelationDescriptorBase):
362    """
363    Accessor to the related objects manager on the reverse side of a
364    many-to-many relation.
365
366    In the example::
367
368        class Pizza(Model):
369            toppings = ManyToManyField(Topping, related_name='pizzas')
370
371    ``Topping.pizzas`` is a ``ReverseManyToManyDescriptor`` instance.
372    """
373
374    @property
375    def through(self) -> Any:
376        # through is provided so that you have easy access to the through
377        # model (Book.authors.through) for inlines, etc. This is done as
378        # a property to ensure that the fully resolved value is returned.
379        return self.rel.through
380
381    def get_related_manager(self, instance: Any) -> ReverseManyToManyManager:
382        """Return the ReverseManyToManyManager for this relation."""
383        return ReverseManyToManyManager(instance, self.rel)
384
385    def _get_set_deprecation_msg_params(self) -> tuple[str, str]:
386        return (
387            "reverse side of a many-to-many set",
388            self.rel.get_accessor_name(),
389        )