1"""
2"Rel objects" for related fields.
3
4"Rel objects" (for lack of a better name) carry information about the relation
5modeled by a related field and provide some utility functions. They're stored
6in the ``remote_field`` attribute of the field.
7
8They also act as reverse fields for the purposes of the Meta API because
9they're the closest concept currently available.
10"""
11
12from django.core import exceptions
13from django.utils.functional import cached_property
14from django.utils.hashable import make_hashable
15
16from . import BLANK_CHOICE_DASH
17from .mixins import FieldCacheMixin
18
19
20class ForeignObjectRel(FieldCacheMixin):
21 """
22 Used by ForeignObject to store information about the relation.
23
24 ``_meta.get_fields()`` returns this class to provide access to the field
25 flags for the reverse relation.
26 """
27
28 # Field flags
29 auto_created = True
30 concrete = False
31 editable = False
32 is_relation = True
33
34 # Reverse relations are always nullable (Django can't enforce that a
35 # foreign key on the related model points to this model).
36 null = True
37 empty_strings_allowed = False
38
39 def __init__(
40 self,
41 field,
42 to,
43 related_name=None,
44 related_query_name=None,
45 limit_choices_to=None,
46 parent_link=False,
47 on_delete=None,
48 ):
49 self.field = field
50 self.model = to
51 self.related_name = related_name
52 self.related_query_name = related_query_name
53 self.limit_choices_to = {} if limit_choices_to is None else limit_choices_to
54 self.parent_link = parent_link
55 self.on_delete = on_delete
56
57 self.symmetrical = False
58 self.multiple = True
59
60 # Some of the following cached_properties can't be initialized in
61 # __init__ as the field doesn't have its model yet. Calling these methods
62 # before field.contribute_to_class() has been called will result in
63 # AttributeError
64 @cached_property
65 def hidden(self):
66 """Should the related object be hidden?"""
67 return bool(self.related_name) and self.related_name[-1] == "+"
68
69 @cached_property
70 def name(self):
71 return self.field.related_query_name()
72
73 @property
74 def remote_field(self):
75 return self.field
76
77 @property
78 def target_field(self):
79 """
80 When filtering against this relation, return the field on the remote
81 model against which the filtering should happen.
82 """
83 target_fields = self.path_infos[-1].target_fields
84 if len(target_fields) > 1:
85 raise exceptions.FieldError(
86 "Can't use target_field for multicolumn relations."
87 )
88 return target_fields[0]
89
90 @cached_property
91 def related_model(self):
92 if not self.field.model:
93 raise AttributeError(
94 "This property can't be accessed before self.field.contribute_to_class "
95 "has been called."
96 )
97 return self.field.model
98
99 @cached_property
100 def many_to_many(self):
101 return self.field.many_to_many
102
103 @cached_property
104 def many_to_one(self):
105 return self.field.one_to_many
106
107 @cached_property
108 def one_to_many(self):
109 return self.field.many_to_one
110
111 @cached_property
112 def one_to_one(self):
113 return self.field.one_to_one
114
115 def get_lookup(self, lookup_name):
116 return self.field.get_lookup(lookup_name)
117
118 def get_lookups(self):
119 return self.field.get_lookups()
120
121 def get_transform(self, name):
122 return self.field.get_transform(name)
123
124 def get_internal_type(self):
125 return self.field.get_internal_type()
126
127 @property
128 def db_type(self):
129 return self.field.db_type
130
131 def __repr__(self):
132 return "<%s: %s.%s>" % (
133 type(self).__name__,
134 self.related_model._meta.app_label,
135 self.related_model._meta.model_name,
136 )
137
138 @property
139 def identity(self):
140 return (
141 self.field,
142 self.model,
143 self.related_name,
144 self.related_query_name,
145 make_hashable(self.limit_choices_to),
146 self.parent_link,
147 self.on_delete,
148 self.symmetrical,
149 self.multiple,
150 )
151
152 def __eq__(self, other):
153 if not isinstance(other, self.__class__):
154 return NotImplemented
155 return self.identity == other.identity
156
157 def __hash__(self):
158 return hash(self.identity)
159
160 def __getstate__(self):
161 state = self.__dict__.copy()
162 # Delete the path_infos cached property because it can be recalculated
163 # at first invocation after deserialization. The attribute must be
164 # removed because subclasses like ManyToOneRel may have a PathInfo
165 # which contains an intermediate M2M table that's been dynamically
166 # created and doesn't exist in the .models module.
167 # This is a reverse relation, so there is no reverse_path_infos to
168 # delete.
169 state.pop("path_infos", None)
170 return state
171
172 def get_choices(
173 self,
174 include_blank=True,
175 blank_choice=BLANK_CHOICE_DASH,
176 limit_choices_to=None,
177 ordering=(),
178 ):
179 """
180 Return choices with a default blank choices included, for use
181 as <select> choices for this field.
182
183 Analog of django.db.models.fields.Field.get_choices(), provided
184 initially for utilization by RelatedFieldListFilter.
185 """
186 limit_choices_to = limit_choices_to or self.limit_choices_to
187 qs = self.related_model._default_manager.complex_filter(limit_choices_to)
188 if ordering:
189 qs = qs.order_by(*ordering)
190 return (blank_choice if include_blank else []) + [(x.pk, str(x)) for x in qs]
191
192 def get_joining_fields(self):
193 return self.field.get_reverse_joining_fields()
194
195 def get_extra_restriction(self, alias, related_alias):
196 return self.field.get_extra_restriction(related_alias, alias)
197
198 def set_field_name(self):
199 """
200 Set the related field's name, this is not available until later stages
201 of app loading, so set_field_name is called from
202 set_attributes_from_rel()
203 """
204 # By default foreign object doesn't relate to any remote field (for
205 # example custom multicolumn joins currently have no remote field).
206 self.field_name = None
207
208 @cached_property
209 def accessor_name(self):
210 return self.get_accessor_name()
211
212 def get_accessor_name(self, model=None):
213 # This method encapsulates the logic that decides what name to give an
214 # accessor descriptor that retrieves related many-to-one or
215 # many-to-many objects. It uses the lowercased object_name + "_set",
216 # but this can be overridden with the "related_name" option. Due to
217 # backwards compatibility ModelForms need to be able to provide an
218 # alternate model. See BaseInlineFormSet.get_default_prefix().
219 opts = model._meta if model else self.related_model._meta
220 model = model or self.related_model
221 if self.multiple:
222 # If this is a symmetrical m2m relation on self, there is no
223 # reverse accessor.
224 if self.symmetrical and model == self.model:
225 return None
226 if self.related_name:
227 return self.related_name
228 return opts.model_name + ("_set" if self.multiple else "")
229
230 def get_path_info(self, filtered_relation=None):
231 if filtered_relation:
232 return self.field.get_reverse_path_info(filtered_relation)
233 else:
234 return self.field.reverse_path_infos
235
236 @cached_property
237 def path_infos(self):
238 return self.get_path_info()
239
240 @cached_property
241 def cache_name(self):
242 """
243 Return the name of the cache key to use for storing an instance of the
244 forward model on the reverse model.
245 """
246 return self.accessor_name
247
248
249class ManyToOneRel(ForeignObjectRel):
250 """
251 Used by the ForeignKey field to store information about the relation.
252
253 ``_meta.get_fields()`` returns this class to provide access to the field
254 flags for the reverse relation.
255
256 Note: Because we somewhat abuse the Rel objects by using them as reverse
257 fields we get the funny situation where
258 ``ManyToOneRel.many_to_one == False`` and
259 ``ManyToOneRel.one_to_many == True``. This is unfortunate but the actual
260 ManyToOneRel class is a private API and there is work underway to turn
261 reverse relations into actual fields.
262 """
263
264 def __init__(
265 self,
266 field,
267 to,
268 field_name,
269 related_name=None,
270 related_query_name=None,
271 limit_choices_to=None,
272 parent_link=False,
273 on_delete=None,
274 ):
275 super().__init__(
276 field,
277 to,
278 related_name=related_name,
279 related_query_name=related_query_name,
280 limit_choices_to=limit_choices_to,
281 parent_link=parent_link,
282 on_delete=on_delete,
283 )
284
285 self.field_name = field_name
286
287 def __getstate__(self):
288 state = super().__getstate__()
289 state.pop("related_model", None)
290 return state
291
292 @property
293 def identity(self):
294 return super().identity + (self.field_name,)
295
296 def get_related_field(self):
297 """
298 Return the Field in the 'to' object to which this relationship is tied.
299 """
300 field = self.model._meta.get_field(self.field_name)
301 if not field.concrete:
302 raise exceptions.FieldDoesNotExist(
303 "No related field named '%s'" % self.field_name
304 )
305 return field
306
307 def set_field_name(self):
308 self.field_name = self.field_name or self.model._meta.pk.name
309
310
311class OneToOneRel(ManyToOneRel):
312 """
313 Used by OneToOneField to store information about the relation.
314
315 ``_meta.get_fields()`` returns this class to provide access to the field
316 flags for the reverse relation.
317 """
318
319 def __init__(
320 self,
321 field,
322 to,
323 field_name,
324 related_name=None,
325 related_query_name=None,
326 limit_choices_to=None,
327 parent_link=False,
328 on_delete=None,
329 ):
330 super().__init__(
331 field,
332 to,
333 field_name,
334 related_name=related_name,
335 related_query_name=related_query_name,
336 limit_choices_to=limit_choices_to,
337 parent_link=parent_link,
338 on_delete=on_delete,
339 )
340
341 self.multiple = False
342
343
344class ManyToManyRel(ForeignObjectRel):
345 """
346 Used by ManyToManyField to store information about the relation.
347
348 ``_meta.get_fields()`` returns this class to provide access to the field
349 flags for the reverse relation.
350 """
351
352 def __init__(
353 self,
354 field,
355 to,
356 related_name=None,
357 related_query_name=None,
358 limit_choices_to=None,
359 symmetrical=True,
360 through=None,
361 through_fields=None,
362 db_constraint=True,
363 ):
364 super().__init__(
365 field,
366 to,
367 related_name=related_name,
368 related_query_name=related_query_name,
369 limit_choices_to=limit_choices_to,
370 )
371
372 if through and not db_constraint:
373 raise ValueError("Can't supply a through model and db_constraint=False")
374 self.through = through
375
376 if through_fields and not through:
377 raise ValueError("Cannot specify through_fields without a through model")
378 self.through_fields = through_fields
379
380 self.symmetrical = symmetrical
381 self.db_constraint = db_constraint
382
383 @property
384 def identity(self):
385 return super().identity + (
386 self.through,
387 make_hashable(self.through_fields),
388 self.db_constraint,
389 )
390
391 def get_related_field(self):
392 """
393 Return the field in the 'to' object to which this relationship is tied.
394 Provided for symmetry with ManyToOneRel.
395 """
396 opts = self.through._meta
397 if self.through_fields:
398 field = opts.get_field(self.through_fields[0])
399 else:
400 for field in opts.fields:
401 rel = getattr(field, "remote_field", None)
402 if rel and rel.model == self.model:
403 break
404 return field.foreign_related_fields[0]