Fixed #12663 -- Formalized the Model._meta API for retrieving fields.

Thanks to Russell Keith-Magee for mentoring this Google Summer of
Code 2014 project and everyone else who helped with the patch!
This commit is contained in:
Daniel Pyrathon 2015-01-06 19:16:35 -05:00 committed by Tim Graham
parent 749d23251b
commit fb48eb0581
58 changed files with 2851 additions and 1195 deletions

View File

@ -337,7 +337,12 @@ class Apps(object):
This is mostly used in tests. This is mostly used in tests.
""" """
# Call expire cache on each model. This will purge
# the relation tree and the fields cache.
self.get_models.cache_clear() self.get_models.cache_clear()
if self.ready:
for model in self.get_models(include_auto_created=True):
model._meta._expire_cache()
### DEPRECATED METHODS GO BELOW THIS LINE ### ### DEPRECATED METHODS GO BELOW THIS LINE ###

View File

@ -762,7 +762,7 @@ class ModelAdminChecks(BaseModelAdminChecks):
def _check_list_editable_item(self, cls, model, field_name, label): def _check_list_editable_item(self, cls, model, field_name, label):
try: try:
field = model._meta.get_field_by_name(field_name)[0] field = model._meta.get_field(field_name)
except FieldDoesNotExist: except FieldDoesNotExist:
return refer_to_missing_field(field=field_name, option=label, return refer_to_missing_field(field=field_name, option=label,
model=model, obj=cls, id='admin.E121') model=model, obj=cls, id='admin.E121')

View File

@ -406,7 +406,7 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
rel_name = None rel_name = None
for part in parts[:-1]: for part in parts[:-1]:
try: try:
field, _, _, _ = model._meta.get_field_by_name(part) field = model._meta.get_field(part)
except FieldDoesNotExist: except FieldDoesNotExist:
# Lookups on non-existent fields are ok, since they're ignored # Lookups on non-existent fields are ok, since they're ignored
# later. # later.
@ -422,7 +422,7 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
else: else:
rel_name = None rel_name = None
elif isinstance(field, ForeignObjectRel): elif isinstance(field, ForeignObjectRel):
model = field.model model = field.related_model
rel_name = model._meta.pk.name rel_name = model._meta.pk.name
else: else:
rel_name = None rel_name = None
@ -473,9 +473,12 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
for inline in admin.inlines: for inline in admin.inlines:
registered_models.add(inline.model) registered_models.add(inline.model)
for related_object in (opts.get_all_related_objects(include_hidden=True) + related_objects = (
opts.get_all_related_many_to_many_objects()): f for f in opts.get_fields(include_hidden=True)
related_model = related_object.model if (f.auto_created and not f.concrete)
)
for related_object in related_objects:
related_model = related_object.related_model
if (any(issubclass(model, related_model) for model in registered_models) and if (any(issubclass(model, related_model) for model in registered_models) and
related_object.field.rel.get_related_field() == field): related_object.field.rel.get_related_field() == field):
return True return True

View File

@ -326,7 +326,7 @@ def date_hierarchy(cl):
""" """
if cl.date_hierarchy: if cl.date_hierarchy:
field_name = cl.date_hierarchy field_name = cl.date_hierarchy
field = cl.opts.get_field_by_name(field_name)[0] field = cl.opts.get_field(field_name)
dates_or_datetimes = 'datetimes' if isinstance(field, models.DateTimeField) else 'dates' dates_or_datetimes = 'datetimes' if isinstance(field, models.DateTimeField) else 'dates'
year_field = '%s__year' % field_name year_field = '%s__year' % field_name
month_field = '%s__month' % field_name month_field = '%s__month' % field_name

View File

@ -25,7 +25,7 @@ def lookup_needs_distinct(opts, lookup_path):
Returns True if 'distinct()' should be used to query the given lookup path. Returns True if 'distinct()' should be used to query the given lookup path.
""" """
field_name = lookup_path.split('__', 1)[0] field_name = lookup_path.split('__', 1)[0]
field = opts.get_field_by_name(field_name)[0] field = opts.get_field(field_name)
if hasattr(field, 'get_path_info') and any(path.m2m for path in field.get_path_info()): if hasattr(field, 'get_path_info') and any(path.m2m for path in field.get_path_info()):
return True return True
return False return False
@ -265,7 +265,7 @@ def model_ngettext(obj, n=None):
def lookup_field(name, obj, model_admin=None): def lookup_field(name, obj, model_admin=None):
opts = obj._meta opts = obj._meta
try: try:
f = opts.get_field(name) f = _get_non_gfk_field(opts, name)
except FieldDoesNotExist: except FieldDoesNotExist:
# For non-field values, the value is either a method, property or # For non-field values, the value is either a method, property or
# returned via a callable. # returned via a callable.
@ -291,6 +291,17 @@ def lookup_field(name, obj, model_admin=None):
return f, attr, value return f, attr, value
def _get_non_gfk_field(opts, name):
"""
For historical reasons, the admin app relies on GenericForeignKeys as being
"not found" by get_field(). This could likely be cleaned up.
"""
field = opts.get_field(name)
if field.is_relation and field.one_to_many and not field.related_model:
raise FieldDoesNotExist()
return field
def label_for_field(name, model, model_admin=None, return_attr=False): def label_for_field(name, model, model_admin=None, return_attr=False):
""" """
Returns a sensible label for a field name. The name can be a callable, Returns a sensible label for a field name. The name can be a callable,
@ -301,7 +312,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False):
""" """
attr = None attr = None
try: try:
field = model._meta.get_field_by_name(name)[0] field = _get_non_gfk_field(model._meta, name)
try: try:
label = field.verbose_name label = field.verbose_name
except AttributeError: except AttributeError:
@ -349,11 +360,10 @@ def label_for_field(name, model, model_admin=None, return_attr=False):
def help_text_for_field(name, model): def help_text_for_field(name, model):
help_text = "" help_text = ""
try: try:
field_data = model._meta.get_field_by_name(name) field = _get_non_gfk_field(model._meta, name)
except FieldDoesNotExist: except FieldDoesNotExist:
pass pass
else: else:
field = field_data[0]
if hasattr(field, 'help_text'): if hasattr(field, 'help_text'):
help_text = field.help_text help_text = field.help_text
return smart_text(help_text) return smart_text(help_text)
@ -425,19 +435,21 @@ def reverse_field_path(model, path):
parent = model parent = model
pieces = path.split(LOOKUP_SEP) pieces = path.split(LOOKUP_SEP)
for piece in pieces: for piece in pieces:
field, model, direct, m2m = parent._meta.get_field_by_name(piece) field = parent._meta.get_field(piece)
# skip trailing data field if extant: # skip trailing data field if extant:
if len(reversed_path) == len(pieces) - 1: # final iteration if len(reversed_path) == len(pieces) - 1: # final iteration
try: try:
get_model_from_relation(field) get_model_from_relation(field)
except NotRelationField: except NotRelationField:
break break
if direct:
# Field should point to another model
if field.is_relation and not (field.auto_created and not field.concrete):
related_name = field.related_query_name() related_name = field.related_query_name()
parent = field.rel.to parent = field.rel.to
else: else:
related_name = field.field.name related_name = field.field.name
parent = field.model parent = field.related_model
reversed_path.insert(0, related_name) reversed_path.insert(0, related_name)
return (parent, LOOKUP_SEP.join(reversed_path)) return (parent, LOOKUP_SEP.join(reversed_path))
@ -458,7 +470,7 @@ def get_fields_from_path(model, path):
parent = get_model_from_relation(fields[-1]) parent = get_model_from_relation(fields[-1])
else: else:
parent = model parent = model
fields.append(parent._meta.get_field_by_name(piece)[0]) fields.append(parent._meta.get_field(piece))
return fields return fields

View File

@ -346,7 +346,7 @@ class ModelAdminValidator(BaseValidator):
check_isseq(cls, 'list_editable', cls.list_editable) check_isseq(cls, 'list_editable', cls.list_editable)
for idx, field_name in enumerate(cls.list_editable): for idx, field_name in enumerate(cls.list_editable):
try: try:
field = model._meta.get_field_by_name(field_name)[0] field = model._meta.get_field(field_name)
except FieldDoesNotExist: except FieldDoesNotExist:
raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a "
"field, '%s', not defined on %s.%s." "field, '%s', not defined on %s.%s."

View File

@ -262,7 +262,7 @@ class ModelDetailView(BaseAdminDocsView):
}) })
# Gather related objects # Gather related objects
for rel in opts.get_all_related_objects() + opts.get_all_related_many_to_many_objects(): for rel in opts.related_objects:
verbose = _("related `%(app_label)s.%(object_name)s` objects") % { verbose = _("related `%(app_label)s.%(object_name)s` objects") % {
'app_label': rel.opts.app_label, 'app_label': rel.opts.app_label,
'object_name': rel.opts.object_name, 'object_name': rel.opts.object_name,

View File

@ -21,6 +21,18 @@ class GenericForeignKey(object):
Provides a generic relation to any object through content-type/object-id Provides a generic relation to any object through content-type/object-id
fields. fields.
""" """
# Field flags
auto_created = False
concrete = False
editable = False
hidden = False
is_relation = True
many_to_many = False
many_to_one = False
one_to_many = True
one_to_one = False
related_model = None
def __init__(self, ct_field="content_type", fk_field="object_id", for_concrete_model=True): def __init__(self, ct_field="content_type", fk_field="object_id", for_concrete_model=True):
self.ct_field = ct_field self.ct_field = ct_field
@ -28,12 +40,13 @@ class GenericForeignKey(object):
self.for_concrete_model = for_concrete_model self.for_concrete_model = for_concrete_model
self.editable = False self.editable = False
self.rel = None self.rel = None
self.column = None
def contribute_to_class(self, cls, name, **kwargs): def contribute_to_class(self, cls, name, **kwargs):
self.name = name self.name = name
self.model = cls self.model = cls
self.cache_attr = "_%s_cache" % name self.cache_attr = "_%s_cache" % name
cls._meta.add_virtual_field(self) cls._meta.add_field(self, virtual=True)
# Only run pre-initialization field assignment on non-abstract models # Only run pre-initialization field assignment on non-abstract models
if not cls._meta.abstract: if not cls._meta.abstract:
@ -243,6 +256,13 @@ class GenericForeignKey(object):
class GenericRelation(ForeignObject): class GenericRelation(ForeignObject):
"""Provides an accessor to generic related objects (e.g. comments)""" """Provides an accessor to generic related objects (e.g. comments)"""
# Field flags
auto_created = False
many_to_many = False
many_to_one = True
one_to_many = False
one_to_one = False
def __init__(self, to, **kwargs): def __init__(self, to, **kwargs):
kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['verbose_name'] = kwargs.get('verbose_name', None)
@ -303,8 +323,7 @@ class GenericRelation(ForeignObject):
def resolve_related_fields(self): def resolve_related_fields(self):
self.to_fields = [self.model._meta.pk.name] self.to_fields = [self.model._meta.pk.name]
return [(self.rel.to._meta.get_field_by_name(self.object_id_field_name)[0], return [(self.rel.to._meta.get_field(self.object_id_field_name), self.model._meta.pk)]
self.model._meta.pk)]
def get_path_info(self): def get_path_info(self):
opts = self.rel.to._meta opts = self.rel.to._meta
@ -345,7 +364,7 @@ class GenericRelation(ForeignObject):
for_concrete_model=self.for_concrete_model) for_concrete_model=self.for_concrete_model)
def get_extra_restriction(self, where_class, alias, remote_alias): def get_extra_restriction(self, where_class, alias, remote_alias):
field = self.rel.to._meta.get_field_by_name(self.content_type_field_name)[0] field = self.rel.to._meta.get_field(self.content_type_field_name)
contenttype_pk = self.get_content_type().pk contenttype_pk = self.get_content_type().pk
cond = where_class() cond = where_class()
lookup = field.get_lookup('exact')(Col(remote_alias, field, field), contenttype_pk) lookup = field.get_lookup('exact')(Col(remote_alias, field, field), contenttype_pk)

View File

@ -758,7 +758,7 @@ class GeoQuerySet(QuerySet):
elif geo_field not in opts.local_fields: elif geo_field not in opts.local_fields:
# This geographic field is inherited from another model, so we have to # This geographic field is inherited from another model, so we have to
# use the db table for the _parent_ model instead. # use the db table for the _parent_ model instead.
tmp_fld, parent_model, direct, m2m = opts.get_field_by_name(geo_field.name) parent_model = geo_field.model._meta.concrete_model
return self.query.get_compiler(self.db)._field_column(geo_field, parent_model._meta.db_table) return self.query.get_compiler(self.db)._field_column(geo_field, parent_model._meta.db_table)
else: else:
return self.query.get_compiler(self.db)._field_column(geo_field) return self.query.get_compiler(self.db)._field_column(geo_field)

View File

@ -118,7 +118,10 @@ class GeoSQLCompiler(compiler.SQLCompiler):
seen = self.query.included_inherited_models.copy() seen = self.query.included_inherited_models.copy()
if start_alias: if start_alias:
seen[None] = start_alias seen[None] = start_alias
for field, model in opts.get_concrete_fields_with_model(): for field in opts.concrete_fields:
model = field.model._meta.concrete_model
if model is opts.model:
model = None
if from_parent and model is not None and issubclass(from_parent, model): if from_parent and model is not None and issubclass(from_parent, model):
# Avoid loading data for already loaded parents. # Avoid loading data for already loaded parents.
continue continue

View File

@ -23,7 +23,7 @@ def kml(request, label, model, field_name=None, compress=False, using=DEFAULT_DB
if field_name: if field_name:
try: try:
field, _, _, _ = klass._meta.get_field_by_name(field_name) field = klass._meta.get_field(field_name)
if not isinstance(field, GeometryField): if not isinstance(field, GeometryField):
raise FieldDoesNotExist raise FieldDoesNotExist
except FieldDoesNotExist: except FieldDoesNotExist:

View File

@ -457,11 +457,10 @@ class LayerMapping(object):
def geometry_field(self): def geometry_field(self):
"Returns the GeometryField instance associated with the geographic column." "Returns the GeometryField instance associated with the geographic column."
# Use the `get_field_by_name` on the model's options so that we # Use `get_field()` on the model's options so that we
# get the correct field instance if there's model inheritance. # get the correct field instance if there's model inheritance.
opts = self.model._meta opts = self.model._meta
fld, model, direct, m2m = opts.get_field_by_name(self.geom_field) return opts.get_field(self.geom_field)
return fld
def make_multi(self, geom_type, model_field): def make_multi(self, geom_type, model_field):
""" """

View File

@ -61,7 +61,7 @@ def add_srs_entry(srs, auth_name='EPSG', auth_srid=None, ref_sys_name=None,
} }
# Backend-specific fields for the SpatialRefSys model. # Backend-specific fields for the SpatialRefSys model.
srs_field_names = SpatialRefSys._meta.get_all_field_names() srs_field_names = {f.name for f in SpatialRefSys._meta.get_fields()}
if 'srtext' in srs_field_names: if 'srtext' in srs_field_names:
kwargs['srtext'] = srs.wkt kwargs['srtext'] = srs.wkt
if 'ref_sys_name' in srs_field_names: if 'ref_sys_name' in srs_field_names:

View File

@ -8,7 +8,7 @@ from __future__ import unicode_literals
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core.serializers import base from django.core.serializers import base
from django.db import models, DEFAULT_DB_ALIAS from django.db import DEFAULT_DB_ALIAS, models
from django.utils.encoding import force_text, is_protected_type from django.utils.encoding import force_text, is_protected_type
from django.utils import six from django.utils import six
@ -101,12 +101,12 @@ def Deserializer(object_list, **options):
if 'pk' in d: if 'pk' in d:
data[Model._meta.pk.attname] = Model._meta.pk.to_python(d.get("pk", None)) data[Model._meta.pk.attname] = Model._meta.pk.to_python(d.get("pk", None))
m2m_data = {} m2m_data = {}
model_fields = Model._meta.get_all_field_names() field_names = {f.name for f in Model._meta.get_fields()}
# Handle each field # Handle each field
for (field_name, field_value) in six.iteritems(d["fields"]): for (field_name, field_value) in six.iteritems(d["fields"]):
if ignore and field_name not in model_fields: if ignore and field_name not in field_names:
# skip fields no longer on model # skip fields no longer on model
continue continue

View File

@ -186,7 +186,7 @@ class Deserializer(base.Deserializer):
# {m2m_accessor_attribute : [list_of_related_objects]}) # {m2m_accessor_attribute : [list_of_related_objects]})
m2m_data = {} m2m_data = {}
model_fields = Model._meta.get_all_field_names() field_names = {f.name for f in Model._meta.get_fields()}
# Deserialize each field. # Deserialize each field.
for field_node in node.getElementsByTagName("field"): for field_node in node.getElementsByTagName("field"):
# If the field is missing the name attribute, bail (are you # If the field is missing the name attribute, bail (are you
@ -198,7 +198,7 @@ class Deserializer(base.Deserializer):
# Get the field from the Model. This will raise a # Get the field from the Model. This will raise a
# FieldDoesNotExist if, well, the field doesn't exist, which will # FieldDoesNotExist if, well, the field doesn't exist, which will
# be propagated correctly unless ignorenonexistent=True is used. # be propagated correctly unless ignorenonexistent=True is used.
if self.ignore and field_name not in model_fields: if self.ignore and field_name not in field_names:
continue continue
field = Model._meta.get_field(field_name) field = Model._meta.get_field(field_name)

View File

@ -199,7 +199,7 @@ class BaseDatabaseCreation(object):
for f in model._meta.local_fields: for f in model._meta.local_fields:
output.extend(self.sql_indexes_for_field(model, f, style)) output.extend(self.sql_indexes_for_field(model, f, style))
for fs in model._meta.index_together: for fs in model._meta.index_together:
fields = [model._meta.get_field_by_name(f)[0] for f in fs] fields = [model._meta.get_field(f) for f in fs]
output.extend(self.sql_indexes_for_fields(model, fields, style)) output.extend(self.sql_indexes_for_fields(model, fields, style))
return output return output
@ -290,7 +290,7 @@ class BaseDatabaseCreation(object):
for f in model._meta.local_fields: for f in model._meta.local_fields:
output.extend(self.sql_destroy_indexes_for_field(model, f, style)) output.extend(self.sql_destroy_indexes_for_field(model, f, style))
for fs in model._meta.index_together: for fs in model._meta.index_together:
fields = [model._meta.get_field_by_name(f)[0] for f in fs] fields = [model._meta.get_field(f) for f in fs]
output.extend(self.sql_destroy_indexes_for_fields(model, fields, style)) output.extend(self.sql_destroy_indexes_for_fields(model, fields, style))
return output return output

View File

