From f233bf47dde1d481108142c8d6b4bb3b3d8c6d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sat, 9 Nov 2013 14:25:15 +0200 Subject: [PATCH] Fixed #21414 -- Removed RelatedObject and deprecated Field.related. --- django/contrib/admin/filters.py | 8 +- django/contrib/admin/options.py | 4 +- django/contrib/admin/utils.py | 2 +- django/contrib/contenttypes/fields.py | 3 +- django/db/models/fields/related.py | 138 ++++++++--- django/db/models/options.py | 12 +- django/db/models/query.py | 4 +- django/db/models/query_utils.py | 8 + django/db/models/related.py | 75 ------ django/db/models/sql/datastructures.py | 2 +- django/db/models/sql/query.py | 5 +- django/forms/models.py | 3 +- docs/internals/deprecation.txt | 2 + docs/releases/1.8.txt | 10 + tests/basic/tests.py | 15 ++ tests/model_meta/test.py | 310 ++++++++++++------------- tests/one_to_one/tests.py | 2 +- 17 files changed, 318 insertions(+), 285 deletions(-) delete mode 100644 django/db/models/related.py diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index 380d80d9a3..0c0eb339ce 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -8,7 +8,7 @@ certain test -- e.g. being a DateField or ForeignKey. import datetime from django.db import models -from django.db.models.fields.related import ManyToManyField +from django.db.models.fields.related import ForeignObjectRel, ManyToManyField from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils.encoding import smart_text, force_text from django.utils.translation import ugettext_lazy as _ @@ -180,7 +180,7 @@ class RelatedFieldListFilter(FieldListFilter): self.title = self.lookup_title def has_output(self): - if (isinstance(self.field, models.related.RelatedObject) and + if (isinstance(self.field, ForeignObjectRel) and self.field.field.null or hasattr(self.field, 'rel') and self.field.null): extra = 1 @@ -210,7 +210,7 @@ class RelatedFieldListFilter(FieldListFilter): }, [self.lookup_kwarg_isnull]), 'display': val, } - if (isinstance(self.field, models.related.RelatedObject) and + if (isinstance(self.field, ForeignObjectRel) and (self.field.field.null or isinstance(self.field.field, ManyToManyField)) or hasattr(self.field, 'rel') and (self.field.null or isinstance(self.field, ManyToManyField))): yield { @@ -223,7 +223,7 @@ class RelatedFieldListFilter(FieldListFilter): FieldListFilter.register(lambda f: ( bool(f.rel) if hasattr(f, 'rel') else - isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter) + isinstance(f, ForeignObjectRel)), RelatedFieldListFilter) class BooleanFieldListFilter(FieldListFilter): diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index b198669acd..062f0d6cf4 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -25,8 +25,8 @@ from django.core.paginator import Paginator from django.core.urlresolvers import reverse from django.db import models, transaction, router from django.db.models.constants import LOOKUP_SEP -from django.db.models.related import RelatedObject from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist +from django.db.models.fields.related import ForeignObjectRel from django.db.models.sql.constants import QUERY_TERMS from django.forms.formsets import all_valid, DELETION_FIELD_NAME from django.forms.models import (modelform_factory, modelformset_factory, @@ -421,7 +421,7 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): rel_name = field.rel.get_related_field().name else: rel_name = None - elif isinstance(field, RelatedObject): + elif isinstance(field, ForeignObjectRel): model = field.model rel_name = model._meta.pk.name else: diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py index 20e3561e76..41b03a81c1 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -304,7 +304,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False): try: label = field.verbose_name except AttributeError: - # field is likely a RelatedObject + # field is likely a ForeignObjectRel label = field.opts.verbose_name except models.FieldDoesNotExist: if name == "__unicode__": diff --git a/django/contrib/contenttypes/fields.py b/django/contrib/contenttypes/fields.py index ee855e7cae..6a883a8550 100644 --- a/django/contrib/contenttypes/fields.py +++ b/django/contrib/contenttypes/fields.py @@ -9,7 +9,7 @@ from django.db import models, router, transaction, DEFAULT_DB_ALIAS from django.db.models import signals, FieldDoesNotExist, DO_NOTHING from django.db.models.base import ModelBase from django.db.models.fields.related import ForeignObject, ForeignObjectRel -from django.db.models.related import PathInfo +from django.db.models.query_utils import PathInfo from django.db.models.expressions import Col from django.contrib.contenttypes.models import ContentType from django.utils.encoding import smart_text, python_2_unicode_compatible @@ -27,6 +27,7 @@ class GenericForeignKey(object): self.fk_field = fk_field self.for_concrete_model = for_concrete_model self.editable = False + self.rel = None def contribute_to_class(self, cls, name, **kwargs): self.name = name diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index a2f4b95286..debc73e24c 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from operator import attrgetter +import warnings from django.apps import apps from django.core import checks @@ -9,13 +10,15 @@ from django.db.backends import utils from django.db.models import signals, Q from django.db.models.deletion import SET_NULL, SET_DEFAULT, CASCADE from django.db.models.fields import (AutoField, Field, IntegerField, - PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist) + PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist, + BLANK_CHOICE_DASH) from django.db.models.lookups import IsNull -from django.db.models.related import RelatedObject, PathInfo from django.db.models.query import QuerySet +from django.db.models.query_utils import PathInfo from django.db.models.expressions import Col from django.utils.encoding import force_text, smart_text from django.utils import six +from django.utils.deprecation import RemovedInDjango20Warning from django.utils.translation import ugettext_lazy as _ from django.utils.functional import curry, cached_property from django.core import exceptions @@ -178,7 +181,7 @@ class RelatedField(Field): return [] try: - self.related + self.rel except AttributeError: return [] @@ -195,13 +198,13 @@ class RelatedField(Field): rel_opts = self.rel.to._meta # rel_opts.object_name == "Target" - rel_name = self.related.get_accessor_name() # i. e. "model_set" + rel_name = self.rel.get_accessor_name() # i. e. "model_set" rel_query_name = self.related_query_name() # i. e. "model" field_name = "%s.%s" % (opts.object_name, self.name) # i. e. "Model.field" # Check clashes between accessor or reverse query name of `field` - # and any other field name -- i. e. accessor for Model.foreign is + # and any other field name -- i.e. accessor for Model.foreign is # model_set and it clashes with Target.model_set. potential_clashes = rel_opts.fields + rel_opts.many_to_many for clash_field in potential_clashes: @@ -324,11 +327,17 @@ class RelatedField(Field): self.verbose_name = self.rel.to._meta.verbose_name self.rel.set_field_name() + @property + def related(self): + warnings.warn( + "Usage of field.related has been deprecated. Use field.rel instead.", + RemovedInDjango20Warning, 2) + return self.rel + def do_related_class(self, other, cls): self.set_attributes_from_rel() - self.related = RelatedObject(other, cls, self) if not cls._meta.abstract: - self.contribute_to_related_class(other, self.related) + self.contribute_to_related_class(other, self.rel) def get_limit_choices_to(self): """Returns 'limit_choices_to' for this model field. @@ -554,7 +563,7 @@ class ReverseSingleRelatedObjectDescriptor(object): # Since we're going to assign directly in the cache, # we must manage the reverse relation cache manually. if not self.field.rel.multiple: - rel_obj_cache_name = self.field.related.get_cache_name() + rel_obj_cache_name = self.field.rel.get_cache_name() for rel_obj in queryset: instance = instances_dict[rel_obj_attr(rel_obj)] setattr(rel_obj, rel_obj_cache_name, instance) @@ -583,7 +592,7 @@ class ReverseSingleRelatedObjectDescriptor(object): # Assuming the database enforces foreign keys, this won't fail. rel_obj = qs.get() if not self.field.rel.multiple: - setattr(rel_obj, self.field.related.get_cache_name(), instance) + setattr(rel_obj, self.field.rel.get_cache_name(), instance) setattr(instance, self.cache_name, rel_obj) if rel_obj is None and not self.field.null: raise self.RelatedObjectDoesNotExist( @@ -635,7 +644,7 @@ class ReverseSingleRelatedObjectDescriptor(object): # cache. This cache also might not exist if the related object # hasn't been accessed yet. if related is not None: - setattr(related, self.field.related.get_cache_name(), None) + setattr(related, self.field.rel.get_cache_name(), None) for lh_field, rh_field in self.field.related_fields: setattr(instance, lh_field.attname, None) @@ -656,7 +665,7 @@ class ReverseSingleRelatedObjectDescriptor(object): # object you just set. setattr(instance, self.cache_name, value) if value is not None and not self.field.rel.multiple: - setattr(value, self.field.related.get_cache_name(), instance) + setattr(value, self.field.rel.get_cache_name(), instance) def create_foreign_related_manager(superclass, rel_field, rel_model): @@ -1248,13 +1257,6 @@ class ReverseManyRelatedObjectsDescriptor(object): class ForeignObjectRel(object): def __init__(self, field, to, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None, related_query_name=None): - try: - to._meta - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT - assert isinstance(to, six.string_types), ( - "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT - ) - self.field = field self.to = to self.related_name = related_name @@ -1263,6 +1265,56 @@ class ForeignObjectRel(object): self.multiple = True self.parent_link = parent_link self.on_delete = on_delete + self.symmetrical = False + + # This and 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 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 opts(self): + return self.model._meta + + @cached_property + def to_opts(self): + return self.to._meta + + @cached_property + def parent_model(self): + return self.to + + @cached_property + def name(self): + return '%s.%s' % (self.opts.app_label, self.opts.model_name) + + def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH, + limit_to_currently_related=False): + """ + Returns choices with a default blank choices included, for use as + SelectField choices for this field. + + Analog of django.db.models.fields.Field.get_choices(), provided + initially for utilization by RelatedFieldListFilter. + """ + first_choice = blank_choice if include_blank else [] + queryset = self.model._default_manager.all() + if limit_to_currently_related: + queryset = queryset.complex_filter( + {'%s__isnull' % self.parent_model._meta.model_name: False} + ) + lst = [(x._get_pk_val(), smart_text(x)) for x in queryset] + return first_choice + lst + + def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): + # Defer to the actual field definition for db prep + return self.field.get_db_prep_lookup(lookup_type, value, connection=connection, prepared=prepared) def is_hidden(self): "Should the related object be hidden?" @@ -1289,6 +1341,34 @@ class ForeignObjectRel(object): return self.field.get_lookup_constraint(constraint_class, alias, targets, sources, lookup_type, raw_value) + 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 lower-cased 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.opts + model = model or self.model + if self.multiple: + # If this is a symmetrical m2m relation on self, there is no reverse accessor. + if self.symmetrical and model == self.to: + return None + if self.related_name: + return self.related_name + if opts.default_related_name: + return self.opts.default_related_name % { + 'model_name': self.opts.model_name.lower(), + 'app_label': self.opts.app_label.lower(), + } + return opts.model_name + ('_set' if self.multiple else '') + + def get_cache_name(self): + return "_%s_cache" % self.get_accessor_name() + + def get_path_info(self): + return self.field.get_reverse_path_info() + class ManyToOneRel(ForeignObjectRel): def __init__(self, field, to, field_name, related_name=None, limit_choices_to=None, @@ -1322,30 +1402,23 @@ class OneToOneRel(ManyToOneRel): self.multiple = False -class ManyToManyRel(object): - def __init__(self, to, related_name=None, limit_choices_to=None, +class ManyToManyRel(ForeignObjectRel): + def __init__(self, field, to, related_name=None, limit_choices_to=None, symmetrical=True, through=None, through_fields=None, db_constraint=True, related_query_name=None): if through and not db_constraint: raise ValueError("Can't supply a through model and db_constraint=False") if through_fields and not through: raise ValueError("Cannot specify through_fields without a through model") - self.to = to - self.related_name = related_name - self.related_query_name = related_query_name - if limit_choices_to is None: - limit_choices_to = {} - self.limit_choices_to = limit_choices_to + super(ManyToManyRel, self).__init__( + field, to, related_name=related_name, + limit_choices_to=limit_choices_to, related_query_name=related_query_name) self.symmetrical = symmetrical self.multiple = True self.through = through self.through_fields = through_fields self.db_constraint = db_constraint - def is_hidden(self): - "Should the related object be hidden?" - return self.related_name and self.related_name[-1] == '+' - def get_related_field(self): """ Returns the field in the 'to' object to which this relationship is tied. @@ -1402,7 +1475,7 @@ class ForeignObject(RelatedField): return [] try: - self.related + self.rel except AttributeError: return [] @@ -1979,7 +2052,8 @@ class ManyToManyField(RelatedField): to = str(to) kwargs['verbose_name'] = kwargs.get('verbose_name', None) - kwargs['rel'] = ManyToManyRel(to, + kwargs['rel'] = ManyToManyRel( + self, to, related_name=kwargs.pop('related_name', None), related_query_name=kwargs.pop('related_query_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), diff --git a/django/db/models/options.py b/django/db/models/options.py index 67010ef654..ffec2653bd 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -408,7 +408,7 @@ class Options(object): the Field instance for the given name, model is the model containing this field (None for local fields), direct is True if the field exists on this model, and m2m is True for many-to-many relations. When - 'direct' is False, 'field_object' is the corresponding RelatedObject + 'direct' is False, 'field_object' is the corresponding ForeignObjectRel for this field (since the field doesn't have an instance associated with it). @@ -456,7 +456,7 @@ class Options(object): for f, model in self.get_fields_with_model(): cache[f.name] = cache[f.attname] = (f, model, True, False) for f in self.virtual_fields: - if hasattr(f, 'related'): + if f.rel: cache[f.name] = cache[f.attname] = ( f, None if f.model == self.model else f.model, True, False) if apps.ready: @@ -508,10 +508,10 @@ class Options(object): if (hasattr(f, 'rel') and f.rel and not isinstance(f.rel.to, six.string_types) and f.generate_reverse_relation): if self == f.rel.to._meta: - cache[f.related] = None - proxy_cache[f.related] = None + cache[f.rel] = None + proxy_cache[f.rel] = None elif self.concrete_model == f.rel.to._meta.concrete_model: - proxy_cache[f.related] = None + proxy_cache[f.rel] = None self._related_objects_cache = cache self._related_objects_proxy_cache = proxy_cache @@ -552,7 +552,7 @@ class Options(object): if (f.rel and not isinstance(f.rel.to, six.string_types) and self == f.rel.to._meta): - cache[f.related] = None + cache[f.rel] = None if apps.ready: self._related_many_to_many_cache = cache return cache diff --git a/django/db/models/query.py b/django/db/models/query.py index 42dfd47eb2..8f82c0cd73 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1477,7 +1477,7 @@ def get_cached_row(row, index_start, using, klass_info, offset=0, if f.unique and rel_obj is not None: # If the field is unique, populate the # reverse descriptor cache on the related object - setattr(rel_obj, f.related.get_cache_name(), obj) + setattr(rel_obj, f.rel.get_cache_name(), obj) # Now do the same, but for reverse related objects. # Only handle the restricted case - i.e., don't do a depth @@ -1497,7 +1497,7 @@ def get_cached_row(row, index_start, using, klass_info, offset=0, rel_obj, index_end = cached_row if obj is not None: # populate the reverse descriptor cache - setattr(obj, f.related.get_cache_name(), rel_obj) + setattr(obj, f.rel.get_cache_name(), rel_obj) if rel_obj is not None: # If the related object exists, populate # the descriptor cache. diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index bb1c52efd4..60fa0421d7 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -7,6 +7,8 @@ circular import difficulties. """ from __future__ import unicode_literals +from collections import namedtuple + from django.apps import apps from django.db.backends import utils from django.db.models.constants import LOOKUP_SEP @@ -14,6 +16,12 @@ from django.utils import six from django.utils import tree +# PathInfo is used when converting lookups (fk__somecol). The contents +# describe the relation in Model terms (model Options and Fields for both +# sides of the relation. The join_field is the field backing the relation. +PathInfo = namedtuple('PathInfo', 'from_opts to_opts target_fields join_field m2m direct') + + class InvalidQuery(Exception): """ The query passed to raw isn't a safe query to use with raw. diff --git a/django/db/models/related.py b/django/db/models/related.py deleted file mode 100644 index 51460eedbe..0000000000 --- a/django/db/models/related.py +++ /dev/null @@ -1,75 +0,0 @@ -from collections import namedtuple - -from django.utils.encoding import smart_text -from django.db.models.fields import BLANK_CHOICE_DASH - -# PathInfo is used when converting lookups (fk__somecol). The contents -# describe the relation in Model terms (model Options and Fields for both -# sides of the relation. The join_field is the field backing the relation. -PathInfo = namedtuple('PathInfo', - 'from_opts to_opts target_fields join_field ' - 'm2m direct') - - -class RelatedObject(object): - def __init__(self, parent_model, model, field): - self.parent_model = parent_model - self.model = model - self.opts = model._meta - self.field = field - self.name = '%s:%s' % (self.opts.app_label, self.opts.model_name) - self.var_name = self.opts.model_name - - def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH, - limit_to_currently_related=False): - """Returns choices with a default blank choices included, for use - as SelectField choices for this field. - - Analogue of django.db.models.fields.Field.get_choices, provided - initially for utilization by RelatedFieldListFilter. - """ - first_choice = blank_choice if include_blank else [] - queryset = self.model._default_manager.all() - if limit_to_currently_related: - queryset = queryset.complex_filter( - {'%s__isnull' % self.parent_model._meta.model_name: False}) - lst = [(x._get_pk_val(), smart_text(x)) for x in queryset] - return first_choice + lst - - def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): - # Defer to the actual field definition for db prep - return self.field.get_db_prep_lookup(lookup_type, value, - connection=connection, prepared=prepared) - - def editable_fields(self): - "Get the fields in this class that should be edited inline." - return [f for f in self.opts.fields + self.opts.many_to_many if f.editable and f != self.field] - - def __repr__(self): - return "" % (self.name, self.field.name) - - def get_accessor_name(self): - # 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 lower-cased object_name + "_set", - # but this can be overridden with the "related_name" option. - if self.field.rel.multiple: - # If this is a symmetrical m2m relation on self, there is no reverse accessor. - if getattr(self.field.rel, 'symmetrical', False) and self.model == self.parent_model: - return None - if self.field.rel.related_name: - return self.field.rel.related_name - if self.opts.default_related_name: - return self.opts.default_related_name % { - 'model_name': self.opts.model_name.lower(), - 'app_label': self.opts.app_label.lower(), - } - return self.opts.model_name + '_set' - else: - return self.field.rel.related_name or (self.opts.model_name) - - def get_cache_name(self): - return "_%s_cache" % self.get_accessor_name() - - def get_path_info(self): - return self.field.get_reverse_path_info() diff --git a/django/db/models/sql/datastructures.py b/django/db/models/sql/datastructures.py index fc5ffc1790..a245e451d6 100644 --- a/django/db/models/sql/datastructures.py +++ b/django/db/models/sql/datastructures.py @@ -55,7 +55,7 @@ class Join(object): # A list of 2-tuples to use in the ON clause of the JOIN. # Each 2-tuple will create one join condition in the ON clause. self.join_cols = join_field.get_joining_columns() - # Along which field (or RelatedObject in the reverse join case) + # Along which field (or ForeignObjectRel in the reverse join case) self.join_field = join_field # Is this join nullabled? self.nullable = nullable diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index cad6372eea..44ece3fdd5 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -13,12 +13,11 @@ import warnings from django.core.exceptions import FieldError from django.db import connections, DEFAULT_DB_ALIAS +from django.db.models.aggregates import Count from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import Col, Ref from django.db.models.fields import FieldDoesNotExist -from django.db.models.query_utils import Q, refs_aggregate -from django.db.models.related import PathInfo -from django.db.models.aggregates import Count +from django.db.models.query_utils import PathInfo, Q, refs_aggregate from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE, ORDER_PATTERN, SelectInfo, INNER, LOUTER) from django.db.models.sql.datastructures import ( diff --git a/django/forms/models.py b/django/forms/models.py index 7ca4954358..c57c8af0b9 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -883,8 +883,7 @@ class BaseInlineFormSet(BaseModelFormSet): @classmethod def get_default_prefix(cls): - from django.db.models.fields.related import RelatedObject - return RelatedObject(cls.fk.rel.to, cls.model, cls.fk).get_accessor_name().replace('+', '') + return cls.fk.rel.get_accessor_name(model=cls.model).replace('+', '') def save_new(self, form, commit=True): # Use commit=False so we can assign the parent key afterwards, then diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 54d86cbf23..fa3683c7fe 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -99,6 +99,8 @@ details on these changes. ``'django.contrib.auth.middleware.SessionAuthenticationMiddleware'`` is in ``MIDDLEWARE_CLASSES``. +* Private attribute ``django.db.models.Field.related`` will be removed. + .. _deprecation-removed-in-1.9: 1.9 diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 3dc075a7c8..e2cb8cd2ff 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -1226,6 +1226,16 @@ to your ``MIDDLEWARE_CLASSES`` sometime before then to opt-in. Please read the ``django.contrib.flatpages.sitemaps.FlatPageSitemap``. The old import location is deprecated and will be removed in Django 1.9. +Model ``Field.related`` +~~~~~~~~~~~~~~~~~~~~~~~ + +Private attribute ``django.db.models.Field.related`` is deprecated in favor +of ``Field.rel``. The latter is an instance of +``django.db.models.fields.related.ForeignObjectRel`` which replaces +``django.db.models.related.RelatedObject``. The ``django.db.models.related`` +module has been removed and the ``Field.related`` attribute will be removed in +Django 2.0. + .. removed-features-1.8: Features removed in 1.8 diff --git a/tests/basic/tests.py b/tests/basic/tests.py index 31e8b724bc..74515047f5 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -2,11 +2,13 @@ from __future__ import unicode_literals from datetime import datetime, timedelta import threading +import warnings from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db import connections, DEFAULT_DB_ALIAS from django.db import DatabaseError from django.db.models.fields import Field +from django.db.models.fields.related import ForeignObjectRel from django.db.models.manager import BaseManager from django.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet, MAX_GET_RESULTS from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature @@ -770,3 +772,16 @@ class ModelRefreshTests(TestCase): a = Article.objects.create(pub_date=self._truncate_ms(datetime.now())) with self.assertNumQueries(0): a.refresh_from_db(fields=[]) + + +class TestRelatedObjectDeprecation(TestCase): + def test_field_related_deprecation(self): + field = SelfRef._meta.get_field_by_name('selfref')[0] + with warnings.catch_warnings(record=True) as warns: + warnings.simplefilter('always') + self.assertIsInstance(field.related, ForeignObjectRel) + self.assertEqual(len(warns), 1) + self.assertEqual( + str(warns.pop().message), + 'Usage of field.related has been deprecated. Use field.rel instead.' + ) diff --git a/tests/model_meta/test.py b/tests/model_meta/test.py index 9901631a70..835ce9c6d5 100644 --- a/tests/model_meta/test.py +++ b/tests/model_meta/test.py @@ -233,114 +233,114 @@ TEST_RESULTS = { }, 'get_all_related_objects_with_model_hidden': { BasePerson: ( - ('model_meta:baseperson_friends_base', None), - ('model_meta:baseperson_friends_base', None), - ('model_meta:baseperson_m2m_base', None), - ('model_meta:baseperson_following_base', None), - ('model_meta:baseperson_following_base', None), - ('model_meta:baseperson_m2m_abstract', None), - ('model_meta:baseperson_friends_abstract', None), - ('model_meta:baseperson_friends_abstract', None), - ('model_meta:baseperson_following_abstract', None), - ('model_meta:baseperson_following_abstract', None), - ('model_meta:person', None), - ('model_meta:relating_basepeople', None), - ('model_meta:relating_basepeople_hidden', None), - ('model_meta:relating', None), - ('model_meta:relating', None), + ('model_meta.baseperson_friends_base', None), + ('model_meta.baseperson_friends_base', None), + ('model_meta.baseperson_m2m_base', None), + ('model_meta.baseperson_following_base', None), + ('model_meta.baseperson_following_base', None), + ('model_meta.baseperson_m2m_abstract', None), + ('model_meta.baseperson_friends_abstract', None), + ('model_meta.baseperson_friends_abstract', None), + ('model_meta.baseperson_following_abstract', None), + ('model_meta.baseperson_following_abstract', None), + ('model_meta.person', None), + ('model_meta.relating_basepeople', None), + ('model_meta.relating_basepeople_hidden', None), + ('model_meta.relating', None), + ('model_meta.relating', None), ), Person: ( - ('model_meta:baseperson_friends_base', BasePerson), - ('model_meta:baseperson_friends_base', BasePerson), - ('model_meta:baseperson_m2m_base', BasePerson), - ('model_meta:baseperson_following_base', BasePerson), - ('model_meta:baseperson_following_base', BasePerson), - ('model_meta:baseperson_m2m_abstract', BasePerson), - ('model_meta:baseperson_friends_abstract', BasePerson), - ('model_meta:baseperson_friends_abstract', BasePerson), - ('model_meta:baseperson_following_abstract', BasePerson), - ('model_meta:baseperson_following_abstract', BasePerson), - ('model_meta:relating_basepeople', BasePerson), - ('model_meta:relating_basepeople_hidden', BasePerson), - ('model_meta:relating', BasePerson), - ('model_meta:relating', BasePerson), - ('model_meta:person_m2m_inherited', None), - ('model_meta:person_friends_inherited', None), - ('model_meta:person_friends_inherited', None), - ('model_meta:person_following_inherited', None), - ('model_meta:person_following_inherited', None), - ('model_meta:relating_people', None), - ('model_meta:relating_people_hidden', None), - ('model_meta:relating', None), - ('model_meta:relating', None), + ('model_meta.baseperson_friends_base', BasePerson), + ('model_meta.baseperson_friends_base', BasePerson), + ('model_meta.baseperson_m2m_base', BasePerson), + ('model_meta.baseperson_following_base', BasePerson), + ('model_meta.baseperson_following_base', BasePerson), + ('model_meta.baseperson_m2m_abstract', BasePerson), + ('model_meta.baseperson_friends_abstract', BasePerson), + ('model_meta.baseperson_friends_abstract', BasePerson), + ('model_meta.baseperson_following_abstract', BasePerson), + ('model_meta.baseperson_following_abstract', BasePerson), + ('model_meta.relating_basepeople', BasePerson), + ('model_meta.relating_basepeople_hidden', BasePerson), + ('model_meta.relating', BasePerson), + ('model_meta.relating', BasePerson), + ('model_meta.person_m2m_inherited', None), + ('model_meta.person_friends_inherited', None), + ('model_meta.person_friends_inherited', None), + ('model_meta.person_following_inherited', None), + ('model_meta.person_following_inherited', None), + ('model_meta.relating_people', None), + ('model_meta.relating_people_hidden', None), + ('model_meta.relating', None), + ('model_meta.relating', None), ), Relation: ( - ('model_meta:baseperson_m2m_base', None), - ('model_meta:baseperson_m2m_abstract', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:person_m2m_inherited', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:proxyperson', None), - ('model_meta:proxyperson', None), - ('model_meta:proxyperson', None), + ('model_meta.baseperson_m2m_base', None), + ('model_meta.baseperson_m2m_abstract', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.person_m2m_inherited', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.proxyperson', None), + ('model_meta.proxyperson', None), + ('model_meta.proxyperson', None), ), }, 'get_all_related_objects_with_model_hidden_local': { BasePerson: ( - ('model_meta:baseperson_friends_base', None), - ('model_meta:baseperson_friends_base', None), - ('model_meta:baseperson_m2m_base', None), - ('model_meta:baseperson_following_base', None), - ('model_meta:baseperson_following_base', None), - ('model_meta:baseperson_m2m_abstract', None), - ('model_meta:baseperson_friends_abstract', None), - ('model_meta:baseperson_friends_abstract', None), - ('model_meta:baseperson_following_abstract', None), - ('model_meta:baseperson_following_abstract', None), - ('model_meta:person', None), - ('model_meta:relating_basepeople', None), - ('model_meta:relating_basepeople_hidden', None), - ('model_meta:relating', None), - ('model_meta:relating', None), + ('model_meta.baseperson_friends_base', None), + ('model_meta.baseperson_friends_base', None), + ('model_meta.baseperson_m2m_base', None), + ('model_meta.baseperson_following_base', None), + ('model_meta.baseperson_following_base', None), + ('model_meta.baseperson_m2m_abstract', None), + ('model_meta.baseperson_friends_abstract', None), + ('model_meta.baseperson_friends_abstract', None), + ('model_meta.baseperson_following_abstract', None), + ('model_meta.baseperson_following_abstract', None), + ('model_meta.person', None), + ('model_meta.relating_basepeople', None), + ('model_meta.relating_basepeople_hidden', None), + ('model_meta.relating', None), + ('model_meta.relating', None), ), Person: ( - ('model_meta:person_m2m_inherited', None), - ('model_meta:person_friends_inherited', None), - ('model_meta:person_friends_inherited', None), - ('model_meta:person_following_inherited', None), - ('model_meta:person_following_inherited', None), - ('model_meta:relating_people', None), - ('model_meta:relating_people_hidden', None), - ('model_meta:relating', None), - ('model_meta:relating', None), + ('model_meta.person_m2m_inherited', None), + ('model_meta.person_friends_inherited', None), + ('model_meta.person_friends_inherited', None), + ('model_meta.person_following_inherited', None), + ('model_meta.person_following_inherited', None), + ('model_meta.relating_people', None), + ('model_meta.relating_people_hidden', None), + ('model_meta.relating', None), + ('model_meta.relating', None), ), Relation: ( - ('model_meta:baseperson_m2m_base', None), - ('model_meta:baseperson_m2m_abstract', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:person_m2m_inherited', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:proxyperson', None), - ('model_meta:proxyperson', None), - ('model_meta:proxyperson', None), + ('model_meta.baseperson_m2m_base', None), + ('model_meta.baseperson_m2m_abstract', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.person_m2m_inherited', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.proxyperson', None), + ('model_meta.proxyperson', None), + ('model_meta.proxyperson', None), ), }, 'get_all_related_objects_with_model_proxy': { @@ -360,67 +360,67 @@ TEST_RESULTS = { }, 'get_all_related_objects_with_model_proxy_hidden': { BasePerson: ( - ('model_meta:baseperson_friends_base', None), - ('model_meta:baseperson_friends_base', None), - ('model_meta:baseperson_m2m_base', None), - ('model_meta:baseperson_following_base', None), - ('model_meta:baseperson_following_base', None), - ('model_meta:baseperson_m2m_abstract', None), - ('model_meta:baseperson_friends_abstract', None), - ('model_meta:baseperson_friends_abstract', None), - ('model_meta:baseperson_following_abstract', None), - ('model_meta:baseperson_following_abstract', None), - ('model_meta:person', None), - ('model_meta:relating_basepeople', None), - ('model_meta:relating_basepeople_hidden', None), - ('model_meta:relating', None), - ('model_meta:relating', None), + ('model_meta.baseperson_friends_base', None), + ('model_meta.baseperson_friends_base', None), + ('model_meta.baseperson_m2m_base', None), + ('model_meta.baseperson_following_base', None), + ('model_meta.baseperson_following_base', None), + ('model_meta.baseperson_m2m_abstract', None), + ('model_meta.baseperson_friends_abstract', None), + ('model_meta.baseperson_friends_abstract', None), + ('model_meta.baseperson_following_abstract', None), + ('model_meta.baseperson_following_abstract', None), + ('model_meta.person', None), + ('model_meta.relating_basepeople', None), + ('model_meta.relating_basepeople_hidden', None), + ('model_meta.relating', None), + ('model_meta.relating', None), ), Person: ( - ('model_meta:baseperson_friends_base', BasePerson), - ('model_meta:baseperson_friends_base', BasePerson), - ('model_meta:baseperson_m2m_base', BasePerson), - ('model_meta:baseperson_following_base', BasePerson), - ('model_meta:baseperson_following_base', BasePerson), - ('model_meta:baseperson_m2m_abstract', BasePerson), - ('model_meta:baseperson_friends_abstract', BasePerson), - ('model_meta:baseperson_friends_abstract', BasePerson), - ('model_meta:baseperson_following_abstract', BasePerson), - ('model_meta:baseperson_following_abstract', BasePerson), - ('model_meta:relating_basepeople', BasePerson), - ('model_meta:relating_basepeople_hidden', BasePerson), - ('model_meta:relating', BasePerson), - ('model_meta:relating', BasePerson), - ('model_meta:person_m2m_inherited', None), - ('model_meta:person_friends_inherited', None), - ('model_meta:person_friends_inherited', None), - ('model_meta:person_following_inherited', None), - ('model_meta:person_following_inherited', None), - ('model_meta:relating_people', None), - ('model_meta:relating_people_hidden', None), - ('model_meta:relating', None), - ('model_meta:relating', None), - ('model_meta:relating', None), - ('model_meta:relating', None), + ('model_meta.baseperson_friends_base', BasePerson), + ('model_meta.baseperson_friends_base', BasePerson), + ('model_meta.baseperson_m2m_base', BasePerson), + ('model_meta.baseperson_following_base', BasePerson), + ('model_meta.baseperson_following_base', BasePerson), + ('model_meta.baseperson_m2m_abstract', BasePerson), + ('model_meta.baseperson_friends_abstract', BasePerson), + ('model_meta.baseperson_friends_abstract', BasePerson), + ('model_meta.baseperson_following_abstract', BasePerson), + ('model_meta.baseperson_following_abstract', BasePerson), + ('model_meta.relating_basepeople', BasePerson), + ('model_meta.relating_basepeople_hidden', BasePerson), + ('model_meta.relating', BasePerson), + ('model_meta.relating', BasePerson), + ('model_meta.person_m2m_inherited', None), + ('model_meta.person_friends_inherited', None), + ('model_meta.person_friends_inherited', None), + ('model_meta.person_following_inherited', None), + ('model_meta.person_following_inherited', None), + ('model_meta.relating_people', None), + ('model_meta.relating_people_hidden', None), + ('model_meta.relating', None), + ('model_meta.relating', None), + ('model_meta.relating', None), + ('model_meta.relating', None), ), Relation: ( - ('model_meta:baseperson_m2m_base', None), - ('model_meta:baseperson_m2m_abstract', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:baseperson', None), - ('model_meta:person_m2m_inherited', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:person', None), - ('model_meta:proxyperson', None), - ('model_meta:proxyperson', None), - ('model_meta:proxyperson', None), + ('model_meta.baseperson_m2m_base', None), + ('model_meta.baseperson_m2m_abstract', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.baseperson', None), + ('model_meta.person_m2m_inherited', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.person', None), + ('model_meta.proxyperson', None), + ('model_meta.proxyperson', None), + ('model_meta.proxyperson', None), ), }, 'get_all_related_many_to_many_with_model': { @@ -643,12 +643,12 @@ class GetFieldByNameTests(OptionsBaseTests): def test_get_related_object(self): field_info = Person._meta.get_field_by_name('relating_baseperson') self.assertEqual(field_info[1:], (BasePerson, False, False)) - self.assertIsInstance(field_info[0], related.RelatedObject) + self.assertIsInstance(field_info[0], related.ForeignObjectRel) def test_get_related_m2m(self): field_info = Person._meta.get_field_by_name('relating_people') self.assertEqual(field_info[1:], (None, False, True)) - self.assertIsInstance(field_info[0], related.RelatedObject) + self.assertIsInstance(field_info[0], related.ForeignObjectRel) def test_get_generic_foreign_key(self): # For historic reasons generic foreign keys aren't available. diff --git a/tests/one_to_one/tests.py b/tests/one_to_one/tests.py index 72d279b83c..73d76d4a48 100644 --- a/tests/one_to_one/tests.py +++ b/tests/one_to_one/tests.py @@ -383,7 +383,7 @@ class OneToOneTests(TestCase): be added to the related model. """ self.assertFalse( - hasattr(Target, HiddenPointer._meta.get_field('target').related.get_accessor_name()) + hasattr(Target, HiddenPointer._meta.get_field('target').rel.get_accessor_name()) ) def test_related_object(self):