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 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]