@ -10,6 +10,11 @@ from django.utils import six
logger = getLogger('django.db.backends.schema') logger = getLogger('django.db.backends.schema')
def _related_non_m2m_objects(opts):
# filters out m2m objects from reverse relations.
return (obj for obj in opts.related_objects if not obj.field.many_to_many)
class BaseDatabaseSchemaEditor(object): class BaseDatabaseSchemaEditor(object):
""" """
This class (and its subclasses) are responsible for emitting schema-changing This class (and its subclasses) are responsible for emitting schema-changing
@ -261,7 +266,7 @@ class BaseDatabaseSchemaEditor(object):
# Add any unique_togethers # Add any unique_togethers
for fields in model._meta.unique_together: for fields in model._meta.unique_together:
columns = [model._meta.get_field_by_name(field)[0].column for field in fields] columns = [model._meta.get_field(field).column for field in fields]
column_sqls.append(self.sql_create_table_unique % { column_sqls.append(self.sql_create_table_unique % {
"columns": ", ".join(self.quote_name(column) for column in columns), "columns": ", ".join(self.quote_name(column) for column in columns),
}) })
@ -309,7 +314,7 @@ class BaseDatabaseSchemaEditor(object):
news = set(tuple(fields) for fields in new_unique_together) news = set(tuple(fields) for fields in new_unique_together)
# Deleted uniques # Deleted uniques
for fields in olds.difference(news): for fields in olds.difference(news):
columns = [model._meta.get_field_by_name(field)[0].column for field in fields] columns = [model._meta.get_field(field).column for field in fields]
constraint_names = self._constraint_names(model, columns, unique=True) constraint_names = self._constraint_names(model, columns, unique=True)
if len(constraint_names) != 1: if len(constraint_names) != 1:
raise ValueError("Found wrong number (%s) of constraints for %s(%s)" % ( raise ValueError("Found wrong number (%s) of constraints for %s(%s)" % (
@ -320,7 +325,7 @@ class BaseDatabaseSchemaEditor(object):
self.execute(self._delete_constraint_sql(self.sql_delete_unique, model, constraint_names[0])) self.execute(self._delete_constraint_sql(self.sql_delete_unique, model, constraint_names[0]))
# Created uniques # Created uniques
for fields in news.difference(olds): for fields in news.difference(olds):
columns = [model._meta.get_field_by_name(field)[0].column for field in fields] columns = [model._meta.get_field(field).column for field in fields]
self.execute(self._create_unique_sql(model, columns)) self.execute(self._create_unique_sql(model, columns))
def alter_index_together(self, model, old_index_together, new_index_together): def alter_index_together(self, model, old_index_together, new_index_together):
@ -333,7 +338,7 @@ class BaseDatabaseSchemaEditor(object):
news = set(tuple(fields) for fields in new_index_together) news = set(tuple(fields) for fields in new_index_together)
# Deleted indexes # Deleted indexes
for fields in olds.difference(news): for fields in olds.difference(news):
columns = [model._meta.get_field_by_name(field)[0].column for field in fields] columns = [model._meta.get_field(field).column for field in fields]
constraint_names = self._constraint_names(model, list(columns), index=True) constraint_names = self._constraint_names(model, list(columns), index=True)
if len(constraint_names) != 1: if len(constraint_names) != 1:
raise ValueError("Found wrong number (%s) of constraints for %s(%s)" % ( raise ValueError("Found wrong number (%s) of constraints for %s(%s)" % (
@ -344,7 +349,7 @@ class BaseDatabaseSchemaEditor(object):
self.execute(self._delete_constraint_sql(self.sql_delete_index, model, constraint_names[0])) self.execute(self._delete_constraint_sql(self.sql_delete_index, model, constraint_names[0]))
# Created indexes # Created indexes
for field_names in news.difference(olds): for field_names in news.difference(olds):
fields = [model._meta.get_field_by_name(field)[0] for field in field_names] fields = [model._meta.get_field(field) for field in field_names]
self.execute(self._create_index_sql(model, fields, suffix="_idx")) self.execute(self._create_index_sql(model, fields, suffix="_idx"))
def alter_db_table(self, model, old_db_table, new_db_table): def alter_db_table(self, model, old_db_table, new_db_table):
@ -511,10 +516,12 @@ class BaseDatabaseSchemaEditor(object):
# Drop incoming FK constraints if we're a primary key and things are going # Drop incoming FK constraints if we're a primary key and things are going
# to change. # to change.
if old_field.primary_key and new_field.primary_key and old_type != new_type: if old_field.primary_key and new_field.primary_key and old_type != new_type:
for rel in new_field.model._meta.get_all_related_objects(): # '_meta.related_field' also contains M2M reverse fields, these
rel_fk_names = self._constraint_names(rel.model, [rel.field.column], foreign_key=True) # will be filtered out
for rel in _related_non_m2m_objects(new_field.model._meta):
rel_fk_names = self._constraint_names(rel.related_model, [rel.field.column], foreign_key=True)
for fk_name in rel_fk_names: for fk_name in rel_fk_names:
self.execute(self._delete_constraint_sql(self.sql_delete_fk, rel.model, fk_name)) self.execute(self._delete_constraint_sql(self.sql_delete_fk, rel.related_model, fk_name))
# Removed an index? (no strict check, as multiple indexes are possible) # Removed an index? (no strict check, as multiple indexes are possible)
if (old_field.db_index and not new_field.db_index and if (old_field.db_index and not new_field.db_index and
not old_field.unique and not not old_field.unique and not
@ -661,7 +668,7 @@ class BaseDatabaseSchemaEditor(object):
# referring to us. # referring to us.
rels_to_update = [] rels_to_update = []
if old_field.primary_key and new_field.primary_key and old_type != new_type: if old_field.primary_key and new_field.primary_key and old_type != new_type:
rels_to_update.extend(new_field.model._meta.get_all_related_objects()) rels_to_update.extend(_related_non_m2m_objects(new_field.model._meta))
# Changed to become primary key? # Changed to become primary key?
# Note that we don't detect unsetting of a PK, as we assume another field # Note that we don't detect unsetting of a PK, as we assume another field
# will always come along and replace it. # will always come along and replace it.
@ -684,14 +691,14 @@ class BaseDatabaseSchemaEditor(object):
} }
) )
# Update all referencing columns # Update all referencing columns
rels_to_update.extend(new_field.model._meta.get_all_related_objects()) rels_to_update.extend(_related_non_m2m_objects(new_field.model._meta))
# Handle our type alters on the other end of rels from the PK stuff above # Handle our type alters on the other end of rels from the PK stuff above
for rel in rels_to_update: for rel in rels_to_update:
rel_db_params = rel.field.db_parameters(connection=self.connection) rel_db_params = rel.field.db_parameters(connection=self.connection)
rel_type = rel_db_params['type'] rel_type = rel_db_params['type']
self.execute( self.execute(
self.sql_alter_column % { self.sql_alter_column % {
"table": self.quote_name(rel.model._meta.db_table), "table": self.quote_name(rel.related_model._meta.db_table),
"changes": self.sql_alter_column_type % { "changes": self.sql_alter_column_type % {
"column": self.quote_name(rel.field.column), "column": self.quote_name(rel.field.column),
"type": rel_type, "type": rel_type,
@ -705,8 +712,9 @@ class BaseDatabaseSchemaEditor(object):
self.execute(self._create_fk_sql(model, new_field, "_fk_%(to_table)s_%(to_column)s")) self.execute(self._create_fk_sql(model, new_field, "_fk_%(to_table)s_%(to_column)s"))
# Rebuild FKs that pointed to us if we previously had to drop them # Rebuild FKs that pointed to us if we previously had to drop them
if old_field.primary_key and new_field.primary_key and old_type != new_type: if old_field.primary_key and new_field.primary_key and old_type != new_type:
for rel in new_field.model._meta.get_all_related_objects(): for rel in new_field.model._meta.related_objects:
self.execute(self._create_fk_sql(rel.model, rel.field, "_fk")) if not rel.many_to_many:
self.execute(self._create_fk_sql(rel.related_model, rel.field, "_fk"))
# Does it have check constraints we need to add? # Does it have check constraints we need to add?
if old_db_params['check'] != new_db_params['check'] and new_db_params['check']: if old_db_params['check'] != new_db_params['check'] and new_db_params['check']:
self.execute( self.execute(
@ -765,14 +773,14 @@ class BaseDatabaseSchemaEditor(object):
new_field.rel.through, new_field.rel.through,
# We need the field that points to the target model, so we can tell alter_field to change it - # We need the field that points to the target model, so we can tell alter_field to change it -
# this is m2m_reverse_field_name() (as opposed to m2m_field_name, which points to our model) # this is m2m_reverse_field_name() (as opposed to m2m_field_name, which points to our model)
old_field.rel.through._meta.get_field_by_name(old_field.m2m_reverse_field_name())[0], old_field.rel.through._meta.get_field(old_field.m2m_reverse_field_name()),
new_field.rel.through._meta.get_field_by_name(new_field.m2m_reverse_field_name())[0], new_field.rel.through._meta.get_field(new_field.m2m_reverse_field_name()),
) )
self.alter_field( self.alter_field(
new_field.rel.through, new_field.rel.through,
# for self-referential models we need to alter field from the other end too # for self-referential models we need to alter field from the other end too
old_field.rel.through._meta.get_field_by_name(old_field.m2m_field_name())[0], old_field.rel.through._meta.get_field(old_field.m2m_field_name()),
new_field.rel.through._meta.get_field_by_name(new_field.m2m_field_name())[0], new_field.rel.through._meta.get_field(new_field.m2m_field_name()),
) )
def _create_index_name(self, model, column_names, suffix=""): def _create_index_name(self, model, column_names, suffix=""):
@ -844,7 +852,7 @@ class BaseDatabaseSchemaEditor(object):
output.append(self._create_index_sql(model, [field], suffix="")) output.append(self._create_index_sql(model, [field], suffix=""))
for field_names in model._meta.index_together: for field_names in model._meta.index_together:
fields = [model._meta.get_field_by_name(field)[0] for field in field_names] fields = [model._meta.get_field(field) for field in field_names]
output.append(self._create_index_sql(model, fields, suffix="_idx")) output.append(self._create_index_sql(model, fields, suffix="_idx"))
return output return output

View File

@ -227,8 +227,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
alter_fields=[( alter_fields=[(
# We need the field that points to the target model, so we can tell alter_field to change it - # We need the field that points to the target model, so we can tell alter_field to change it -
# this is m2m_reverse_field_name() (as opposed to m2m_field_name, which points to our model) # this is m2m_reverse_field_name() (as opposed to m2m_field_name, which points to our model)
old_field.rel.through._meta.get_field_by_name(old_field.m2m_reverse_field_name())[0], old_field.rel.through._meta.get_field(old_field.m2m_reverse_field_name()),
new_field.rel.through._meta.get_field_by_name(new_field.m2m_reverse_field_name())[0], new_field.rel.through._meta.get_field(new_field.m2m_reverse_field_name()),
)], )],
override_uniques=(new_field.m2m_field_name(), new_field.m2m_reverse_field_name()), override_uniques=(new_field.m2m_field_name(), new_field.m2m_reverse_field_name()),
) )

View File

@ -163,7 +163,7 @@ class MigrationAutodetector(object):
old_model_name = self.renamed_models.get((app_label, model_name), model_name) old_model_name = self.renamed_models.get((app_label, model_name), model_name)
old_model_state = self.from_state.models[app_label, old_model_name] old_model_state = self.from_state.models[app_label, old_model_name]
for field_name, field in old_model_state.fields: for field_name, field in old_model_state.fields:
old_field = self.old_apps.get_model(app_label, old_model_name)._meta.get_field_by_name(field_name)[0] old_field = self.old_apps.get_model(app_label, old_model_name)._meta.get_field(field_name)
if (hasattr(old_field, "rel") and getattr(old_field.rel, "through", None) if (hasattr(old_field, "rel") and getattr(old_field.rel, "through", None)
and not old_field.rel.through._meta.auto_created): and not old_field.rel.through._meta.auto_created):
through_key = ( through_key = (
@ -685,26 +685,14 @@ class MigrationAutodetector(object):
# and the removal of all its own related fields, and if it's # and the removal of all its own related fields, and if it's
# a through model the field that references it. # a through model the field that references it.
dependencies = [] dependencies = []
for related_object in model._meta.get_all_related_objects(): for related_object in model._meta.related_objects:
dependencies.append(( related_object_app_label = related_object.related_model._meta.app_label
related_object.model._meta.app_label, object_name = related_object.related_model._meta.object_name
related_object.model._meta.object_name, field_name = related_object.field.name
related_object.field.name, dependencies.append((related_object_app_label, object_name, field_name, False))
False, if not related_object.many_to_many:
)) dependencies.append((related_object_app_label, object_name, field_name, "alter"))
dependencies.append((
related_object.model._meta.app_label,
related_object.model._meta.object_name,
related_object.field.name,
"alter",
))
for related_object in model._meta.get_all_related_many_to_many_objects():
dependencies.append((
related_object.model._meta.app_label,
related_object.model._meta.object_name,
related_object.field.name,
False,
))
for name, field in sorted(related_fields.items()): for name, field in sorted(related_fields.items()):
dependencies.append((app_label, model_name, name, False)) dependencies.append((app_label, model_name, name, False))
# We're referenced in another field's through= # We're referenced in another field's through=
@ -743,7 +731,7 @@ class MigrationAutodetector(object):
for app_label, model_name, field_name in sorted(self.new_field_keys - self.old_field_keys): for app_label, model_name, field_name in sorted(self.new_field_keys - self.old_field_keys):
old_model_name = self.renamed_models.get((app_label, model_name), model_name) old_model_name = self.renamed_models.get((app_label, model_name), model_name)
old_model_state = self.from_state.models[app_label, old_model_name] old_model_state = self.from_state.models[app_label, old_model_name]
field = self.new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0] field = self.new_apps.get_model(app_label, model_name)._meta.get_field(field_name)
# Scan to see if this is actually a rename! # Scan to see if this is actually a rename!
field_dec = self.deep_deconstruct(field) field_dec = self.deep_deconstruct(field)
for rem_app_label, rem_model_name, rem_field_name in sorted(self.old_field_keys - self.new_field_keys): for rem_app_label, rem_model_name, rem_field_name in sorted(self.old_field_keys - self.new_field_keys):
@ -776,7 +764,7 @@ class MigrationAutodetector(object):
self._generate_added_field(app_label, model_name, field_name) self._generate_added_field(app_label, model_name, field_name)
def _generate_added_field(self, app_label, model_name, field_name): def _generate_added_field(self, app_label, model_name, field_name):
field = self.new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0] field = self.new_apps.get_model(app_label, model_name)._meta.get_field(field_name)
# Fields that are foreignkeys/m2ms depend on stuff # Fields that are foreignkeys/m2ms depend on stuff
dependencies = [] dependencies = []
if field.rel and field.rel.to: if field.rel and field.rel.to:
@ -847,8 +835,8 @@ class MigrationAutodetector(object):
# Did the field change? # Did the field change?
old_model_name = self.renamed_models.get((app_label, model_name), model_name) old_model_name = self.renamed_models.get((app_label, model_name), model_name)
old_field_name = self.renamed_fields.get((app_label, model_name, field_name), field_name) old_field_name = self.renamed_fields.get((app_label, model_name, field_name), field_name)
old_field = self.old_apps.get_model(app_label, old_model_name)._meta.get_field_by_name(old_field_name)[0] old_field = self.old_apps.get_model(app_label, old_model_name)._meta.get_field(old_field_name)
new_field = self.new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0] new_field = self.new_apps.get_model(app_label, model_name)._meta.get_field(field_name)
# Implement any model renames on relations; these are handled by RenameModel # Implement any model renames on relations; these are handled by RenameModel
# so we need to exclude them from the comparison # so we need to exclude them from the comparison
if hasattr(new_field, "rel") and getattr(new_field.rel, "to", None): if hasattr(new_field, "rel") and getattr(new_field.rel, "to", None):

View File

@ -44,7 +44,7 @@ class AddField(Operation):
to_model = to_state.apps.get_model(app_label, self.model_name) to_model = to_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, to_model): if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
field = to_model._meta.get_field_by_name(self.name)[0] field = to_model._meta.get_field(self.name)
if not self.preserve_default: if not self.preserve_default:
field.default = self.field.default field.default = self.field.default
schema_editor.add_field( schema_editor.add_field(
@ -57,7 +57,7 @@ class AddField(Operation):
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, from_model): if self.allowed_to_migrate(schema_editor.connection.alias, from_model):
schema_editor.remove_field(from_model, from_model._meta.get_field_by_name(self.name)[0]) schema_editor.remove_field(from_model, from_model._meta.get_field(self.name))
def describe(self): def describe(self):
return "Add field %s to %s" % (self.name, self.model_name) return "Add field %s to %s" % (self.name, self.model_name)
@ -100,13 +100,13 @@ class RemoveField(Operation):
def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, from_model): if self.allowed_to_migrate(schema_editor.connection.alias, from_model):
schema_editor.remove_field(from_model, from_model._meta.get_field_by_name(self.name)[0]) schema_editor.remove_field(from_model, from_model._meta.get_field(self.name))
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
to_model = to_state.apps.get_model(app_label, self.model_name) to_model = to_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, to_model): if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
schema_editor.add_field(from_model, to_model._meta.get_field_by_name(self.name)[0]) schema_editor.add_field(from_model, to_model._meta.get_field(self.name))
def describe(self): def describe(self):
return "Remove field %s from %s" % (self.name, self.model_name) return "Remove field %s from %s" % (self.name, self.model_name)
@ -158,8 +158,8 @@ class AlterField(Operation):
to_model = to_state.apps.get_model(app_label, self.model_name) to_model = to_state.apps.get_model(app_label, self.model_name)
if self.allowed_to_migrate(schema_editor.connection.alias, to_model): if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
from_field = from_model._meta.get_field_by_name(self.name)[0] from_field = from_model._meta.get_field(self.name)
to_field = to_model._meta.get_field_by_name(self.name)[0] to_field = to_model._meta.get_field(self.name)
# If the field is a relatedfield with an unresolved rel.to, just # If the field is a relatedfield with an unresolved rel.to, just
# set it equal to the other field side. Bandaid fix for AlterField # set it equal to the other field side. Bandaid fix for AlterField
# migrations that are part of a RenameModel change. # migrations that are part of a RenameModel change.
@ -231,8 +231,8 @@ class RenameField(Operation):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
schema_editor.alter_field( schema_editor.alter_field(
from_model, from_model,
from_model._meta.get_field_by_name(self.old_name)[0], from_model._meta.get_field(self.old_name),
to_model._meta.get_field_by_name(self.new_name)[0], to_model._meta.get_field(self.new_name),
) )
def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
@ -241,8 +241,8 @@ class RenameField(Operation):
from_model = from_state.apps.get_model(app_label, self.model_name) from_model = from_state.apps.get_model(app_label, self.model_name)
schema_editor.alter_field( schema_editor.alter_field(
from_model, from_model,
from_model._meta.get_field_by_name(self.new_name)[0], from_model._meta.get_field(self.new_name),
to_model._meta.get_field_by_name(self.old_name)[0], to_model._meta.get_field(self.old_name),
) )
def describe(self): def describe(self):

View File

@ -138,25 +138,27 @@ class RenameModel(Operation):
) )
def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
# Get all of the related objects we need to repoint
apps = state.apps apps = state.apps
model = apps.get_model(app_label, self.old_name) model = apps.get_model(app_label, self.old_name)
model._meta.apps = apps model._meta.apps = apps
related_objects = model._meta.get_all_related_objects() # Get all of the related objects we need to repoint
related_m2m_objects = model._meta.get_all_related_many_to_many_objects() all_related_objects = (
f for f in model._meta.get_fields(include_hidden=True)
if f.auto_created and not f.concrete and not (f.hidden or f.many_to_many)
)
# Rename the model # Rename the model
state.models[app_label, self.new_name.lower()] = state.models[app_label, self.old_name.lower()] state.models[app_label, self.new_name.lower()] = state.models[app_label, self.old_name.lower()]
state.models[app_label, self.new_name.lower()].name = self.new_name state.models[app_label, self.new_name.lower()].name = self.new_name
state.remove_model(app_label, self.old_name) state.remove_model(app_label, self.old_name)
# Repoint the FKs and M2Ms pointing to us # Repoint the FKs and M2Ms pointing to us
for related_object in (related_objects + related_m2m_objects): for related_object in all_related_objects:
# Use the new related key for self referential related objects. # Use the new related key for self referential related objects.
if related_object.model == model: if related_object.related_model == model:
related_key = (app_label, self.new_name.lower()) related_key = (app_label, self.new_name.lower())
else: else:
related_key = ( related_key = (
related_object.model._meta.app_label, related_object.related_model._meta.app_label,
related_object.model._meta.object_name.lower(), related_object.related_model._meta.object_name.lower(),
) )
new_fields = [] new_fields = []
for name, field in state.models[related_key].fields: for name, field in state.models[related_key].fields:
@ -179,21 +181,19 @@ class RenameModel(Operation):
new_model._meta.db_table, new_model._meta.db_table,
) )
# Alter the fields pointing to us # Alter the fields pointing to us
related_objects = old_model._meta.get_all_related_objects() for related_object in old_model._meta.related_objects:
related_m2m_objects = old_model._meta.get_all_related_many_to_many_objects() if related_object.related_model == old_model:
for related_object in (related_objects + related_m2m_objects):
if related_object.model == old_model:
model = new_model model = new_model
related_key = (app_label, self.new_name.lower()) related_key = (app_label, self.new_name.lower())
else: else:
model = related_object.model model = related_object.related_model
related_key = ( related_key = (
related_object.model._meta.app_label, related_object.related_model._meta.app_label,
related_object.model._meta.object_name.lower(), related_object.related_model._meta.object_name.lower(),
) )
to_field = to_state.apps.get_model( to_field = to_state.apps.get_model(
*related_key *related_key
)._meta.get_field_by_name(related_object.field.name)[0] )._meta.get_field(related_object.field.name)
schema_editor.alter_field( schema_editor.alter_field(
model, model,
related_object.field, related_object.field,
@ -394,11 +394,11 @@ class AlterOrderWithRespectTo(Operation):
from_model = from_state.apps.get_model(app_label, self.name) from_model = from_state.apps.get_model(app_label, self.name)
# Remove a field if we need to # Remove a field if we need to
if from_model._meta.order_with_respect_to and not to_model._meta.order_with_respect_to: if from_model._meta.order_with_respect_to and not to_model._meta.order_with_respect_to:
schema_editor.remove_field(from_model, from_model._meta.get_field_by_name("_order")[0]) schema_editor.remove_field(from_model, from_model._meta.get_field("_order"))
# Add a field if we need to (altering the column is untouched as # Add a field if we need to (altering the column is untouched as
# it's likely a rename) # it's likely a rename)
elif to_model._meta.order_with_respect_to and not from_model._meta.order_with_respect_to: elif to_model._meta.order_with_respect_to and not from_model._meta.order_with_respect_to:
field = to_model._meta.get_field_by_name("_order")[0] field = to_model._meta.get_field("_order")
if not field.has_default(): if not field.has_default():
field.default = 0 field.default = 0
schema_editor.add_field( schema_editor.add_field(

View File

@ -50,15 +50,15 @@ class ProjectState(object):
model_name = model_name.lower() model_name = model_name.lower()
try: try:
related_old = { related_old = {
f.model for f in f.related_model for f in
self.apps.get_model(app_label, model_name)._meta.get_all_related_objects() self.apps.get_model(app_label, model_name)._meta.related_objects
} }
except LookupError: except LookupError:
related_old = set() related_old = set()
self._reload_one_model(app_label, model_name) self._reload_one_model(app_label, model_name)
# Reload models if there are relations # Reload models if there are relations
model = self.apps.get_model(app_label, model_name) model = self.apps.get_model(app_label, model_name)
related_m2m = {f.rel.to for f, _ in model._meta.get_m2m_with_model()} related_m2m = {f.related_model for f in model._meta.many_to_many}
for rel_model in related_old.union(related_m2m): for rel_model in related_old.union(related_m2m):
self._reload_one_model(rel_model._meta.app_label, rel_model._meta.model_name) self._reload_one_model(rel_model._meta.app_label, rel_model._meta.model_name)
if related_m2m: if related_m2m:

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
import copy import copy
import inspect import inspect
from itertools import chain
import sys import sys
import warnings import warnings
@ -175,12 +176,12 @@ class ModelBase(type):
new_class.add_to_class(obj_name, obj) new_class.add_to_class(obj_name, obj)
# All the fields of any type declared on this model # All the fields of any type declared on this model
new_fields = ( new_fields = chain(
new_class._meta.local_fields + new_class._meta.local_fields,
new_class._meta.local_many_to_many + new_class._meta.local_many_to_many,
new_class._meta.virtual_fields new_class._meta.virtual_fields
) )
field_names = set(f.name for f in new_fields) field_names = {f.name for f in new_fields}
# Basic setup for proxy models. # Basic setup for proxy models.
if is_proxy: if is_proxy:
@ -202,6 +203,7 @@ class ModelBase(type):
raise TypeError("Proxy model '%s' has no non-abstract model base class." % name) raise TypeError("Proxy model '%s' has no non-abstract model base class." % name)
new_class._meta.setup_proxy(base) new_class._meta.setup_proxy(base)
new_class._meta.concrete_model = base._meta.concrete_model new_class._meta.concrete_model = base._meta.concrete_model
base._meta.concrete_model._meta.proxied_children.append(new_class._meta)
else: else:
new_class._meta.concrete_model = new_class new_class._meta.concrete_model = new_class
@ -342,7 +344,7 @@ class ModelBase(type):
# Give the class a docstring -- its definition. # Give the class a docstring -- its definition.
if cls.__doc__ is None: if cls.__doc__ is None:
cls.__doc__ = "%s(%s)" % (cls.__name__, ", ".join(f.attname for f in opts.fields)) cls.__doc__ = "%s(%s)" % (cls.__name__, ", ".join(f.name for f in opts.fields))
get_absolute_url_override = settings.ABSOLUTE_URL_OVERRIDES.get( get_absolute_url_override = settings.ABSOLUTE_URL_OVERRIDES.get(
'%s.%s' % (opts.app_label, opts.model_name) '%s.%s' % (opts.app_label, opts.model_name)
@ -630,7 +632,7 @@ class Model(six.with_metaclass(ModelBase)):
and not use this method. and not use this method.
""" """
try: try:
field = self._meta.get_field_by_name(field_name)[0] field = self._meta.get_field(field_name)
except FieldDoesNotExist: except FieldDoesNotExist:
return getattr(self, field_name) return getattr(self, field_name)
return getattr(self, field.attname) return getattr(self, field.attname)
@ -1438,12 +1440,17 @@ class Model(six.with_metaclass(ModelBase)):
def _check_local_fields(cls, fields, option): def _check_local_fields(cls, fields, option):
from django.db import models from django.db import models
# In order to avoid hitting the relation tree prematurely, we use our
# own fields_map instead of using get_field()
forward_fields_map = {
field.name: field for field in cls._meta._get_fields(reverse=False)
}
errors = [] errors = []
for field_name in fields: for field_name in fields:
try: try:
field = cls._meta.get_field(field_name, field = forward_fields_map[field_name]
many_to_many=True) except KeyError:
except FieldDoesNotExist:
errors.append( errors.append(
checks.Error( checks.Error(
"'%s' refers to the non-existent field '%s'." % (option, field_name), "'%s' refers to the non-existent field '%s'." % (option, field_name),
@ -1484,7 +1491,6 @@ class Model(six.with_metaclass(ModelBase)):
def _check_ordering(cls): def _check_ordering(cls):
""" Check "ordering" option -- is it a list of strings and do all fields """ Check "ordering" option -- is it a list of strings and do all fields
exist? """ exist? """
if not cls._meta.ordering: if not cls._meta.ordering:
return [] return []
@ -1500,7 +1506,6 @@ class Model(six.with_metaclass(ModelBase)):
] ]
errors = [] errors = []
fields = cls._meta.ordering fields = cls._meta.ordering
# Skip '?' fields. # Skip '?' fields.
@ -1518,28 +1523,30 @@ class Model(six.with_metaclass(ModelBase)):
# Skip ordering on pk. This is always a valid order_by field # Skip ordering on pk. This is always a valid order_by field
# but is an alias and therefore won't be found by opts.get_field. # but is an alias and therefore won't be found by opts.get_field.
fields = (f for f in fields if f != 'pk') fields = {f for f in fields if f != 'pk'}
for field_name in fields: # Check for invalid or non-existent fields in ordering.
try: invalid_fields = []
cls._meta.get_field(field_name, many_to_many=False)
except FieldDoesNotExist: # Any field name that is not present in field_names does not exist.
if field_name.endswith('_id'): # Also, ordering by m2m fields is not allowed.
try: opts = cls._meta
field = cls._meta.get_field(field_name[:-3], many_to_many=False) valid_fields = set(chain.from_iterable(
except FieldDoesNotExist: (f.name, f.attname) if not (f.auto_created and not f.concrete) else (f.field.related_query_name(),)
pass for f in chain(opts.fields, opts.related_objects)
else: ))
if field.attname == field_name:
continue invalid_fields.extend(fields - valid_fields)
errors.append(
checks.Error( for invalid_field in invalid_fields:
"'ordering' refers to the non-existent field '%s'." % field_name, errors.append(
hint=None, checks.Error(
obj=cls, "'ordering' refers to the non-existent field '%s'." % invalid_field,
id='models.E015', hint=None,
) obj=cls,
id='models.E015',
) )
)
return errors return errors
@classmethod @classmethod

View File

@ -1,4 +1,5 @@
from collections import OrderedDict from collections import OrderedDict
from itertools import chain
from operator import attrgetter from operator import attrgetter
from django.db import connections, transaction, IntegrityError from django.db import connections, transaction, IntegrityError
@ -51,6 +52,23 @@ def DO_NOTHING(collector, field, sub_objs, using):
pass pass
def get_candidate_relations_to_delete(opts):
# Collect models that contain candidate relations to delete. This may include
# relations coming from proxy models.
candidate_models = {opts}
candidate_models = candidate_models.union(opts.concrete_model._meta.proxied_children)
# For each model, get all candidate fields.
candidate_model_fields = chain.from_iterable(
opts.get_fields(include_hidden=True) for opts in candidate_models
)
# The candidate relations are the ones that come from N-1 and 1-1 relations.
# N-N (i.e., many-to-many) relations aren't candidates for deletion.
return (
f for f in candidate_model_fields
if f.auto_created and not f.concrete and (f.one_to_one or f.many_to_one)
)
class Collector(object): class Collector(object):
def __init__(self, using): def __init__(self, using):
self.using = using self.using = using
@ -134,8 +152,7 @@ class Collector(object):
return False return False
# Foreign keys pointing to this model, both from m2m and other # Foreign keys pointing to this model, both from m2m and other
# models. # models.
for related in opts.get_all_related_objects( for related in get_candidate_relations_to_delete(opts):
include_hidden=True, include_proxy_eq=True):
if related.field.rel.on_delete is not DO_NOTHING: if related.field.rel.on_delete is not DO_NOTHING:
return False return False
for field in model._meta.virtual_fields: for field in model._meta.virtual_fields:
@ -184,7 +201,7 @@ class Collector(object):
model = new_objs[0].__class__ model = new_objs[0].__class__
# Recursively collect concrete model's parent models, but not their # Recursively collect concrete model's parent models, but not their
# related objects. These will be found by meta.get_all_related_objects() # related objects. These will be found by meta.get_fields()
concrete_model = model._meta.concrete_model concrete_model = model._meta.concrete_model
for ptr in six.itervalues(concrete_model._meta.parents): for ptr in six.itervalues(concrete_model._meta.parents):
if ptr: if ptr:
@ -199,8 +216,7 @@ class Collector(object):
reverse_dependency=True) reverse_dependency=True)
if collect_related: if collect_related:
for related in model._meta.get_all_related_objects( for related in get_candidate_relations_to_delete(model._meta):
include_hidden=True, include_proxy_eq=True):
field = related.field field = related.field
if field.rel.on_delete == DO_NOTHING: if field.rel.on_delete == DO_NOTHING:
continue continue
@ -225,7 +241,7 @@ class Collector(object):
Gets a QuerySet of objects related to ``objs`` via the relation ``related``. Gets a QuerySet of objects related to ``objs`` via the relation ``related``.
""" """
return related.model._base_manager.using(self.using).filter( return related.related_model._base_manager.using(self.using).filter(
**{"%s__in" % related.field.name: objs} **{"%s__in" % related.field.name: objs}
) )

View File

@ -31,7 +31,9 @@ from django.utils.ipv6 import clean_ipv6_address
from django.utils import six from django.utils import six
from django.utils.itercompat import is_iterable from django.utils.itercompat import is_iterable
# imported for backwards compatibility # When the _meta object was formalized, this exception was moved to
# django.core.exceptions. It is retained here for backwards compatibility
# purposes.
from django.core.exceptions import FieldDoesNotExist # NOQA from django.core.exceptions import FieldDoesNotExist # NOQA
# Avoid "TypeError: Item in ``from list'' not a string" -- unicode_literals # Avoid "TypeError: Item in ``from list'' not a string" -- unicode_literals
@ -61,7 +63,7 @@ BLANK_CHOICE_DASH = [("", "---------")]
def _load_field(app_label, model_name, field_name): def _load_field(app_label, model_name, field_name):
return apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0] return apps.get_model(app_label, model_name)._meta.get_field(field_name)
# A guide to Field parameters: # A guide to Field parameters:
@ -116,6 +118,15 @@ class Field(RegisterLookupMixin):
system_check_deprecated_details = None system_check_deprecated_details = None
system_check_removed_details = None system_check_removed_details = None
# Field flags
hidden = False
many_to_many = None
many_to_one = None
one_to_many = None
one_to_one = None
related_model = None
# Generic field type description, usually overridden by subclasses # Generic field type description, usually overridden by subclasses
def _description(self): def _description(self):
return _('Field of type: %(field_type)s') % { return _('Field of type: %(field_type)s') % {
@ -137,6 +148,7 @@ class Field(RegisterLookupMixin):
self.max_length, self._unique = max_length, unique self.max_length, self._unique = max_length, unique
self.blank, self.null = blank, null self.blank, self.null = blank, null
self.rel = rel self.rel = rel
self.is_relation = self.rel is not None
self.default = default self.default = default
self.editable = editable self.editable = editable
self.serialize = serialize self.serialize = serialize
@ -603,6 +615,7 @@ class Field(RegisterLookupMixin):
if not self.name: if not self.name:
self.name = name self.name = name
self.attname, self.column = self.get_attname_column() self.attname, self.column = self.get_attname_column()
self.concrete = self.column is not None
if self.verbose_name is None and self.name: if self.verbose_name is None and self.name:
self.verbose_name = self.name.replace('_', ' ') self.verbose_name = self.name.replace('_', ' ')
@ -610,7 +623,7 @@ class Field(RegisterLookupMixin):
self.set_attributes_from_name(name) self.set_attributes_from_name(name)
self.model = cls self.model = cls
if virtual_only: if virtual_only:
cls._meta.add_virtual_field(self) cls._meta.add_field(self, virtual=True)
else: else:
cls._meta.add_field(self) cls._meta.add_field(self)
if self.choices: if self.choices:

View File

@ -98,6 +98,18 @@ signals.class_prepared.connect(do_pending_lookups)
class RelatedField(Field): class RelatedField(Field):
# Field flags
one_to_many = False
one_to_one = False
many_to_many = False
many_to_one = False
@cached_property
def related_model(self):
# Can't cache this property until all the models are loaded.
apps.check_models_ready()
return self.rel.to
def check(self, **kwargs): def check(self, **kwargs):
errors = super(RelatedField, self).check(**kwargs) errors = super(RelatedField, self).check(**kwargs)
errors.extend(self._check_related_name_is_valid()) errors.extend(self._check_related_name_is_valid())
@ -235,13 +247,10 @@ class RelatedField(Field):
# Check clashes between accessors/reverse query names of `field` and # Check clashes between accessors/reverse query names of `field` and
# any other field accessor -- i. e. Model.foreign accessor clashes with # any other field accessor -- i. e. Model.foreign accessor clashes with
# Model.m2m accessor. # Model.m2m accessor.
potential_clashes = rel_opts.get_all_related_many_to_many_objects() potential_clashes = (r for r in rel_opts.related_objects if r.field is not self)
potential_clashes += rel_opts.get_all_related_objects()
potential_clashes = (r for r in potential_clashes
if r.field is not self)
for clash_field in potential_clashes: for clash_field in potential_clashes:
clash_name = "%s.%s" % ( # i. e. "Model.m2m" clash_name = "%s.%s" % ( # i. e. "Model.m2m"
clash_field.model._meta.object_name, clash_field.related_model._meta.object_name,
clash_field.field.name) clash_field.field.name)
if clash_field.get_accessor_name() == rel_name: if clash_field.get_accessor_name() == rel_name:
errors.append( errors.append(
@ -392,7 +401,7 @@ class SingleRelatedObjectDescriptor(object):
# consistency with `ReverseSingleRelatedObjectDescriptor`. # consistency with `ReverseSingleRelatedObjectDescriptor`.
return type( return type(
str('RelatedObjectDoesNotExist'), str('RelatedObjectDoesNotExist'),
(self.related.model.DoesNotExist, AttributeError), (self.related.related_model.DoesNotExist, AttributeError),
{} {}
) )
@ -400,11 +409,11 @@ class SingleRelatedObjectDescriptor(object):
return hasattr(instance, self.cache_name) return hasattr(instance, self.cache_name)
def get_queryset(self, **hints): def get_queryset(self, **hints):
manager = self.related.model._default_manager manager = self.related.related_model._default_manager
# If the related manager indicates that it should be used for # If the related manager indicates that it should be used for
# related fields, respect that. # related fields, respect that.
if not getattr(manager, 'use_for_related_fields', False): if not getattr(manager, 'use_for_related_fields', False):
manager = self.related.model._base_manager manager = self.related.related_model._base_manager
return manager.db_manager(hints=hints).all() return manager.db_manager(hints=hints).all()
def get_prefetch_queryset(self, instances, queryset=None): def get_prefetch_queryset(self, instances, queryset=None):
@ -441,7 +450,7 @@ class SingleRelatedObjectDescriptor(object):
params['%s__%s' % (self.related.field.name, rh_field.name)] = getattr(instance, rh_field.attname) params['%s__%s' % (self.related.field.name, rh_field.name)] = getattr(instance, rh_field.attname)
try: try:
rel_obj = self.get_queryset(instance=instance).get(**params) rel_obj = self.get_queryset(instance=instance).get(**params)
except self.related.model.DoesNotExist: except self.related.related_model.DoesNotExist:
rel_obj = None rel_obj = None
else: else:
setattr(rel_obj, self.related.field.get_cache_name(), instance) setattr(rel_obj, self.related.field.get_cache_name(), instance)
@ -470,7 +479,7 @@ class SingleRelatedObjectDescriptor(object):
self.related.get_accessor_name(), self.related.get_accessor_name(),
) )
) )
elif value is not None and not isinstance(value, self.related.model): elif value is not None and not isinstance(value, self.related.related_model):
raise ValueError( raise ValueError(
'Cannot assign "%r": "%s.%s" must be a "%s" instance.' % ( 'Cannot assign "%r": "%s.%s" must be a "%s" instance.' % (
value, value,
@ -825,9 +834,9 @@ class ForeignRelatedObjectsDescriptor(object):
# Dynamically create a class that subclasses the related model's default # Dynamically create a class that subclasses the related model's default
# manager. # manager.
return create_foreign_related_manager( return create_foreign_related_manager(
self.related.model._default_manager.__class__, self.related.related_model._default_manager.__class__,
self.related.field, self.related.field,
self.related.model, self.related.related_model,
) )
@ -1148,7 +1157,7 @@ class ManyRelatedObjectsDescriptor(object):
# Dynamically create a class that subclasses the related # Dynamically create a class that subclasses the related
# model's default manager. # model's default manager.
return create_many_related_manager( return create_many_related_manager(
self.related.model._default_manager.__class__, self.related.related_model._default_manager.__class__,
self.related.field.rel self.related.field.rel
) )
@ -1156,7 +1165,7 @@ class ManyRelatedObjectsDescriptor(object):
if instance is None: if instance is None:
return self return self
rel_model = self.related.model rel_model = self.related.related_model
manager = self.related_manager_cls( manager = self.related_manager_cls(
model=rel_model, model=rel_model,
@ -1255,6 +1264,12 @@ class ReverseManyRelatedObjectsDescriptor(object):
class ForeignObjectRel(object): class ForeignObjectRel(object):
# Field flags
auto_created = True
concrete = False
editable = False
is_relation = True
def __init__(self, field, to, related_name=None, limit_choices_to=None, def __init__(self, field, to, related_name=None, limit_choices_to=None,
parent_link=False, on_delete=None, related_query_name=None): parent_link=False, on_delete=None, related_query_name=None):
self.field = field self.field = field
@ -1267,32 +1282,55 @@ class ForeignObjectRel(object):
self.on_delete = on_delete self.on_delete = on_delete
self.symmetrical = False self.symmetrical = False
# This and the following cached_properties can't be initialized in # Some of the following cached_properties can't be initialized in
# __init__ as the field doesn't have its model yet. Calling these methods # __init__ as the field doesn't have its model yet. Calling these methods
# before field.contribute_to_class() has been called will result in # before field.contribute_to_class() has been called will result in
# AttributeError # AttributeError
@cached_property @cached_property
def model(self): def model(self):
if not self.field.model: return self.to
raise AttributeError(
"This property can't be accessed before self.field.contribute_to_class has been called.")
return self.field.model
@cached_property @cached_property
def opts(self): def opts(self):
return self.model._meta return self.related_model._meta
@cached_property @cached_property
def to_opts(self): def to_opts(self):
return self.to._meta return self.to._meta
@cached_property @cached_property
def parent_model(self): def hidden(self):
return self.to return self.is_hidden()
@cached_property @cached_property
def name(self): def name(self):
return '%s.%s' % (self.opts.app_label, self.opts.model_name) return self.field.related_query_name()
@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 __repr__(self):
return '<%s: %s.%s>' % (type(self).__name__, self.opts.app_label, self.opts.model_name)
def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH, def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH,
limit_to_currently_related=False): limit_to_currently_related=False):
@ -1304,10 +1342,10 @@ class ForeignObjectRel(object):
initially for utilization by RelatedFieldListFilter. initially for utilization by RelatedFieldListFilter.
""" """
first_choice = blank_choice if include_blank else [] first_choice = blank_choice if include_blank else []
queryset = self.model._default_manager.all() queryset = self.related_model._default_manager.all()
if limit_to_currently_related: if limit_to_currently_related:
queryset = queryset.complex_filter( queryset = queryset.complex_filter(
{'%s__isnull' % self.parent_model._meta.model_name: False} {'%s__isnull' % self.related_model._meta.model_name: False}
) )
lst = [(x._get_pk_val(), smart_text(x)) for x in queryset] lst = [(x._get_pk_val(), smart_text(x)) for x in queryset]
return first_choice + lst return first_choice + lst
@ -1318,7 +1356,7 @@ class ForeignObjectRel(object):
def is_hidden(self): def is_hidden(self):
"Should the related object be hidden?" "Should the related object be hidden?"
return self.related_name and self.related_name[-1] == '+' return self.related_name is not None and self.related_name[-1] == '+'
def get_joining_columns(self): def get_joining_columns(self):
return self.field.get_reverse_joining_columns() return self.field.get_reverse_joining_columns()
@ -1349,7 +1387,7 @@ class ForeignObjectRel(object):
# Due to backwards compatibility ModelForms need to be able to provide # Due to backwards compatibility ModelForms need to be able to provide
# an alternate model. See BaseInlineFormSet.get_default_prefix(). # an alternate model. See BaseInlineFormSet.get_default_prefix().
opts = model._meta if model else self.opts opts = model._meta if model else self.opts
model = model or self.model model = model or self.related_model
if self.multiple: if self.multiple:
# If this is a symmetrical m2m relation on self, there is no reverse accessor. # If this is a symmetrical m2m relation on self, there is no reverse accessor.
if self.symmetrical and model == self.to: if self.symmetrical and model == self.to:
@ -1383,11 +1421,11 @@ class ManyToOneRel(ForeignObjectRel):
Returns the Field in the 'to' object to which this relationship is Returns the Field in the 'to' object to which this relationship is
tied. tied.
""" """
data = self.to._meta.get_field_by_name(self.field_name) field = self.to._meta.get_field(self.field_name)
if not data[2]: if not field.concrete:
raise FieldDoesNotExist("No related field named '%s'" % raise FieldDoesNotExist("No related field named '%s'" %
self.field_name) self.field_name)
return data[0] return field
def set_field_name(self): def set_field_name(self):
self.field_name = self.field_name or self.to._meta.pk.name self.field_name = self.field_name or self.to._meta.pk.name
@ -1419,6 +1457,10 @@ class ManyToManyRel(ForeignObjectRel):
self.through_fields = through_fields self.through_fields = through_fields
self.db_constraint = db_constraint self.db_constraint = db_constraint
def is_hidden(self):
"Should the related object be hidden?"
return self.related_name is not None and self.related_name[-1] == '+'
def get_related_field(self): def get_related_field(self):
""" """
Returns the field in the 'to' object to which this relationship is tied. Returns the field in the 'to' object to which this relationship is tied.
@ -1436,8 +1478,13 @@ class ManyToManyRel(ForeignObjectRel):
class ForeignObject(RelatedField): class ForeignObject(RelatedField):
# Field flags
many_to_many = False
many_to_one = False
one_to_many = True
one_to_one = False
requires_unique_target = True requires_unique_target = True
generate_reverse_relation = True
related_accessor_class = ForeignRelatedObjectsDescriptor related_accessor_class = ForeignRelatedObjectsDescriptor
def __init__(self, to, from_fields, to_fields, swappable=True, **kwargs): def __init__(self, to, from_fields, to_fields, swappable=True, **kwargs):
@ -1556,9 +1603,9 @@ class ForeignObject(RelatedField):
from_field_name = self.from_fields[index] from_field_name = self.from_fields[index]
to_field_name = self.to_fields[index] to_field_name = self.to_fields[index]
from_field = (self if from_field_name == 'self' from_field = (self if from_field_name == 'self'
else self.opts.get_field_by_name(from_field_name)[0]) else self.opts.get_field(from_field_name))
to_field = (self.rel.to._meta.pk if to_field_name is None to_field = (self.rel.to._meta.pk if to_field_name is None
else self.rel.to._meta.get_field_by_name(to_field_name)[0]) else self.rel.to._meta.get_field(to_field_name))
related_fields.append((from_field, to_field)) related_fields.append((from_field, to_field))
return related_fields return related_fields
@ -1731,7 +1778,7 @@ class ForeignObject(RelatedField):
def contribute_to_related_class(self, cls, related): def contribute_to_related_class(self, cls, related):
# Internal FK's - i.e., those with a related name ending with '+' - # Internal FK's - i.e., those with a related name ending with '+' -
# and swapped models don't get a related descriptor. # and swapped models don't get a related descriptor.
if not self.rel.is_hidden() and not related.model._meta.swapped: if not self.rel.is_hidden() and not related.related_model._meta.swapped:
setattr(cls, related.get_accessor_name(), self.related_accessor_class(related)) setattr(cls, related.get_accessor_name(), self.related_accessor_class(related))
# While 'limit_choices_to' might be a callable, simply pass # While 'limit_choices_to' might be a callable, simply pass
# it along for later - this is too early because it's still # it along for later - this is too early because it's still
@ -1741,6 +1788,12 @@ class ForeignObject(RelatedField):
class ForeignKey(ForeignObject): class ForeignKey(ForeignObject):
# Field flags
many_to_many = False
many_to_one = False
one_to_many = True
one_to_one = False
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { default_error_messages = {
'invalid': _('%(model)s instance with %(field)s %(value)r does not exist.') 'invalid': _('%(model)s instance with %(field)s %(value)r does not exist.')
@ -1951,6 +2004,12 @@ class OneToOneField(ForeignKey):
always returns the object pointed to (since there will only ever be one), always returns the object pointed to (since there will only ever be one),
rather than returning a list. rather than returning a list.
""" """
# Field flags
many_to_many = False
many_to_one = False
one_to_many = False
one_to_one = True
related_accessor_class = SingleRelatedObjectDescriptor related_accessor_class = SingleRelatedObjectDescriptor
description = _("One-to-one relationship") description = _("One-to-one relationship")
@ -2036,6 +2095,12 @@ def create_many_to_many_intermediary_model(field, klass):
class ManyToManyField(RelatedField): class ManyToManyField(RelatedField):
# Field flags
many_to_many = True
many_to_one = False
one_to_many = False
one_to_one = False
description = _("Many-to-many relationship") description = _("Many-to-many relationship")
def __init__(self, to, db_constraint=True, swappable=True, **kwargs): def __init__(self, to, db_constraint=True, swappable=True, **kwargs):
@ -2050,7 +2115,6 @@ class ManyToManyField(RelatedField):
# Class names must be ASCII in Python 2.x, so we forcibly coerce it # Class names must be ASCII in Python 2.x, so we forcibly coerce it
# here to break early if there's a problem. # here to break early if there's a problem.
to = str(to) to = str(to)
kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['verbose_name'] = kwargs.get('verbose_name', None)
kwargs['rel'] = ManyToManyRel( kwargs['rel'] = ManyToManyRel(
self, to, self, to,
@ -2357,8 +2421,8 @@ class ManyToManyField(RelatedField):
""" """
pathinfos = [] pathinfos = []
int_model = self.rel.through int_model = self.rel.through
linkfield1 = int_model._meta.get_field_by_name(self.m2m_field_name())[0] linkfield1 = int_model._meta.get_field(self.m2m_field_name())
linkfield2 = int_model._meta.get_field_by_name(self.m2m_reverse_field_name())[0] linkfield2 = int_model._meta.get_field(self.m2m_reverse_field_name())
if direct: if direct:
join1infos = linkfield1.get_reverse_path_info() join1infos = linkfield1.get_reverse_path_info()
join2infos = linkfield2.get_path_info() join2infos = linkfield2.get_path_info()
@ -2398,8 +2462,8 @@ class ManyToManyField(RelatedField):
else: else:
link_field_name = None link_field_name = None
for f in self.rel.through._meta.fields: for f in self.rel.through._meta.fields:
if hasattr(f, 'rel') and f.rel and f.rel.to == related.model and \ if (f.is_relation and f.rel.to == related.related_model and
(link_field_name is None or link_field_name == f.name): (link_field_name is None or link_field_name == f.name)):
setattr(self, cache_attr, getattr(f, attr)) setattr(self, cache_attr, getattr(f, attr))
return getattr(self, cache_attr) return getattr(self, cache_attr)
@ -2414,8 +2478,9 @@ class ManyToManyField(RelatedField):
else: else:
link_field_name = None link_field_name = None
for f in self.rel.through._meta.fields: for f in self.rel.through._meta.fields:
if hasattr(f, 'rel') and f.rel and f.rel.to == related.parent_model: # NOTE f.rel.to != f.related_model
if link_field_name is None and related.model == related.parent_model: if f.is_relation and f.rel.to == related.model:
if link_field_name is None and related.related_model == related.model:
# If this is an m2m-intermediate to self, # If this is an m2m-intermediate to self,
# the first foreign key you find will be # the first foreign key you find will be
# the source column. Keep searching for # the source column. Keep searching for
@ -2479,7 +2544,7 @@ class ManyToManyField(RelatedField):
def contribute_to_related_class(self, cls, related): def contribute_to_related_class(self, cls, related):
# Internal M2Ms (i.e., those with a related name ending with '+') # Internal M2Ms (i.e., those with a related name ending with '+')
# and swapped models don't get a related descriptor. # and swapped models don't get a related descriptor.
if not self.rel.is_hidden() and not related.model._meta.swapped: if not self.rel.is_hidden() and not related.related_model._meta.swapped:
setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related)) setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related))
# Set up the accessors for the column names on the m2m table # Set up the accessors for the column names on the m2m table

View File

@ -2,7 +2,6 @@ import copy
from importlib import import_module from importlib import import_module
import inspect import inspect
from django.core.exceptions import FieldDoesNotExist
from django.db import router from django.db import router
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.utils import six from django.utils import six
@ -23,15 +22,12 @@ def ensure_default_manager(cls):
setattr(cls, 'objects', SwappedManagerDescriptor(cls)) setattr(cls, 'objects', SwappedManagerDescriptor(cls))
return return
if not getattr(cls, '_default_manager', None): if not getattr(cls, '_default_manager', None):
# Create the default manager, if needed. if any(f.name == 'objects' for f in cls._meta.fields):
try:
cls._meta.get_field('objects')
raise ValueError( raise ValueError(
"Model %s must specify a custom Manager, because it has a " "Model %s must specify a custom Manager, because it has a "
"field named 'objects'" % cls.__name__ "field named 'objects'" % cls.__name__
) )
except FieldDoesNotExist: # Create the default manager, if needed.
pass
cls.add_to_class('objects', Manager()) cls.add_to_class('objects', Manager())
cls._base_manager = cls.objects cls._base_manager = cls.objects
elif not getattr(cls, '_base_manager', None): elif not getattr(cls, '_base_manager', None):

View File

@ -1,20 +1,31 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from bisect import bisect from bisect import bisect
from collections import OrderedDict from collections import OrderedDict, defaultdict
from itertools import chain
import warnings
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import ManyToManyRel from django.db.models.fields.related import ManyToManyField
from django.db.models.fields import AutoField from django.db.models.fields import AutoField
from django.db.models.fields.proxy import OrderWrt from django.db.models.fields.proxy import OrderWrt
from django.utils import six from django.utils import six
from django.utils.datastructures import ImmutableList
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text, smart_text, python_2_unicode_compatible from django.utils.encoding import force_text, smart_text, python_2_unicode_compatible
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.lru_cache import lru_cache
from django.utils.text import camel_case_to_spaces from django.utils.text import camel_case_to_spaces
from django.utils.translation import activate, deactivate_all, get_language, string_concat from django.utils.translation import activate, deactivate_all, get_language, string_concat
EMPTY_RELATION_TREE = tuple()
IMMUTABLE_WARNING = (
"The return type of '%s' should never be mutated. If you want to manipulate this list "
"for your own use, make a copy first."
)
DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering', DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
'unique_together', 'permissions', 'get_latest_by', 'unique_together', 'permissions', 'get_latest_by',
@ -24,6 +35,24 @@ DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
'select_on_save', 'default_related_name') 'select_on_save', 'default_related_name')
class raise_deprecation(object):
def __init__(self, suggested_alternative):
self.suggested_alternative = suggested_alternative
def __call__(self, fn):
def wrapper(*args, **kwargs):
warnings.warn(
"'%s is an unofficial API that has been deprecated. "
"You may be able to replace it with '%s'" % (
fn.__name__,
self.suggested_alternative,
),
RemovedInDjango20Warning, stacklevel=2
)
return fn(*args, **kwargs)
return wrapper
def normalize_together(option_together): def normalize_together(option_together):
""" """
option_together can be either a tuple of tuples, or a single option_together can be either a tuple of tuples, or a single
@ -46,9 +75,19 @@ def normalize_together(option_together):
return option_together return option_together
def make_immutable_fields_list(name, data):
return ImmutableList(data, warning=IMMUTABLE_WARNING % name)
@python_2_unicode_compatible @python_2_unicode_compatible
class Options(object): class Options(object):
FORWARD_PROPERTIES = ('fields', 'many_to_many', 'concrete_fields',
'local_concrete_fields', '_forward_fields_map')
REVERSE_PROPERTIES = ('related_objects', 'fields_map', '_relation_tree')
def __init__(self, meta, app_label=None): def __init__(self, meta, app_label=None):
self._get_fields_cache = {}
self.proxied_children = []
self.local_fields = [] self.local_fields = []
self.local_many_to_many = [] self.local_many_to_many = []
self.virtual_fields = [] self.virtual_fields = []
@ -103,6 +142,31 @@ class Options(object):
self.default_related_name = None self.default_related_name = None
@lru_cache(maxsize=None)
def _map_model(self, link):
# This helper function is used to allow backwards compatibility with
# the previous API. No future methods should use this function.
# It maps a field to (field, model or related_model,) depending on the
# field type.
model = link.model._meta.concrete_model
if model is self.model:
model = None
return link, model
@lru_cache(maxsize=None)
def _map_model_details(self, link):
# This helper function is used to allow backwards compatibility with
# the previous API. No future methods should use this function.
# This function maps a field to a tuple of:
# (field, model or related_model, direct, is_m2m) depending on the
# field type.
direct = not link.auto_created or link.concrete
model = link.model._meta.concrete_model
if model is self.model:
model = None
m2m = link.is_relation and link.many_to_many
return link, model, direct, m2m
@property @property
def app_config(self): def app_config(self):
# Don't go through get_app_config to avoid triggering imports. # Don't go through get_app_config to avoid triggering imports.
@ -183,7 +247,17 @@ class Options(object):
def _prepare(self, model): def _prepare(self, model):
if self.order_with_respect_to: if self.order_with_respect_to:
self.order_with_respect_to = self.get_field(self.order_with_respect_to) # The app registry will not be ready at this point, so we cannot
# use get_field().
query = self.order_with_respect_to
try:
self.order_with_respect_to = next(
f for f in self._get_fields(reverse=False)
if f.name == query or f.attname == query
)
except StopIteration:
raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, query))
self.ordering = ('_order',) self.ordering = ('_order',)
if not any(isinstance(field, OrderWrt) for field in model._meta.local_fields): if not any(isinstance(field, OrderWrt) for field in model._meta.local_fields):
model.add_to_class('_order', OrderWrt()) model.add_to_class('_order', OrderWrt())
@ -208,56 +282,41 @@ class Options(object):
auto_created=True) auto_created=True)
model.add_to_class('id', auto) model.add_to_class('id', auto)
def add_field(self, field): def add_field(self, field, virtual=False):
# Insert the given field in the order in which it was created, using # Insert the given field in the order in which it was created, using
# the "creation_counter" attribute of the field. # the "creation_counter" attribute of the field.
# Move many-to-many related fields from self.fields into # Move many-to-many related fields from self.fields into
# self.many_to_many. # self.many_to_many.
if field.rel and isinstance(field.rel, ManyToManyRel): if virtual:
self.virtual_fields.append(field)
elif field.is_relation and field.many_to_many:
self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field) self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field)
if hasattr(self, '_m2m_cache'):
del self._m2m_cache
else: else:
self.local_fields.insert(bisect(self.local_fields, field), field) self.local_fields.insert(bisect(self.local_fields, field), field)
self.setup_pk(field) self.setup_pk(field)
if hasattr(self, '_field_cache'):
del self._field_cache
del self._field_name_cache
# The fields, concrete_fields and local_concrete_fields are
# implemented as cached properties for performance reasons.
# The attrs will not exists if the cached property isn't
# accessed yet, hence the try-excepts.
try:
del self.fields
except AttributeError:
pass
try:
del self.concrete_fields
except AttributeError:
pass
try:
del self.local_concrete_fields
except AttributeError:
pass
if hasattr(self, '_name_map'): # If the field being added is a relation to another known field,
del self._name_map # expire the cache on this field and the forward cache on the field
# being referenced, because there will be new relationships in the
def add_virtual_field(self, field): # cache. Otherwise, expire the cache of references *to* this field.
self.virtual_fields.append(field) # The mechanism for getting at the related model is slightly odd -
# ideally, we'd just ask for field.related_model. However, related_model
# is a cached property, and all the models haven't been loaded yet, so
# we need to make sure we don't cache a string reference.
if field.is_relation and hasattr(field.rel, 'to') and field.rel.to:
try:
field.rel.to._meta._expire_cache(forward=False)
except AttributeError:
pass
self._expire_cache()
else:
self._expire_cache(reverse=False)
def setup_pk(self, field): def setup_pk(self, field):
if not self.pk and field.primary_key: if not self.pk and field.primary_key:
self.pk = field self.pk = field
field.serialize = False field.serialize = False
def pk_index(self):
"""
Returns the index of the primary key field in the self.concrete_fields
list.
"""
return self.concrete_fields.index(self.pk)
def setup_proxy(self, target): def setup_proxy(self, target):
""" """
Does the internal setup so that the current model is a proxy for Does the internal setup so that the current model is a proxy for
@ -273,6 +332,7 @@ class Options(object):
def __str__(self): def __str__(self):
return "%s.%s" % (smart_text(self.app_label), smart_text(self.model_name)) return "%s.%s" % (smart_text(self.app_label), smart_text(self.model_name))
@property
def verbose_name_raw(self): def verbose_name_raw(self):
""" """
There are a few places where the untranslated verbose name is needed There are a few places where the untranslated verbose name is needed
@ -284,9 +344,9 @@ class Options(object):
raw = force_text(self.verbose_name) raw = force_text(self.verbose_name)
activate(lang) activate(lang)
return raw return raw
verbose_name_raw = property(verbose_name_raw)
def _swapped(self): @property
def swapped(self):
""" """
Has this model been swapped out for another? If so, return the model Has this model been swapped out for another? If so, return the model
name of the replacement; otherwise, return None. name of the replacement; otherwise, return None.
@ -310,253 +370,253 @@ class Options(object):
if '%s.%s' % (swapped_label, swapped_object.lower()) not in (None, model_label): if '%s.%s' % (swapped_label, swapped_object.lower()) not in (None, model_label):
return swapped_for return swapped_for
return None return None
swapped = property(_swapped)
@cached_property @cached_property
def fields(self): def fields(self):
""" """
The getter for self.fields. This returns the list of field objects Returns a list of all forward fields on the model and its parents,
available to this model (including through parent models). excluding ManyToManyFields.
Callers are not permitted to modify this list, since it's a reference Private API intended only to be used by Django itself; get_fields()
to this instance (not a copy). combined with filtering of field properties is the public API for
obtaining this field list.
""" """
try: # For legacy reasons, the fields property should only contain forward
self._field_name_cache # fields that are not virtual or with a m2m cardinality. Therefore we
except AttributeError: # pass these three filters as filters to the generator.
self._fill_fields_cache() # The third lambda is a longwinded way of checking f.related_model - we don't
return self._field_name_cache # use that property directly because related_model is a cached property,
# and all the models may not have been loaded yet; we don't want to cache
# the string reference to the related_model.
is_not_an_m2m_field = lambda f: not (f.is_relation and f.many_to_many)
is_not_a_generic_relation = lambda f: not (f.is_relation and f.many_to_one)
is_not_a_generic_foreign_key = lambda f: not (
f.is_relation and f.one_to_many and not (hasattr(f.rel, 'to') and f.rel.to)
)
return make_immutable_fields_list(
"fields",
(f for f in self._get_fields(reverse=False) if
is_not_an_m2m_field(f) and is_not_a_generic_relation(f)
and is_not_a_generic_foreign_key(f))
)
@cached_property @cached_property
def concrete_fields(self): def concrete_fields(self):
return [f for f in self.fields if f.column is not None] """
Returns a list of all concrete fields on the model and its parents.
Private API intended only to be used by Django itself; get_fields()
combined with filtering of field properties is the public API for
obtaining this field list.
"""
return make_immutable_fields_list(
"concrete_fields", (f for f in self.fields if f.concrete)
)
@cached_property @cached_property
def local_concrete_fields(self): def local_concrete_fields(self):
return [f for f in self.local_fields if f.column is not None] """
Returns a list of all concrete fields on the model.
Private API intended only to be used by Django itself; get_fields()
combined with filtering of field properties is the public API for
obtaining this field list.
"""
return make_immutable_fields_list(
"local_concrete_fields", (f for f in self.local_fields if f.concrete)
)
@raise_deprecation(suggested_alternative="get_fields()")
def get_fields_with_model(self): def get_fields_with_model(self):
""" return [self._map_model(f) for f in self.get_fields()]
Returns a sequence of (field, model) pairs for all fields. The "model"
element is None for fields on the current model. Mostly of use when
constructing queries so that we know which model a field belongs to.
"""
try:
self._field_cache
except AttributeError:
self._fill_fields_cache()
return self._field_cache
@raise_deprecation(suggested_alternative="get_fields()")
def get_concrete_fields_with_model(self): def get_concrete_fields_with_model(self):
return [(field, model) for field, model in self.get_fields_with_model() if return [self._map_model(f) for f in self.concrete_fields]
field.column is not None]
def _fill_fields_cache(self): @cached_property
cache = [] def many_to_many(self):
for parent in self.parents: """
for field, model in parent._meta.get_fields_with_model(): Returns a list of all many to many fields on the model and its parents.
if model:
cache.append((field, model))
else:
cache.append((field, parent))
cache.extend((f, None) for f in self.local_fields)
self._field_cache = tuple(cache)
self._field_name_cache = [x for x, _ in cache]
def _many_to_many(self): Private API intended only to be used by Django itself; get_fields()
try: combined with filtering of field properties is the public API for
self._m2m_cache obtaining this list.
except AttributeError: """
self._fill_m2m_cache() return make_immutable_fields_list(
return list(self._m2m_cache) "many_to_many",
many_to_many = property(_many_to_many) (f for f in self._get_fields(reverse=False)
if f.is_relation and f.many_to_many)
)
@cached_property
def related_objects(self):
"""
Returns all related objects pointing to the current model. The related
objects can come from a one-to-one, one-to-many, or many-to-many field
relation type.
Private API intended only to be used by Django itself; get_fields()
combined with filtering of field properties is the public API for
obtaining this field list.
"""
all_related_fields = self._get_fields(forward=False, reverse=True, include_hidden=True)
return make_immutable_fields_list(
"related_objects",
(obj for obj in all_related_fields
if not obj.hidden or obj.field.many_to_many)
)
@raise_deprecation(suggested_alternative="get_fields()")
def get_m2m_with_model(self): def get_m2m_with_model(self):
""" return [self._map_model(f) for f in self.many_to_many]
The many-to-many version of get_fields_with_model().
"""
try:
self._m2m_cache
except AttributeError:
self._fill_m2m_cache()
return list(six.iteritems(self._m2m_cache))
def _fill_m2m_cache(self): @cached_property
cache = OrderedDict() def _forward_fields_map(self):
for parent in self.parents: res = {}
for field, model in parent._meta.get_m2m_with_model(): # call get_fields() with export_ordered_set=True in order to have a
if model: # field_instance -> names map
cache[field] = model fields = self._get_fields(reverse=False)
else: for field in fields:
cache[field] = parent res[field.name] = field
for field in self.local_many_to_many: # Due to the way Django's internals work, get_field() should also
cache[field] = None # be able to fetch a field by attname. In the case of a concrete
self._m2m_cache = cache # field with relation, includes the *_id name too
def get_field(self, name, many_to_many=True):
"""
Returns the requested field by name. Raises FieldDoesNotExist on error.
"""
to_search = (self.fields + self.many_to_many) if many_to_many else self.fields
for f in to_search:
if f.name == name:
return f
raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, name))
def get_field_by_name(self, name):
"""
Returns the (field_object, model, direct, m2m), where field_object is
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 ForeignObjectRel
for this field (since the field doesn't have an instance associated
with it).
Uses a cache internally, so after the first access, this is very fast.
"""
try:
try: try:
return self._name_map[name] res[field.attname] = field
except AttributeError: except AttributeError:
cache = self.init_name_map() pass
return cache[name] return res
except KeyError:
raise FieldDoesNotExist('%s has no field named %r'
% (self.object_name, name))
def get_all_field_names(self): @cached_property
def fields_map(self):
res = {}
fields = self._get_fields(forward=False, include_hidden=True)
for field in fields:
res[field.name] = field
# Due to the way Django's internals work, get_field() should also
# be able to fetch a field by attname. In the case of a concrete
# field with relation, includes the *_id name too
try:
res[field.attname] = field
except AttributeError:
pass
return res
def get_field(self, field_name, many_to_many=None):
""" """
Returns a list of all field names that are possible for this model Returns a field instance given a field name. The field can be either a
(including reverse relation names). This is used for pretty printing forward or reverse field, unless many_to_many is specified; if it is,
debugging output (a list of choices), so any internal-only field names only forward fields will be returned.
are not included.
The many_to_many argument exists for backwards compatibility reasons;
it has been deprecated and will be removed in Django 2.0.
""" """
m2m_in_kwargs = many_to_many is not None
if m2m_in_kwargs:
# Always throw a warning if many_to_many is used regardless of
# whether it alters the return type or not.
warnings.warn(
"The 'many_to_many' argument on get_field() is deprecated; "
"use a filter on field.many_to_many instead.",
RemovedInDjango20Warning
)
try: try:
cache = self._name_map # In order to avoid premature loading of the relation tree
except AttributeError: # (expensive) we prefer checking if the field is a forward field.
cache = self.init_name_map() field = self._forward_fields_map[field_name]
names = sorted(cache.keys())
# Internal-only names end with "+" (symmetrical m2m related names being
# the main example). Trim them.
return [val for val in names if not val.endswith('+')]
def init_name_map(self): if many_to_many is False and field.many_to_many:
""" raise FieldDoesNotExist(
Initialises the field name -> field object mapping. '%s has no field named %r' % (self.object_name, field_name)
""" )
cache = {}
# We intentionally handle related m2m objects first so that symmetrical
# m2m accessor names can be overridden, if necessary.
for f, model in self.get_all_related_m2m_objects_with_model():
cache[f.field.related_query_name()] = (f, model, False, True)
for f, model in self.get_all_related_objects_with_model():
cache[f.field.related_query_name()] = (f, model, False, False)
for f, model in self.get_m2m_with_model():
cache[f.name] = cache[f.attname] = (f, model, True, True)
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 f.rel:
cache[f.name] = cache[f.attname] = (
f, None if f.model == self.model else f.model, True, False)
if apps.ready:
self._name_map = cache
return cache
return field
except KeyError:
# If the app registry is not ready, reverse fields are
# unavailable, therefore we throw a FieldDoesNotExist exception.
if not self.apps.ready:
raise FieldDoesNotExist(
"%s has no field named %r. The app cache isn't "
"ready yet, so if this is a forward field, it won't "
"be available yet." % (self.object_name, field_name)
)
try:
if m2m_in_kwargs:
# Previous API does not allow searching reverse fields.
raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, field_name))
# Retrieve field instance by name from cached or just-computed
# field map.
return self.fields_map[field_name]
except KeyError:
raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, field_name))
@raise_deprecation(suggested_alternative="get_field()")
def get_field_by_name(self, name):
return self._map_model_details(self.get_field(name))
@raise_deprecation(suggested_alternative="get_fields()")
def get_all_field_names(self):
names = set()
fields = self.get_fields()
for field in fields:
# For backwards compatibility GenericForeignKey should not be
# included in the results.
if field.is_relation and field.one_to_many and field.related_model is None:
continue
names.add(field.name)
if hasattr(field, 'attname'):
names.add(field.attname)
return list(names)
@raise_deprecation(suggested_alternative="get_fields()")
def get_all_related_objects(self, local_only=False, include_hidden=False, def get_all_related_objects(self, local_only=False, include_hidden=False,
include_proxy_eq=False): include_proxy_eq=False):
return [k for k, v in self.get_all_related_objects_with_model(
local_only=local_only, include_hidden=include_hidden,
include_proxy_eq=include_proxy_eq)]
def get_all_related_objects_with_model(self, local_only=False, include_parents = local_only is False
include_hidden=False, fields = self._get_fields(
forward=False, reverse=True,
include_parents=include_parents,
include_hidden=include_hidden,
)
fields = (obj for obj in fields if not isinstance(obj.field, ManyToManyField))
if include_proxy_eq:
children = chain.from_iterable(c._relation_tree
for c in self.concrete_model._meta.proxied_children
if c is not self)
relations = (f.rel for f in children
if include_hidden or not f.rel.field.rel.is_hidden())
fields = chain(fields, relations)
return list(fields)
@raise_deprecation(suggested_alternative="get_fields()")
def get_all_related_objects_with_model(self, local_only=False, include_hidden=False,
include_proxy_eq=False): include_proxy_eq=False):
""" return [
Returns a list of (related-object, model) pairs. Similar to self._map_model(f) for f in self.get_all_related_objects(
get_fields_with_model(). local_only=local_only,
""" include_hidden=include_hidden,
try: include_proxy_eq=include_proxy_eq,
self._related_objects_cache )
except AttributeError: ]
self._fill_related_objects_cache()
predicates = []
if local_only:
predicates.append(lambda k, v: not v)
if not include_hidden:
predicates.append(lambda k, v: not k.field.rel.is_hidden())
cache = (self._related_objects_proxy_cache if include_proxy_eq
else self._related_objects_cache)
return [t for t in cache.items() if all(p(*t) for p in predicates)]
def _fill_related_objects_cache(self):
cache = OrderedDict()
parent_list = self.get_parent_list()
for parent in self.parents:
for obj, model in parent._meta.get_all_related_objects_with_model(include_hidden=True):
if (obj.field.creation_counter < 0 or obj.field.rel.parent_link) and obj.model not in parent_list:
continue
if not model:
cache[obj] = parent
else:
cache[obj] = model
# Collect also objects which are in relation to some proxy child/parent of self.
proxy_cache = cache.copy()
for klass in self.apps.get_models(include_auto_created=True):
if not klass._meta.swapped:
for f in klass._meta.local_fields + klass._meta.virtual_fields:
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.rel] = None
proxy_cache[f.rel] = None
elif self.concrete_model == f.rel.to._meta.concrete_model:
proxy_cache[f.rel] = None
self._related_objects_cache = cache
self._related_objects_proxy_cache = proxy_cache
@raise_deprecation(suggested_alternative="get_fields()")
def get_all_related_many_to_many_objects(self, local_only=False): def get_all_related_many_to_many_objects(self, local_only=False):
try: fields = self._get_fields(
cache = self._related_many_to_many_cache forward=False, reverse=True,
except AttributeError: include_parents=local_only is not True, include_hidden=True
cache = self._fill_related_many_to_many_cache() )
if local_only: return [obj for obj in fields if isinstance(obj.field, ManyToManyField)]
return [k for k, v in cache.items() if not v]
return list(cache)
@raise_deprecation(suggested_alternative="get_fields()")
def get_all_related_m2m_objects_with_model(self): def get_all_related_m2m_objects_with_model(self):
""" fields = self._get_fields(forward=False, reverse=True, include_hidden=True)
Returns a list of (related-m2m-object, model) pairs. Similar to return [self._map_model(obj) for obj in fields if isinstance(obj.field, ManyToManyField)]
get_fields_with_model().
"""
try:
cache = self._related_many_to_many_cache
except AttributeError:
cache = self._fill_related_many_to_many_cache()
return list(six.iteritems(cache))
def _fill_related_many_to_many_cache(self):
cache = OrderedDict()
parent_list = self.get_parent_list()
for parent in self.parents:
for obj, model in parent._meta.get_all_related_m2m_objects_with_model():
if obj.field.creation_counter < 0 and obj.model not in parent_list:
continue
if not model:
cache[obj] = parent
else:
cache[obj] = model
for klass in self.apps.get_models():
if not klass._meta.swapped:
for f in klass._meta.local_many_to_many:
if (f.rel
and not isinstance(f.rel.to, six.string_types)
and self == f.rel.to._meta):
cache[f.rel] = None
if apps.ready:
self._related_many_to_many_cache = cache
return cache
def get_base_chain(self, model): def get_base_chain(self, model):
""" """
@ -605,3 +665,173 @@ class Options(object):
# of the chain to the ancestor is that parent # of the chain to the ancestor is that parent
# links # links
return self.parents[parent] or parent_link return self.parents[parent] or parent_link
def _populate_directed_relation_graph(self):
"""
This method is used by each model to find its reverse objects. As this
method is very expensive and is accessed frequently (it looks up every
field in a model, in every app), it is computed on first access and then
is set as a property on every model.
"""
related_objects_graph = defaultdict(list)
all_models = self.apps.get_models(include_auto_created=True)
for model in all_models:
fields_with_relations = (
f for f in model._meta._get_fields(reverse=False)
if f.is_relation and f.related_model is not None
)
if model._meta.auto_created:
fields_with_relations = (
f for f in fields_with_relations
if not f.many_to_many
)
for f in fields_with_relations:
if not isinstance(f.rel.to, six.string_types):
# Set options_instance -> field
related_objects_graph[f.rel.to._meta].append(f)
for model in all_models:
# Set the relation_tree using the internal __dict__. In this way
# we avoid calling the cached property. In attribute lookup,
# __dict__ takes precedence over a data descriptor (such as
# @cached_property). This means that the _meta._relation_tree is
# only called if related_objects is not in __dict__.
related_objects = related_objects_graph[model._meta]
# If related_objects are empty, it makes sense to set
# EMPTY_RELATION_TREE. This will avoid allocating multiple empty
# relation trees.
relation_tree = EMPTY_RELATION_TREE
if related_objects:
relation_tree = related_objects
model._meta.__dict__['_relation_tree'] = relation_tree
@cached_property
def _relation_tree(self):
# If cache is not present, populate the cache
self._populate_directed_relation_graph()
# It may happen, often when the registry is not ready, that a not yet
# registered model is queried. In this very rare case we simply return
# an EMPTY_RELATION_TREE. When the registry will be ready, cache will
# be flushed and this model will be computed properly.
return self.__dict__.get('_relation_tree', EMPTY_RELATION_TREE)
def _expire_cache(self, forward=True, reverse=True):
# This method is usually called by apps.cache_clear(), when the
# registry is finalized, or when a new field is added.
properties_to_expire = []
if forward:
properties_to_expire.extend(self.FORWARD_PROPERTIES)
if reverse and not self.abstract:
properties_to_expire.extend(self.REVERSE_PROPERTIES)
for cache_key in properties_to_expire:
try:
delattr(self, cache_key)
except AttributeError:
pass
self._get_fields_cache = {}
def get_fields(self, include_parents=True, include_hidden=False):
"""
Returns a list of fields associated to the model. By default will only
return forward fields. This can be changed by enabling or disabling
field types using the parameters:
- include_parents: include fields derived from inheritance
- include_hidden: include fields that have a related_name that
starts with a "+"
"""
return self._get_fields(include_parents=include_parents, include_hidden=include_hidden)
def _get_fields(self, forward=True, reverse=True, include_parents=True, include_hidden=False,
export_ordered_set=False):
# This helper function is used to allow recursion in ``get_fields()``
# implementation and to provide a fast way for Django's internals to
# access specific subsets of fields.
# Creates a cache key composed of all arguments
cache_key = (forward, reverse, include_parents, include_hidden, export_ordered_set)
try:
# In order to avoid list manipulation. Always return a shallow copy
# of the results.
return self._get_fields_cache[cache_key]
except KeyError:
pass
# Using an OrderedDict preserves the order of insertion. This is
# important when displaying a ModelForm or the contrib.admin panel
# and no specific ordering is provided.
fields = OrderedDict()
options = {
'include_parents': include_parents,
'include_hidden': include_hidden,
'export_ordered_set': True,
}
# Abstract models cannot hold reverse fields.
if reverse and not self.abstract:
if include_parents:
parent_list = self.get_parent_list()
# Recursively call _get_fields() on each parent, with the same
# options provided in this call.
for parent in self.parents:
for obj, _ in six.iteritems(parent._meta._get_fields(forward=False, **options)):
if obj.many_to_many:
# In order for a reverse ManyToManyRel object to be
# valid, its creation counter must be > 0 and must
# be in the parent list.
if not (obj.field.creation_counter < 0 and obj.related_model not in parent_list):
fields[obj] = True
elif not ((obj.field.creation_counter < 0 or obj.field.rel.parent_link)
and obj.related_model not in parent_list):
fields[obj] = True
# Tree is computed once and cached until the app cache is expired.
# It is composed of a list of fields pointing to the current model
# from other models. If the model is a proxy model, then we also
# add the concrete model.
all_fields = (
self._relation_tree if not self.proxy else
chain(self._relation_tree, self.concrete_model._meta._relation_tree)
)
# Pull out all related objects from forward fields
for field in (f.rel for f in all_fields):
# If hidden fields should be included or the relation is not
# intentionally hidden, add to the fields dict.
if include_hidden or not field.hidden:
fields[field] = True
if forward:
if include_parents:
for parent in self.parents:
# Add the forward fields of each parent.
fields.update(parent._meta._get_fields(reverse=False, **options))
fields.update(
(field, True,)
for field in chain(self.local_fields, self.local_many_to_many)
)
if not export_ordered_set:
# By default, fields contains field instances as keys and all
# possible names if the field instance as values. When
# _get_fields() is called, we only want to return field instances,
# so we just preserve the keys.
fields = list(fields.keys())
# Virtual fields are not inheritable, therefore they are inserted
# only when the recursive _get_fields() call comes to an end.
if forward:
fields.extend(self.virtual_fields)
fields = make_immutable_fields_list("get_fields()", fields)
# Store result into cache for later access
self._get_fields_cache[cache_key] = fields
# In order to avoid list manipulation. Always
# return a shallow copy of the results
return fields

