Plain is headed towards 1.0! Subscribe for development updates →

  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 functools import cached_property
 13
 14from plain import exceptions
 15from plain.utils.hashable import make_hashable
 16
 17from . import BLANK_CHOICE_DASH
 18from .mixins import FieldCacheMixin
 19
 20
 21class ForeignObjectRel(FieldCacheMixin):
 22    """
 23    Used by ForeignObject to store information about the relation.
 24
 25    ``_meta.get_fields()`` returns this class to provide access to the field
 26    flags for the reverse relation.
 27    """
 28
 29    # Field flags
 30    auto_created = True
 31    concrete = 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    allow_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    def get_lookup(self, lookup_name):
111        return self.field.get_lookup(lookup_name)
112
113    def get_internal_type(self):
114        return self.field.get_internal_type()
115
116    @property
117    def db_type(self):
118        return self.field.db_type
119
120    def __repr__(self):
121        return f"<{type(self).__name__}: {self.related_model._meta.package_label}.{self.related_model._meta.model_name}>"
122
123    @property
124    def identity(self):
125        return (
126            self.field,
127            self.model,
128            self.related_name,
129            self.related_query_name,
130            make_hashable(self.limit_choices_to),
131            self.parent_link,
132            self.on_delete,
133            self.symmetrical,
134            self.multiple,
135        )
136
137    def __eq__(self, other):
138        if not isinstance(other, self.__class__):
139            return NotImplemented
140        return self.identity == other.identity
141
142    def __hash__(self):
143        return hash(self.identity)
144
145    def __getstate__(self):
146        state = self.__dict__.copy()
147        # Delete the path_infos cached property because it can be recalculated
148        # at first invocation after deserialization. The attribute must be
149        # removed because subclasses like ManyToOneRel may have a PathInfo
150        # which contains an intermediate M2M table that's been dynamically
151        # created and doesn't exist in the .models module.
152        # This is a reverse relation, so there is no reverse_path_infos to
153        # delete.
154        state.pop("path_infos", None)
155        return state
156
157    def get_choices(
158        self,
159        include_blank=True,
160        blank_choice=BLANK_CHOICE_DASH,
161        limit_choices_to=None,
162        ordering=(),
163    ):
164        """
165        Return choices with a default blank choices included, for use
166        as <select> choices for this field.
167
168        Analog of plain.models.fields.Field.get_choices(), provided
169        initially for utilization by RelatedFieldListFilter.
170        """
171        limit_choices_to = limit_choices_to or self.limit_choices_to
172        qs = self.related_model._default_manager.complex_filter(limit_choices_to)
173        if ordering:
174            qs = qs.order_by(*ordering)
175        return (blank_choice if include_blank else []) + [(x.pk, str(x)) for x in qs]
176
177    def is_hidden(self):
178        """Should the related object be hidden?"""
179        return bool(self.related_name) and self.related_name[-1] == "+"
180
181    def get_joining_columns(self):
182        return self.field.get_reverse_joining_columns()
183
184    def get_extra_restriction(self, alias, related_alias):
185        return self.field.get_extra_restriction(related_alias, alias)
186
187    def set_field_name(self):
188        """
189        Set the related field's name, this is not available until later stages
190        of app loading, so set_field_name is called from
191        set_attributes_from_rel()
192        """
193        # By default foreign object doesn't relate to any remote field (for
194        # example custom multicolumn joins currently have no remote field).
195        self.field_name = None
196
197    def get_accessor_name(self, model=None):
198        # This method encapsulates the logic that decides what name to give an
199        # accessor descriptor that retrieves related many-to-one or
200        # many-to-many objects. It uses the lowercased object_name + "_set",
201        # but this can be overridden with the "related_name" option. Due to
202        # backwards compatibility ModelForms need to be able to provide an
203        # alternate model. See BaseInlineFormSet.get_default_prefix().
204        opts = model._meta if model else self.related_model._meta
205        model = model or self.related_model
206        if self.multiple:
207            # If this is a symmetrical m2m relation on self, there is no
208            # reverse accessor.
209            if self.symmetrical and model == self.model:
210                return None
211        if self.related_name:
212            return self.related_name
213        return opts.model_name + ("_set" if self.multiple else "")
214
215    def get_path_info(self, filtered_relation=None):
216        if filtered_relation:
217            return self.field.get_reverse_path_info(filtered_relation)
218        else:
219            return self.field.reverse_path_infos
220
221    @cached_property
222    def path_infos(self):
223        return self.get_path_info()
224
225    def get_cache_name(self):
226        """
227        Return the name of the cache key to use for storing an instance of the
228        forward model on the reverse model.
229        """
230        return self.get_accessor_name()
231
232
233class ManyToOneRel(ForeignObjectRel):
234    """
235    Used by the ForeignKey field to store information about the relation.
236
237    ``_meta.get_fields()`` returns this class to provide access to the field
238    flags for the reverse relation.
239
240    Note: Because we somewhat abuse the Rel objects by using them as reverse
241    fields we get the funny situation where
242    ``ManyToOneRel.many_to_one == False`` and
243    ``ManyToOneRel.one_to_many == True``. This is unfortunate but the actual
244    ManyToOneRel class is a private API and there is work underway to turn
245    reverse relations into actual fields.
246    """
247
248    def __init__(
249        self,
250        field,
251        to,
252        field_name,
253        related_name=None,
254        related_query_name=None,
255        limit_choices_to=None,
256        parent_link=False,
257        on_delete=None,
258    ):
259        super().__init__(
260            field,
261            to,
262            related_name=related_name,
263            related_query_name=related_query_name,
264            limit_choices_to=limit_choices_to,
265            parent_link=parent_link,
266            on_delete=on_delete,
267        )
268
269        self.field_name = field_name
270
271    def __getstate__(self):
272        state = super().__getstate__()
273        state.pop("related_model", None)
274        return state
275
276    @property
277    def identity(self):
278        return super().identity + (self.field_name,)
279
280    def get_related_field(self):
281        """
282        Return the Field in the 'to' object to which this relationship is tied.
283        """
284        field = self.model._meta.get_field(self.field_name)
285        if not field.concrete:
286            raise exceptions.FieldDoesNotExist(
287                f"No related field named '{self.field_name}'"
288            )
289        return field
290
291    def set_field_name(self):
292        self.field_name = self.field_name or self.model._meta.pk.name
293
294
295class ManyToManyRel(ForeignObjectRel):
296    """
297    Used by ManyToManyField to store information about the relation.
298
299    ``_meta.get_fields()`` returns this class to provide access to the field
300    flags for the reverse relation.
301    """
302
303    def __init__(
304        self,
305        field,
306        to,
307        *,
308        through,
309        through_fields=None,
310        related_name=None,
311        related_query_name=None,
312        limit_choices_to=None,
313        symmetrical=True,
314    ):
315        super().__init__(
316            field,
317            to,
318            related_name=related_name,
319            related_query_name=related_query_name,
320            limit_choices_to=limit_choices_to,
321        )
322
323        self.through = through
324        self.through_fields = through_fields
325
326        self.symmetrical = symmetrical
327        self.db_constraint = True
328
329    @property
330    def identity(self):
331        return super().identity + (
332            self.through,
333            make_hashable(self.through_fields),
334            self.db_constraint,
335        )
336
337    def get_related_field(self):
338        """
339        Return the field in the 'to' object to which this relationship is tied.
340        Provided for symmetry with ManyToOneRel.
341        """
342        opts = self.through._meta
343        if self.through_fields:
344            field = opts.get_field(self.through_fields[0])
345        else:
346            for field in opts.fields:
347                rel = getattr(field, "remote_field", None)
348                if rel and rel.model == self.model:
349                    break
350        return field.foreign_related_fields[0]