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 )