View File

@ -252,9 +252,8 @@ class QuerySet(object):
# If only/defer clauses have been specified, # If only/defer clauses have been specified,
# build the list of fields that are to be loaded. # build the list of fields that are to be loaded.
if only_load: if only_load:
for field, model in self.model._meta.get_concrete_fields_with_model(): for field in self.model._meta.concrete_fields:
if model is None: model = field.model._meta.model
model = self.model
try: try:
if field.name in only_load[model]: if field.name in only_load[model]:
# Add a field that has been explicitly included # Add a field that has been explicitly included
@ -818,7 +817,7 @@ class QuerySet(object):
obj = self._clone() obj = self._clone()
names = getattr(self, '_fields', None) names = getattr(self, '_fields', None)
if names is None: if names is None:
names = set(self.model._meta.get_all_field_names()) names = {f.name for f in self.model._meta.get_fields()}
# Add the annotations to the query # Add the annotations to the query
for alias, annotation in annotations.items(): for alias, annotation in annotations.items():
@ -1329,7 +1328,8 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,
skip = set() skip = set()
init_list = [] init_list = []
# Build the list of fields that *haven't* been requested # Build the list of fields that *haven't* been requested
for field, model in klass._meta.get_concrete_fields_with_model(): for field in klass._meta.concrete_fields:
model = field.model._meta.concrete_model
if from_parent and model and issubclass(from_parent, model): if from_parent and model and issubclass(from_parent, model):
# Avoid loading fields already loaded for parent model for # Avoid loading fields already loaded for parent model for
# child models. # child models.
@ -1381,18 +1381,19 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,
reverse_related_fields = [] reverse_related_fields = []
if restricted: if restricted:
for o in klass._meta.get_all_related_objects(): for o in klass._meta.related_objects:
if o.field.unique and select_related_descend(o.field, restricted, requested, if o.field.unique and select_related_descend(o.field, restricted, requested,
only_load.get(o.model), reverse=True): only_load.get(o.related_model), reverse=True):
next = requested[o.field.related_query_name()] next = requested[o.field.related_query_name()]
parent = klass if issubclass(o.model, klass) else None parent = klass if issubclass(o.related_model, klass) else None
klass_info = get_klass_info(o.model, max_depth=max_depth, cur_depth=cur_depth + 1, klass_info = get_klass_info(o.related_model, max_depth=max_depth, cur_depth=cur_depth + 1,
requested=next, only_load=only_load, from_parent=parent) requested=next, only_load=only_load, from_parent=parent)
reverse_related_fields.append((o.field, klass_info)) reverse_related_fields.append((o.field, klass_info))
if field_names: if field_names:
pk_idx = field_names.index(klass._meta.pk.attname) pk_idx = field_names.index(klass._meta.pk.attname)
else: else:
pk_idx = klass._meta.pk_index() meta = klass._meta
pk_idx = meta.concrete_fields.index(meta.pk)
return klass, field_names, field_count, related_fields, reverse_related_fields, pk_idx return klass, field_names, field_count, related_fields, reverse_related_fields, pk_idx
@ -1485,7 +1486,10 @@ def get_cached_row(row, index_start, using, klass_info, offset=0,
for f, klass_info in reverse_related_fields: for f, klass_info in reverse_related_fields:
# Transfer data from this object to childs. # Transfer data from this object to childs.
parent_data = [] parent_data = []
for rel_field, rel_model in klass_info[0]._meta.get_fields_with_model(): for rel_field in klass_info[0]._meta.fields:
rel_model = rel_field.model._meta.concrete_model
if rel_model == klass_info[0]._meta.model:
rel_model = None
if rel_model is not None and isinstance(obj, rel_model): if rel_model is not None and isinstance(obj, rel_model):
parent_data.append((rel_field, getattr(obj, rel_field.attname))) parent_data.append((rel_field, getattr(obj, rel_field.attname)))
# Recursively retrieve the data for the related object # Recursively retrieve the data for the related object

View File

@ -109,7 +109,7 @@ class DeferredAttribute(object):
# self.field_name is the attname of the field, but only() takes the # self.field_name is the attname of the field, but only() takes the
# actual name, so we need to translate it here. # actual name, so we need to translate it here.
try: try:
f = opts.get_field_by_name(self.field_name)[0] f = opts.get_field(self.field_name)
except FieldDoesNotExist: except FieldDoesNotExist:
f = [f for f in opts.fields if f.attname == self.field_name][0] f = [f for f in opts.fields if f.attname == self.field_name][0]
name = f.name name = f.name
@ -136,7 +136,7 @@ class DeferredAttribute(object):
field is a primary key field. field is a primary key field.
""" """
opts = instance._meta opts = instance._meta
f = opts.get_field_by_name(name)[0] f = opts.get_field(name)
link_field = opts.get_ancestor_link(f.model) link_field = opts.get_ancestor_link(f.model)
if f.primary_key and f != link_field: if f.primary_key and f != link_field:
return getattr(instance, link_field.attname) return getattr(instance, link_field.attname)

View File

@ -298,7 +298,12 @@ class SQLCompiler(object):
# be used by local fields. # be used by local fields.
seen_models = {None: start_alias} seen_models = {None: start_alias}
for field, model in opts.get_concrete_fields_with_model(): for field in opts.concrete_fields:
model = field.model._meta.concrete_model
# A proxy model will have a different model and concrete_model. We
# will assign None if the field belongs to this model.
if model == opts.model:
model = None
if from_parent and model is not None and issubclass(from_parent, model): if from_parent and model is not None and issubclass(from_parent, model):
# Avoid loading data for already loaded parents. # Avoid loading data for already loaded parents.
continue continue
@ -601,10 +606,10 @@ class SQLCompiler(object):
connections to the root model). connections to the root model).
""" """
def _get_field_choices(): def _get_field_choices():
direct_choices = (f.name for (f, _) in opts.get_fields_with_model() if f.rel) direct_choices = (f.name for f in opts.fields if f.is_relation)
reverse_choices = ( reverse_choices = (
f.field.related_query_name() f.field.related_query_name()
for f in opts.get_all_related_objects() if f.field.unique for f in opts.related_objects if f.field.unique
) )
return chain(direct_choices, reverse_choices) return chain(direct_choices, reverse_choices)
@ -628,12 +633,13 @@ class SQLCompiler(object):
else: else:
restricted = False restricted = False
for f, model in opts.get_fields_with_model(): for f in opts.fields:
field_model = f.model._meta.concrete_model
fields_found.add(f.name) fields_found.add(f.name)
if restricted: if restricted:
next = requested.get(f.name, {}) next = requested.get(f.name, {})
if not f.rel: if not f.is_relation:
# If a non-related field is used like a relation, # If a non-related field is used like a relation,
# or if a single non-relational field is given. # or if a single non-relational field is given.
if next or (cur_depth == 1 and f.name in requested): if next or (cur_depth == 1 and f.name in requested):
@ -647,10 +653,6 @@ class SQLCompiler(object):
else: else:
next = False next = False
# The get_fields_with_model() returns None for fields that live
# in the field's local model. So, for those fields we want to use
# the f.model - that is the field's local model.
field_model = model or f.model
if not select_related_descend(f, restricted, requested, if not select_related_descend(f, restricted, requested,
only_load.get(field_model)): only_load.get(field_model)):
continue continue
@ -666,9 +668,9 @@ class SQLCompiler(object):
if restricted: if restricted:
related_fields = [ related_fields = [
(o.field, o.model) (o.field, o.related_model)
for o in opts.get_all_related_objects() for o in opts.related_objects
if o.field.unique if o.field.unique and not o.many_to_many
] ]
for f, model in related_fields: for f, model in related_fields:
if not select_related_descend(f, restricted, requested, if not select_related_descend(f, restricted, requested,
@ -760,7 +762,7 @@ class SQLCompiler(object):
if self.query.select: if self.query.select:
fields = [f.field for f in self.query.select] fields = [f.field for f in self.query.select]
elif self.query.default_cols: elif self.query.default_cols:
fields = self.query.get_meta().concrete_fields fields = list(self.query.get_meta().concrete_fields)
else: else:
fields = [] fields = []

View File

@ -11,6 +11,7 @@ from itertools import count, product
from collections import Mapping, OrderedDict from collections import Mapping, OrderedDict
import copy import copy
from itertools import chain
import warnings import warnings
from django.core.exceptions import FieldDoesNotExist, FieldError from django.core.exceptions import FieldDoesNotExist, FieldError
@ -33,6 +34,13 @@ from django.utils.tree import Node
__all__ = ['Query', 'RawQuery'] __all__ = ['Query', 'RawQuery']
def get_field_names_from_opts(opts):
return set(chain.from_iterable(
(f.name, f.attname) if f.concrete else (f.name,)
for f in opts.get_fields()
))
class RawQuery(object): class RawQuery(object):
""" """
A single raw SQL query A single raw SQL query
@ -593,9 +601,9 @@ class Query(object):
opts = orig_opts opts = orig_opts
for name in parts[:-1]: for name in parts[:-1]:
old_model = cur_model old_model = cur_model
source = opts.get_field_by_name(name)[0] source = opts.get_field(name)
if is_reverse_o2o(source): if is_reverse_o2o(source):
cur_model = source.model cur_model = source.related_model
else: else:
cur_model = source.rel.to cur_model = source.rel.to
opts = cur_model._meta opts = cur_model._meta
@ -605,8 +613,11 @@ class Query(object):
if not is_reverse_o2o(source): if not is_reverse_o2o(source):
must_include[old_model].add(source) must_include[old_model].add(source)
add_to_dict(must_include, cur_model, opts.pk) add_to_dict(must_include, cur_model, opts.pk)
field, model, _, _ = opts.get_field_by_name(parts[-1]) field = opts.get_field(parts[-1])
if model is None: is_reverse_object = field.auto_created and not field.concrete
model = field.related_model if is_reverse_object else field.model
model = model._meta.concrete_model
if model == opts.model:
model = cur_model model = cur_model
if not is_reverse_o2o(field): if not is_reverse_o2o(field):
add_to_dict(seen, model, field) add_to_dict(seen, model, field)
@ -618,10 +629,11 @@ class Query(object):
# models. # models.
workset = {} workset = {}
for model, values in six.iteritems(seen): for model, values in six.iteritems(seen):
for field, m in model._meta.get_fields_with_model(): for field in model._meta.fields:
if field in values: if field in values:
continue continue
add_to_dict(workset, m or model, field) m = field.model._meta.concrete_model
add_to_dict(workset, m, field)
for model, values in six.iteritems(must_include): for model, values in six.iteritems(must_include):
# If we haven't included a model in workset, we don't add the # If we haven't included a model in workset, we don't add the
# corresponding must_include fields for that model, since an # corresponding must_include fields for that model, since an
@ -934,8 +946,9 @@ class Query(object):
root_alias = self.tables[0] root_alias = self.tables[0]
seen = {None: root_alias} seen = {None: root_alias}
for field, model in opts.get_fields_with_model(): for field in opts.fields:
if model not in seen: model = field.model._meta.concrete_model
if model is not opts.model and model not in seen:
self.join_parent_model(opts, model, root_alias, seen) self.join_parent_model(opts, model, root_alias, seen)
self.included_inherited_models = seen self.included_inherited_models = seen
@ -1368,7 +1381,19 @@ class Query(object):
if name == 'pk': if name == 'pk':
name = opts.pk.name name = opts.pk.name
try: try:
field, model, _, _ = opts.get_field_by_name(name) field = opts.get_field(name)
# Fields that contain one-to-many relations with a generic
# model (like a GenericForeignKey) cannot generate reverse
# relations and therefore cannot be used for reverse querying.
if field.is_relation and not field.related_model:
raise FieldError(
"Field %r does not generate an automatic reverse "
"relation and therefore cannot be used for reverse "
"querying. If it is a GenericForeignKey, consider "
"adding a GenericRelation." % name
)
model = field.model._meta.concrete_model
except FieldDoesNotExist: except FieldDoesNotExist:
# is it an annotation? # is it an annotation?
if self._annotations and name in self._annotations: if self._annotations and name in self._annotations:
@ -1382,14 +1407,15 @@ class Query(object):
# one step. # one step.
pos -= 1 pos -= 1
if pos == -1 or fail_on_missing: if pos == -1 or fail_on_missing:
available = opts.get_all_field_names() + list(self.annotation_select) field_names = list(get_field_names_from_opts(opts))
available = sorted(field_names + list(self.annotation_select))
raise FieldError("Cannot resolve keyword %r into field. " raise FieldError("Cannot resolve keyword %r into field. "
"Choices are: %s" % (name, ", ".join(available))) "Choices are: %s" % (name, ", ".join(available)))
break break
# Check if we need any joins for concrete inheritance cases (the # Check if we need any joins for concrete inheritance cases (the
# field lives in parent, but we are currently in one of its # field lives in parent, but we are currently in one of its
# children) # children)
if model: if model is not opts.model:
# The field lives on a base class of the current model. # The field lives on a base class of the current model.
# Skip the chain of proxy to the concrete proxied model # Skip the chain of proxy to the concrete proxied model
proxied_model = opts.concrete_model proxied_model = opts.concrete_model
@ -1432,7 +1458,7 @@ class Query(object):
return path, final_field, targets, names[pos + 1:] return path, final_field, targets, names[pos + 1:]
def raise_field_error(self, opts, name): def raise_field_error(self, opts, name):
available = opts.get_all_field_names() + list(self.annotation_select) available = list(get_field_names_from_opts(opts)) + list(self.annotation_select)
raise FieldError("Cannot resolve keyword %r into field. " raise FieldError("Cannot resolve keyword %r into field. "
"Choices are: %s" % (name, ", ".join(available))) "Choices are: %s" % (name, ", ".join(available)))
@ -1693,7 +1719,7 @@ class Query(object):
# from the model on which the lookup failed. # from the model on which the lookup failed.
raise raise
else: else:
names = sorted(opts.get_all_field_names() + list(self.extra) names = sorted(list(get_field_names_from_opts(opts)) + list(self.extra)
+ list(self.annotation_select)) + list(self.annotation_select))
raise FieldError("Cannot resolve keyword %r into field. " raise FieldError("Cannot resolve keyword %r into field. "
"Choices are: %s" % (name, ", ".join(names))) "Choices are: %s" % (name, ", ".join(names)))

View File

@ -120,13 +120,15 @@ class UpdateQuery(Query):
""" """
values_seq = [] values_seq = []
for name, val in six.iteritems(values): for name, val in six.iteritems(values):
field, model, direct, m2m = self.get_meta().get_field_by_name(name) field = self.get_meta().get_field(name)
if not direct or m2m: direct = not (field.auto_created and not field.concrete) or not field.concrete
model = field.model._meta.concrete_model
if not direct or (field.is_relation and field.many_to_many):
raise FieldError( raise FieldError(
'Cannot update model field %r (only non-relations and ' 'Cannot update model field %r (only non-relations and '
'foreign keys permitted).' % field 'foreign keys permitted).' % field
) )
if model: if model is not self.get_meta().model:
self.add_related_update(model, field, val) self.add_related_update(model, field, val)
continue continue
values_seq.append((field, model, val)) values_seq.append((field, model, val))

View File

@ -6,6 +6,7 @@ and database field objects.
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict from collections import OrderedDict
from itertools import chain
import warnings import warnings
from django.core.exceptions import ( from django.core.exceptions import (
@ -89,7 +90,7 @@ def save_instance(form, instance, fields=None, fail_message='saved',
# Note that for historical reasons we want to include also # Note that for historical reasons we want to include also
# virtual_fields here. (GenericRelation was previously a fake # virtual_fields here. (GenericRelation was previously a fake
# m2m field). # m2m field).
for f in opts.many_to_many + opts.virtual_fields: for f in chain(opts.many_to_many, opts.virtual_fields):
if not hasattr(f, 'save_form_data'): if not hasattr(f, 'save_form_data'):
continue continue
if fields and f.name not in fields: if fields and f.name not in fields:
@ -127,7 +128,7 @@ def model_to_dict(instance, fields=None, exclude=None):
from django.db.models.fields.related import ManyToManyField from django.db.models.fields.related import ManyToManyField
opts = instance._meta opts = instance._meta
data = {} data = {}
for f in opts.concrete_fields + opts.virtual_fields + opts.many_to_many: for f in chain(opts.concrete_fields, opts.virtual_fields, opts.many_to_many):
if not getattr(f, 'editable', False): if not getattr(f, 'editable', False):
continue continue
if fields and f.name not in fields: if fields and f.name not in fields:
@ -186,7 +187,7 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
from django.db.models.fields import Field as ModelField from django.db.models.fields import Field as ModelField
sortable_virtual_fields = [f for f in opts.virtual_fields sortable_virtual_fields = [f for f in opts.virtual_fields
if isinstance(f, ModelField)] if isinstance(f, ModelField)]
for f in sorted(opts.concrete_fields + sortable_virtual_fields + opts.many_to_many): for f in sorted(chain(opts.concrete_fields, sortable_virtual_fields, opts.many_to_many)):
if not getattr(f, 'editable', False): if not getattr(f, 'editable', False):
continue continue
if fields is not None and f.name not in fields: if fields is not None and f.name not in fields:

View File

@ -217,9 +217,9 @@ The ``Field.__init__()`` method takes the following parameters:
* :attr:`~django.db.models.Field.db_tablespace`: Only for index creation, if the * :attr:`~django.db.models.Field.db_tablespace`: Only for index creation, if the
backend supports :doc:`tablespaces </topics/db/tablespaces>`. You can usually backend supports :doc:`tablespaces </topics/db/tablespaces>`. You can usually
ignore this option. ignore this option.
* ``auto_created``: ``True`` if the field was automatically created, as for the * :attr:`~django.db.models.Field.auto_created`: ``True`` if the field was
:class:`~django.db.models.OneToOneField` used by model inheritance. For automatically created, as for the :class:`~django.db.models.OneToOneField`
advanced use only. used by model inheritance. For advanced use only.
All of the options without an explanation in the above list have the same All of the options without an explanation in the above list have the same
meaning they do for normal Django fields. See the :doc:`field documentation meaning they do for normal Django fields. See the :doc:`field documentation

View File

@ -56,6 +56,19 @@ details on these changes.
* ``django.template.resolve_variable`` will be removed. * ``django.template.resolve_variable`` will be removed.
* The following private APIs will be removed from
:class:`django.db.models.options.Options` (``Model._meta``):
* ``get_field_by_name()``
* ``get_all_field_names()``
* ``get_fields_with_model()``
* ``get_concrete_fields_with_model()``
* ``get_m2m_with_model()``
* ``get_all_related_objects()``
* ``get_all_related_objects_with_model()``
* ``get_all_related_many_to_many_objects()``
* ``get_all_related_m2m_objects_with_model()``
* The ``error_message`` argument of ``django.forms.RegexField`` will be removed. * The ``error_message`` argument of ``django.forms.RegexField`` will be removed.
* The ``unordered_list`` filter will no longer support old style lists. * The ``unordered_list`` filter will no longer support old style lists.

View File

@ -1790,3 +1790,95 @@ Field API reference
This method must be added to fields prior to 1.7 to migrate its data This method must be added to fields prior to 1.7 to migrate its data
using :doc:`/topics/migrations`. using :doc:`/topics/migrations`.
.. _model-field-attributes:
=========================
Field attribute reference
=========================
.. versionadded:: 1.8
Every ``Field`` instance contains several attributes that allow
introspecting its behavior. Use these attributes instead of ``isinstance``
checks when you need to write code that depends on a field's functionality.
These attributes can be used together with the :ref:`Model._meta API
<model-meta-field-api>` to narrow down a search for specific field types.
Custom model fields should implement these flags.
Attributes for fields
=====================
.. attribute:: Field.auto_created
Boolean flag that indicates if the field was automatically created, such
as the ``OneToOneField`` used by model inheritance.
.. attribute:: Field.concrete
Boolean flag that indicates if the field has a database column associated
with it.
.. attribute:: Field.hidden
Boolean flag that indicates if a field is used to back another non-hidden
field's functionality (e.g. the ``content_type`` and ``object_id`` fields
that make up a ``GenericForeignKey``). The ``hidden`` flag is used to
distinguish what constitutes the public subset of fields on the model from
all the fields on the model.
.. note::
:meth:`Options.get_fields()
<django.db.models.options.Options.get_fields()>`
excludes hidden fields by default. Pass in ``include_hidden=True`` to
return hidden fields in the results.
.. attribute:: Field.is_relation
Boolean flag that indicates if a field contains references to one or
more other models for its functionality (e.g. ``ForeignKey``,
``ManyToManyField``, ``OneToOneField``, etc.).
.. attribute:: Field.model
Returns the model on which the field is defined. If a field is defined on
a superclass of a model, ``model`` will refer to the superclass, not the
class of the instance.
Attributes for fields with relations
====================================
These attributes are used to query for the cardinality and other details of a
relation. These attribute are present on all fields; however, they will only
have meaningful values if the field is a relation type
(:attr:`Field.is_relation=True <Field.is_relation>`).
.. attribute:: Field.one_to_many
Boolean flag that is ``True`` if the field has a one-to-many relation, such
as a ``ForeignKey``; ``False`` otherwise.
.. attribute:: Field.one_to_one
Boolean flag that is ``True`` if the field has a one-to-one relation, such
as a ``OneToOneField``; ``False`` otherwise.
.. attribute:: Field.many_to_many
Boolean flag that is ``True`` if the field has a many-to-many relation;
``False`` otherwise. The only field included with Django where this is
``True`` is ``ManyToManyField``.
.. attribute:: Field.many_to_one
Boolean flag that is ``True`` if the field has a many-to-one relation, such
as a ``GenericRelation`` or the reverse of a ``ForeignKey``; ``False``
otherwise.
.. attribute:: Field.related_model
Points to the model the field relates to. For example, ``Author`` in
``ForeignKey(Author)``. If a field has a generic relation (such as a
``GenericForeignKey`` or a ``GenericRelation``) then ``related_model``
will be ``None``.

View File

@ -8,6 +8,7 @@ Model API reference. For introductory material, see :doc:`/topics/db/models`.
:maxdepth: 1 :maxdepth: 1
fields fields
meta
relations relations
class class
options options

287
docs/ref/models/meta.txt Normal file
View File

@ -0,0 +1,287 @@
===================
Model ``_meta`` API
===================
.. module:: django.db.models.options
:synopsis: Model meta-class layer
.. class:: Options
The model ``_meta`` API is at the core of the Django ORM. It enables other
parts of the system such as lookups, queries, forms, and the admin to
understand the capabilities of each model. The API is accessible through
the ``_meta`` attribute of each model class, which is an instance of an
``django.db.models.options.Options`` object.
Methods that it provides can be used to:
* Retrieve all field instances of a model
* Retrieve a single field instance of a model by name
.. versionchanged:: 1.8
The Model ``_meta`` API has always existed as a Django internal, but
wasn't formally documented and supported. As part of the effort to
make this API public, some of the already existing API entry points
have changed slightly. A :ref:`migration guide <migrating-old-meta-api>`
has been provided to assist in converting your code to use the new,
official API.
.. _model-meta-field-api:
Field access API
================
Retrieving a single field instance of a model by name
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. method:: Options.get_field(field_name)
Returns the field instance given a name of a field.
``field_name`` can be the name of a field on the model, a field
on an abstract or inherited model, or a field defined on another
model that points to the model. In the latter case, the ``field_name``
will be the ``related_name`` defined by the user or the name automatically
generated by Django itself.
:attr:`Hidden fields <django.db.models.Field.hidden>` cannot be retrieved
by name.
If a field with the given name is not found a
:class:`~django.core.exceptions.FieldDoesNotExist` exception will be
raised.
.. code-block:: python
>>> from django.contrib.auth.models import User
# A field on the model
>>> User._meta.get_field('username')
<django.db.models.fields.CharField: username>
# A field from another model that has a relation with the current model
>>> User._meta.get_field('logentry')
<ManyToOneRel: admin.logentry>
# A non existent field
>>> User._meta.get_field('does_not_exist')
Traceback (most recent call last):
...
FieldDoesNotExist: User has no field named 'does_not_exist'
.. deprecated:: 1.8
:meth:`Options.get_field()` previously accepted a ``many_to_many``
parameter which could be set to ``False`` to avoid searching
``ManyToManyField``\s. The old behavior has been preserved for
backwards compatibility; however, the parameter and this behavior
has been deprecated.
If you wish to filter out ``ManyToManyField``\s, you can inspect the
:attr:`Field.many_to_many <django.db.models.Field.many_to_many>`
attribute after calling ``get_field()``.
Retrieving all field instances of a model
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. method:: Options.get_fields(include_parents=True, include_hidden=False)
.. versionadded:: 1.8
Returns a tuple of fields associated with a model. ``get_fields()`` accepts
two parameters that can be used to control which fields are returned:
``include_parents``
``True`` by default. Recursively includes fields defined on parent
classes. If set to ``False``, ``get_fields()`` will only search for
fields declared directly on the current model. Fields from models that
directly inherit from abstract models or proxy classes are considered
to be local, not on the parent.
``include_hidden``
``False`` by default. If set to ``True``, ``get_fields()`` will include
fields that are used to back other field's functionality. This will
also include any fields that have a ``related_name`` (such
as :class:`~django.db.models.ManyToManyField`, or
:class:`~django.db.models.ForeignKey`) that start with a "+".
.. code-block:: python
>>> from django.contrib.auth.models import User
>>> User._meta.get_fields()
(<ManyToOneRel: admin.logentry>,
<django.db.models.fields.AutoField: id>,
<django.db.models.fields.CharField: password>,
<django.db.models.fields.DateTimeField: last_login>,
<django.db.models.fields.BooleanField: is_superuser>,
<django.db.models.fields.CharField: username>,
<django.db.models.fields.CharField: first_name>,
<django.db.models.fields.CharField: last_name>,
<django.db.models.fields.EmailField: email>,
<django.db.models.fields.BooleanField: is_staff>,
<django.db.models.fields.BooleanField: is_active>,
<django.db.models.fields.DateTimeField: date_joined>,
<django.db.models.fields.related.ManyToManyField: groups>,
<django.db.models.fields.related.ManyToManyField: user_permissions>)
# Also include hidden fields.
>>> User._meta.get_fields(include_hidden=True)
(<ManyToOneRel: auth.user_groups>,
<ManyToOneRel: auth.user_user_permissions>,
<ManyToOneRel: admin.logentry>,
<django.db.models.fields.AutoField: id>,
<django.db.models.fields.CharField: password>,
<django.db.models.fields.DateTimeField: last_login>,
<django.db.models.fields.BooleanField: is_superuser>,
<django.db.models.fields.CharField: username>,
<django.db.models.fields.CharField: first_name>,
<django.db.models.fields.CharField: last_name>,
<django.db.models.fields.EmailField: email>,
<django.db.models.fields.BooleanField: is_staff>,
<django.db.models.fields.BooleanField: is_active>,
<django.db.models.fields.DateTimeField: date_joined>,
<django.db.models.fields.related.ManyToManyField: groups>,
<django.db.models.fields.related.ManyToManyField: user_permissions>)
.. _migrating-old-meta-api:
Migrating from the old API
==========================
As part of the formalization of the ``Model._meta`` API (from the
:class:`django.db.models.options.Options` class), a number of methods and
properties have been deprecated and will be removed in Django 2.0.
These old APIs can be replicated by either:
* invoking :meth:`Options.get_field()
<django.db.models.options.Options.get_field()>`, or;
* invoking :meth:`Options.get_fields()
<django.db.models.options.Options.get_fields()>` to retrieve a list of all
fields, and then filtering this list using the :ref:`field attributes
<model-field-attributes>` that describe (or retrieve, in the case of
``_with_model`` variants) the properties of the desired fields.
Although it's possible to make strictly equivalent replacements of the old
methods, that might not be the best approach. Taking the time to refactor any
field loops to make better use of the new API - and possibly include fields
that were previously excluded - will almost certainly result in better code.
Assuming you have a model named ``MyModel``, the following substitutions
can be made to convert your code to the new API:
* ``MyModel._meta.get_field(name)``::
f = MyModel._meta.get_field(name)
then check if:
- ``f.auto_created == False``, because the new ``get_field()``
API will find "reverse" relations), and:
- ``f.is_relation and f.related_model is None``, because the new
``get_field()`` API will find
:class:`~django.contrib.contenttypes.fields.GenericForeignKey` relations;
* ``MyModel._meta.get_field_by_name(name)``:
``get_field_by_name()`` returned four values:
``(field, model, direct, m2m)``:
- ``field`` can be found by ``MyModel._meta.get_field(name)``
- ``model`` can be found through the
:attr:`~django.db.models.Field.model` attribute on the field.
- ``direct`` can be found by: ``not field.auto_created or field.concrete``
The :attr:`~django.db.models.Field.auto_created` check excludes
all "forward" and "reverse" relations that are created by Django, but
this also includes ``AutoField`` and ``OneToOneField`` on proxy models.
We avoid filtering out these attributes using the
:attr:`concrete <django.db.models.Field.concrete>` attribute.
- ``m2m`` can be found through the
:attr:`~django.db.models.Field.many_to_many` attribute on the field.
* ``MyModel._meta.get_fields_with_model()``::
[
(f, f.model if f.model != MyModel else None)
for f in MyModel._meta.get_fields()
if not f.is_relation
or f.one_to_one
or (f.one_to_many and f.related_model)
]
* ``MyModel._meta.get_concrete_fields_with_model()``::
[
(f, f.model if f.model != MyModel else None)
for f in MyModel._meta.get_fields()
if f.concrete and (
not f.is_relation
or f.one_to_one
or (f.one_to_many and f.related_model)
)
]
* ``MyModel._meta.get_m2m_with_model()``::
[
(f, f.model if f.model != MyModel else None)
for f in MyModel._meta.get_fields()
if f.many_to_many and not f.auto_created
]
* ``MyModel._meta.get_all_related_objects()``::
[
f for f in MyModel._meta.get_fields()
if f.many_to_one and f.auto_created
]
* ``MyModel._meta.get_all_related_objects_with_model()``::
[
(f, f.model if f.model != MyModel else None)
for f in MyModel._meta.get_fields()
if f.many_to_one and f.auto_created
]
* ``MyModel._meta.get_all_related_many_to_many_objects()``::
[
f for f in MyModel._meta.get_fields(include_hidden=True)
if f.many_to_many and f.auto_created
]
* ``MyModel._meta.get_all_related_m2m_objects_with_model()``::
[
(f, f.model if f.model != MyModel else None)
for f in MyModel._meta.get_fields(include_hidden=True)
if f.many_to_many and f.auto_created
]
* ``MyModel._meta.get_all_field_names()``::
from itertools import chain
list(set(chain.from_iterable(
(field.name, field.attname) if hasattr(field, 'attname') else (field.name,)
for field in MyModel._meta.get_fields()
# For complete backwards compatibility, you may want to exclude
# GenericForeignKey from the results.
if not (field.one_to_many and field.related_model is None)
)))
This provides a 100% backwards compatible replacement, ensuring that both
field names and attribute names ``ForeignKey``\s are included, but fields
associated with ``GenericForeignKey``\s are not. A simpler version would be::
[f.name for f in MyModel._meta.get_fields()]
While this isn't 100% backwards compatible, it may be sufficient in many
situations.

View File

@ -29,6 +29,22 @@ Like Django 1.7, Django 1.8 requires Python 2.7 or above, though we
What's new in Django 1.8 What's new in Django 1.8
======================== ========================
``Model._meta`` API
~~~~~~~~~~~~~~~~~~~
Django now has a formalized API for :doc:`Model._meta </ref/models/meta>`,
providing an officially supported way to :ref:`retrieve fields
<model-meta-field-api>` and filter fields based on their :ref:`attributes
<model-field-attributes>`.
The ``Model._meta`` object has been part of Django since the days of pre-0.96
"Magic Removal" -- it just wasn't an official, stable API. In recognition of
this, we've endeavored to maintain backwards-compatibility with the old
API endpoint where possible. However, API endpoints that aren't part of the
new official API have been deprecated and will eventually be removed. A
:ref:`guide to migrating from the old API to the new API
<migrating-old-meta-api>` has been provided.
Security enhancements Security enhancements
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
@ -998,6 +1014,26 @@ Miscellaneous
Features deprecated in 1.8 Features deprecated in 1.8
========================== ==========================
Selected methods in ``django.db.models.options.Options``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As part of the formalization of the ``Model._meta`` API (from the
:class:`django.db.models.options.Options` class), a number of methods have been
deprecated and will be removed in in Django 2.0:
* ``get_all_field_names()``
* ``get_all_related_objects()``
* ``get_all_related_objects_with_model()``
* ``get_all_related_many_to_many_objects()``
* ``get_all_related_m2m_objects_with_model()``
* ``get_concrete_fields_with_model()``
* ``get_field_by_name()``
* ``get_fields_with_model()``
* ``get_m2m_with_model()``
A :ref:`migration guide <migrating-old-meta-api>` has been provided to assist
in converting your code from the old API to the new, official API.
Loading ``cycle`` and ``firstof`` template tags from ``future`` library Loading ``cycle`` and ``firstof`` template tags from ``future`` library
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -1020,7 +1020,7 @@ class DBConstraintTestCase(TestCase):
self.assertEqual(models.Object.objects.count(), 2) self.assertEqual(models.Object.objects.count(), 2)
self.assertEqual(obj.related_objects.count(), 1) self.assertEqual(obj.related_objects.count(), 1)
intermediary_model = models.Object._meta.get_field_by_name("related_objects")[0].rel.through intermediary_model = models.Object._meta.get_field("related_objects").rel.through
intermediary_model.objects.create(from_object_id=obj.id, to_object_id=12345) intermediary_model.objects.create(from_object_id=obj.id, to_object_id=12345)
self.assertEqual(obj.related_objects.count(), 1) self.assertEqual(obj.related_objects.count(), 1)
self.assertEqual(intermediary_model.objects.count(), 2) self.assertEqual(intermediary_model.objects.count(), 2)

View File

@ -776,7 +776,7 @@ class ModelRefreshTests(TestCase):
class TestRelatedObjectDeprecation(TestCase): class TestRelatedObjectDeprecation(TestCase):
def test_field_related_deprecation(self): def test_field_related_deprecation(self):
field = SelfRef._meta.get_field_by_name('selfref')[0] field = SelfRef._meta.get_field('selfref')
with warnings.catch_warnings(record=True) as warns: with warnings.catch_warnings(record=True) as warns:
warnings.simplefilter('always') warnings.simplefilter('always')
self.assertIsInstance(field.related, ForeignObjectRel) self.assertIsInstance(field.related, ForeignObjectRel)

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
import os import os
import warnings import warnings
from django.apps import apps
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core import management from django.core import management
from django.db import connection, IntegrityError from django.db import connection, IntegrityError
@ -76,6 +77,7 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase):
]) ])
def test_loading_and_dumping(self): def test_loading_and_dumping(self):
apps.clear_cache()
Site.objects.all().delete() Site.objects.all().delete()
# Load fixture 1. Single JSON file, with two objects. # Load fixture 1. Single JSON file, with two objects.
management.call_command('loaddata', 'fixture1.json', verbosity=0) management.call_command('loaddata', 'fixture1.json', verbosity=0)

