Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain-models/plain/models/fields/reverse_related.py: 61%
153 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
1"""
2"Rel objects" for related fields.
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.
8They also act as reverse fields for the purposes of the Meta API because
9they're the closest concept currently available.
10"""
12from plain import exceptions
13from plain.utils.functional import cached_property
14from plain.utils.hashable import make_hashable
16from . import BLANK_CHOICE_DASH
17from .mixins import FieldCacheMixin
20class ForeignObjectRel(FieldCacheMixin):
21 """
22 Used by ForeignObject to store information about the relation.
24 ``_meta.get_fields()`` returns this class to provide access to the field
25 flags for the reverse relation.
26 """
28 # Field flags
29 auto_created = True
30 concrete = False
31 editable = False
32 is_relation = True
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
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
57 self.symmetrical = False
58 self.multiple = True
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()
68 @cached_property
69 def name(self):
70 return self.field.related_query_name()
72 @property
73 def remote_field(self):
74 return self.field
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]
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
98 @cached_property
99 def many_to_many(self):
100 return self.field.many_to_many
102 @cached_property
103 def many_to_one(self):
104 return self.field.one_to_many
106 @cached_property
107 def one_to_many(self):
108 return self.field.many_to_one
110 @cached_property
111 def one_to_one(self):
112 return self.field.one_to_one
114 def get_lookup(self, lookup_name):
115 return self.field.get_lookup(lookup_name)
117 def get_internal_type(self):
118 return self.field.get_internal_type()
120 @property
121 def db_type(self):
122 return self.field.db_type
124 def __repr__(self):
125 return f"<{type(self).__name__}: {self.related_model._meta.package_label}.{self.related_model._meta.model_name}>"
127 @property
128 def identity(self):
129 return (
130 self.field,
131 self.model,
132 self.related_name,
133 self.related_query_name,
134 make_hashable(self.limit_choices_to),
135 self.parent_link,
136 self.on_delete,
137 self.symmetrical,
138 self.multiple,
139 )
141 def __eq__(self, other):
142 if not isinstance(other, self.__class__):
143 return NotImplemented
144 return self.identity == other.identity
146 def __hash__(self):
147 return hash(self.identity)
149 def __getstate__(self):
150 state = self.__dict__.copy()
151 # Delete the path_infos cached property because it can be recalculated
152 # at first invocation after deserialization. The attribute must be
153 # removed because subclasses like ManyToOneRel may have a PathInfo
154 # which contains an intermediate M2M table that's been dynamically
155 # created and doesn't exist in the .models module.
156 # This is a reverse relation, so there is no reverse_path_infos to
157 # delete.
158 state.pop("path_infos", None)
159 return state
161 def get_choices(
162 self,
163 include_blank=True,
164 blank_choice=BLANK_CHOICE_DASH,
165 limit_choices_to=None,
166 ordering=(),
167 ):
168 """
169 Return choices with a default blank choices included, for use
170 as <select> choices for this field.
172 Analog of plain.models.fields.Field.get_choices(), provided
173 initially for utilization by RelatedFieldListFilter.
174 """
175 limit_choices_to = limit_choices_to or self.limit_choices_to
176 qs = self.related_model._default_manager.complex_filter(limit_choices_to)
177 if ordering:
178 qs = qs.order_by(*ordering)
179 return (blank_choice if include_blank else []) + [(x.pk, str(x)) for x in qs]
181 def is_hidden(self):
182 """Should the related object be hidden?"""
183 return bool(self.related_name) and self.related_name[-1] == "+"
185 def get_joining_columns(self):
186 return self.field.get_reverse_joining_columns()
188 def get_extra_restriction(self, alias, related_alias):
189 return self.field.get_extra_restriction(related_alias, alias)
191 def set_field_name(self):
192 """
193 Set the related field's name, this is not available until later stages
194 of app loading, so set_field_name is called from
195 set_attributes_from_rel()
196 """
197 # By default foreign object doesn't relate to any remote field (for
198 # example custom multicolumn joins currently have no remote field).
199 self.field_name = None
201 def get_accessor_name(self, model=None):
202 # This method encapsulates the logic that decides what name to give an
203 # accessor descriptor that retrieves related many-to-one or
204 # many-to-many objects. It uses the lowercased object_name + "_set",
205 # but this can be overridden with the "related_name" option. Due to
206 # backwards compatibility ModelForms need to be able to provide an
207 # alternate model. See BaseInlineFormSet.get_default_prefix().
208 opts = model._meta if model else self.related_model._meta
209 model = model or self.related_model
210 if self.multiple:
211 # If this is a symmetrical m2m relation on self, there is no
212 # reverse accessor.
213 if self.symmetrical and model == self.model:
214 return None
215 if self.related_name:
216 return self.related_name
217 return opts.model_name + ("_set" if self.multiple else "")
219 def get_path_info(self, filtered_relation=None):
220 if filtered_relation:
221 return self.field.get_reverse_path_info(filtered_relation)
222 else:
223 return self.field.reverse_path_infos
225 @cached_property
226 def path_infos(self):
227 return self.get_path_info()
229 def get_cache_name(self):
230 """
231 Return the name of the cache key to use for storing an instance of the
232 forward model on the reverse model.
233 """
234 return self.get_accessor_name()
237class ManyToOneRel(ForeignObjectRel):
238 """
239 Used by the ForeignKey field to store information about the relation.
241 ``_meta.get_fields()`` returns this class to provide access to the field
242 flags for the reverse relation.
244 Note: Because we somewhat abuse the Rel objects by using them as reverse
245 fields we get the funny situation where
246 ``ManyToOneRel.many_to_one == False`` and
247 ``ManyToOneRel.one_to_many == True``. This is unfortunate but the actual
248 ManyToOneRel class is a private API and there is work underway to turn
249 reverse relations into actual fields.
250 """
252 def __init__(
253 self,
254 field,
255 to,
256 field_name,
257 related_name=None,
258 related_query_name=None,
259 limit_choices_to=None,
260 parent_link=False,
261 on_delete=None,
262 ):
263 super().__init__(
264 field,
265 to,
266 related_name=related_name,
267 related_query_name=related_query_name,
268 limit_choices_to=limit_choices_to,
269 parent_link=parent_link,
270 on_delete=on_delete,
271 )
273 self.field_name = field_name
275 def __getstate__(self):
276 state = super().__getstate__()
277 state.pop("related_model", None)
278 return state
280 @property
281 def identity(self):
282 return super().identity + (self.field_name,)
284 def get_related_field(self):
285 """
286 Return the Field in the 'to' object to which this relationship is tied.
287 """
288 field = self.model._meta.get_field(self.field_name)
289 if not field.concrete:
290 raise exceptions.FieldDoesNotExist(
291 f"No related field named '{self.field_name}'"
292 )
293 return field
295 def set_field_name(self):
296 self.field_name = self.field_name or self.model._meta.pk.name
299class OneToOneRel(ManyToOneRel):
300 """
301 Used by OneToOneField to store information about the relation.
303 ``_meta.get_fields()`` returns this class to provide access to the field
304 flags for the reverse relation.
305 """
307 def __init__(
308 self,
309 field,
310 to,
311 field_name,
312 related_name=None,
313 related_query_name=None,
314 limit_choices_to=None,
315 parent_link=False,
316 on_delete=None,
317 ):
318 super().__init__(
319 field,
320 to,
321 field_name,
322 related_name=related_name,
323 related_query_name=related_query_name,
324 limit_choices_to=limit_choices_to,
325 parent_link=parent_link,
326 on_delete=on_delete,
327 )
329 self.multiple = False
332class ManyToManyRel(ForeignObjectRel):
333 """
334 Used by ManyToManyField to store information about the relation.
336 ``_meta.get_fields()`` returns this class to provide access to the field
337 flags for the reverse relation.
338 """
340 def __init__(
341 self,
342 field,
343 to,
344 related_name=None,
345 related_query_name=None,
346 limit_choices_to=None,
347 symmetrical=True,
348 through=None,
349 through_fields=None,
350 db_constraint=True,
351 ):
352 super().__init__(
353 field,
354 to,
355 related_name=related_name,
356 related_query_name=related_query_name,
357 limit_choices_to=limit_choices_to,
358 )
360 if through and not db_constraint:
361 raise ValueError("Can't supply a through model and db_constraint=False")
362 self.through = through
364 if through_fields and not through:
365 raise ValueError("Cannot specify through_fields without a through model")
366 self.through_fields = through_fields
368 self.symmetrical = symmetrical
369 self.db_constraint = db_constraint
371 @property
372 def identity(self):
373 return super().identity + (
374 self.through,
375 make_hashable(self.through_fields),
376 self.db_constraint,
377 )
379 def get_related_field(self):
380 """
381 Return the field in the 'to' object to which this relationship is tied.
382 Provided for symmetry with ManyToOneRel.
383 """
384 opts = self.through._meta
385 if self.through_fields:
386 field = opts.get_field(self.through_fields[0])
387 else:
388 for field in opts.fields:
389 rel = getattr(field, "remote_field", None)
390 if rel and rel.model == self.model:
391 break
392 return field.foreign_related_fields[0]