diff options
| author | Daniel Pyrathon <pirosb3@gmail.com> | 2015-01-06 19:16:35 -0500 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2015-01-06 19:25:12 -0500 |
| commit | fb48eb05816b1ac87d58696cdfe48be18c901f16 (patch) | |
| tree | 3d2e981b6f3fafdeb7310d0734fb148ebb7f6aef /django | |
| parent | 749d23251bbd6564341405e6f8c1da129b8307e7 (diff) | |
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!
Diffstat (limited to 'django')
34 files changed, 889 insertions, 490 deletions
diff --git a/django/apps/registry.py b/django/apps/registry.py index fe53d965de..68aa411d91 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 ee47f911b1..896d59155c 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 35758cfdaf..3e682f9b10 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 544c5503ec..266b880d99 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 5da461351a..0f617b3fb9 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 92ede613ec..67b97f776e 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 2b45301f3a..2ffd402c95 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 2297ad63a9..07d47becf8 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 f2e7657850..c05688dec9 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 dd156ea4b6..1501c98136 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 c0c2f83598..d12ed53298 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 1c848f105e..2ff23fc38b 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 e5aa5a7039..1460be2de9 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 b4712c76be..f8dd7aebac 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 de82a969c1..0b759799d2 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 5e0248d4df..42a58c03e8 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 40db888e50..dfda05bc19 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 9aeca56bf7..e0433b0c13 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 e3dd87e3bd..8535a98e73 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 54251cf6ed..6ce5e37296 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 f07f667c51..6dd66ae454 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 8a45a0d2f2..6626e31442 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 ec5d4c7c1c..945cd0154b 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 c61f865be2..016fc5637e 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 8dc9c554dd..a1995452a3 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 2e05e57e51..9ef6c1350a 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 179ea7cee3..aa2df3f0e8 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 79954a87e6..6b48d2733b 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] - - 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. + 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. """ - try: - self._field_cache - except AttributeError: - self._fill_fields_cache() - return self._field_cache + 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): + 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) + ) - def get_m2m_with_model(self): + @cached_property + def related_objects(self): """ - The many-to-many version of get_fields_with_model(). + 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. """ - try: - self._m2m_cache - except AttributeError: - self._fill_m2m_cache() - return list(six.iteritems(self._m2m_cache)) + 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) + ) - 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 + @raise_deprecation(suggested_alternative="get_fields()") + def get_m2m_with_model(self): + return [self._map_model(f) for f in self.many_to_many] - 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)) + @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: + res[field.attname] = field + except AttributeError: + pass + return res - def get_field_by_name(self, name): + @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 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). + 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. - Uses a cache internally, so after the first access, this is very fast. + The many_to_many argument exists for backwards compatibility reasons; + it has been deprecated and will be removed in Django 2.0. """ + m2m_in_kwargs = many_to_many is not None + if m2m_in_kwargs: + # Always throw a warning if many_to_many is used regardless of + # whether it alters the return type or not. + warnings.warn( + "The 'many_to_many' argument on get_field() is deprecated; " + "use a filter on field.many_to_many instead.", + RemovedInDjango20Warning + ) + try: - try: - return self._name_map[name] - except AttributeError: - cache = self.init_name_map() - return cache[name] + # 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] + + 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: - raise FieldDoesNotExist('%s has no field named %r' - % (self.object_name, name)) + # 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) + ) - def get_all_field_names(self): - """ - 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. - """ 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('+')] + 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)) - 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 + # 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_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)] + 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)) - 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 + 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): + 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 318e3ecf84..7a447f0313 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 69bc878caa..e8b6cfb8c1 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 e8948cb9e1..1c0b99e897 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 eb16419091..1d1dbd8162 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 12bde13bf3..bae9f11c23 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 c57c8af0b9..443d0559a6 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: |