View File

@ -403,7 +403,8 @@ class GenericRelationsTests(TestCase):
self.assertEqual(tag.content_object.id, diamond.id) self.assertEqual(tag.content_object.id, diamond.id)
def test_query_content_type(self): def test_query_content_type(self):
with six.assertRaisesRegex(self, FieldError, "^Cannot resolve keyword 'content_object' into field."): msg = "Field 'content_object' does not generate an automatic reverse relation"
with self.assertRaisesMessage(FieldError, msg):
TaggedItem.objects.get(content_object='') TaggedItem.objects.get(content_object='')

View File

@ -260,7 +260,7 @@ class GenericRelationTests(TestCase):
form = GenericRelationForm({'links': None}) form = GenericRelationForm({'links': None})
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
form.save() form.save()
links = HasLinkThing._meta.get_field_by_name('links')[0] links = HasLinkThing._meta.get_field('links')
self.assertEqual(links.save_form_data_calls, 1) self.assertEqual(links.save_form_data_calls, 1)
def test_ticket_22998(self): def test_ticket_22998(self):

View File

@ -5,6 +5,12 @@ from .models import Issue, User, UnicodeReferenceModel
class RelatedObjectTests(TestCase): class RelatedObjectTests(TestCase):
def test_related_objects_have_name_attribute(self):
for field_name in ('test_issue_client', 'test_issue_cc'):
obj = User._meta.get_field(field_name)
self.assertEqual(field_name, obj.field.related_query_name())
def test_m2m_and_m2o(self): def test_m2m_and_m2o(self):
r = User.objects.create(username="russell") r = User.objects.create(username="russell")
g = User.objects.create(username="gustav") g = User.objects.create(username="gustav")

