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