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