View File

@ -437,11 +437,11 @@ class ManyToOneTests(TestCase):
expected_message = "Cannot resolve keyword 'notafield' into field. Choices are: %s" expected_message = "Cannot resolve keyword 'notafield' into field. Choices are: %s"
self.assertRaisesMessage(FieldError, self.assertRaisesMessage(FieldError,
expected_message % ', '.join(Reporter._meta.get_all_field_names()), expected_message % ', '.join(sorted(f.name for f in Reporter._meta.get_fields())),
Article.objects.values_list, Article.objects.values_list,
'reporter__notafield') 'reporter__notafield')
self.assertRaisesMessage(FieldError, self.assertRaisesMessage(FieldError,
expected_message % ', '.join(['EXTRA'] + Article._meta.get_all_field_names()), expected_message % ', '.join(['EXTRA'] + sorted(f.name for f in Article._meta.get_fields())),
Article.objects.extra(select={'EXTRA': 'EXTRA_SELECT'}).values_list, Article.objects.extra(select={'EXTRA': 'EXTRA_SELECT'}).values_list,
'notafield') 'notafield')

View File

@ -202,8 +202,8 @@ class StateTests(TestCase):
)) ))
new_apps = project_state.apps new_apps = project_state.apps
self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field_by_name("name")[0].max_length, 100) self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field("name").max_length, 100)
self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field_by_name("hidden")[0].null, False) self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field("hidden").null, False)
self.assertEqual(len(new_apps.get_model("migrations", "SubTag")._meta.local_fields), 2) self.assertEqual(len(new_apps.get_model("migrations", "SubTag")._meta.local_fields), 2)

