mirror of
https://github.com/django/django.git
synced 2025-05-07 23:46:30 +00:00
PathInfo values are ostensibly static over the lifetime of the object for which they're requested, so the data can be memoized, quickly amortising the cost over the process' duration.
350 lines
12 KiB
Python
350 lines
12 KiB
Python
"""
|
|
"Rel objects" for related fields.
|
|
|
|
"Rel objects" (for lack of a better name) carry information about the relation
|
|
modeled by a related field and provide some utility functions. They're stored
|
|
in the ``remote_field`` attribute of the field.
|
|
|
|
They also act as reverse fields for the purposes of the Meta API because
|
|
they're the closest concept currently available.
|
|
"""
|
|
|
|
from django.core import exceptions
|
|
from django.utils.functional import cached_property
|
|
from django.utils.hashable import make_hashable
|
|
|
|
from . import BLANK_CHOICE_DASH
|
|
from .mixins import FieldCacheMixin
|
|
|
|
|
|
class ForeignObjectRel(FieldCacheMixin):
|
|
"""
|
|
Used by ForeignObject to store information about the relation.
|
|
|
|
``_meta.get_fields()`` returns this class to provide access to the field
|
|
flags for the reverse relation.
|
|
"""
|
|
|
|
# Field flags
|
|
auto_created = True
|
|
concrete = False
|
|
editable = False
|
|
is_relation = True
|
|
|
|
# Reverse relations are always nullable (Django can't enforce that a
|
|
# foreign key on the related model points to this model).
|
|
null = True
|
|
empty_strings_allowed = False
|
|
|
|
def __init__(self, field, to, related_name=None, related_query_name=None,
|
|
limit_choices_to=None, parent_link=False, on_delete=None):
|
|
self.field = field
|
|
self.model = to
|
|
self.related_name = related_name
|
|
self.related_query_name = related_query_name
|
|
self.limit_choices_to = {} if limit_choices_to is None else limit_choices_to
|
|
self.parent_link = parent_link
|
|
self.on_delete = on_delete
|
|
|
|
self.symmetrical = False
|
|
self.multiple = True
|
|
|
|
# Some of the following cached_properties can't be initialized in
|
|
# __init__ as the field doesn't have its model yet. Calling these methods
|
|
# before field.contribute_to_class() has been called will result in
|
|
# AttributeError
|
|
@cached_property
|
|
def hidden(self):
|
|
return self.is_hidden()
|
|
|
|
@cached_property
|
|
def name(self):
|
|
return self.field.related_query_name()
|
|
|
|
@property
|
|
def remote_field(self):
|
|
return self.field
|
|
|
|
@property
|
|
def target_field(self):
|
|
"""
|
|
When filtering against this relation, return the field on the remote
|
|
model against which the filtering should happen.
|
|
"""
|
|
target_fields = self.path_infos[-1].target_fields
|
|
if len(target_fields) > 1:
|
|
raise exceptions.FieldError("Can't use target_field for multicolumn relations.")
|
|
return target_fields[0]
|
|
|
|
@cached_property
|
|
def related_model(self):
|
|
if not self.field.model:
|
|
raise AttributeError(
|
|
"This property can't be accessed before self.field.contribute_to_class has been called.")
|
|
return self.field.model
|
|
|
|
@cached_property
|
|
def many_to_many(self):
|
|
return self.field.many_to_many
|
|
|
|
@cached_property
|
|
def many_to_one(self):
|
|
return self.field.one_to_many
|
|
|
|
@cached_property
|
|
def one_to_many(self):
|
|
return self.field.many_to_one
|
|
|
|
@cached_property
|
|
def one_to_one(self):
|
|
return self.field.one_to_one
|
|
|
|
def get_lookup(self, lookup_name):
|
|
return self.field.get_lookup(lookup_name)
|
|
|
|
def get_internal_type(self):
|
|
return self.field.get_internal_type()
|
|
|
|
@property
|
|
def db_type(self):
|
|
return self.field.db_type
|
|
|
|
def __repr__(self):
|
|
return '<%s: %s.%s>' % (
|
|
type(self).__name__,
|
|
self.related_model._meta.app_label,
|
|
self.related_model._meta.model_name,
|
|
)
|
|
|
|
@property
|
|
def identity(self):
|
|
return (
|
|
self.field,
|
|
self.model,
|
|
self.related_name,
|
|
self.related_query_name,
|
|
make_hashable(self.limit_choices_to),
|
|
self.parent_link,
|
|
self.on_delete,
|
|
self.symmetrical,
|
|
self.multiple,
|
|
)
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, self.__class__):
|
|
return NotImplemented
|
|
return self.identity == other.identity
|
|
|
|
def __hash__(self):
|
|
return hash(self.identity)
|
|
|
|
def __getstate__(self):
|
|
state = self.__dict__.copy()
|
|
# Delete the path_infos cached property because it can be recalculated
|
|
# at first invocation after deserialization. The attribute must be
|
|
# removed because subclasses like ManyToOneRel may have a PathInfo
|
|
# which contains an intermediate M2M table that's been dynamically
|
|
# created and doesn't exist in the .models module.
|
|
# This is a reverse relation, so there is no reverse_path_infos to
|
|
# delete.
|
|
state.pop('path_infos', None)
|
|
return state
|
|
|
|
def get_choices(
|
|
self, include_blank=True, blank_choice=BLANK_CHOICE_DASH,
|
|
limit_choices_to=None, ordering=(),
|
|
):
|
|
"""
|
|
Return choices with a default blank choices included, for use
|
|
as <select> choices for this field.
|
|
|
|
Analog of django.db.models.fields.Field.get_choices(), provided
|
|
initially for utilization by RelatedFieldListFilter.
|
|
"""
|
|
limit_choices_to = limit_choices_to or self.limit_choices_to
|
|
qs = self.related_model._default_manager.complex_filter(limit_choices_to)
|
|
if ordering:
|
|
qs = qs.order_by(*ordering)
|
|
return (blank_choice if include_blank else []) + [
|
|
(x.pk, str(x)) for x in qs
|
|
]
|
|
|
|
def is_hidden(self):
|
|
"""Should the related object be hidden?"""
|
|
return bool(self.related_name) and self.related_name[-1] == '+'
|
|
|
|
def get_joining_columns(self):
|
|
return self.field.get_reverse_joining_columns()
|
|
|
|
def get_extra_restriction(self, alias, related_alias):
|
|
return self.field.get_extra_restriction(related_alias, alias)
|
|
|
|
def set_field_name(self):
|
|
"""
|
|
Set the related field's name, this is not available until later stages
|
|
of app loading, so set_field_name is called from
|
|
set_attributes_from_rel()
|
|
"""
|
|
# By default foreign object doesn't relate to any remote field (for
|
|
# example custom multicolumn joins currently have no remote field).
|
|
self.field_name = None
|
|
|
|
def get_accessor_name(self, model=None):
|
|
# This method encapsulates the logic that decides what name to give an
|
|
# accessor descriptor that retrieves related many-to-one or
|
|
# many-to-many objects. It uses the lowercased object_name + "_set",
|
|
# but this can be overridden with the "related_name" option. Due to
|
|
# backwards compatibility ModelForms need to be able to provide an
|
|
# alternate model. See BaseInlineFormSet.get_default_prefix().
|
|
opts = model._meta if model else self.related_model._meta
|
|
model = model or self.related_model
|
|
if self.multiple:
|
|
# If this is a symmetrical m2m relation on self, there is no reverse accessor.
|
|
if self.symmetrical and model == self.model:
|
|
return None
|
|
if self.related_name:
|
|
return self.related_name
|
|
return opts.model_name + ('_set' if self.multiple else '')
|
|
|
|
def get_path_info(self, filtered_relation=None):
|
|
if filtered_relation:
|
|
return self.field.get_reverse_path_info(filtered_relation)
|
|
else:
|
|
return self.field.reverse_path_infos
|
|
|
|
@cached_property
|
|
def path_infos(self):
|
|
return self.get_path_info()
|
|
|
|
def get_cache_name(self):
|
|
"""
|
|
Return the name of the cache key to use for storing an instance of the
|
|
forward model on the reverse model.
|
|
"""
|
|
return self.get_accessor_name()
|
|
|
|
|
|
class ManyToOneRel(ForeignObjectRel):
|
|
"""
|
|
Used by the ForeignKey field to store information about the relation.
|
|
|
|
``_meta.get_fields()`` returns this class to provide access to the field
|
|
flags for the reverse relation.
|
|
|
|
Note: Because we somewhat abuse the Rel objects by using them as reverse
|
|
fields we get the funny situation where
|
|
``ManyToOneRel.many_to_one == False`` and
|
|
``ManyToOneRel.one_to_many == True``. This is unfortunate but the actual
|
|
ManyToOneRel class is a private API and there is work underway to turn
|
|
reverse relations into actual fields.
|
|
"""
|
|
|
|
def __init__(self, field, to, field_name, related_name=None, related_query_name=None,
|
|
limit_choices_to=None, parent_link=False, on_delete=None):
|
|
super().__init__(
|
|
field, to,
|
|
related_name=related_name,
|
|
related_query_name=related_query_name,
|
|
limit_choices_to=limit_choices_to,
|
|
parent_link=parent_link,
|
|
on_delete=on_delete,
|
|
)
|
|
|
|
self.field_name = field_name
|
|
|
|
def __getstate__(self):
|
|
state = super().__getstate__()
|
|
state.pop('related_model', None)
|
|
return state
|
|
|
|
@property
|
|
def identity(self):
|
|
return super().identity + (self.field_name,)
|
|
|
|
def get_related_field(self):
|
|
"""
|
|
Return the Field in the 'to' object to which this relationship is tied.
|
|
"""
|
|
field = self.model._meta.get_field(self.field_name)
|
|
if not field.concrete:
|
|
raise exceptions.FieldDoesNotExist("No related field named '%s'" % self.field_name)
|
|
return field
|
|
|
|
def set_field_name(self):
|
|
self.field_name = self.field_name or self.model._meta.pk.name
|
|
|
|
|
|
class OneToOneRel(ManyToOneRel):
|
|
"""
|
|
Used by OneToOneField to store information about the relation.
|
|
|
|
``_meta.get_fields()`` returns this class to provide access to the field
|
|
flags for the reverse relation.
|
|
"""
|
|
|
|
def __init__(self, field, to, field_name, related_name=None, related_query_name=None,
|
|
limit_choices_to=None, parent_link=False, on_delete=None):
|
|
super().__init__(
|
|
field, to, field_name,
|
|
related_name=related_name,
|
|
related_query_name=related_query_name,
|
|
limit_choices_to=limit_choices_to,
|
|
parent_link=parent_link,
|
|
on_delete=on_delete,
|
|
)
|
|
|
|
self.multiple = False
|
|
|
|
|
|
class ManyToManyRel(ForeignObjectRel):
|
|
"""
|
|
Used by ManyToManyField to store information about the relation.
|
|
|
|
``_meta.get_fields()`` returns this class to provide access to the field
|
|
flags for the reverse relation.
|
|
"""
|
|
|
|
def __init__(self, field, to, related_name=None, related_query_name=None,
|
|
limit_choices_to=None, symmetrical=True, through=None,
|
|
through_fields=None, db_constraint=True):
|
|
super().__init__(
|
|
field, to,
|
|
related_name=related_name,
|
|
related_query_name=related_query_name,
|
|
limit_choices_to=limit_choices_to,
|
|
)
|
|
|
|
if through and not db_constraint:
|
|
raise ValueError("Can't supply a through model and db_constraint=False")
|
|
self.through = through
|
|
|
|
if through_fields and not through:
|
|
raise ValueError("Cannot specify through_fields without a through model")
|
|
self.through_fields = through_fields
|
|
|
|
self.symmetrical = symmetrical
|
|
self.db_constraint = db_constraint
|
|
|
|
@property
|
|
def identity(self):
|
|
return super().identity + (
|
|
self.through,
|
|
make_hashable(self.through_fields),
|
|
self.db_constraint,
|
|
)
|
|
|
|
def get_related_field(self):
|
|
"""
|
|
Return the field in the 'to' object to which this relationship is tied.
|
|
Provided for symmetry with ManyToOneRel.
|
|
"""
|
|
opts = self.through._meta
|
|
if self.through_fields:
|
|
field = opts.get_field(self.through_fields[0])
|
|
else:
|
|
for field in opts.fields:
|
|
rel = getattr(field, 'remote_field', None)
|
|
if rel and rel.model == self.model:
|
|
break
|
|
return field.foreign_related_fields[0]
|