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