View File

@ -8,6 +8,11 @@ except ImportError:
Image = None Image = None
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db.models.fields.related import (
ForeignObject, ForeignKey, ManyToManyField, OneToOneField,
)
from django.db import models from django.db import models
from django.db.models.fields.files import ImageFieldFile, ImageField from django.db.models.fields.files import ImageFieldFile, ImageField
from django.utils import six from django.utils import six
@ -295,6 +300,52 @@ if Image:
height_field='headshot_height', height_field='headshot_height',
width_field='headshot_width') width_field='headshot_width')
class AllFieldsModel(models.Model):
big_integer = models.BigIntegerField()
binary = models.BinaryField()
boolean = models.BooleanField(default=False)
char = models.CharField(max_length=10)
csv = models.CommaSeparatedIntegerField(max_length=10)
date = models.DateField()
datetime = models.DateTimeField()
decimal = models.DecimalField(decimal_places=2, max_digits=2)
duration = models.DurationField()
email = models.EmailField()
file_path = models.FilePathField()
floatf = models.FloatField()
integer = models.IntegerField()
ip_address = models.IPAddressField()
generic_ip = models.GenericIPAddressField()
null_boolean = models.NullBooleanField()
positive_integer = models.PositiveIntegerField()
positive_small_integer = models.PositiveSmallIntegerField()
slug = models.SlugField()
small_integer = models.SmallIntegerField()
text = models.TextField()
time = models.TimeField()
url = models.URLField()
uuid = models.UUIDField()
fo = ForeignObject(
'self',
from_fields=['abstract_non_concrete_id'],
to_fields=['id'],
related_name='reverse'
)
fk = ForeignKey(
'self',
related_name='reverse2'
)
m2m = ManyToManyField('self')
oto = OneToOneField('self')
object_id = models.PositiveIntegerField()
content_type = models.ForeignKey(ContentType)
gfk = GenericForeignKey()
gr = GenericRelation(DataModel)
############################################################################### ###############################################################################

View File

@ -0,0 +1,220 @@
from django import test
from django.contrib.contenttypes.fields import (
GenericForeignKey, GenericRelation,
)
from django.db import models
from django.db.models.fields.related import (
ForeignObject, ForeignKey, OneToOneField, ManyToManyField,
ManyToOneRel, ForeignObjectRel,
)
from .models import AllFieldsModel
NON_CONCRETE_FIELDS = (
ForeignObject,
GenericForeignKey,
GenericRelation,
)
NON_EDITABLE_FIELDS = (
models.BinaryField,
GenericForeignKey,
GenericRelation,
)
RELATION_FIELDS = (
ForeignKey,
ForeignObject,
ManyToManyField,
OneToOneField,
GenericForeignKey,
GenericRelation,
)
ONE_TO_MANY_CLASSES = {
ForeignObject,
ForeignKey,
GenericForeignKey,
}
MANY_TO_ONE_CLASSES = {
ForeignObjectRel,
ManyToOneRel,
GenericRelation,
}
MANY_TO_MANY_CLASSES = {
ManyToManyField,
}
ONE_TO_ONE_CLASSES = {
OneToOneField,
}
FLAG_PROPERTIES = (
'concrete',
'editable',
'is_relation',
'model',
'hidden',
'one_to_many',
'many_to_one',
'many_to_many',
'one_to_one',
'related_model',
)
FLAG_PROPERTIES_FOR_RELATIONS = (
'one_to_many',
'many_to_one',
'many_to_many',
'one_to_one',
)
class FieldFlagsTests(test.TestCase):
@classmethod
def setUpClass(cls):
super(FieldFlagsTests, cls).setUpClass()
cls.fields = (
list(AllFieldsModel._meta.fields) +
list(AllFieldsModel._meta.virtual_fields)
)
cls.all_fields = (
cls.fields +
list(AllFieldsModel._meta.many_to_many) +
list(AllFieldsModel._meta.virtual_fields)
)
cls.fields_and_reverse_objects = (
cls.all_fields +
list(AllFieldsModel._meta.related_objects)
)
def test_each_field_should_have_a_concrete_attribute(self):
self.assertTrue(all(f.concrete.__class__ == bool for f in self.fields))
def test_each_field_should_have_an_editable_attribute(self):
self.assertTrue(all(f.editable.__class__ == bool for f in self.all_fields))
def test_each_field_should_have_a_has_rel_attribute(self):
self.assertTrue(all(f.is_relation.__class__ == bool for f in self.all_fields))
def test_each_object_should_have_auto_created(self):
self.assertTrue(
all(f.auto_created.__class__ == bool
for f in self.fields_and_reverse_objects)
)
def test_non_concrete_fields(self):
for field in self.fields:
if type(field) in NON_CONCRETE_FIELDS:
self.assertFalse(field.concrete)
else:
self.assertTrue(field.concrete)
def test_non_editable_fields(self):
for field in self.all_fields:
if type(field) in NON_EDITABLE_FIELDS:
self.assertFalse(field.editable)
else:
self.assertTrue(field.editable)
def test_related_fields(self):
for field in self.all_fields:
if type(field) in RELATION_FIELDS:
self.assertTrue(field.is_relation)
else:
self.assertFalse(field.is_relation)
def test_field_names_should_always_be_available(self):
for field in self.fields_and_reverse_objects:
self.assertTrue(field.name)
def test_all_field_types_should_have_flags(self):
for field in self.fields_and_reverse_objects:
for flag in FLAG_PROPERTIES:
self.assertTrue(hasattr(field, flag), "Field %s does not have flag %s" % (field, flag))
if field.is_relation:
true_cardinality_flags = sum(
getattr(field, flag) is True
for flag in FLAG_PROPERTIES_FOR_RELATIONS
)
# If the field has a relation, there should be only one of the
# 4 cardinality flags available.
self.assertEqual(1, true_cardinality_flags)
def test_cardinality_m2m(self):
m2m_type_fields = (
f for f in self.all_fields
if f.is_relation and f.many_to_many
)
# Test classes are what we expect
self.assertEqual(MANY_TO_MANY_CLASSES, {f.__class__ for f in m2m_type_fields})
# Ensure all m2m reverses are m2m
for field in m2m_type_fields:
reverse_field = field.rel
self.assertTrue(reverse_field.is_relation)
self.assertTrue(reverse_field.many_to_many)
self.assertTrue(reverse_field.related_model)
def test_cardinality_o2m(self):
o2m_type_fields = [
f for f in self.fields_and_reverse_objects
if f.is_relation and f.one_to_many
]
# Test classes are what we expect
self.assertEqual(ONE_TO_MANY_CLASSES, {f.__class__ for f in o2m_type_fields})
# Ensure all o2m reverses are m2o
for field in o2m_type_fields:
if field.concrete:
reverse_field = field.rel
self.assertTrue(reverse_field.is_relation and reverse_field.many_to_one)
def test_cardinality_m2o(self):
m2o_type_fields = [
f for f in self.fields_and_reverse_objects
if f.is_relation and f.many_to_one
]
# Test classes are what we expect
self.assertEqual(MANY_TO_ONE_CLASSES, {f.__class__ for f in m2o_type_fields})
# Ensure all m2o reverses are o2m
for obj in m2o_type_fields:
if hasattr(obj, 'field'):
reverse_field = obj.field
self.assertTrue(reverse_field.is_relation and reverse_field.one_to_many)
def test_cardinality_o2o(self):
o2o_type_fields = [
f for f in self.all_fields
if f.is_relation and f.one_to_one
]
# Test classes are what we expect
self.assertEqual(ONE_TO_ONE_CLASSES, {f.__class__ for f in o2o_type_fields})
# Ensure all o2o reverses are o2o
for obj in o2o_type_fields:
if hasattr(obj, 'field'):
reverse_field = obj.field
self.assertTrue(reverse_field.is_relation and reverse_field.one_to_one)
def test_hidden_flag(self):
incl_hidden = set(AllFieldsModel._meta.get_fields(include_hidden=True))
no_hidden = set(AllFieldsModel._meta.get_fields())
fields_that_should_be_hidden = (incl_hidden - no_hidden)
for f in incl_hidden:
self.assertEqual(f in fields_that_should_be_hidden, f.hidden)
def test_model_and_reverse_model_should_equal_on_relations(self):
for field in AllFieldsModel._meta.get_fields():
is_concrete_forward_field = field.concrete and field.related_model
if is_concrete_forward_field:
reverse_field = field.rel
self.assertEqual(field.model, reverse_field.related_model)
self.assertEqual(field.related_model, reverse_field.model)

View File

@ -198,7 +198,7 @@ class ForeignKeyTests(test.TestCase):
self.assertEqual(warnings, expected_warnings) self.assertEqual(warnings, expected_warnings)
def test_related_name_converted_to_text(self): def test_related_name_converted_to_text(self):
rel_name = Bar._meta.get_field_by_name('a')[0].rel.related_name rel_name = Bar._meta.get_field('a').rel.related_name
self.assertIsInstance(rel_name, six.text_type) self.assertIsInstance(rel_name, six.text_type)

796
tests/model_meta/results.py Normal file
View File

