From fb48eb05816b1ac87d58696cdfe48be18c901f16 Mon Sep 17 00:00:00 2001 From: Daniel Pyrathon Date: Tue, 6 Jan 2015 19:16:35 -0500 Subject: [PATCH] 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! --- django/apps/registry.py | 5 + django/contrib/admin/checks.py | 2 +- django/contrib/admin/options.py | 13 +- .../contrib/admin/templatetags/admin_list.py | 2 +- django/contrib/admin/utils.py | 30 +- django/contrib/admin/validation.py | 2 +- django/contrib/admindocs/views.py | 2 +- django/contrib/contenttypes/fields.py | 27 +- django/contrib/gis/db/models/query.py | 2 +- django/contrib/gis/db/models/sql/compiler.py | 5 +- django/contrib/gis/sitemaps/views.py | 2 +- django/contrib/gis/utils/layermapping.py | 5 +- django/contrib/gis/utils/srs.py | 2 +- django/core/serializers/python.py | 6 +- django/core/serializers/xml_serializer.py | 4 +- django/db/backends/creation.py | 4 +- django/db/backends/schema.py | 44 +- django/db/backends/sqlite3/schema.py | 4 +- django/db/migrations/autodetector.py | 38 +- django/db/migrations/operations/fields.py | 20 +- django/db/migrations/operations/models.py | 34 +- django/db/migrations/state.py | 6 +- django/db/models/base.py | 69 +- django/db/models/deletion.py | 28 +- django/db/models/fields/__init__.py | 19 +- django/db/models/fields/related.py | 149 +++- django/db/models/manager.py | 8 +- django/db/models/options.py | 730 ++++++++++------ django/db/models/query.py | 26 +- django/db/models/query_utils.py | 4 +- django/db/models/sql/compiler.py | 28 +- django/db/models/sql/query.py | 52 +- django/db/models/sql/subqueries.py | 8 +- django/forms/models.py | 7 +- docs/howto/custom-model-fields.txt | 6 +- docs/internals/deprecation.txt | 13 + docs/ref/models/fields.txt | 92 ++ docs/ref/models/index.txt | 1 + docs/ref/models/meta.txt | 287 +++++++ docs/releases/1.8.txt | 36 + tests/backends/tests.py | 2 +- tests/basic/tests.py | 2 +- tests/fixtures/tests.py | 2 + tests/generic_relations/tests.py | 3 +- tests/generic_relations_regress/tests.py | 2 +- tests/m2m_and_m2o/tests.py | 6 + tests/many_to_one/tests.py | 4 +- tests/migrations/test_state.py | 4 +- tests/model_fields/models.py | 51 ++ tests/model_fields/test_field_flags.py | 220 +++++ tests/model_fields/tests.py | 2 +- tests/model_meta/results.py | 796 ++++++++++++++++++ tests/model_meta/test.py | 661 --------------- tests/model_meta/test_legacy.py | 166 ++++ tests/model_meta/tests.py | 247 ++++++ tests/queries/tests.py | 2 +- tests/queryset_pickle/tests.py | 2 +- tests/schema/tests.py | 52 +- 58 files changed, 2851 insertions(+), 1195 deletions(-) create mode 100644 docs/ref/models/meta.txt create mode 100644 tests/model_fields/test_field_flags.py create mode 100644 tests/model_meta/results.py delete mode 100644 tests/model_meta/test.py create mode 100644 tests/model_meta/test_legacy.py create mode 100644 tests/model_meta/tests.py diff --git a/django/apps/registry.py b/django/apps/registry.py index fe53d965de1..68aa411d91f 100644 --- a/django/apps/registry.py +++ b/django/apps/registry.py @@ -337,7 +337,12 @@ class Apps(object): 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() + if self.ready: + for model in self.get_models(include_auto_created=True): + model._meta._expire_cache() ### DEPRECATED METHODS GO BELOW THIS LINE ### diff --git a/django/contrib/admin/checks.py b/django/contrib/admin/checks.py index ee47f911b17..896d59155c3 100644 --- a/django/contrib/admin/checks.py +++ b/django/contrib/admin/checks.py @@ -762,7 +762,7 @@ class ModelAdminChecks(BaseModelAdminChecks): def _check_list_editable_item(self, cls, model, field_name, label): try: - field = model._meta.get_field_by_name(field_name)[0] + field = model._meta.get_field(field_name) except FieldDoesNotExist: return refer_to_missing_field(field=field_name, option=label, model=model, obj=cls, id='admin.E121') diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 35758cfdaf7..3e682f9b102 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -406,7 +406,7 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): rel_name = None for part in parts[:-1]: try: - field, _, _, _ = model._meta.get_field_by_name(part) + field = model._meta.get_field(part) except FieldDoesNotExist: # Lookups on non-existent fields are ok, since they're ignored # later. @@ -422,7 +422,7 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): else: rel_name = None elif isinstance(field, ForeignObjectRel): - model = field.model + model = field.related_model rel_name = model._meta.pk.name else: rel_name = None @@ -473,9 +473,12 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): for inline in admin.inlines: registered_models.add(inline.model) - for related_object in (opts.get_all_related_objects(include_hidden=True) + - opts.get_all_related_many_to_many_objects()): - related_model = related_object.model + related_objects = ( + f for f in opts.get_fields(include_hidden=True) + 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 related_object.field.rel.get_related_field() == field): return True diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 544c5503ec7..266b880d994 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -326,7 +326,7 @@ def date_hierarchy(cl): """ if 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' year_field = '%s__year' % field_name month_field = '%s__month' % field_name diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py index 5da461351a7..0f617b3fb96 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -25,7 +25,7 @@ def lookup_needs_distinct(opts, lookup_path): Returns True if 'distinct()' should be used to query the given lookup path. """ 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()): return True return False @@ -265,7 +265,7 @@ def model_ngettext(obj, n=None): def lookup_field(name, obj, model_admin=None): opts = obj._meta try: - f = opts.get_field(name) + f = _get_non_gfk_field(opts, name) except FieldDoesNotExist: # For non-field values, the value is either a method, property or # returned via a callable. @@ -291,6 +291,17 @@ def lookup_field(name, obj, model_admin=None): 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): """ 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 try: - field = model._meta.get_field_by_name(name)[0] + field = _get_non_gfk_field(model._meta, name) try: label = field.verbose_name 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): help_text = "" try: - field_data = model._meta.get_field_by_name(name) + field = _get_non_gfk_field(model._meta, name) except FieldDoesNotExist: pass else: - field = field_data[0] if hasattr(field, 'help_text'): help_text = field.help_text return smart_text(help_text) @@ -425,19 +435,21 @@ def reverse_field_path(model, path): parent = model pieces = path.split(LOOKUP_SEP) 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: if len(reversed_path) == len(pieces) - 1: # final iteration try: get_model_from_relation(field) except NotRelationField: 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() parent = field.rel.to else: related_name = field.field.name - parent = field.model + parent = field.related_model reversed_path.insert(0, related_name) 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]) else: parent = model - fields.append(parent._meta.get_field_by_name(piece)[0]) + fields.append(parent._meta.get_field(piece)) return fields diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 92ede613ec7..67b97f776ed 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -346,7 +346,7 @@ class ModelAdminValidator(BaseValidator): check_isseq(cls, 'list_editable', cls.list_editable) for idx, field_name in enumerate(cls.list_editable): try: - field = model._meta.get_field_by_name(field_name)[0] + field = model._meta.get_field(field_name) except FieldDoesNotExist: raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " "field, '%s', not defined on %s.%s." diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index 2b45301f3a4..2ffd402c95d 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -262,7 +262,7 @@ class ModelDetailView(BaseAdminDocsView): }) # 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") % { 'app_label': rel.opts.app_label, 'object_name': rel.opts.object_name, diff --git a/django/contrib/contenttypes/fields.py b/django/contrib/contenttypes/fields.py index 2297ad63a9b..07d47becf86 100644 --- a/django/contrib/contenttypes/fields.py +++ b/django/contrib/contenttypes/fields.py @@ -21,6 +21,18 @@ class GenericForeignKey(object): Provides a generic relation to any object through content-type/object-id 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): self.ct_field = ct_field @@ -28,12 +40,13 @@ class GenericForeignKey(object): self.for_concrete_model = for_concrete_model self.editable = False self.rel = None + self.column = None def contribute_to_class(self, cls, name, **kwargs): self.name = name self.model = cls 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 if not cls._meta.abstract: @@ -243,6 +256,13 @@ class GenericForeignKey(object): class GenericRelation(ForeignObject): """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): kwargs['verbose_name'] = kwargs.get('verbose_name', None) @@ -303,8 +323,7 @@ class GenericRelation(ForeignObject): def resolve_related_fields(self): self.to_fields = [self.model._meta.pk.name] - return [(self.rel.to._meta.get_field_by_name(self.object_id_field_name)[0], - self.model._meta.pk)] + return [(self.rel.to._meta.get_field(self.object_id_field_name), self.model._meta.pk)] def get_path_info(self): opts = self.rel.to._meta @@ -345,7 +364,7 @@ class GenericRelation(ForeignObject): for_concrete_model=self.for_concrete_model) 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 cond = where_class() lookup = field.get_lookup('exact')(Col(remote_alias, field, field), contenttype_pk) diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index f2e76578508..c05688dec9b 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -758,7 +758,7 @@ class GeoQuerySet(QuerySet): elif geo_field not in opts.local_fields: # This geographic field is inherited from another model, so we have to # 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) else: return self.query.get_compiler(self.db)._field_column(geo_field) diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py index dd156ea4b62..1501c981369 100644 --- a/django/contrib/gis/db/models/sql/compiler.py +++ b/django/contrib/gis/db/models/sql/compiler.py @@ -118,7 +118,10 @@ class GeoSQLCompiler(compiler.SQLCompiler): seen = self.query.included_inherited_models.copy() if 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): # Avoid loading data for already loaded parents. continue diff --git a/django/contrib/gis/sitemaps/views.py b/django/contrib/gis/sitemaps/views.py index c0c2f835983..d12ed53298e 100644 --- a/django/contrib/gis/sitemaps/views.py +++ b/django/contrib/gis/sitemaps/views.py @@ -23,7 +23,7 @@ def kml(request, label, model, field_name=None, compress=False, using=DEFAULT_DB if field_name: try: - field, _, _, _ = klass._meta.get_field_by_name(field_name) + field = klass._meta.get_field(field_name) if not isinstance(field, GeometryField): raise FieldDoesNotExist except FieldDoesNotExist: diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index 1c848f105ef..2ff23fc38be 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -457,11 +457,10 @@ class LayerMapping(object): def geometry_field(self): "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. opts = self.model._meta - fld, model, direct, m2m = opts.get_field_by_name(self.geom_field) - return fld + return opts.get_field(self.geom_field) def make_multi(self, geom_type, model_field): """ diff --git a/django/contrib/gis/utils/srs.py b/django/contrib/gis/utils/srs.py index e5aa5a70395..1460be2de90 100644 --- a/django/contrib/gis/utils/srs.py +++ b/django/contrib/gis/utils/srs.py @@ -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. - 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: kwargs['srtext'] = srs.wkt if 'ref_sys_name' in srs_field_names: diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index b4712c76be5..f8dd7aebac5 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals from django.apps import apps from django.conf import settings 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 import six @@ -101,12 +101,12 @@ def Deserializer(object_list, **options): if 'pk' in d: data[Model._meta.pk.attname] = Model._meta.pk.to_python(d.get("pk", None)) m2m_data = {} - model_fields = Model._meta.get_all_field_names() + field_names = {f.name for f in Model._meta.get_fields()} # Handle each field 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 continue diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index de82a969c17..0b759799d2e 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -186,7 +186,7 @@ class Deserializer(base.Deserializer): # {m2m_accessor_attribute : [list_of_related_objects]}) m2m_data = {} - model_fields = Model._meta.get_all_field_names() + field_names = {f.name for f in Model._meta.get_fields()} # Deserialize each field. for field_node in node.getElementsByTagName("field"): # 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 # FieldDoesNotExist if, well, the field doesn't exist, which will # 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 field = Model._meta.get_field(field_name) diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 5e0248d4df2..42a58c03e85 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -199,7 +199,7 @@ class BaseDatabaseCreation(object): for f in model._meta.local_fields: output.extend(self.sql_indexes_for_field(model, f, style)) 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)) return output @@ -290,7 +290,7 @@ class BaseDatabaseCreation(object): for f in model._meta.local_fields: output.extend(self.sql_destroy_indexes_for_field(model, f, style)) 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)) return output diff --git a/django/db/backends/schema.py b/django/db/backends/schema.py index 40db888e501..dfda05bc19b 100644 --- a/django/db/backends/schema.py +++ b/django/db/backends/schema.py @@ -10,6 +10,11 @@ from django.utils import six 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): """ This class (and its subclasses) are responsible for emitting schema-changing @@ -261,7 +266,7 @@ class BaseDatabaseSchemaEditor(object): # Add any unique_togethers 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 % { "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) # Deleted uniques 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) if len(constraint_names) != 1: 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])) # Created uniques 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)) 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) # Deleted indexes 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) if len(constraint_names) != 1: 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])) # Created indexes 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")) 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 # to change. 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(): - rel_fk_names = self._constraint_names(rel.model, [rel.field.column], foreign_key=True) + # '_meta.related_field' also contains M2M reverse fields, these + # 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: - 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) if (old_field.db_index and not new_field.db_index and not old_field.unique and not @@ -661,7 +668,7 @@ class BaseDatabaseSchemaEditor(object): # referring to us. rels_to_update = [] 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? # Note that we don't detect unsetting of a PK, as we assume another field # will always come along and replace it. @@ -684,14 +691,14 @@ class BaseDatabaseSchemaEditor(object): } ) # 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 for rel in rels_to_update: rel_db_params = rel.field.db_parameters(connection=self.connection) rel_type = rel_db_params['type'] self.execute( 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 % { "column": self.quote_name(rel.field.column), "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")) # 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: - for rel in new_field.model._meta.get_all_related_objects(): - self.execute(self._create_fk_sql(rel.model, rel.field, "_fk")) + for rel in new_field.model._meta.related_objects: + 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? if old_db_params['check'] != new_db_params['check'] and new_db_params['check']: self.execute( @@ -765,14 +773,14 @@ class BaseDatabaseSchemaEditor(object): new_field.rel.through, # 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) - old_field.rel.through._meta.get_field_by_name(old_field.m2m_reverse_field_name())[0], - new_field.rel.through._meta.get_field_by_name(new_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(new_field.m2m_reverse_field_name()), ) self.alter_field( new_field.rel.through, # 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], - new_field.rel.through._meta.get_field_by_name(new_field.m2m_field_name())[0], + old_field.rel.through._meta.get_field(old_field.m2m_field_name()), + new_field.rel.through._meta.get_field(new_field.m2m_field_name()), ) 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="")) 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")) return output diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index 9aeca56bf73..e0433b0c13b 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -227,8 +227,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): alter_fields=[( # 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) - old_field.rel.through._meta.get_field_by_name(old_field.m2m_reverse_field_name())[0], - new_field.rel.through._meta.get_field_by_name(new_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(new_field.m2m_reverse_field_name()), )], override_uniques=(new_field.m2m_field_name(), new_field.m2m_reverse_field_name()), ) diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index e3dd87e3bd2..8535a98e735 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -163,7 +163,7 @@ class MigrationAutodetector(object): 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] 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) and not old_field.rel.through._meta.auto_created): through_key = ( @@ -685,26 +685,14 @@ class MigrationAutodetector(object): # and the removal of all its own related fields, and if it's # a through model the field that references it. dependencies = [] - for related_object in model._meta.get_all_related_objects(): - dependencies.append(( - related_object.model._meta.app_label, - related_object.model._meta.object_name, - related_object.field.name, - False, - )) - 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 related_object in model._meta.related_objects: + related_object_app_label = related_object.related_model._meta.app_label + object_name = related_object.related_model._meta.object_name + field_name = related_object.field.name + dependencies.append((related_object_app_label, object_name, field_name, False)) + if not related_object.many_to_many: + dependencies.append((related_object_app_label, object_name, field_name, "alter")) + for name, field in sorted(related_fields.items()): dependencies.append((app_label, model_name, name, False)) # 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): 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] - 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! 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): @@ -776,7 +764,7 @@ class MigrationAutodetector(object): self._generate_added_field(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 dependencies = [] if field.rel and field.rel.to: @@ -847,8 +835,8 @@ class MigrationAutodetector(object): # Did the field change? 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 = self.old_apps.get_model(app_label, old_model_name)._meta.get_field_by_name(old_field_name)[0] - new_field = self.new_apps.get_model(app_label, 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(old_field_name) + 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 # so we need to exclude them from the comparison if hasattr(new_field, "rel") and getattr(new_field.rel, "to", None): diff --git a/django/db/migrations/operations/fields.py b/django/db/migrations/operations/fields.py index 54251cf6edc..6ce5e372969 100644 --- a/django/db/migrations/operations/fields.py +++ b/django/db/migrations/operations/fields.py @@ -44,7 +44,7 @@ class AddField(Operation): to_model = to_state.apps.get_model(app_label, self.model_name) if self.allowed_to_migrate(schema_editor.connection.alias, to_model): 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: field.default = self.field.default schema_editor.add_field( @@ -57,7 +57,7 @@ class AddField(Operation): def database_backwards(self, app_label, schema_editor, from_state, to_state): from_model = from_state.apps.get_model(app_label, self.model_name) 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): 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): from_model = from_state.apps.get_model(app_label, self.model_name) 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): to_model = to_state.apps.get_model(app_label, self.model_name) if self.allowed_to_migrate(schema_editor.connection.alias, to_model): 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): 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) if self.allowed_to_migrate(schema_editor.connection.alias, to_model): from_model = from_state.apps.get_model(app_label, self.model_name) - from_field = from_model._meta.get_field_by_name(self.name)[0] - to_field = to_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(self.name) # If the field is a relatedfield with an unresolved rel.to, just # set it equal to the other field side. Bandaid fix for AlterField # 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) schema_editor.alter_field( from_model, - from_model._meta.get_field_by_name(self.old_name)[0], - to_model._meta.get_field_by_name(self.new_name)[0], + from_model._meta.get_field(self.old_name), + to_model._meta.get_field(self.new_name), ) 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) schema_editor.alter_field( from_model, - from_model._meta.get_field_by_name(self.new_name)[0], - to_model._meta.get_field_by_name(self.old_name)[0], + from_model._meta.get_field(self.new_name), + to_model._meta.get_field(self.old_name), ) def describe(self): diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py index f07f667c51f..6dd66ae4541 100644 --- a/django/db/migrations/operations/models.py +++ b/django/db/migrations/operations/models.py @@ -138,25 +138,27 @@ class RenameModel(Operation): ) def state_forwards(self, app_label, state): - # Get all of the related objects we need to repoint apps = state.apps model = apps.get_model(app_label, self.old_name) model._meta.apps = apps - related_objects = model._meta.get_all_related_objects() - related_m2m_objects = model._meta.get_all_related_many_to_many_objects() + # Get all of the related objects we need to repoint + 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 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.remove_model(app_label, self.old_name) # 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. - if related_object.model == model: + if related_object.related_model == model: related_key = (app_label, self.new_name.lower()) else: related_key = ( - related_object.model._meta.app_label, - related_object.model._meta.object_name.lower(), + related_object.related_model._meta.app_label, + related_object.related_model._meta.object_name.lower(), ) new_fields = [] for name, field in state.models[related_key].fields: @@ -179,21 +181,19 @@ class RenameModel(Operation): new_model._meta.db_table, ) # Alter the fields pointing to us - related_objects = old_model._meta.get_all_related_objects() - related_m2m_objects = old_model._meta.get_all_related_many_to_many_objects() - for related_object in (related_objects + related_m2m_objects): - if related_object.model == old_model: + for related_object in old_model._meta.related_objects: + if related_object.related_model == old_model: model = new_model related_key = (app_label, self.new_name.lower()) else: - model = related_object.model + model = related_object.related_model related_key = ( - related_object.model._meta.app_label, - related_object.model._meta.object_name.lower(), + related_object.related_model._meta.app_label, + related_object.related_model._meta.object_name.lower(), ) to_field = to_state.apps.get_model( *related_key - )._meta.get_field_by_name(related_object.field.name)[0] + )._meta.get_field(related_object.field.name) schema_editor.alter_field( model, related_object.field, @@ -394,11 +394,11 @@ class AlterOrderWithRespectTo(Operation): from_model = from_state.apps.get_model(app_label, self.name) # Remove a field if we need 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 # it's likely a rename) 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(): field.default = 0 schema_editor.add_field( diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index 8a45a0d2f29..6626e31442c 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -50,15 +50,15 @@ class ProjectState(object): model_name = model_name.lower() try: related_old = { - f.model for f in - self.apps.get_model(app_label, model_name)._meta.get_all_related_objects() + f.related_model for f in + self.apps.get_model(app_label, model_name)._meta.related_objects } except LookupError: related_old = set() self._reload_one_model(app_label, model_name) # Reload models if there are relations 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): self._reload_one_model(rel_model._meta.app_label, rel_model._meta.model_name) if related_m2m: diff --git a/django/db/models/base.py b/django/db/models/base.py index ec5d4c7c1c3..945cd0154b7 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import copy import inspect +from itertools import chain import sys import warnings @@ -175,12 +176,12 @@ class ModelBase(type): new_class.add_to_class(obj_name, obj) # All the fields of any type declared on this model - new_fields = ( - new_class._meta.local_fields + - new_class._meta.local_many_to_many + + new_fields = chain( + new_class._meta.local_fields, + new_class._meta.local_many_to_many, 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. if is_proxy: @@ -202,6 +203,7 @@ class ModelBase(type): raise TypeError("Proxy model '%s' has no non-abstract model base class." % name) new_class._meta.setup_proxy(base) new_class._meta.concrete_model = base._meta.concrete_model + base._meta.concrete_model._meta.proxied_children.append(new_class._meta) else: new_class._meta.concrete_model = new_class @@ -342,7 +344,7 @@ class ModelBase(type): # Give the class a docstring -- its definition. 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( '%s.%s' % (opts.app_label, opts.model_name) @@ -630,7 +632,7 @@ class Model(six.with_metaclass(ModelBase)): and not use this method. """ try: - field = self._meta.get_field_by_name(field_name)[0] + field = self._meta.get_field(field_name) except FieldDoesNotExist: return getattr(self, field_name) return getattr(self, field.attname) @@ -1438,12 +1440,17 @@ class Model(six.with_metaclass(ModelBase)): def _check_local_fields(cls, fields, option): 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 = [] for field_name in fields: try: - field = cls._meta.get_field(field_name, - many_to_many=True) - except FieldDoesNotExist: + field = forward_fields_map[field_name] + except KeyError: errors.append( checks.Error( "'%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): """ Check "ordering" option -- is it a list of strings and do all fields exist? """ - if not cls._meta.ordering: return [] @@ -1500,7 +1506,6 @@ class Model(six.with_metaclass(ModelBase)): ] errors = [] - fields = cls._meta.ordering # Skip '?' fields. @@ -1518,28 +1523,30 @@ class Model(six.with_metaclass(ModelBase)): # 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. - fields = (f for f in fields if f != 'pk') + fields = {f for f in fields if f != 'pk'} - for field_name in fields: - try: - cls._meta.get_field(field_name, many_to_many=False) - except FieldDoesNotExist: - if field_name.endswith('_id'): - try: - field = cls._meta.get_field(field_name[:-3], many_to_many=False) - except FieldDoesNotExist: - pass - else: - if field.attname == field_name: - continue - errors.append( - checks.Error( - "'ordering' refers to the non-existent field '%s'." % field_name, - hint=None, - obj=cls, - id='models.E015', - ) + # Check for invalid or non-existent fields in ordering. + invalid_fields = [] + + # Any field name that is not present in field_names does not exist. + # Also, ordering by m2m fields is not allowed. + opts = cls._meta + valid_fields = set(chain.from_iterable( + (f.name, f.attname) if not (f.auto_created and not f.concrete) else (f.field.related_query_name(),) + for f in chain(opts.fields, opts.related_objects) + )) + + invalid_fields.extend(fields - valid_fields) + + for invalid_field in invalid_fields: + errors.append( + checks.Error( + "'ordering' refers to the non-existent field '%s'." % invalid_field, + hint=None, + obj=cls, + id='models.E015', ) + ) return errors @classmethod diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index c61f865be2c..016fc5637e7 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from itertools import chain from operator import attrgetter from django.db import connections, transaction, IntegrityError @@ -51,6 +52,23 @@ def DO_NOTHING(collector, field, sub_objs, using): 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): def __init__(self, using): self.using = using @@ -134,8 +152,7 @@ class Collector(object): return False # Foreign keys pointing to this model, both from m2m and other # models. - for related in opts.get_all_related_objects( - include_hidden=True, include_proxy_eq=True): + for related in get_candidate_relations_to_delete(opts): if related.field.rel.on_delete is not DO_NOTHING: return False for field in model._meta.virtual_fields: @@ -184,7 +201,7 @@ class Collector(object): model = new_objs[0].__class__ # 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 for ptr in six.itervalues(concrete_model._meta.parents): if ptr: @@ -199,8 +216,7 @@ class Collector(object): reverse_dependency=True) if collect_related: - for related in model._meta.get_all_related_objects( - include_hidden=True, include_proxy_eq=True): + for related in get_candidate_relations_to_delete(model._meta): field = related.field if field.rel.on_delete == DO_NOTHING: continue @@ -225,7 +241,7 @@ class Collector(object): 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} ) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 8dc9c554ddc..a1995452a3c 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -31,7 +31,9 @@ from django.utils.ipv6 import clean_ipv6_address from django.utils import six 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 # 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): - 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: @@ -116,6 +118,15 @@ class Field(RegisterLookupMixin): system_check_deprecated_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 def _description(self): return _('Field of type: %(field_type)s') % { @@ -137,6 +148,7 @@ class Field(RegisterLookupMixin): self.max_length, self._unique = max_length, unique self.blank, self.null = blank, null self.rel = rel + self.is_relation = self.rel is not None self.default = default self.editable = editable self.serialize = serialize @@ -603,6 +615,7 @@ class Field(RegisterLookupMixin): if not self.name: self.name = name self.attname, self.column = self.get_attname_column() + self.concrete = self.column is not None if self.verbose_name is None and self.name: self.verbose_name = self.name.replace('_', ' ') @@ -610,7 +623,7 @@ class Field(RegisterLookupMixin): self.set_attributes_from_name(name) self.model = cls if virtual_only: - cls._meta.add_virtual_field(self) + cls._meta.add_field(self, virtual=True) else: cls._meta.add_field(self) if self.choices: diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 2e05e57e514..9ef6c1350a4 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -98,6 +98,18 @@ signals.class_prepared.connect(do_pending_lookups) 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): errors = super(RelatedField, self).check(**kwargs) 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 # any other field accessor -- i. e. Model.foreign accessor clashes with # Model.m2m accessor. - potential_clashes = rel_opts.get_all_related_many_to_many_objects() - potential_clashes += rel_opts.get_all_related_objects() - potential_clashes = (r for r in potential_clashes - if r.field is not self) + potential_clashes = (r for r in rel_opts.related_objects if r.field is not self) for clash_field in potential_clashes: 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) if clash_field.get_accessor_name() == rel_name: errors.append( @@ -392,7 +401,7 @@ class SingleRelatedObjectDescriptor(object): # consistency with `ReverseSingleRelatedObjectDescriptor`. return type( 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) 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 # related fields, respect that. 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() 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) try: rel_obj = self.get_queryset(instance=instance).get(**params) - except self.related.model.DoesNotExist: + except self.related.related_model.DoesNotExist: rel_obj = None else: setattr(rel_obj, self.related.field.get_cache_name(), instance) @@ -470,7 +479,7 @@ class SingleRelatedObjectDescriptor(object): 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( 'Cannot assign "%r": "%s.%s" must be a "%s" instance.' % ( value, @@ -825,9 +834,9 @@ class ForeignRelatedObjectsDescriptor(object): # Dynamically create a class that subclasses the related model's default # manager. return create_foreign_related_manager( - self.related.model._default_manager.__class__, + self.related.related_model._default_manager.__class__, 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 # model's default manager. return create_many_related_manager( - self.related.model._default_manager.__class__, + self.related.related_model._default_manager.__class__, self.related.field.rel ) @@ -1156,7 +1165,7 @@ class ManyRelatedObjectsDescriptor(object): if instance is None: return self - rel_model = self.related.model + rel_model = self.related.related_model manager = self.related_manager_cls( model=rel_model, @@ -1255,6 +1264,12 @@ class ReverseManyRelatedObjectsDescriptor(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, parent_link=False, on_delete=None, related_query_name=None): self.field = field @@ -1267,32 +1282,55 @@ class ForeignObjectRel(object): self.on_delete = on_delete 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 # before field.contribute_to_class() has been called will result in # AttributeError @cached_property def model(self): - if not self.field.model: - raise AttributeError( - "This property can't be accessed before self.field.contribute_to_class has been called.") - return self.field.model + return self.to @cached_property def opts(self): - return self.model._meta + return self.related_model._meta @cached_property def to_opts(self): return self.to._meta @cached_property - def parent_model(self): - return self.to + def hidden(self): + return self.is_hidden() @cached_property 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, limit_to_currently_related=False): @@ -1304,10 +1342,10 @@ class ForeignObjectRel(object): initially for utilization by RelatedFieldListFilter. """ 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: 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] return first_choice + lst @@ -1318,7 +1356,7 @@ class ForeignObjectRel(object): def is_hidden(self): "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): 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 # an alternate model. See BaseInlineFormSet.get_default_prefix(). opts = model._meta if model else self.opts - model = model or self.model + model = model or self.related_model if self.multiple: # If this is a symmetrical m2m relation on self, there is no reverse accessor. if self.symmetrical and model == self.to: @@ -1383,11 +1421,11 @@ class ManyToOneRel(ForeignObjectRel): Returns the Field in the 'to' object to which this relationship is tied. """ - data = self.to._meta.get_field_by_name(self.field_name) - if not data[2]: + field = self.to._meta.get_field(self.field_name) + if not field.concrete: raise FieldDoesNotExist("No related field named '%s'" % self.field_name) - return data[0] + return field def set_field_name(self): 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.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): """ Returns the field in the 'to' object to which this relationship is tied. @@ -1436,8 +1478,13 @@ class ManyToManyRel(ForeignObjectRel): 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 - generate_reverse_relation = True related_accessor_class = ForeignRelatedObjectsDescriptor 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] to_field_name = self.to_fields[index] 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 - 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)) return related_fields @@ -1731,7 +1778,7 @@ class ForeignObject(RelatedField): def contribute_to_related_class(self, cls, related): # Internal FK's - i.e., those with a related name ending with '+' - # 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)) # While 'limit_choices_to' might be a callable, simply pass # it along for later - this is too early because it's still @@ -1741,6 +1788,12 @@ class ForeignObject(RelatedField): 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 default_error_messages = { '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), 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 description = _("One-to-one relationship") @@ -2036,6 +2095,12 @@ def create_many_to_many_intermediary_model(field, klass): 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") 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 # here to break early if there's a problem. to = str(to) - kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['rel'] = ManyToManyRel( self, to, @@ -2357,8 +2421,8 @@ class ManyToManyField(RelatedField): """ pathinfos = [] int_model = self.rel.through - linkfield1 = int_model._meta.get_field_by_name(self.m2m_field_name())[0] - linkfield2 = int_model._meta.get_field_by_name(self.m2m_reverse_field_name())[0] + linkfield1 = int_model._meta.get_field(self.m2m_field_name()) + linkfield2 = int_model._meta.get_field(self.m2m_reverse_field_name()) if direct: join1infos = linkfield1.get_reverse_path_info() join2infos = linkfield2.get_path_info() @@ -2398,8 +2462,8 @@ class ManyToManyField(RelatedField): else: link_field_name = None for f in self.rel.through._meta.fields: - if hasattr(f, 'rel') and f.rel and f.rel.to == related.model and \ - (link_field_name is None or link_field_name == f.name): + if (f.is_relation and f.rel.to == related.related_model and + (link_field_name is None or link_field_name == f.name)): setattr(self, cache_attr, getattr(f, attr)) return getattr(self, cache_attr) @@ -2414,8 +2478,9 @@ class ManyToManyField(RelatedField): else: link_field_name = None for f in self.rel.through._meta.fields: - if hasattr(f, 'rel') and f.rel and f.rel.to == related.parent_model: - if link_field_name is None and related.model == related.parent_model: + # NOTE f.rel.to != f.related_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, # the first foreign key you find will be # the source column. Keep searching for @@ -2479,7 +2544,7 @@ class ManyToManyField(RelatedField): def contribute_to_related_class(self, cls, related): # Internal M2Ms (i.e., those with a related name ending with '+') # 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)) # Set up the accessors for the column names on the m2m table diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 179ea7cee3a..aa2df3f0e86 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -2,7 +2,6 @@ import copy from importlib import import_module import inspect -from django.core.exceptions import FieldDoesNotExist from django.db import router from django.db.models.query import QuerySet from django.utils import six @@ -23,15 +22,12 @@ def ensure_default_manager(cls): setattr(cls, 'objects', SwappedManagerDescriptor(cls)) return if not getattr(cls, '_default_manager', None): - # Create the default manager, if needed. - try: - cls._meta.get_field('objects') + if any(f.name == 'objects' for f in cls._meta.fields): raise ValueError( "Model %s must specify a custom Manager, because it has a " "field named 'objects'" % cls.__name__ ) - except FieldDoesNotExist: - pass + # Create the default manager, if needed. cls.add_to_class('objects', Manager()) cls._base_manager = cls.objects elif not getattr(cls, '_base_manager', None): diff --git a/django/db/models/options.py b/django/db/models/options.py index 79954a87e61..6b48d2733be 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -1,20 +1,31 @@ from __future__ import unicode_literals 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.conf import settings 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.proxy import OrderWrt 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.functional import cached_property +from django.utils.lru_cache import lru_cache from django.utils.text import camel_case_to_spaces 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', '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') +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): """ option_together can be either a tuple of tuples, or a single @@ -46,9 +75,19 @@ def normalize_together(option_together): return option_together +def make_immutable_fields_list(name, data): + return ImmutableList(data, warning=IMMUTABLE_WARNING % name) + + @python_2_unicode_compatible 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): + self._get_fields_cache = {} + self.proxied_children = [] self.local_fields = [] self.local_many_to_many = [] self.virtual_fields = [] @@ -103,6 +142,31 @@ class Options(object): 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 def app_config(self): # Don't go through get_app_config to avoid triggering imports. @@ -183,7 +247,17 @@ class Options(object): def _prepare(self, model): 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',) if not any(isinstance(field, OrderWrt) for field in model._meta.local_fields): model.add_to_class('_order', OrderWrt()) @@ -208,56 +282,41 @@ class Options(object): auto_created=True) 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 # the "creation_counter" attribute of the field. # Move many-to-many related fields from self.fields into # 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) - if hasattr(self, '_m2m_cache'): - del self._m2m_cache else: self.local_fields.insert(bisect(self.local_fields, field), 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'): - del self._name_map - - def add_virtual_field(self, field): - self.virtual_fields.append(field) + # If the field being added is a relation to another known field, + # expire the cache on this field and the forward cache on the field + # being referenced, because there will be new relationships in the + # cache. Otherwise, expire the cache of references *to* this 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): if not self.pk and field.primary_key: self.pk = field 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): """ Does the internal setup so that the current model is a proxy for @@ -273,6 +332,7 @@ class Options(object): def __str__(self): return "%s.%s" % (smart_text(self.app_label), smart_text(self.model_name)) + @property def verbose_name_raw(self): """ 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) activate(lang) 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 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): return swapped_for return None - swapped = property(_swapped) @cached_property def fields(self): """ - The getter for self.fields. This returns the list of field objects - available to this model (including through parent models). + Returns a list of all forward fields on the model and its parents, + excluding ManyToManyFields. - Callers are not permitted to modify this list, since it's a reference - to this instance (not a copy). + 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. """ - try: - self._field_name_cache - except AttributeError: - self._fill_fields_cache() - return self._field_name_cache + # For legacy reasons, the fields property should only contain forward + # fields that are not virtual or with a m2m cardinality. Therefore we + # pass these three filters as filters to the generator. + # The third lambda is a longwinded way of checking f.related_model - we don't + # 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 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 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): - """ - 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 + return [self._map_model(f) for f in self.get_fields()] + @raise_deprecation(suggested_alternative="get_fields()") def get_concrete_fields_with_model(self): - return [(field, model) for field, model in self.get_fields_with_model() if - field.column is not None] + return [self._map_model(f) for f in self.concrete_fields] - def _fill_fields_cache(self): - cache = [] - for parent in self.parents: - for field, model in parent._meta.get_fields_with_model(): - 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] + @cached_property + def many_to_many(self): + """ + Returns a list of all many to many fields on the model and its parents. - def _many_to_many(self): - try: - self._m2m_cache - except AttributeError: - self._fill_m2m_cache() - return list(self._m2m_cache) - many_to_many = property(_many_to_many) + 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 list. + """ + return make_immutable_fields_list( + "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): - """ - 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)) + return [self._map_model(f) for f in self.many_to_many] - def _fill_m2m_cache(self): - cache = OrderedDict() - for parent in self.parents: - for field, model in parent._meta.get_m2m_with_model(): - if model: - cache[field] = model - else: - cache[field] = parent - for field in self.local_many_to_many: - cache[field] = None - self._m2m_cache = cache - - 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: + @cached_property + def _forward_fields_map(self): + res = {} + # call get_fields() with export_ordered_set=True in order to have a + # field_instance -> names map + fields = self._get_fields(reverse=False) + 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: - return self._name_map[name] + res[field.attname] = field except AttributeError: - cache = self.init_name_map() - return cache[name] - except KeyError: - raise FieldDoesNotExist('%s has no field named %r' - % (self.object_name, name)) + pass + return res - 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 - (including reverse relation names). This is used for pretty printing - debugging output (a list of choices), so any internal-only field names - are not included. + Returns a field instance given a field name. The field can be either a + forward or reverse field, unless many_to_many is specified; if it is, + only forward fields will be returned. + + 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: - cache = self._name_map - except AttributeError: - cache = self.init_name_map() - 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('+')] + # In order to avoid premature loading of the relation tree + # (expensive) we prefer checking if the field is a forward field. + field = self._forward_fields_map[field_name] - def init_name_map(self): - """ - Initialises the field name -> field object mapping. - """ - 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 + if many_to_many is False and field.many_to_many: + raise FieldDoesNotExist( + '%s has no field named %r' % (self.object_name, field_name) + ) + 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, 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_hidden=False, + include_parents = local_only is 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): - """ - Returns a list of (related-object, model) pairs. Similar to - get_fields_with_model(). - """ - try: - 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 + return [ + self._map_model(f) for f in self.get_all_related_objects( + local_only=local_only, + include_hidden=include_hidden, + include_proxy_eq=include_proxy_eq, + ) + ] + @raise_deprecation(suggested_alternative="get_fields()") def get_all_related_many_to_many_objects(self, local_only=False): - try: - cache = self._related_many_to_many_cache - except AttributeError: - cache = self._fill_related_many_to_many_cache() - if local_only: - return [k for k, v in cache.items() if not v] - return list(cache) + fields = self._get_fields( + forward=False, reverse=True, + include_parents=local_only is not True, include_hidden=True + ) + return [obj for obj in fields if isinstance(obj.field, ManyToManyField)] + @raise_deprecation(suggested_alternative="get_fields()") def get_all_related_m2m_objects_with_model(self): - """ - Returns a list of (related-m2m-object, model) pairs. Similar to - 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 + fields = self._get_fields(forward=False, reverse=True, include_hidden=True) + return [self._map_model(obj) for obj in fields if isinstance(obj.field, ManyToManyField)] def get_base_chain(self, model): """ @@ -605,3 +665,173 @@ class Options(object): # of the chain to the ancestor is that parent # links 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 diff --git a/django/db/models/query.py b/django/db/models/query.py index 318e3ecf846..7a447f03131 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -252,9 +252,8 @@ class QuerySet(object): # If only/defer clauses have been specified, # build the list of fields that are to be loaded. if only_load: - for field, model in self.model._meta.get_concrete_fields_with_model(): - if model is None: - model = self.model + for field in self.model._meta.concrete_fields: + model = field.model._meta.model try: if field.name in only_load[model]: # Add a field that has been explicitly included @@ -818,7 +817,7 @@ class QuerySet(object): obj = self._clone() names = getattr(self, '_fields', 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 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() init_list = [] # 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): # Avoid loading fields already loaded for parent model for # child models. @@ -1381,18 +1381,19 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None, reverse_related_fields = [] 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, - only_load.get(o.model), reverse=True): + only_load.get(o.related_model), reverse=True): next = requested[o.field.related_query_name()] - parent = klass if issubclass(o.model, klass) else None - klass_info = get_klass_info(o.model, max_depth=max_depth, cur_depth=cur_depth + 1, + parent = klass if issubclass(o.related_model, klass) else None + 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) reverse_related_fields.append((o.field, klass_info)) if field_names: pk_idx = field_names.index(klass._meta.pk.attname) 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 @@ -1485,7 +1486,10 @@ def get_cached_row(row, index_start, using, klass_info, offset=0, for f, klass_info in reverse_related_fields: # Transfer data from this object to childs. 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): parent_data.append((rel_field, getattr(obj, rel_field.attname))) # Recursively retrieve the data for the related object diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index 69bc878caa2..e8b6cfb8c19 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -109,7 +109,7 @@ class DeferredAttribute(object): # self.field_name is the attname of the field, but only() takes the # actual name, so we need to translate it here. try: - f = opts.get_field_by_name(self.field_name)[0] + f = opts.get_field(self.field_name) except FieldDoesNotExist: f = [f for f in opts.fields if f.attname == self.field_name][0] name = f.name @@ -136,7 +136,7 @@ class DeferredAttribute(object): field is a primary key field. """ opts = instance._meta - f = opts.get_field_by_name(name)[0] + f = opts.get_field(name) link_field = opts.get_ancestor_link(f.model) if f.primary_key and f != link_field: return getattr(instance, link_field.attname) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index e8948cb9e1e..1c0b99e897d 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -298,7 +298,12 @@ class SQLCompiler(object): # be used by local fields. 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): # Avoid loading data for already loaded parents. continue @@ -601,10 +606,10 @@ class SQLCompiler(object): connections to the root model). """ 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 = ( 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) @@ -628,12 +633,13 @@ class SQLCompiler(object): else: 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) if restricted: next = requested.get(f.name, {}) - if not f.rel: + if not f.is_relation: # If a non-related field is used like a relation, # or if a single non-relational field is given. if next or (cur_depth == 1 and f.name in requested): @@ -647,10 +653,6 @@ class SQLCompiler(object): else: 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, only_load.get(field_model)): continue @@ -666,9 +668,9 @@ class SQLCompiler(object): if restricted: related_fields = [ - (o.field, o.model) - for o in opts.get_all_related_objects() - if o.field.unique + (o.field, o.related_model) + for o in opts.related_objects + if o.field.unique and not o.many_to_many ] for f, model in related_fields: if not select_related_descend(f, restricted, requested, @@ -760,7 +762,7 @@ class SQLCompiler(object): if self.query.select: fields = [f.field for f in self.query.select] elif self.query.default_cols: - fields = self.query.get_meta().concrete_fields + fields = list(self.query.get_meta().concrete_fields) else: fields = [] diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index eb164190915..1d1dbd8162c 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -11,6 +11,7 @@ from itertools import count, product from collections import Mapping, OrderedDict import copy +from itertools import chain import warnings from django.core.exceptions import FieldDoesNotExist, FieldError @@ -33,6 +34,13 @@ from django.utils.tree import Node __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): """ A single raw SQL query @@ -593,9 +601,9 @@ class Query(object): opts = orig_opts for name in parts[:-1]: old_model = cur_model - source = opts.get_field_by_name(name)[0] + source = opts.get_field(name) if is_reverse_o2o(source): - cur_model = source.model + cur_model = source.related_model else: cur_model = source.rel.to opts = cur_model._meta @@ -605,8 +613,11 @@ class Query(object): if not is_reverse_o2o(source): must_include[old_model].add(source) add_to_dict(must_include, cur_model, opts.pk) - field, model, _, _ = opts.get_field_by_name(parts[-1]) - if model is None: + field = opts.get_field(parts[-1]) + 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 if not is_reverse_o2o(field): add_to_dict(seen, model, field) @@ -618,10 +629,11 @@ class Query(object): # models. workset = {} 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: 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): # If we haven't included a model in workset, we don't add the # corresponding must_include fields for that model, since an @@ -934,8 +946,9 @@ class Query(object): root_alias = self.tables[0] seen = {None: root_alias} - for field, model in opts.get_fields_with_model(): - if model not in seen: + for field in opts.fields: + 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.included_inherited_models = seen @@ -1368,7 +1381,19 @@ class Query(object): if name == 'pk': name = opts.pk.name 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: # is it an annotation? if self._annotations and name in self._annotations: @@ -1382,14 +1407,15 @@ class Query(object): # one step. pos -= 1 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. " "Choices are: %s" % (name, ", ".join(available))) break # Check if we need any joins for concrete inheritance cases (the # field lives in parent, but we are currently in one of its # children) - if model: + if model is not opts.model: # The field lives on a base class of the current model. # Skip the chain of proxy to the concrete proxied model proxied_model = opts.concrete_model @@ -1432,7 +1458,7 @@ class Query(object): return path, final_field, targets, names[pos + 1:] 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. " "Choices are: %s" % (name, ", ".join(available))) @@ -1693,7 +1719,7 @@ class Query(object): # from the model on which the lookup failed. raise 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)) raise FieldError("Cannot resolve keyword %r into field. " "Choices are: %s" % (name, ", ".join(names))) diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 12bde13bf34..bae9f11c235 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -120,13 +120,15 @@ class UpdateQuery(Query): """ values_seq = [] for name, val in six.iteritems(values): - field, model, direct, m2m = self.get_meta().get_field_by_name(name) - if not direct or m2m: + field = self.get_meta().get_field(name) + 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( 'Cannot update model field %r (only non-relations and ' 'foreign keys permitted).' % field ) - if model: + if model is not self.get_meta().model: self.add_related_update(model, field, val) continue values_seq.append((field, model, val)) diff --git a/django/forms/models.py b/django/forms/models.py index c57c8af0b96..443d0559a6f 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -6,6 +6,7 @@ and database field objects. from __future__ import unicode_literals from collections import OrderedDict +from itertools import chain import warnings 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 # virtual_fields here. (GenericRelation was previously a fake # 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'): continue 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 opts = instance._meta 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): continue 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 sortable_virtual_fields = [f for f in opts.virtual_fields 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): continue if fields is not None and f.name not in fields: diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index 568831523c5..64747422222 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -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 backend supports :doc:`tablespaces `. You can usually ignore this option. -* ``auto_created``: ``True`` if the field was automatically created, as for the - :class:`~django.db.models.OneToOneField` used by model inheritance. For - advanced use only. +* :attr:`~django.db.models.Field.auto_created`: ``True`` if the field was + automatically created, as for the :class:`~django.db.models.OneToOneField` + used by model inheritance. For advanced use only. 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 diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index b74f550ba86..e2c89d360b6 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -56,6 +56,19 @@ details on these changes. * ``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 ``unordered_list`` filter will no longer support old style lists. diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 35997801fb0..bc214fae00c 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -1790,3 +1790,95 @@ Field API reference This method must be added to fields prior to 1.7 to migrate its data 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 +` 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() + ` + 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 `). + +.. 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``. diff --git a/docs/ref/models/index.txt b/docs/ref/models/index.txt index d1d3680dd87..284743e7d09 100644 --- a/docs/ref/models/index.txt +++ b/docs/ref/models/index.txt @@ -8,6 +8,7 @@ Model API reference. For introductory material, see :doc:`/topics/db/models`. :maxdepth: 1 fields + meta relations class options diff --git a/docs/ref/models/meta.txt b/docs/ref/models/meta.txt new file mode 100644 index 00000000000..fdad7b2ad09 --- /dev/null +++ b/docs/ref/models/meta.txt @@ -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 ` + 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 ` 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') + + + # A field from another model that has a relation with the current model + >>> User._meta.get_field('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 ` + 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() + (, + , + , + , + , + , + , + , + , + , + , + , + , + ) + + # Also include hidden fields. + >>> User._meta.get_fields(include_hidden=True) + (, + , + , + , + , + , + , + , + , + , + , + , + , + , + , + ) + +.. _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() + `, or; + +* invoking :meth:`Options.get_fields() + ` to retrieve a list of all + fields, and then filtering this list using the :ref:`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 ` 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. diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 460d637d9de..457850159f5 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -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 ======================== +``Model._meta`` API +~~~~~~~~~~~~~~~~~~~ + +Django now has a formalized API for :doc:`Model._meta `, +providing an officially supported way to :ref:`retrieve fields +` and filter fields based on their :ref:`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 +` has been provided. + Security enhancements ~~~~~~~~~~~~~~~~~~~~~ @@ -998,6 +1014,26 @@ Miscellaneous 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 ` 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/backends/tests.py b/tests/backends/tests.py index 7fbb57ba0ca..9eeb90335c2 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -1020,7 +1020,7 @@ class DBConstraintTestCase(TestCase): self.assertEqual(models.Object.objects.count(), 2) 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) self.assertEqual(obj.related_objects.count(), 1) self.assertEqual(intermediary_model.objects.count(), 2) diff --git a/tests/basic/tests.py b/tests/basic/tests.py index 74515047f55..3ba94d058d1 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -776,7 +776,7 @@ class ModelRefreshTests(TestCase): class TestRelatedObjectDeprecation(TestCase): 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: warnings.simplefilter('always') self.assertIsInstance(field.related, ForeignObjectRel) diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py index adef8ec5e5b..0201693dba6 100644 --- a/tests/fixtures/tests.py +++ b/tests/fixtures/tests.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import os import warnings +from django.apps import apps from django.contrib.sites.models import Site from django.core import management from django.db import connection, IntegrityError @@ -76,6 +77,7 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): ]) def test_loading_and_dumping(self): + apps.clear_cache() Site.objects.all().delete() # Load fixture 1. Single JSON file, with two objects. management.call_command('loaddata', 'fixture1.json', verbosity=0) diff --git a/tests/generic_relations/tests.py b/tests/generic_relations/tests.py index e7f7b02f5b2..c6230b057e1 100644 --- a/tests/generic_relations/tests.py +++ b/tests/generic_relations/tests.py @@ -403,7 +403,8 @@ class GenericRelationsTests(TestCase): self.assertEqual(tag.content_object.id, diamond.id) 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='') diff --git a/tests/generic_relations_regress/tests.py b/tests/generic_relations_regress/tests.py index 65afa9f298d..88243bade0c 100644 --- a/tests/generic_relations_regress/tests.py +++ b/tests/generic_relations_regress/tests.py @@ -260,7 +260,7 @@ class GenericRelationTests(TestCase): form = GenericRelationForm({'links': None}) self.assertTrue(form.is_valid()) 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) def test_ticket_22998(self): diff --git a/tests/m2m_and_m2o/tests.py b/tests/m2m_and_m2o/tests.py index e950a839d21..2317f62300e 100644 --- a/tests/m2m_and_m2o/tests.py +++ b/tests/m2m_and_m2o/tests.py @@ -5,6 +5,12 @@ from .models import Issue, User, UnicodeReferenceModel 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): r = User.objects.create(username="russell") g = User.objects.create(username="gustav") diff --git a/tests/many_to_one/tests.py b/tests/many_to_one/tests.py index 0195e5d2d8b..e82d8eefe60 100644 --- a/tests/many_to_one/tests.py +++ b/tests/many_to_one/tests.py @@ -437,11 +437,11 @@ class ManyToOneTests(TestCase): expected_message = "Cannot resolve keyword 'notafield' into field. Choices are: %s" 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, 'reporter__notafield') 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, 'notafield') diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py index 6f510813afb..e069649810e 100644 --- a/tests/migrations/test_state.py +++ b/tests/migrations/test_state.py @@ -202,8 +202,8 @@ class StateTests(TestCase): )) 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_by_name("hidden")[0].null, False) + 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("hidden").null, False) self.assertEqual(len(new_apps.get_model("migrations", "SubTag")._meta.local_fields), 2) diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py index 2a7bebc9ddf..8927ae8830b 100644 --- a/tests/model_fields/models.py +++ b/tests/model_fields/models.py @@ -8,6 +8,11 @@ except ImportError: Image = None 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.models.fields.files import ImageFieldFile, ImageField from django.utils import six @@ -295,6 +300,52 @@ if Image: height_field='headshot_height', 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) + + ############################################################################### diff --git a/tests/model_fields/test_field_flags.py b/tests/model_fields/test_field_flags.py new file mode 100644 index 00000000000..08a57db5019 --- /dev/null +++ b/tests/model_fields/test_field_flags.py @@ -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) diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py index e63cc007dd9..d891645ca0c 100644 --- a/tests/model_fields/tests.py +++ b/tests/model_fields/tests.py @@ -198,7 +198,7 @@ class ForeignKeyTests(test.TestCase): self.assertEqual(warnings, expected_warnings) 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) diff --git a/tests/model_meta/results.py b/tests/model_meta/results.py new file mode 100644 index 00000000000..1efc49ed235 --- /dev/null +++ b/tests/model_meta/results.py @@ -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', + ], + }, +} diff --git a/tests/model_meta/test.py b/tests/model_meta/test.py deleted file mode 100644 index ea861d5ad5b..00000000000 --- a/tests/model_meta/test.py +++ /dev/null @@ -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) diff --git a/tests/model_meta/test_legacy.py b/tests/model_meta/test_legacy.py new file mode 100644 index 00000000000..60bfb1641ff --- /dev/null +++ b/tests/model_meta/test_legacy.py @@ -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)) diff --git a/tests/model_meta/tests.py b/tests/model_meta/tests.py new file mode 100644 index 00000000000..31ffa4ac938 --- /dev/null +++ b/tests/model_meta/tests.py @@ -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], []) diff --git a/tests/queries/tests.py b/tests/queries/tests.py index aed122a7c13..900149b873a 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -2093,7 +2093,7 @@ class CloneTests(TestCase): testing is impossible, this is a sanity check against invalid use of 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) opts_class.__deepcopy__ = lambda obj, memo: self.fail("Model fields shouldn't be cloned") try: diff --git a/tests/queryset_pickle/tests.py b/tests/queryset_pickle/tests.py index 22b61199f47..91fe304f9d8 100644 --- a/tests/queryset_pickle/tests.py +++ b/tests/queryset_pickle/tests.py @@ -79,7 +79,7 @@ class PickleabilityTestCase(TestCase): m1 = M2MModel.objects.create() g1 = Group.objects.create(name='foof') 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() dumped = pickle.dumps(original) reloaded = pickle.loads(dumped) diff --git a/tests/schema/tests.py b/tests/schema/tests.py index e9de2131a96..b10ad07251b 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -138,7 +138,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Book, - Book._meta.get_field_by_name("author")[0], + Book._meta.get_field("author"), new_field, strict=True, ) @@ -393,7 +393,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Author, - Author._meta.get_field_by_name("name")[0], + Author._meta.get_field("name"), new_field, strict=True, ) @@ -424,7 +424,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Note, - Note._meta.get_field_by_name("info")[0], + Note._meta.get_field("info"), new_field, strict=True, ) @@ -451,7 +451,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Author, - Author._meta.get_field_by_name("height")[0], + Author._meta.get_field("height"), new_field ) # Ensure the field is right afterwards @@ -479,7 +479,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( AuthorWithDefaultHeight, - AuthorWithDefaultHeight._meta.get_field_by_name("height")[0], + AuthorWithDefaultHeight._meta.get_field("height"), new_field, ) # Ensure the field is right afterwards @@ -512,7 +512,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Book, - Book._meta.get_field_by_name("author")[0], + Book._meta.get_field("author"), new_field, strict=True, ) @@ -542,7 +542,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Author, - Author._meta.get_field_by_name("id")[0], + Author._meta.get_field("id"), new_field, strict=True, ) @@ -568,7 +568,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Author, - Author._meta.get_field_by_name("name")[0], + Author._meta.get_field("name"), new_field, strict=True, ) @@ -587,7 +587,7 @@ class SchemaTests(TransactionTestCase): editor.create_model(TagM2MTest) editor.create_model(BookWithM2M) # 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") def test_m2m_create_through(self): @@ -661,7 +661,7 @@ class SchemaTests(TransactionTestCase): self.assertEqual(len(self.column_classes(AuthorTag)), 3) # "Alter" the field's blankness. This should not actually do anything. 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.contribute_to_class(AuthorWithM2MThrough, "tags") editor.alter_field( @@ -683,7 +683,7 @@ class SchemaTests(TransactionTestCase): editor.create_model(TagM2MTest) editor.create_model(UniqueTest) # 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: for name, details in constraints.items(): if details['columns'] == ["tagm2mtest_id"] and details['foreign_key']: @@ -698,11 +698,11 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Author, - BookWithM2M._meta.get_field_by_name("tags")[0], + BookWithM2M._meta.get_field("tags"), new_field, ) # 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 constraints = self.get_constraints(new_field.rel.through._meta.db_table) if connection.features.supports_foreign_keys: @@ -715,10 +715,10 @@ class SchemaTests(TransactionTestCase): finally: # Cleanup through table separately 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 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") def test_check_constraints(self): @@ -741,7 +741,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Author, - Author._meta.get_field_by_name("height")[0], + Author._meta.get_field("height"), new_field, strict=True, ) @@ -754,7 +754,7 @@ class SchemaTests(TransactionTestCase): editor.alter_field( Author, new_field, - Author._meta.get_field_by_name("height")[0], + Author._meta.get_field("height"), strict=True, ) constraints = self.get_constraints(Author._meta.db_table) @@ -781,7 +781,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Tag, - Tag._meta.get_field_by_name("slug")[0], + Tag._meta.get_field("slug"), new_field, strict=True, ) @@ -809,8 +809,8 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Tag, - Tag._meta.get_field_by_name("slug")[0], - TagUniqueRename._meta.get_field_by_name("slug2")[0], + Tag._meta.get_field("slug"), + TagUniqueRename._meta.get_field("slug2"), strict=True, ) # Ensure the field is still unique @@ -976,7 +976,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( Book, - Book._meta.get_field_by_name("title")[0], + Book._meta.get_field("title"), new_field, strict=True, ) @@ -990,7 +990,7 @@ class SchemaTests(TransactionTestCase): editor.alter_field( Book, new_field, - Book._meta.get_field_by_name("title")[0], + Book._meta.get_field("title"), strict=True, ) # Ensure the table is there and has the index again @@ -1002,7 +1002,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.add_field( Book, - BookWithSlug._meta.get_field_by_name("slug")[0], + BookWithSlug._meta.get_field("slug"), ) self.assertIn( "slug", @@ -1014,7 +1014,7 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field( BookWithSlug, - BookWithSlug._meta.get_field_by_name("slug")[0], + BookWithSlug._meta.get_field("slug"), new_field2, strict=True, ) @@ -1039,10 +1039,10 @@ class SchemaTests(TransactionTestCase): new_field.set_attributes_from_name("slug") new_field.model = Tag 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( Tag, - Tag._meta.get_field_by_name("slug")[0], + Tag._meta.get_field("slug"), new_field, ) # Ensure the PK changed