@ -0,0 +1,796 @@
from .models import (
AbstractPerson, BasePerson, Person, Relating, Relation,
)
TEST_RESULTS = {
'get_all_field_names': {
Person: [
'baseperson_ptr',
'baseperson_ptr_id',
'content_type_abstract',
'content_type_abstract_id',
'content_type_base',
'content_type_base_id',
'content_type_concrete',
'content_type_concrete_id',
'data_abstract',
'data_base',
'data_inherited',
'data_not_concrete_abstract',
'data_not_concrete_base',
'data_not_concrete_inherited',
'fk_abstract',
'fk_abstract_id',
'fk_base',
'fk_base_id',
'fk_inherited',
'fk_inherited_id',
'followers_abstract',
'followers_base',
'followers_concrete',
'following_abstract',
'following_base',
'following_inherited',
'friends_abstract',
'friends_base',
'friends_inherited',
'generic_relation_abstract',
'generic_relation_base',
'generic_relation_concrete',
'id',
'm2m_abstract',
'm2m_base',
'm2m_inherited',
'object_id_abstract',
'object_id_base',
'object_id_concrete',
'relating_basepeople',
'relating_baseperson',
'relating_people',
'relating_person',
],
BasePerson: [
'content_type_abstract',
'content_type_abstract_id',
'content_type_base',
'content_type_base_id',
'data_abstract',
'data_base',
'data_not_concrete_abstract',
'data_not_concrete_base',
'fk_abstract',
'fk_abstract_id',
'fk_base',
'fk_base_id',
'followers_abstract',
'followers_base',
'following_abstract',
'following_base',
'friends_abstract',
'friends_base',
'generic_relation_abstract',
'generic_relation_base',
'id',
'm2m_abstract',
'm2m_base',
'object_id_abstract',
'object_id_base',
'person',
'relating_basepeople',
'relating_baseperson'
],
AbstractPerson: [
'content_type_abstract',
'content_type_abstract_id',
'data_abstract',
'data_not_concrete_abstract',
'fk_abstract',
'fk_abstract_id',
'following_abstract',
'friends_abstract',
'generic_relation_abstract',
'm2m_abstract',
'object_id_abstract',
],
Relating: [
'basepeople',
'basepeople_hidden',
'baseperson',
'baseperson_hidden',
'baseperson_hidden_id',
'baseperson_id',
'id',
'people',
'people_hidden',
'person',
'person_hidden',
'person_hidden_id',
'person_id',
'proxyperson',
'proxyperson_hidden',
'proxyperson_hidden_id',
'proxyperson_id',
],
},
'fields': {
Person: [
'id',
'data_abstract',
'fk_abstract_id',
'data_not_concrete_abstract',
'content_type_abstract_id',
'object_id_abstract',
'data_base',
'fk_base_id',
'data_not_concrete_base',
'content_type_base_id',
'object_id_base',
'baseperson_ptr_id',
'data_inherited',
'fk_inherited_id',
'data_not_concrete_inherited',
'content_type_concrete_id',
'object_id_concrete',
],
BasePerson: [
'id',
'data_abstract',
'fk_abstract_id',
'data_not_concrete_abstract',
'content_type_abstract_id',
'object_id_abstract',
'data_base',
'fk_base_id',
'data_not_concrete_base',
'content_type_base_id',
'object_id_base',
],
AbstractPerson: [
'data_abstract',
'fk_abstract_id',
'data_not_concrete_abstract',
'content_type_abstract_id',
'object_id_abstract',
],
Relating: [
'id',
'baseperson_id',
'baseperson_hidden_id',
'person_id',
'person_hidden_id',
'proxyperson_id',
'proxyperson_hidden_id',
],
},
'local_fields': {
Person: [
'baseperson_ptr_id',
'data_inherited',
'fk_inherited_id',
'data_not_concrete_inherited',
'content_type_concrete_id',
'object_id_concrete',
],
BasePerson: [
'id',
'data_abstract',
'fk_abstract_id',
'data_not_concrete_abstract',
'content_type_abstract_id',
'object_id_abstract',
'data_base',
'fk_base_id',
'data_not_concrete_base',
'content_type_base_id',
'object_id_base',
],
AbstractPerson: [
'data_abstract',
'fk_abstract_id',
'data_not_concrete_abstract',
'content_type_abstract_id',
'object_id_abstract',
],
Relating: [
'id',
'baseperson_id',
'baseperson_hidden_id',
'person_id',
'person_hidden_id',
'proxyperson_id',
'proxyperson_hidden_id',
],
},
'local_concrete_fields': {
Person: [
'baseperson_ptr_id',
'data_inherited',
'fk_inherited_id',
'content_type_concrete_id',
'object_id_concrete',
],
BasePerson: [
'id',
'data_abstract',
'fk_abstract_id',
'content_type_abstract_id',
'object_id_abstract',
'data_base',
'fk_base_id',
'content_type_base_id',
'object_id_base',
],
AbstractPerson: [
'data_abstract',
'fk_abstract_id',
'content_type_abstract_id',
'object_id_abstract',
],
Relating: [
'id',
'baseperson_id',
'baseperson_hidden_id',
'person_id',
'person_hidden_id',
'proxyperson_id',
'proxyperson_hidden_id',
],
},
'many_to_many': {
Person: [
'm2m_abstract',
'friends_abstract',
'following_abstract',
'm2m_base',
'friends_base',
'following_base',
'm2m_inherited',
'friends_inherited',
'following_inherited',
],
BasePerson: [
'm2m_abstract',
'friends_abstract',
'following_abstract',
'm2m_base',
'friends_base',
'following_base',
],
AbstractPerson: [
'm2m_abstract',
'friends_abstract',
'following_abstract',
],
Relating: [
'basepeople',
'basepeople_hidden',
'people',
'people_hidden',
],
},
'many_to_many_with_model': {
Person: [
BasePerson,
BasePerson,
BasePerson,
BasePerson,
BasePerson,
BasePerson,
None,
None,
None,
],
BasePerson: [
None,
None,
None,
None,
None,
None,
],
AbstractPerson: [
None,
None,
None,
],
Relating: [
None,
None,
None,
None,
],
},
'get_all_related_objects_with_model_legacy': {
Person: (
('relating_baseperson', BasePerson),
('relating_person', None),
),
BasePerson: (
('person', None),
('relating_baseperson', None),
),
Relation: (
('fk_abstract_rel', None),
('fo_abstract_rel', None),
('fk_base_rel', None),
('fo_base_rel', None),
('fk_concrete_rel', None),
('fo_concrete_rel', None),
),
},
'get_all_related_objects_with_model_hidden_local': {
Person: (
('+', None),
('+', None),
('Person_following_inherited+', None),
('Person_following_inherited+', None),
('Person_friends_inherited+', None),
('Person_friends_inherited+', None),
('Person_m2m_inherited+', None),
('Relating_people+', None),
('Relating_people_hidden+', None),
('followers_concrete', None),
('friends_inherited_rel_+', None),
('relating_people', None),
('relating_person', None),
),
BasePerson: (
('+', None),
('+', None),
('BasePerson_following_abstract+', None),
('BasePerson_following_abstract+', None),
('BasePerson_following_base+', None),
('BasePerson_following_base+', None),
('BasePerson_friends_abstract+', None),
('BasePerson_friends_abstract+', None),
('BasePerson_friends_base+', None),
('BasePerson_friends_base+', None),
('BasePerson_m2m_abstract+', None),
('BasePerson_m2m_base+', None),
('Relating_basepeople+', None),
('Relating_basepeople_hidden+', None),
('followers_abstract', None),
('followers_base', None),
('friends_abstract_rel_+', None),
('friends_base_rel_+', None),
('person', None),
('relating_basepeople', None),
('relating_baseperson', None),
),
Relation: (
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('BasePerson_m2m_abstract+', None),
('BasePerson_m2m_base+', None),
('Person_m2m_inherited+', None),
('fk_abstract_rel', None),
('fk_base_rel', None),
('fk_concrete_rel', None),
('fo_abstract_rel', None),
('fo_base_rel', None),
('fo_concrete_rel', None),
('m2m_abstract_rel', None),
('m2m_base_rel', None),
('m2m_concrete_rel', None),
),
},
'get_all_related_objects_with_model_hidden': {
Person: (
('+', BasePerson),
('+', BasePerson),
('+', None),
('+', None),
('BasePerson_following_abstract+', BasePerson),
('BasePerson_following_abstract+', BasePerson),
('BasePerson_following_base+', BasePerson),
('BasePerson_following_base+', BasePerson),
('BasePerson_friends_abstract+', BasePerson),
('BasePerson_friends_abstract+', BasePerson),
('BasePerson_friends_base+', BasePerson),
('BasePerson_friends_base+', BasePerson),
('BasePerson_m2m_abstract+', BasePerson),
('BasePerson_m2m_base+', BasePerson),
('Person_following_inherited+', None),
('Person_following_inherited+', None),
('Person_friends_inherited+', None),
('Person_friends_inherited+', None),
('Person_m2m_inherited+', None),
('Relating_basepeople+', BasePerson),
('Relating_basepeople_hidden+', BasePerson),
('Relating_people+', None),
('Relating_people_hidden+', None),
('followers_abstract', BasePerson),
('followers_base', BasePerson),
('followers_concrete', None),
('friends_abstract_rel_+', BasePerson),
('friends_base_rel_+', BasePerson),
('friends_inherited_rel_+', None),
('relating_basepeople', BasePerson),
('relating_baseperson', BasePerson),
('relating_people', None),
('relating_person', None),
),
BasePerson: (
('+', None),
('+', None),
('BasePerson_following_abstract+', None),
('BasePerson_following_abstract+', None),
('BasePerson_following_base+', None),
('BasePerson_following_base+', None),
('BasePerson_friends_abstract+', None),
('BasePerson_friends_abstract+', None),
('BasePerson_friends_base+', None),
('BasePerson_friends_base+', None),
('BasePerson_m2m_abstract+', None),
('BasePerson_m2m_base+', None),
('Relating_basepeople+', None),
('Relating_basepeople_hidden+', None),
('followers_abstract', None),
('followers_base', None),
('friends_abstract_rel_+', None),
('friends_base_rel_+', None),
('person', None),
('relating_basepeople', None),
('relating_baseperson', None),
),
Relation: (
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('BasePerson_m2m_abstract+', None),
('BasePerson_m2m_base+', None),
('Person_m2m_inherited+', None),
('fk_abstract_rel', None),
('fk_base_rel', None),
('fk_concrete_rel', None),
('fo_abstract_rel', None),
('fo_base_rel', None),
('fo_concrete_rel', None),
('m2m_abstract_rel', None),
('m2m_base_rel', None),
('m2m_concrete_rel', None),
),
},
'get_all_related_objects_with_model_local': {
Person: (
('followers_concrete', None),
('relating_person', None),
('relating_people', None),
),
BasePerson: (
('followers_abstract', None),
('followers_base', None),
('person', None),
('relating_baseperson', None),
('relating_basepeople', None),
),
Relation: (
('fk_abstract_rel', None),
('fo_abstract_rel', None),
('fk_base_rel', None),
('fo_base_rel', None),
('m2m_abstract_rel', None),
('m2m_base_rel', None),
('fk_concrete_rel', None),
('fo_concrete_rel', None),
('m2m_concrete_rel', None),
),
},
'get_all_related_objects_with_model': {
Person: (
('followers_abstract', BasePerson),
('followers_base', BasePerson),
('relating_baseperson', BasePerson),
('relating_basepeople', BasePerson),
('followers_concrete', None),
('relating_person', None),
('relating_people', None),
),
BasePerson: (
('followers_abstract', None),
('followers_base', None),
('person', None),
('relating_baseperson', None),
('relating_basepeople', None),
),
Relation: (
('fk_abstract_rel', None),
('fo_abstract_rel', None),
('fk_base_rel', None),
('fo_base_rel', None),
('m2m_abstract_rel', None),
('m2m_base_rel', None),
('fk_concrete_rel', None),
('fo_concrete_rel', None),
('m2m_concrete_rel', None),
),
},
'get_all_related_objects_with_model_local_legacy': {
Person: (
('relating_person', None),
),
BasePerson: (
('person', None),
('relating_baseperson', None)
),
Relation: (
('fk_abstract_rel', None),
('fo_abstract_rel', None),
('fk_base_rel', None),
('fo_base_rel', None),
('fk_concrete_rel', None),
('fo_concrete_rel', None),
),
},
'get_all_related_objects_with_model_hidden_legacy': {
BasePerson: (
('+', None),
('BasePerson_following_abstract+', None),
('BasePerson_following_abstract+', None),
('BasePerson_following_base+', None),
('BasePerson_following_base+', None),
('BasePerson_friends_abstract+', None),
('BasePerson_friends_abstract+', None),
('BasePerson_friends_base+', None),
('BasePerson_friends_base+', None),
('BasePerson_m2m_abstract+', None),
('BasePerson_m2m_base+', None),
('Relating_basepeople+', None),
('Relating_basepeople_hidden+', None),
('person', None),
('relating_baseperson', None),
),
Person: (
('+', BasePerson),
('+', None),
('BasePerson_following_abstract+', BasePerson),
('BasePerson_following_abstract+', BasePerson),
('BasePerson_following_base+', BasePerson),
('BasePerson_following_base+', BasePerson),
('BasePerson_friends_abstract+', BasePerson),
('BasePerson_friends_abstract+', BasePerson),
('BasePerson_friends_base+', BasePerson),
('BasePerson_friends_base+', BasePerson),
('BasePerson_m2m_abstract+', BasePerson),
('BasePerson_m2m_base+', BasePerson),
('Person_following_inherited+', None),
('Person_following_inherited+', None),
('Person_friends_inherited+', None),
('Person_friends_inherited+', None),
('Person_m2m_inherited+', None),
('Relating_basepeople+', BasePerson),
('Relating_basepeople_hidden+', BasePerson),
('Relating_people+', None),
('Relating_people_hidden+', None),
('relating_baseperson', BasePerson),
('relating_person', None),
),
Relation: (
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('BasePerson_m2m_abstract+', None),
('BasePerson_m2m_base+', None),
('Person_m2m_inherited+', None),
('fk_abstract_rel', None),
('fk_base_rel', None),
('fk_concrete_rel', None),
('fo_abstract_rel', None),
('fo_base_rel', None),
('fo_concrete_rel', None),
),
},
'get_all_related_objects_with_model_hidden_local_legacy': {
BasePerson: (
('+', None),
('BasePerson_following_abstract+', None),
('BasePerson_following_abstract+', None),
('BasePerson_following_base+', None),
('BasePerson_following_base+', None),
('BasePerson_friends_abstract+', None),
('BasePerson_friends_abstract+', None),
('BasePerson_friends_base+', None),
('BasePerson_friends_base+', None),
('BasePerson_m2m_abstract+', None),
('BasePerson_m2m_base+', None),
('Relating_basepeople+', None),
('Relating_basepeople_hidden+', None),
('person', None),
('relating_baseperson', None),
),
Person: (
('+', None),
('Person_following_inherited+', None),
('Person_following_inherited+', None),
('Person_friends_inherited+', None),
('Person_friends_inherited+', None),
('Person_m2m_inherited+', None),
('Relating_people+', None),
('Relating_people_hidden+', None),
('relating_person', None),
),
Relation: (
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('BasePerson_m2m_abstract+', None),
('BasePerson_m2m_base+', None),
('Person_m2m_inherited+', None),
('fk_abstract_rel', None),
('fk_base_rel', None),
('fk_concrete_rel', None),
('fo_abstract_rel', None),
('fo_base_rel', None),
('fo_concrete_rel', None),
),
},
'get_all_related_objects_with_model_proxy_legacy': {
BasePerson: (
('person', None),
('relating_baseperson', None),
),
Person: (
('relating_baseperson', BasePerson),
('relating_person', None), ('relating_proxyperson', None),
),
Relation: (
('fk_abstract_rel', None), ('fo_abstract_rel', None),
('fk_base_rel', None), ('fo_base_rel', None),
('fk_concrete_rel', None), ('fo_concrete_rel', None),
),
},
'get_all_related_objects_with_model_proxy_hidden_legacy': {
BasePerson: (
('+', None),
('BasePerson_following_abstract+', None),
('BasePerson_following_abstract+', None),
('BasePerson_following_base+', None),
('BasePerson_following_base+', None),
('BasePerson_friends_abstract+', None),
('BasePerson_friends_abstract+', None),
('BasePerson_friends_base+', None),
('BasePerson_friends_base+', None),
('BasePerson_m2m_abstract+', None),
('BasePerson_m2m_base+', None),
('Relating_basepeople+', None),
('Relating_basepeople_hidden+', None),
('person', None),
('relating_baseperson', None),
),
Person: (
('+', BasePerson),
('+', None),
('+', None),
('BasePerson_following_abstract+', BasePerson),
('BasePerson_following_abstract+', BasePerson),
('BasePerson_following_base+', BasePerson),
('BasePerson_following_base+', BasePerson),
('BasePerson_friends_abstract+', BasePerson),
('BasePerson_friends_abstract+', BasePerson),
('BasePerson_friends_base+', BasePerson),
('BasePerson_friends_base+', BasePerson),
('BasePerson_m2m_abstract+', BasePerson),
('BasePerson_m2m_base+', BasePerson),
('Person_following_inherited+', None),
('Person_following_inherited+', None),
('Person_friends_inherited+', None),
('Person_friends_inherited+', None),
('Person_m2m_inherited+', None),
('Relating_basepeople+', BasePerson),
('Relating_basepeople_hidden+', BasePerson),
('Relating_people+', None),
('Relating_people_hidden+', None),
('relating_baseperson', BasePerson),
('relating_person', None),
('relating_proxyperson', None),
),
Relation: (
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('+', None),
('BasePerson_m2m_abstract+', None),
('BasePerson_m2m_base+', None),
('Person_m2m_inherited+', None),
('fk_abstract_rel', None),
('fk_base_rel', None),
('fk_concrete_rel', None),
('fo_abstract_rel', None),
('fo_base_rel', None),
('fo_concrete_rel', None),
),
},
'get_all_related_many_to_many_with_model_legacy': {
BasePerson: (
('friends_abstract_rel_+', None),
('followers_abstract', None),
('friends_base_rel_+', None),
('followers_base', None),
('relating_basepeople', None),
('+', None),
),
Person: (
('friends_abstract_rel_+', BasePerson),
('followers_abstract', BasePerson),
('friends_base_rel_+', BasePerson),
('followers_base', BasePerson),
('relating_basepeople', BasePerson),
('+', BasePerson),
('friends_inherited_rel_+', None),
('followers_concrete', None),
('relating_people', None),
('+', None),
),
Relation: (
('m2m_abstract_rel', None),
('m2m_base_rel', None),
('m2m_concrete_rel', None),
),
},
'get_all_related_many_to_many_local_legacy': {
BasePerson: [
'friends_abstract_rel_+',
'followers_abstract',
'friends_base_rel_+',
'followers_base',
'relating_basepeople',
'+',
],
Person: [
'friends_inherited_rel_+',
'followers_concrete',
'relating_people',
'+',
],
Relation: [
'm2m_abstract_rel',
'm2m_base_rel',
'm2m_concrete_rel',
],
},
'virtual_fields': {
AbstractPerson: [
'generic_relation_abstract',
'content_object_abstract',
],
BasePerson: [
'generic_relation_base',
'content_object_base',
'generic_relation_abstract',
'content_object_abstract',
],
Person: [
'content_object_concrete',
'generic_relation_concrete',
'generic_relation_base',
'content_object_base',
'generic_relation_abstract',
'content_object_abstract',
],
},
}

View File

@ -1,661 +0,0 @@
from django import test
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields import related, CharField, Field
from .models import (
AbstractPerson, BasePerson, Person, Relating, Relation
)
TEST_RESULTS = {
'fields': {
Person: [
'id',
'data_abstract',
'fk_abstract_id',
'data_not_concrete_abstract',
'content_type_abstract_id',
'object_id_abstract',
'data_base',
'fk_base_id',
'data_not_concrete_base',
'content_type_base_id',
'object_id_base',
'baseperson_ptr_id',
'data_inherited',
'fk_inherited_id',
'data_not_concrete_inherited',
'content_type_concrete_id',
'object_id_concrete',
],
BasePerson: [
'id',
'data_abstract',
'fk_abstract_id',
'data_not_concrete_abstract',
'content_type_abstract_id',
'object_id_abstract',
'data_base',
'fk_base_id',
'data_not_concrete_base',
'content_type_base_id',
'object_id_base',
],
AbstractPerson: [
'data_abstract',
'fk_abstract_id',
'data_not_concrete_abstract',
'content_type_abstract_id',
'object_id_abstract',
],
Relating: [
'id',
'baseperson_id',
'baseperson_hidden_id',
'person_id',
'person_hidden_id',
'proxyperson_id',
'proxyperson_hidden_id',
],
},
'local_fields': {
Person: [
'baseperson_ptr_id',
'data_inherited',
'fk_inherited_id',
'data_not_concrete_inherited',
'content_type_concrete_id',
'object_id_concrete',
],
BasePerson: [
'id',
'data_abstract',
'fk_abstract_id',
'data_not_concrete_abstract',
'content_type_abstract_id',
'object_id_abstract',
'data_base',
'fk_base_id',
'data_not_concrete_base',
'content_type_base_id',
'object_id_base',
],
AbstractPerson: [
'data_abstract',
'fk_abstract_id',
'data_not_concrete_abstract',
'content_type_abstract_id',
'object_id_abstract',
],
Relating: [
'id',
'baseperson_id',
'baseperson_hidden_id',
'person_id',
'person_hidden_id',
'proxyperson_id',
'proxyperson_hidden_id',
],
},
'local_concrete_fields': {
Person: [
'baseperson_ptr_id',
'data_inherited',
'fk_inherited_id',
'content_type_concrete_id',
'object_id_concrete',
],
BasePerson: [
'id',
'data_abstract',
'fk_abstract_id',
'content_type_abstract_id',
'object_id_abstract',
'data_base',
'fk_base_id',
'content_type_base_id',
'object_id_base',
],
AbstractPerson: [
'data_abstract',
'fk_abstract_id',
'content_type_abstract_id',
'object_id_abstract',
],
Relating: [
'id',
'baseperson_id',
'baseperson_hidden_id',
'person_id',
'person_hidden_id',
'proxyperson_id',
'proxyperson_hidden_id',
],
},
'many_to_many': {
Person: [
'm2m_abstract',
'friends_abstract',
'following_abstract',
'm2m_base',
'friends_base',
'following_base',
'm2m_inherited',
'friends_inherited',
'following_inherited',
],
BasePerson: [
'm2m_abstract',
'friends_abstract',
'following_abstract',
'm2m_base',
'friends_base',
'following_base',
],
AbstractPerson: [
'm2m_abstract',
'friends_abstract',
'following_abstract',
],
Relating: [
'basepeople',
'basepeople_hidden',
'people',
'people_hidden',
],
},
'many_to_many_with_model': {
Person: [
BasePerson,
BasePerson,
BasePerson,
BasePerson,
BasePerson,
BasePerson,
None,
None,
None,
],
BasePerson: [
None,
None,
None,
None,
None,
None,
],
AbstractPerson: [
None,
None,
None,
],
Relating: [
None,
None,
None,
None,
],
},
'get_all_related_objects_with_model': {
Person: (
('relating_baseperson', BasePerson),
('relating_person', None),
),
BasePerson: (
('person', None),
('relating_baseperson', None),
),
Relation: (
('fk_abstract_rel', None),
('fo_abstract_rel', None),
('fk_base_rel', None),
('fo_base_rel', None),
('fk_concrete_rel', None),
('fo_concrete_rel', None),
),
},
'get_all_related_objects_with_model_local': {
Person: (
('relating_person', None),
),
BasePerson: (
('person', None),
('relating_baseperson', None)
),
Relation: (
('fk_abstract_rel', None),
('fo_abstract_rel', None),
('fk_base_rel', None),
('fo_base_rel', None),
('fk_concrete_rel', None),
('fo_concrete_rel', None),
),
},
'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),
),
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),
),
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),
),
},
'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),
),
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),
),
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),
),
},
'get_all_related_objects_with_model_proxy': {
BasePerson: (
('person', None),
('relating_baseperson', None),
),
Person: (
('relating_baseperson', BasePerson),
('relating_person', None), ('relating_proxyperson', None),
),
Relation: (
('fk_abstract_rel', None), ('fo_abstract_rel', None),
('fk_base_rel', None), ('fo_base_rel', None),
('fk_concrete_rel', None), ('fo_concrete_rel', None),
),
},
'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),
),
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),
),
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),
),
},
'get_all_related_many_to_many_with_model': {
BasePerson: (
('friends_abstract_rel_+', None),
('followers_abstract', None),
('friends_base_rel_+', None),
('followers_base', None),
('relating_basepeople', None),
('+', None),
),
Person: (
('friends_abstract_rel_+', BasePerson),
('followers_abstract', BasePerson),
('friends_base_rel_+', BasePerson),
('followers_base', BasePerson),
('relating_basepeople', BasePerson),
('+', BasePerson),
('friends_inherited_rel_+', None),
('followers_concrete', None),
('relating_people', None),
('+', None),
),
Relation: (
('m2m_abstract_rel', None),
('m2m_base_rel', None),
('m2m_concrete_rel', None),
),
},
'get_all_related_many_to_many_local': {
BasePerson: [
'friends_abstract_rel_+',
'followers_abstract',
'friends_base_rel_+',
'followers_base',
'relating_basepeople',
'+',
],
Person: [
'friends_inherited_rel_+',
'followers_concrete',
'relating_people',
'+',
],
Relation: [
'm2m_abstract_rel',
'm2m_base_rel',
'm2m_concrete_rel',
],
},
'virtual_fields': {
AbstractPerson: [
'generic_relation_abstract',
'content_object_abstract',
],
BasePerson: [
'generic_relation_base',
'content_object_base',
'generic_relation_abstract',
'content_object_abstract',
],
Person: [
'content_object_concrete',
'generic_relation_concrete',
'generic_relation_base',
'content_object_base',
'generic_relation_abstract',
'content_object_abstract',
],
},
}
class OptionsBaseTests(test.TestCase):
def _map_rq_names(self, res):
return tuple((o.field.related_query_name(), m) for o, m in res)
def _map_names(self, res):
return tuple((f.name, m) for f, m in res)
class DataTests(OptionsBaseTests):
def test_fields(self):
for model, expected_result in TEST_RESULTS['fields'].items():
fields = model._meta.fields
self.assertEqual([f.attname for f in fields], expected_result)
def test_local_fields(self):
is_data_field = lambda f: isinstance(f, Field) and not isinstance(f, related.ManyToManyField)
for model, expected_result in TEST_RESULTS['local_fields'].items():
fields = model._meta.local_fields
self.assertEqual([f.attname for f in fields], expected_result)
self.assertTrue(all([f.model is model for f in fields]))
self.assertTrue(all([is_data_field(f) for f in fields]))
def test_local_concrete_fields(self):
for model, expected_result in TEST_RESULTS['local_concrete_fields'].items():
fields = model._meta.local_concrete_fields
self.assertEqual([f.attname for f in fields], expected_result)
self.assertTrue(all([f.column is not None for f in fields]))
class M2MTests(OptionsBaseTests):
def test_many_to_many(self):
for model, expected_result in TEST_RESULTS['many_to_many'].items():
fields = model._meta.many_to_many
self.assertEqual([f.attname for f in fields], expected_result)
self.assertTrue(all([isinstance(f.rel, related.ManyToManyRel) for f in fields]))
def test_many_to_many_with_model(self):
for model, expected_result in TEST_RESULTS['many_to_many_with_model'].items():
models = [model for field, model in model._meta.get_m2m_with_model()]
self.assertEqual(models, expected_result)
class RelatedObjectsTests(OptionsBaseTests):
def setUp(self):
self.key_name = lambda r: r[0]
def test_related_objects(self):
result_key = 'get_all_related_objects_with_model'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model()
self.assertEqual(self._map_rq_names(objects), expected)
def test_related_objects_local(self):
result_key = 'get_all_related_objects_with_model_local'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model(local_only=True)
self.assertEqual(self._map_rq_names(objects), expected)
def test_related_objects_include_hidden(self):
result_key = 'get_all_related_objects_with_model_hidden'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model(include_hidden=True)
self.assertEqual(
sorted(self._map_names(objects), key=self.key_name),
sorted(expected, key=self.key_name)
)
def test_related_objects_include_hidden_local_only(self):
result_key = 'get_all_related_objects_with_model_hidden_local'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model(
include_hidden=True, local_only=True)
self.assertEqual(
sorted(self._map_names(objects), key=self.key_name),
sorted(expected, key=self.key_name)
)
def test_related_objects_proxy(self):
result_key = 'get_all_related_objects_with_model_proxy'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model(
include_proxy_eq=True)
self.assertEqual(self._map_rq_names(objects), expected)
def test_related_objects_proxy_hidden(self):
result_key = 'get_all_related_objects_with_model_proxy_hidden'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model(
include_proxy_eq=True, include_hidden=True)
self.assertEqual(
sorted(self._map_names(objects), key=self.key_name),
sorted(expected, key=self.key_name)
)
class RelatedM2MTests(OptionsBaseTests):
def test_related_m2m_with_model(self):
result_key = 'get_all_related_many_to_many_with_model'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_m2m_objects_with_model()
self.assertEqual(self._map_rq_names(objects), expected)
def test_related_m2m_local_only(self):
result_key = 'get_all_related_many_to_many_local'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_many_to_many_objects(local_only=True)
self.assertEqual([o.field.related_query_name() for o in objects], expected)
def test_related_m2m_asymmetrical(self):
m2m = Person._meta.many_to_many
self.assertIn('following_base', [f.attname for f in m2m])
related_m2m = Person._meta.get_all_related_many_to_many_objects()
self.assertIn('followers_base', [o.field.related_query_name() for o in related_m2m])
def test_related_m2m_symmetrical(self):
m2m = Person._meta.many_to_many
self.assertIn('friends_base', [f.attname for f in m2m])
related_m2m = Person._meta.get_all_related_many_to_many_objects()
self.assertIn('friends_inherited_rel_+', [o.field.related_query_name() for o in related_m2m])
class VirtualFieldsTests(OptionsBaseTests):
def test_virtual_fields(self):
for model, expected_names in TEST_RESULTS['virtual_fields'].items():
objects = model._meta.virtual_fields
self.assertEqual(sorted([f.name for f in objects]), sorted(expected_names))
class GetFieldByNameTests(OptionsBaseTests):
def test_get_data_field(self):
field_info = Person._meta.get_field_by_name('data_abstract')
self.assertEqual(field_info[1:], (BasePerson, True, False))
self.assertIsInstance(field_info[0], CharField)
def test_get_m2m_field(self):
field_info = Person._meta.get_field_by_name('m2m_base')
self.assertEqual(field_info[1:], (BasePerson, True, True))
self.assertIsInstance(field_info[0], related.ManyToManyField)
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.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.ForeignObjectRel)
def test_get_generic_foreign_key(self):
# For historic reasons generic foreign keys aren't available.
with self.assertRaises(FieldDoesNotExist):
Person._meta.get_field_by_name('content_object_base')
def test_get_generic_relation(self):
field_info = Person._meta.get_field_by_name('generic_relation_base')
self.assertEqual(field_info[1:], (None, True, False))
self.assertIsInstance(field_info[0], GenericRelation)

View File

@ -0,0 +1,166 @@
import warnings
from django import test
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields import related, CharField
from django.utils.deprecation import RemovedInDjango20Warning
from .models import BasePerson, Person
from .results import TEST_RESULTS
class OptionsBaseTests(test.TestCase):
def _map_related_query_names(self, res):
return tuple((o.field.related_query_name(), m) for o, m in res)
def _map_names(self, res):
return tuple((f.name, m) for f, m in res)
class M2MTests(OptionsBaseTests):
def test_many_to_many_with_model(self):
for model, expected_result in TEST_RESULTS['many_to_many_with_model'].items():
with warnings.catch_warnings(record=True) as warning:
warnings.simplefilter("always")
models = [model for field, model in model._meta.get_m2m_with_model()]
self.assertEqual([RemovedInDjango20Warning], [w.message.__class__ for w in warning])
self.assertEqual(models, expected_result)
@test.ignore_warnings(category=RemovedInDjango20Warning)
class RelatedObjectsTests(OptionsBaseTests):
key_name = lambda self, r: r[0]
def test_related_objects(self):
result_key = 'get_all_related_objects_with_model_legacy'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model()
self.assertEqual(self._map_related_query_names(objects), expected)
def test_related_objects_local(self):
result_key = 'get_all_related_objects_with_model_local_legacy'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model(local_only=True)
self.assertEqual(self._map_related_query_names(objects), expected)
def test_related_objects_include_hidden(self):
result_key = 'get_all_related_objects_with_model_hidden_legacy'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model(include_hidden=True)
self.assertEqual(
sorted(self._map_names(objects), key=self.key_name),
sorted(expected, key=self.key_name)
)
def test_related_objects_include_hidden_local_only(self):
result_key = 'get_all_related_objects_with_model_hidden_local_legacy'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model(
include_hidden=True, local_only=True)
self.assertEqual(
sorted(self._map_names(objects), key=self.key_name),
sorted(expected, key=self.key_name)
)
def test_related_objects_proxy(self):
result_key = 'get_all_related_objects_with_model_proxy_legacy'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model(
include_proxy_eq=True)
self.assertEqual(self._map_related_query_names(objects), expected)
def test_related_objects_proxy_hidden(self):
result_key = 'get_all_related_objects_with_model_proxy_hidden_legacy'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_objects_with_model(
include_proxy_eq=True, include_hidden=True)
self.assertEqual(
sorted(self._map_names(objects), key=self.key_name),
sorted(expected, key=self.key_name)
)
@test.ignore_warnings(category=RemovedInDjango20Warning)
class RelatedM2MTests(OptionsBaseTests):
def test_related_m2m_with_model(self):
result_key = 'get_all_related_many_to_many_with_model_legacy'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_m2m_objects_with_model()
self.assertEqual(self._map_related_query_names(objects), expected)
def test_related_m2m_local_only(self):
result_key = 'get_all_related_many_to_many_local_legacy'
for model, expected in TEST_RESULTS[result_key].items():
objects = model._meta.get_all_related_many_to_many_objects(local_only=True)
self.assertEqual([o.field.related_query_name() for o in objects], expected)
def test_related_m2m_asymmetrical(self):
m2m = Person._meta.many_to_many
self.assertTrue('following_base' in [f.attname for f in m2m])
related_m2m = Person._meta.get_all_related_many_to_many_objects()
self.assertTrue('followers_base' in [o.field.related_query_name() for o in related_m2m])
def test_related_m2m_symmetrical(self):
m2m = Person._meta.many_to_many
self.assertTrue('friends_base' in [f.attname for f in m2m])
related_m2m = Person._meta.get_all_related_many_to_many_objects()
self.assertIn('friends_inherited_rel_+', [o.field.related_query_name() for o in related_m2m])
@test.ignore_warnings(category=RemovedInDjango20Warning)
class GetFieldByNameTests(OptionsBaseTests):
def test_get_data_field(self):
field_info = Person._meta.get_field_by_name('data_abstract')
self.assertEqual(field_info[1:], (BasePerson, True, False))
self.assertIsInstance(field_info[0], CharField)
def test_get_m2m_field(self):
field_info = Person._meta.get_field_by_name('m2m_base')
self.assertEqual(field_info[1:], (BasePerson, True, True))
self.assertIsInstance(field_info[0], related.ManyToManyField)
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.assertTrue(field_info[0].auto_created)
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.assertTrue(field_info[0].auto_created)
def test_get_generic_relation(self):
field_info = Person._meta.get_field_by_name('generic_relation_base')
self.assertEqual(field_info[1:], (None, True, False))
self.assertIsInstance(field_info[0], GenericRelation)
def test_get_m2m_field_invalid(self):
with warnings.catch_warnings(record=True) as warning:
warnings.simplefilter("always")
self.assertRaises(
FieldDoesNotExist,
Person._meta.get_field,
**{'field_name': 'm2m_base', 'many_to_many': False}
)
self.assertEqual(Person._meta.get_field('m2m_base', many_to_many=True).name, 'm2m_base')
# 2 RemovedInDjango20Warning messages should be raised, one for each call of get_field()
# with the 'many_to_many' argument.
self.assertEqual(
[RemovedInDjango20Warning, RemovedInDjango20Warning],
[w.message.__class__ for w in warning]
)
@test.ignore_warnings(category=RemovedInDjango20Warning)
class GetAllFieldNamesTestCase(OptionsBaseTests):
def test_get_all_field_names(self):
for model, expected_names in TEST_RESULTS['get_all_field_names'].items():
objects = model._meta.get_all_field_names()
self.assertEqual(sorted(map(str, objects)), sorted(expected_names))

247
tests/model_meta/tests.py Normal file
View File

@ -0,0 +1,247 @@
from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields import related, CharField, Field
from django.db.models.options import IMMUTABLE_WARNING, EMPTY_RELATION_TREE
from django.test import TestCase
from .models import Relation, AbstractPerson, BasePerson, Person, ProxyPerson, Relating
from .results import TEST_RESULTS
class OptionsBaseTests(TestCase):
def _map_related_query_names(self, res):
return tuple((o.name, m) for o, m in res)
def _map_names(self, res):
return tuple((f.name, m) for f, m in res)
def _model(self, current_model, field):
model = field.model._meta.concrete_model
return None if model == current_model else model
def _details(self, current_model, relation):
direct = isinstance(relation, Field) or isinstance(relation, GenericForeignKey)
model = relation.model._meta.concrete_model
if model == current_model:
model = None
field = relation if direct else relation.field
m2m = isinstance(field, related.ManyToManyField)
return relation, model, direct, m2m
class GetFieldsTests(OptionsBaseTests):
def test_get_fields_is_immutable(self):
msg = IMMUTABLE_WARNING % "get_fields()"
for _ in range(2):
# Running unit test twice to ensure both non-cached and cached result
# are immutable.
fields = Person._meta.get_fields()
with self.assertRaisesMessage(AttributeError, msg):
fields += ["errors"]
class DataTests(OptionsBaseTests):
def test_fields(self):
for model, expected_result in TEST_RESULTS['fields'].items():
fields = model._meta.fields
self.assertEqual([f.attname for f in fields], expected_result)
def test_local_fields(self):
is_data_field = lambda f: isinstance(f, Field) and not isinstance(f, related.ManyToManyField)
for model, expected_result in TEST_RESULTS['local_fields'].items():
fields = model._meta.local_fields
self.assertEqual([f.attname for f in fields], expected_result)
for f in fields:
self.assertEqual(f.model, model)
self.assertTrue(is_data_field(f))
def test_local_concrete_fields(self):
for model, expected_result in TEST_RESULTS['local_concrete_fields'].items():
fields = model._meta.local_concrete_fields
self.assertEqual([f.attname for f in fields], expected_result)
for f in fields:
self.assertIsNotNone(f.column)
class M2MTests(OptionsBaseTests):
def test_many_to_many(self):
for model, expected_result in TEST_RESULTS['many_to_many'].items():
fields = model._meta.many_to_many
self.assertEqual([f.attname for f in fields], expected_result)
for f in fields:
self.assertTrue(f.many_to_many and f.is_relation)
def test_many_to_many_with_model(self):
for model, expected_result in TEST_RESULTS['many_to_many_with_model'].items():
models = [self._model(model, field) for field in model._meta.many_to_many]
self.assertEqual(models, expected_result)
class RelatedObjectsTests(OptionsBaseTests):
key_name = lambda self, r: r[0]
def test_related_objects(self):
result_key = 'get_all_related_objects_with_model'
for model, expected in TEST_RESULTS[result_key].items():
objects = [
(field, self._model(model, field))
for field in model._meta.get_fields()
if field.auto_created and not field.concrete
]
self.assertEqual(self._map_related_query_names(objects), expected)
def test_related_objects_local(self):
result_key = 'get_all_related_objects_with_model_local'
for model, expected in TEST_RESULTS[result_key].items():
objects = [
(field, self._model(model, field))
for field in model._meta.get_fields(include_parents=False)
if field.auto_created and not field.concrete
]
self.assertEqual(self._map_related_query_names(objects), expected)
def test_related_objects_include_hidden(self):
result_key = 'get_all_related_objects_with_model_hidden'
for model, expected in TEST_RESULTS[result_key].items():
objects = [
(field, self._model(model, field))
for field in model._meta.get_fields(include_hidden=True)
if field.auto_created and not field.concrete
]
self.assertEqual(
sorted(self._map_names(objects), key=self.key_name),
sorted(expected, key=self.key_name)
)
def test_related_objects_include_hidden_local_only(self):
result_key = 'get_all_related_objects_with_model_hidden_local'
for model, expected in TEST_RESULTS[result_key].items():
objects = [
(field, self._model(model, field))
for field in model._meta.get_fields(include_hidden=True, include_parents=False)
if field.auto_created and not field.concrete
]
self.assertEqual(
sorted(self._map_names(objects), key=self.key_name),
sorted(expected, key=self.key_name)
)
class VirtualFieldsTests(OptionsBaseTests):
def test_virtual_fields(self):
for model, expected_names in TEST_RESULTS['virtual_fields'].items():
objects = model._meta.virtual_fields
self.assertEqual(sorted([f.name for f in objects]), sorted(expected_names))
class GetFieldByNameTests(OptionsBaseTests):
def test_get_data_field(self):
field_info = self._details(Person, Person._meta.get_field('data_abstract'))
self.assertEqual(field_info[1:], (BasePerson, True, False))
self.assertIsInstance(field_info[0], CharField)
def test_get_m2m_field(self):
field_info = self._details(Person, Person._meta.get_field('m2m_base'))
self.assertEqual(field_info[1:], (BasePerson, True, True))
self.assertIsInstance(field_info[0], related.ManyToManyField)
def test_get_related_object(self):
field_info = self._details(Person, Person._meta.get_field('relating_baseperson'))
self.assertEqual(field_info[1:], (BasePerson, False, False))
self.assertIsInstance(field_info[0], related.ForeignObjectRel)
def test_get_related_m2m(self):
field_info = self._details(Person, Person._meta.get_field('relating_people'))
self.assertEqual(field_info[1:], (None, False, True))
self.assertIsInstance(field_info[0], related.ForeignObjectRel)
def test_get_generic_relation(self):
field_info = self._details(Person, Person._meta.get_field('generic_relation_base'))
self.assertEqual(field_info[1:], (None, True, False))
self.assertIsInstance(field_info[0], GenericRelation)
def test_get_fields_only_searaches_forward_on_apps_not_ready(self):
opts = Person._meta
# If apps registry is not ready, get_field() searches over only
# forward fields.
opts.apps.ready = False
try:
# 'data_abstract' is a forward field, and therefore will be found
self.assertTrue(opts.get_field('data_abstract'))
msg = (
"Person has no field named 'relating_baseperson'. The app "
"cache isn't ready yet, so if this is a forward field, it "
"won't be available yet."
)
# 'data_abstract' is a reverse field, and will raise an exception
with self.assertRaisesMessage(FieldDoesNotExist, msg):
opts.get_field('relating_baseperson')
finally:
opts.apps.ready = True
class RelationTreeTests(TestCase):
all_models = (Relation, AbstractPerson, BasePerson, Person, ProxyPerson, Relating)
def setUp(self):
apps.clear_cache()
def test_clear_cache_clears_relation_tree(self):
# The apps.clear_cache is setUp() should have deleted all trees.
# Exclude abstract models that are not included in the Apps registry
# and have no cache.
all_models_with_cache = (m for m in self.all_models if not m._meta.abstract)
for m in all_models_with_cache:
self.assertNotIn('_relation_tree', m._meta.__dict__)
def test_first_relation_tree_access_populates_all(self):
# On first access, relation tree should have populated cache.
self.assertTrue(self.all_models[0]._meta._relation_tree)
# AbstractPerson does not have any relations, so relation_tree
# should just return an EMPTY_RELATION_TREE.
self.assertEqual(AbstractPerson._meta._relation_tree, EMPTY_RELATION_TREE)
# All the other models should already have their relation tree
# in the internal __dict__ .
all_models_but_abstractperson = (m for m in self.all_models if m is not AbstractPerson)
for m in all_models_but_abstractperson:
self.assertIn('_relation_tree', m._meta.__dict__)
def test_relations_related_objects(self):
# Testing non hidden related objects
self.assertEqual(
sorted([field.related_query_name() for field in Relation._meta._relation_tree
if not field.rel.field.rel.is_hidden()]),
sorted([
'fk_abstract_rel', 'fk_abstract_rel', 'fk_abstract_rel', 'fk_base_rel', 'fk_base_rel',
'fk_base_rel', 'fk_concrete_rel', 'fk_concrete_rel', 'fo_abstract_rel', 'fo_abstract_rel',
'fo_abstract_rel', 'fo_base_rel', 'fo_base_rel', 'fo_base_rel', 'fo_concrete_rel',
'fo_concrete_rel', 'm2m_abstract_rel', 'm2m_abstract_rel', 'm2m_abstract_rel',
'm2m_base_rel', 'm2m_base_rel', 'm2m_base_rel', 'm2m_concrete_rel', 'm2m_concrete_rel',
])
)
# Testing hidden related objects
self.assertEqual(
sorted([field.related_query_name() for field in BasePerson._meta._relation_tree]),
sorted([
'+', '+', 'BasePerson_following_abstract+', 'BasePerson_following_abstract+',
'BasePerson_following_base+', 'BasePerson_following_base+', 'BasePerson_friends_abstract+',
'BasePerson_friends_abstract+', 'BasePerson_friends_base+', 'BasePerson_friends_base+',
'BasePerson_m2m_abstract+', 'BasePerson_m2m_base+', 'Relating_basepeople+',
'Relating_basepeople_hidden+', 'followers_abstract', 'followers_abstract', 'followers_abstract',
'followers_base', 'followers_base', 'followers_base', 'friends_abstract_rel_+', 'friends_abstract_rel_+',
'friends_abstract_rel_+', 'friends_base_rel_+', 'friends_base_rel_+', 'friends_base_rel_+', 'person',
'person', 'relating_basepeople', 'relating_baseperson',
])
)
self.assertEqual([field.related_query_name() for field in AbstractPerson._meta._relation_tree], [])

View File

@ -2093,7 +2093,7 @@ class CloneTests(TestCase):
testing is impossible, this is a sanity check against invalid use of testing is impossible, this is a sanity check against invalid use of
deepcopy. refs #16759. deepcopy. refs #16759.
""" """
opts_class = type(Note._meta.get_field_by_name("misc")[0]) opts_class = type(Note._meta.get_field("misc"))
note_deepcopy = getattr(opts_class, "__deepcopy__", None) note_deepcopy = getattr(opts_class, "__deepcopy__", None)
opts_class.__deepcopy__ = lambda obj, memo: self.fail("Model fields shouldn't be cloned") opts_class.__deepcopy__ = lambda obj, memo: self.fail("Model fields shouldn't be cloned")
try: try:

View File

@ -79,7 +79,7 @@ class PickleabilityTestCase(TestCase):
m1 = M2MModel.objects.create() m1 = M2MModel.objects.create()
g1 = Group.objects.create(name='foof') g1 = Group.objects.create(name='foof')
m1.groups.add(g1) m1.groups.add(g1)
m2m_through = M2MModel._meta.get_field_by_name('groups')[0].rel.through m2m_through = M2MModel._meta.get_field('groups').rel.through
original = m2m_through.objects.get() original = m2m_through.objects.get()
dumped = pickle.dumps(original) dumped = pickle.dumps(original)
reloaded = pickle.loads(dumped) reloaded = pickle.loads(dumped)

View File

@ -138,7 +138,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Book, Book,
Book._meta.get_field_by_name("author")[0], Book._meta.get_field("author"),
new_field, new_field,
strict=True, strict=True,
) )
@ -393,7 +393,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Author, Author,
Author._meta.get_field_by_name("name")[0], Author._meta.get_field("name"),
new_field, new_field,
strict=True, strict=True,
) )
@ -424,7 +424,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Note, Note,
Note._meta.get_field_by_name("info")[0], Note._meta.get_field("info"),
new_field, new_field,
strict=True, strict=True,
) )
@ -451,7 +451,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Author, Author,
Author._meta.get_field_by_name("height")[0], Author._meta.get_field("height"),
new_field new_field
) )
# Ensure the field is right afterwards # Ensure the field is right afterwards
@ -479,7 +479,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
AuthorWithDefaultHeight, AuthorWithDefaultHeight,
AuthorWithDefaultHeight._meta.get_field_by_name("height")[0], AuthorWithDefaultHeight._meta.get_field("height"),
new_field, new_field,
) )
# Ensure the field is right afterwards # Ensure the field is right afterwards
@ -512,7 +512,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Book, Book,
Book._meta.get_field_by_name("author")[0], Book._meta.get_field("author"),
new_field, new_field,
strict=True, strict=True,
) )
@ -542,7 +542,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Author, Author,
Author._meta.get_field_by_name("id")[0], Author._meta.get_field("id"),
new_field, new_field,
strict=True, strict=True,
) )
@ -568,7 +568,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Author, Author,
Author._meta.get_field_by_name("name")[0], Author._meta.get_field("name"),
new_field, new_field,
strict=True, strict=True,
) )
@ -587,7 +587,7 @@ class SchemaTests(TransactionTestCase):
editor.create_model(TagM2MTest) editor.create_model(TagM2MTest)
editor.create_model(BookWithM2M) editor.create_model(BookWithM2M)
# Ensure there is now an m2m table there # Ensure there is now an m2m table there
columns = self.column_classes(BookWithM2M._meta.get_field_by_name("tags")[0].rel.through) columns = self.column_classes(BookWithM2M._meta.get_field("tags").rel.through)
self.assertEqual(columns['tagm2mtest_id'][0], "IntegerField") self.assertEqual(columns['tagm2mtest_id'][0], "IntegerField")
def test_m2m_create_through(self): def test_m2m_create_through(self):
@ -661,7 +661,7 @@ class SchemaTests(TransactionTestCase):
self.assertEqual(len(self.column_classes(AuthorTag)), 3) self.assertEqual(len(self.column_classes(AuthorTag)), 3)
# "Alter" the field's blankness. This should not actually do anything. # "Alter" the field's blankness. This should not actually do anything.
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
old_field = AuthorWithM2MThrough._meta.get_field_by_name("tags")[0] old_field = AuthorWithM2MThrough._meta.get_field("tags")
new_field = ManyToManyField("schema.TagM2MTest", related_name="authors", through="AuthorTag") new_field = ManyToManyField("schema.TagM2MTest", related_name="authors", through="AuthorTag")
new_field.contribute_to_class(AuthorWithM2MThrough, "tags") new_field.contribute_to_class(AuthorWithM2MThrough, "tags")
editor.alter_field( editor.alter_field(
@ -683,7 +683,7 @@ class SchemaTests(TransactionTestCase):
editor.create_model(TagM2MTest) editor.create_model(TagM2MTest)
editor.create_model(UniqueTest) editor.create_model(UniqueTest)
# Ensure the M2M exists and points to TagM2MTest # Ensure the M2M exists and points to TagM2MTest
constraints = self.get_constraints(BookWithM2M._meta.get_field_by_name("tags")[0].rel.through._meta.db_table) constraints = self.get_constraints(BookWithM2M._meta.get_field("tags").rel.through._meta.db_table)
if connection.features.supports_foreign_keys: if connection.features.supports_foreign_keys:
for name, details in constraints.items(): for name, details in constraints.items():
if details['columns'] == ["tagm2mtest_id"] and details['foreign_key']: if details['columns'] == ["tagm2mtest_id"] and details['foreign_key']:
@ -698,11 +698,11 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Author, Author,
BookWithM2M._meta.get_field_by_name("tags")[0], BookWithM2M._meta.get_field("tags"),
new_field, new_field,
) )
# Ensure old M2M is gone # Ensure old M2M is gone
self.assertRaises(DatabaseError, self.column_classes, BookWithM2M._meta.get_field_by_name("tags")[0].rel.through) self.assertRaises(DatabaseError, self.column_classes, BookWithM2M._meta.get_field("tags").rel.through)
# Ensure the new M2M exists and points to UniqueTest # Ensure the new M2M exists and points to UniqueTest
constraints = self.get_constraints(new_field.rel.through._meta.db_table) constraints = self.get_constraints(new_field.rel.through._meta.db_table)
if connection.features.supports_foreign_keys: if connection.features.supports_foreign_keys:
@ -715,10 +715,10 @@ class SchemaTests(TransactionTestCase):
finally: finally:
# Cleanup through table separately # Cleanup through table separately
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.remove_field(BookWithM2M, BookWithM2M._meta.get_field_by_name("uniques")[0]) editor.remove_field(BookWithM2M, BookWithM2M._meta.get_field("uniques"))
# Cleanup model states # Cleanup model states
BookWithM2M._meta.local_many_to_many.remove(new_field) BookWithM2M._meta.local_many_to_many.remove(new_field)
del BookWithM2M._meta._m2m_cache BookWithM2M._meta._expire_cache()
@unittest.skipUnless(connection.features.supports_column_check_constraints, "No check constraints") @unittest.skipUnless(connection.features.supports_column_check_constraints, "No check constraints")
def test_check_constraints(self): def test_check_constraints(self):
@ -741,7 +741,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Author, Author,
Author._meta.get_field_by_name("height")[0], Author._meta.get_field("height"),
new_field, new_field,
strict=True, strict=True,
) )
@ -754,7 +754,7 @@ class SchemaTests(TransactionTestCase):
editor.alter_field( editor.alter_field(
Author, Author,
new_field, new_field,
Author._meta.get_field_by_name("height")[0], Author._meta.get_field("height"),
strict=True, strict=True,
) )
constraints = self.get_constraints(Author._meta.db_table) constraints = self.get_constraints(Author._meta.db_table)
@ -781,7 +781,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Tag, Tag,
Tag._meta.get_field_by_name("slug")[0], Tag._meta.get_field("slug"),
new_field, new_field,
strict=True, strict=True,
) )
@ -809,8 +809,8 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Tag, Tag,
Tag._meta.get_field_by_name("slug")[0], Tag._meta.get_field("slug"),
TagUniqueRename._meta.get_field_by_name("slug2")[0], TagUniqueRename._meta.get_field("slug2"),
strict=True, strict=True,
) )
# Ensure the field is still unique # Ensure the field is still unique
@ -976,7 +976,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
Book, Book,
Book._meta.get_field_by_name("title")[0], Book._meta.get_field("title"),
new_field, new_field,
strict=True, strict=True,
) )
@ -990,7 +990,7 @@ class SchemaTests(TransactionTestCase):
editor.alter_field( editor.alter_field(
Book, Book,
new_field, new_field,
Book._meta.get_field_by_name("title")[0], Book._meta.get_field("title"),
strict=True, strict=True,
) )
# Ensure the table is there and has the index again # Ensure the table is there and has the index again
@ -1002,7 +1002,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.add_field( editor.add_field(
Book, Book,
BookWithSlug._meta.get_field_by_name("slug")[0], BookWithSlug._meta.get_field("slug"),
) )
self.assertIn( self.assertIn(
"slug", "slug",
@ -1014,7 +1014,7 @@ class SchemaTests(TransactionTestCase):
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.alter_field( editor.alter_field(
BookWithSlug, BookWithSlug,
BookWithSlug._meta.get_field_by_name("slug")[0], BookWithSlug._meta.get_field("slug"),
new_field2, new_field2,
strict=True, strict=True,
) )
@ -1039,10 +1039,10 @@ class SchemaTests(TransactionTestCase):
new_field.set_attributes_from_name("slug") new_field.set_attributes_from_name("slug")
new_field.model = Tag new_field.model = Tag
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
editor.remove_field(Tag, Tag._meta.get_field_by_name("id")[0]) editor.remove_field(Tag, Tag._meta.get_field("id"))
editor.alter_field( editor.alter_field(
Tag, Tag,
Tag._meta.get_field_by_name("slug")[0], Tag._meta.get_field("slug"),
new_field, new_field,
) )
# Ensure the PK changed # Ensure the PK changed