import collections from itertools import chain from django.apps import apps from django.conf import settings from django.contrib.admin.utils import NotRelationField, flatten, get_fields_from_path from django.core import checks from django.core.exceptions import FieldDoesNotExist from django.db import models from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import Combinable from django.forms.models import BaseModelForm, BaseModelFormSet, _get_foreign_key from django.template import engines from django.template.backends.django import DjangoTemplates from django.utils.module_loading import import_string def _issubclass(cls, classinfo): """ issubclass() variant that doesn't raise an exception if cls isn't a class. """ try: return issubclass(cls, classinfo) except TypeError: return False def _contains_subclass(class_path, candidate_paths): """ Return whether or not a dotted class path (or a subclass of that class) is found in a list of candidate paths. """ cls = import_string(class_path) for path in candidate_paths: try: candidate_cls = import_string(path) except ImportError: # ImportErrors are raised elsewhere. continue if _issubclass(candidate_cls, cls): return True return False def check_admin_app(app_configs, **kwargs): from django.contrib.admin.sites import all_sites errors = [] for site in all_sites: errors.extend(site.check(app_configs)) return errors def check_dependencies(**kwargs): """ Check that the admin's dependencies are correctly installed. """ from django.contrib.admin.sites import all_sites if not apps.is_installed("django.contrib.admin"): return [] errors = [] app_dependencies = ( ("django.contrib.contenttypes", 401), ("django.contrib.auth", 405), ("django.contrib.messages", 406), ) for app_name, error_code in app_dependencies: if not apps.is_installed(app_name): errors.append( checks.Error( "'%s' must be in INSTALLED_APPS in order to use the admin " "application." % app_name, id="admin.E%d" % error_code, ) ) for engine in engines.all(): if isinstance(engine, DjangoTemplates): django_templates_instance = engine.engine break else: django_templates_instance = None if not django_templates_instance: errors.append( checks.Error( "A 'django.template.backends.django.DjangoTemplates' instance " "must be configured in TEMPLATES in order to use the admin " "application.", id="admin.E403", ) ) else: if ( "django.contrib.auth.context_processors.auth" not in django_templates_instance.context_processors and _contains_subclass( "django.contrib.auth.backends.ModelBackend", settings.AUTHENTICATION_BACKENDS, ) ): errors.append( checks.Error( "'django.contrib.auth.context_processors.auth' must be " "enabled in DjangoTemplates (TEMPLATES) if using the default " "auth backend in order to use the admin application.", id="admin.E402", ) ) if ( "django.contrib.messages.context_processors.messages" not in django_templates_instance.context_processors ): errors.append( checks.Error( "'django.contrib.messages.context_processors.messages' must " "be enabled in DjangoTemplates (TEMPLATES) in order to use " "the admin application.", id="admin.E404", ) ) sidebar_enabled = any(site.enable_nav_sidebar for site in all_sites) if ( sidebar_enabled and "django.template.context_processors.request" not in django_templates_instance.context_processors ): errors.append( checks.Warning( "'django.template.context_processors.request' must be enabled " "in DjangoTemplates (TEMPLATES) in order to use the admin " "navigation sidebar.", id="admin.W411", ) ) if not _contains_subclass( "django.contrib.auth.middleware.AuthenticationMiddleware", settings.MIDDLEWARE ): errors.append( checks.Error( "'django.contrib.auth.middleware.AuthenticationMiddleware' must " "be in MIDDLEWARE in order to use the admin application.", id="admin.E408", ) ) if not _contains_subclass( "django.contrib.messages.middleware.MessageMiddleware", settings.MIDDLEWARE ): errors.append( checks.Error( "'django.contrib.messages.middleware.MessageMiddleware' must " "be in MIDDLEWARE in order to use the admin application.", id="admin.E409", ) ) if not _contains_subclass( "django.contrib.sessions.middleware.SessionMiddleware", settings.MIDDLEWARE ): errors.append( checks.Error( "'django.contrib.sessions.middleware.SessionMiddleware' must " "be in MIDDLEWARE in order to use the admin application.", hint=( "Insert " "'django.contrib.sessions.middleware.SessionMiddleware' " "before " "'django.contrib.auth.middleware.AuthenticationMiddleware'." ), id="admin.E410", ) ) return errors class BaseModelAdminChecks: def check(self, admin_obj, **kwargs): return [ *self._check_autocomplete_fields(admin_obj), *self._check_raw_id_fields(admin_obj), *self._check_fields(admin_obj), *self._check_fieldsets(admin_obj), *self._check_exclude(admin_obj), *self._check_form(admin_obj), *self._check_filter_vertical(admin_obj), *self._check_filter_horizontal(admin_obj), *self._check_radio_fields(admin_obj), *self._check_prepopulated_fields(admin_obj), *self._check_view_on_site_url(admin_obj), *self._check_ordering(admin_obj), *self._check_readonly_fields(admin_obj), ] def _check_autocomplete_fields(self, obj): """ Check that `autocomplete_fields` is a list or tuple of model fields. """ if not isinstance(obj.autocomplete_fields, (list, tuple)): return must_be( "a list or tuple", option="autocomplete_fields", obj=obj, id="admin.E036", ) else: return list( chain.from_iterable( [ self._check_autocomplete_fields_item( obj, field_name, "autocomplete_fields[%d]" % index ) for index, field_name in enumerate(obj.autocomplete_fields) ] ) ) def _check_autocomplete_fields_item(self, obj, field_name, label): """ Check that an item in `autocomplete_fields` is a ForeignKey or a ManyToManyField and that the item has a related ModelAdmin with search_fields defined. """ try: field = obj.model._meta.get_field(field_name) except FieldDoesNotExist: return refer_to_missing_field( field=field_name, option=label, obj=obj, id="admin.E037" ) else: if not field.many_to_many and not isinstance(field, models.ForeignKey): return must_be( "a foreign key or a many-to-many field", option=label, obj=obj, id="admin.E038", ) related_admin = obj.admin_site._registry.get(field.remote_field.model) if related_admin is None: return [ checks.Error( 'An admin for model "%s" has to be registered ' "to be referenced by %s.autocomplete_fields." % ( field.remote_field.model.__name__, type(obj).__name__, ), obj=obj.__class__, id="admin.E039", ) ] elif not related_admin.search_fields: return [ checks.Error( '%s must define "search_fields", because it\'s ' "referenced by %s.autocomplete_fields." % ( related_admin.__class__.__name__, type(obj).__name__, ), obj=obj.__class__, id="admin.E040", ) ] return [] def _check_raw_id_fields(self, obj): """Check that `raw_id_fields` only contains field names that are listed on the model.""" if not isinstance(obj.raw_id_fields, (list, tuple)): return must_be( "a list or tuple", option="raw_id_fields", obj=obj, id="admin.E001" ) else: return list( chain.from_iterable( self._check_raw_id_fields_item( obj, field_name, "raw_id_fields[%d]" % index ) for index, field_name in enumerate(obj.raw_id_fields) ) ) def _check_raw_id_fields_item(self, obj, field_name, label): """Check an item of `raw_id_fields`, i.e. check that field named `field_name` exists in model `model` and is a ForeignKey or a ManyToManyField.""" try: field = obj.model._meta.get_field(field_name) except FieldDoesNotExist: return refer_to_missing_field( field=field_name, option=label, obj=obj, id="admin.E002" ) else: # Using attname is not supported. if field.name != field_name: return refer_to_missing_field( field=field_name, option=label, obj=obj, id="admin.E002", ) if not field.many_to_many and not isinstance(field, models.ForeignKey): return must_be( "a foreign key or a many-to-many field", option=label, obj=obj, id="admin.E003", ) else: return [] def _check_fields(self, obj): """Check that `fields` only refer to existing fields, doesn't contain duplicates. Check if at most one of `fields` and `fieldsets` is defined. """ if obj.fields is None: return [] elif not isinstance(obj.fields, (list, tuple)): return must_be("a list or tuple", option="fields", obj=obj, id="admin.E004") elif obj.fieldsets: return [ checks.Error( "Both 'fieldsets' and 'fields' are specified.", obj=obj.__class__, id="admin.E005", ) ] fields = flatten(obj.fields) if len(fields) != len(set(fields)): return [ checks.Error( "The value of 'fields' contains duplicate field(s).", obj=obj.__class__, id="admin.E006", ) ] return list( chain.from_iterable( self._check_field_spec(obj, field_name, "fields") for field_name in obj.fields ) ) def _check_fieldsets(self, obj): """Check that fieldsets is properly formatted and doesn't contain duplicates.""" if obj.fieldsets is None: return [] elif not isinstance(obj.fieldsets, (list, tuple)): return must_be( "a list or tuple", option="fieldsets", obj=obj, id="admin.E007" ) else: seen_fields = [] return list( chain.from_iterable( self._check_fieldsets_item( obj, fieldset, "fieldsets[%d]" % index, seen_fields ) for index, fieldset in enumerate(obj.fieldsets) ) ) def _check_fieldsets_item(self, obj, fieldset, label, seen_fields): """Check an item of `fieldsets`, i.e. check that this is a pair of a set name and a dictionary containing "fields" key.""" if not isinstance(fieldset, (list, tuple)): return must_be("a list or tuple", option=label, obj=obj, id="admin.E008") elif len(fieldset) != 2: return must_be("of length 2", option=label, obj=obj, id="admin.E009") elif not isinstance(fieldset[1], dict): return must_be( "a dictionary", option="%s[1]" % label, obj=obj, id="admin.E010" ) elif "fields" not in fieldset[1]: return [ checks.Error( "The value of '%s[1]' must contain the key 'fields'." % label, obj=obj.__class__, id="admin.E011", ) ] elif not isinstance(fieldset[1]["fields"], (list, tuple)): return must_be( "a list or tuple", option="%s[1]['fields']" % label, obj=obj, id="admin.E008", ) seen_fields.extend(flatten(fieldset[1]["fields"])) if len(seen_fields) != len(set(seen_fields)): return [ checks.Error( "There are duplicate field(s) in '%s[1]'." % label, obj=obj.__class__, id="admin.E012", ) ] return list( chain.from_iterable( self._check_field_spec(obj, fieldset_fields, '%s[1]["fields"]' % label) for fieldset_fields in fieldset[1]["fields"] ) ) def _check_field_spec(self, obj, fields, label): """`fields` should be an item of `fields` or an item of fieldset[1]['fields'] for any `fieldset` in `fieldsets`. It should be a field name or a tuple of field names.""" if isinstance(fields, tuple): return list( chain.from_iterable( self._check_field_spec_item( obj, field_name, "%s[%d]" % (label, index) ) for index, field_name in enumerate(fields) ) ) else: return self._check_field_spec_item(obj, fields, label) def _check_field_spec_item(self, obj, field_name, label): if field_name in obj.readonly_fields: # Stuff can be put in fields that isn't actually a model field if # it's in readonly_fields, readonly_fields will handle the # validation of such things. return [] else: try: field = obj.model._meta.get_field(field_name) except FieldDoesNotExist: # If we can't find a field on the model that matches, it could # be an extra field on the form. return [] else: if ( isinstance(field, models.ManyToManyField) and not field.remote_field.through._meta.auto_created ): return [ checks.Error( "The value of '%s' cannot include the ManyToManyField " "'%s', because that field manually specifies a " "relationship model." % (label, field_name), obj=obj.__class__, id="admin.E013", ) ] else: return [] def _check_exclude(self, obj): """Check that exclude is a sequence without duplicates.""" if obj.exclude is None: # default value is None return [] elif not isinstance(obj.exclude, (list, tuple)): return must_be( "a list or tuple", option="exclude", obj=obj, id="admin.E014" ) elif len(obj.exclude) > len(set(obj.exclude)): return [ checks.Error( "The value of 'exclude' contains duplicate field(s).", obj=obj.__class__, id="admin.E015", ) ] else: return [] def _check_form(self, obj): """Check that form subclasses BaseModelForm.""" if not _issubclass(obj.form, BaseModelForm): return must_inherit_from( parent="BaseModelForm", option="form", obj=obj, id="admin.E016" ) else: return [] def _check_filter_vertical(self, obj): """Check that filter_vertical is a sequence of field names.""" if not isinstance(obj.filter_vertical, (list, tuple)): return must_be( "a list or tuple", option="filter_vertical", obj=obj, id="admin.E017" ) else: return list( chain.from_iterable( self._check_filter_item( obj, field_name, "filter_vertical[%d]" % index ) for index, field_name in enumerate(obj.filter_vertical) ) ) def _check_filter_horizontal(self, obj): """Check that filter_horizontal is a sequence of field names.""" if not isinstance(obj.filter_horizontal, (list, tuple)): return must_be( "a list or tuple", option="filter_horizontal", obj=obj, id="admin.E018" ) else: return list( chain.from_iterable( self._check_filter_item( obj, field_name, "filter_horizontal[%d]" % index ) for index, field_name in enumerate(obj.filter_horizontal) ) ) def _check_filter_item(self, obj, field_name, label): """Check one item of `filter_vertical` or `filter_horizontal`, i.e. check that given field exists and is a ManyToManyField.""" try: field = obj.model._meta.get_field(field_name) except FieldDoesNotExist: return refer_to_missing_field( field=field_name, option=label, obj=obj, id="admin.E019" ) else: if not field.many_to_many: return must_be( "a many-to-many field", option=label, obj=obj, id="admin.E020" ) else: return [] def _check_radio_fields(self, obj): """Check that `radio_fields` is a dictionary.""" if not isinstance(obj.radio_fields, dict): return must_be( "a dictionary", option="radio_fields", obj=obj, id="admin.E021" ) else: return list( chain.from_iterable( self._check_radio_fields_key(obj, field_name, "radio_fields") + self._check_radio_fields_value( obj, val, 'radio_fields["%s"]' % field_name ) for field_name, val in obj.radio_fields.items() ) ) def _check_radio_fields_key(self, obj, field_name, label): """Check that a key of `radio_fields` dictionary is name of existing field and that the field is a ForeignKey or has `choices` defined.""" try: field = obj.model._meta.get_field(field_name) except FieldDoesNotExist: return refer_to_missing_field( field=field_name, option=label, obj=obj, id="admin.E022" ) else: if not (isinstance(field, models.ForeignKey) or field.choices): return [ checks.Error( "The value of '%s' refers to '%s', which is not an " "instance of ForeignKey, and does not have a 'choices' " "definition." % (label, field_name), obj=obj.__class__, id="admin.E023", ) ] else: return [] def _check_radio_fields_value(self, obj, val, label): """Check type of a value of `radio_fields` dictionary.""" from django.contrib.admin.options import HORIZONTAL, VERTICAL if val not in (HORIZONTAL, VERTICAL): return [ checks.Error( "The value of '%s' must be either admin.HORIZONTAL or " "admin.VERTICAL." % label, obj=obj.__class__, id="admin.E024", ) ] else: return [] def _check_view_on_site_url(self, obj): if not callable(obj.view_on_site) and not isinstance(obj.view_on_site, bool): return [ checks.Error( "The value of 'view_on_site' must be a callable or a boolean " "value.", obj=obj.__class__, id="admin.E025", ) ] else: return [] def _check_prepopulated_fields(self, obj): """Check that `prepopulated_fields` is a dictionary containing allowed field types.""" if not isinstance(obj.prepopulated_fields, dict): return must_be( "a dictionary", option="prepopulated_fields", obj=obj, id="admin.E026" ) else: return list( chain.from_iterable( self._check_prepopulated_fields_key( obj, field_name, "prepopulated_fields" ) + self._check_prepopulated_fields_value( obj, val, 'prepopulated_fields["%s"]' % field_name ) for field_name, val in obj.prepopulated_fields.items() ) ) def _check_prepopulated_fields_key(self, obj, field_name, label): """Check a key of `prepopulated_fields` dictionary, i.e. check that it is a name of existing field and the field is one of the allowed types. """ try: field = obj.model._meta.get_field(field_name) except FieldDoesNotExist: return refer_to_missing_field( field=field_name, option=label, obj=obj, id="admin.E027" ) else: if isinstance( field, (models.DateTimeField, models.ForeignKey, models.ManyToManyField) ): return [ checks.Error( "The value of '%s' refers to '%s', which must not be a " "DateTimeField, a ForeignKey, a OneToOneField, or a " "ManyToManyField." % (label, field_name), obj=obj.__class__, id="admin.E028", ) ] else: return [] def _check_prepopulated_fields_value(self, obj, val, label): """Check a value of `prepopulated_fields` dictionary, i.e. it's an iterable of existing fields.""" if not isinstance(val, (list, tuple)): return must_be("a list or tuple", option=label, obj=obj, id="admin.E029") else: return list( chain.from_iterable( self._check_prepopulated_fields_value_item( obj, subfield_name, "%s[%r]" % (label, index) ) for index, subfield_name in enumerate(val) ) ) def _check_prepopulated_fields_value_item(self, obj, field_name, label): """For `prepopulated_fields` equal to {"slug": ("title",)}, `field_name` is "title".""" try: obj.model._meta.get_field(field_name) except FieldDoesNotExist: return refer_to_missing_field( field=field_name, option=label, obj=obj, id="admin.E030" ) else: return [] def _check_ordering(self, obj): """Check that ordering refers to existing fields or is random.""" # ordering = None if obj.ordering is None: # The default value is None return [] elif not isinstance(obj.ordering, (list, tuple)): return must_be( "a list or tuple", option="ordering", obj=obj, id="admin.E031" ) else: return list( chain.from_iterable( self._check_ordering_item(obj, field_name, "ordering[%d]" % index) for index, field_name in enumerate(obj.ordering) ) ) def _check_ordering_item(self, obj, field_name, label): """Check that `ordering` refers to existing fields.""" if isinstance(field_name, (Combinable, models.OrderBy)): if not isinstance(field_name, models.OrderBy): field_name = field_name.asc() if isinstance(field_name.expression, models.F): field_name = field_name.expression.name else: return [] if field_name == "?" and len(obj.ordering) != 1: return [ checks.Error( "The value of 'ordering' has the random ordering marker '?', " "but contains other fields as well.", hint='Either remove the "?", or remove the other fields.', obj=obj.__class__, id="admin.E032", ) ] elif field_name == "?": return [] elif LOOKUP_SEP in field_name: # Skip ordering in the format field1__field2 (FIXME: checking # this format would be nice, but it's a little fiddly). return [] else: if field_name.startswith("-"): field_name = field_name[1:] if field_name == "pk": return [] try: obj.model._meta.get_field(field_name) except FieldDoesNotExist: return refer_to_missing_field( field=field_name, option=label, obj=obj, id="admin.E033" ) else: return [] def _check_readonly_fields(self, obj): """Check that readonly_fields refers to proper attribute or field.""" if obj.readonly_fields == (): return [] elif not isinstance(obj.readonly_fields, (list, tuple)): return must_be( "a list or tuple", option="readonly_fields", obj=obj, id="admin.E034" ) else: return list( chain.from_iterable( self._check_readonly_fields_item( obj, field_name, "readonly_fields[%d]" % index ) for index, field_name in enumerate(obj.readonly_fields) ) ) def _check_readonly_fields_item(self, obj, field_name, label): if callable(field_name): return [] elif hasattr(obj, field_name): return [] elif hasattr(obj.model, field_name): return [] else: try: obj.model._meta.get_field(field_name) except FieldDoesNotExist: return [ checks.Error( "The value of '%s' is not a callable, an attribute of " "'%s', or an attribute of '%s'." % ( label, obj.__class__.__name__, obj.model._meta.label, ), obj=obj.__class__, id="admin.E035", ) ] else: return [] class ModelAdminChecks(BaseModelAdminChecks): def check(self, admin_obj, **kwargs): return [ *super().check(admin_obj), *self._check_save_as(admin_obj), *self._check_save_on_top(admin_obj), *self._check_inlines(admin_obj), *self._check_list_display(admin_obj), *self._check_list_display_links(admin_obj), *self._check_list_filter(admin_obj), *self._check_list_select_related(admin_obj), *self._check_list_per_page(admin_obj), *self._check_list_max_show_all(admin_obj), *self._check_list_editable(admin_obj), *self._check_search_fields(admin_obj), *self._check_date_hierarchy(admin_obj), *self._check_action_permission_methods(admin_obj), *self._check_actions_uniqueness(admin_obj), ] def _check_save_as(self, obj): """Check save_as is a boolean.""" if not isinstance(obj.save_as, bool): return must_be("a boolean", option="save_as", obj=obj, id="admin.E101") else: return [] def _check_save_on_top(self, obj): """Check save_on_top is a boolean.""" if not isinstance(obj.save_on_top, bool): return must_be("a boolean", option="save_on_top", obj=obj, id="admin.E102") else: return [] def _check_inlines(self, obj): """Check all inline model admin classes.""" if not isinstance(obj.inlines, (list, tuple)): return must_be( "a list or tuple", option="inlines", obj=obj, id="admin.E103" ) else: return list( chain.from_iterable( self._check_inlines_item(obj, item, "inlines[%d]" % index) for index, item in enumerate(obj.inlines) ) ) def _check_inlines_item(self, obj, inline, label): """Check one inline model admin.""" try: inline_label = inline.__module__ + "." + inline.__name__ except AttributeError: return [ checks.Error( "'%s' must inherit from 'InlineModelAdmin'." % obj, obj=obj.__class__, id="admin.E104", ) ] from django.contrib.admin.options import InlineModelAdmin if not _issubclass(inline, InlineModelAdmin): return [ checks.Error( "'%s' must inherit from 'InlineModelAdmin'." % inline_label, obj=obj.__class__, id="admin.E104", ) ] elif not inline.model: return [ checks.Error( "'%s' must have a 'model' attribute." % inline_label, obj=obj.__class__, id="admin.E105", ) ] elif not _issubclass(inline.model, models.Model): return must_be( "a Model", option="%s.model" % inline_label, obj=obj, id="admin.E106" ) else: return inline(obj.model, obj.admin_site).check() def _check_list_display(self, obj): """Check that list_display only contains fields or usable attributes.""" if not isinstance(obj.list_display, (list, tuple)): return must_be( "a list or tuple", option="list_display", obj=obj, id="admin.E107" ) else: return list( chain.from_iterable( self._check_list_display_item(obj, item, "list_display[%d]" % index) for index, item in enumerate(obj.list_display) ) ) def _check_list_display_item(self, obj, item, label): if callable(item): return [] elif hasattr(obj, item): return [] try: field = obj.model._meta.get_field(item) except FieldDoesNotExist: try: field = getattr(obj.model, item) except AttributeError: return [ checks.Error( "The value of '%s' refers to '%s', which is not a " "callable, an attribute of '%s', or an attribute or " "method on '%s'." % ( label, item, obj.__class__.__name__, obj.model._meta.label, ), obj=obj.__class__, id="admin.E108", ) ] if isinstance(field, models.ManyToManyField): return [ checks.Error( "The value of '%s' must not be a ManyToManyField." % label, obj=obj.__class__, id="admin.E109", ) ] return [] def _check_list_display_links(self, obj): """Check that list_display_links is a unique subset of list_display.""" from django.contrib.admin.options import ModelAdmin if obj.list_display_links is None: return [] elif not isinstance(obj.list_display_links, (list, tuple)): return must_be( "a list, a tuple, or None", option="list_display_links", obj=obj, id="admin.E110", ) # Check only if ModelAdmin.get_list_display() isn't overridden. elif obj.get_list_display.__func__ is ModelAdmin.get_list_display: return list( chain.from_iterable( self._check_list_display_links_item( obj, field_name, "list_display_links[%d]" % index ) for index, field_name in enumerate(obj.list_display_links) ) ) return [] def _check_list_display_links_item(self, obj, field_name, label): if field_name not in obj.list_display: return [ checks.Error( "The value of '%s' refers to '%s', which is not defined in " "'list_display'." % (label, field_name), obj=obj.__class__, id="admin.E111", ) ] else: return [] def _check_list_filter(self, obj): if not isinstance(obj.list_filter, (list, tuple)): return must_be( "a list or tuple", option="list_filter", obj=obj, id="admin.E112" ) else: return list( chain.from_iterable( self._check_list_filter_item(obj, item, "list_filter[%d]" % index) for index, item in enumerate(obj.list_filter) ) ) def _check_list_filter_item(self, obj, item, label): """ Check one item of `list_filter`, i.e. check if it is one of three options: 1. 'field' -- a basic field filter, possibly w/ relationships (e.g. 'field__rel') 2. ('field', SomeFieldListFilter) - a field-based list filter class 3. SomeListFilter - a non-field list filter class """ from django.contrib.admin import FieldListFilter, ListFilter if callable(item) and not isinstance(item, models.Field): # If item is option 3, it should be a ListFilter... if not _issubclass(item, ListFilter): return must_inherit_from( parent="ListFilter", option=label, obj=obj, id="admin.E113" ) # ... but not a FieldListFilter. elif issubclass(item, FieldListFilter): return [ checks.Error( "The value of '%s' must not inherit from 'FieldListFilter'." % label, obj=obj.__class__, id="admin.E114", ) ] else: return [] elif isinstance(item, (tuple, list)): # item is option #2 field, list_filter_class = item if not _issubclass(list_filter_class, FieldListFilter): return must_inherit_from( parent="FieldListFilter", option="%s[1]" % label, obj=obj, id="admin.E115", ) else: return [] else: # item is option #1 field = item # Validate the field string try: get_fields_from_path(obj.model, field) except (NotRelationField, FieldDoesNotExist): return [ checks.Error( "The value of '%s' refers to '%s', which does not refer to a " "Field." % (label, field), obj=obj.__class__, id="admin.E116", ) ] else: return [] def _check_list_select_related(self, obj): """Check that list_select_related is a boolean, a list or a tuple.""" if not isinstance(obj.list_select_related, (bool, list, tuple)): return must_be( "a boolean, tuple or list", option="list_select_related", obj=obj, id="admin.E117", ) else: return [] def _check_list_per_page(self, obj): """Check that list_per_page is an integer.""" if not isinstance(obj.list_per_page, int): return must_be( "an integer", option="list_per_page", obj=obj, id="admin.E118" ) else: return [] def _check_list_max_show_all(self, obj): """Check that list_max_show_all is an integer.""" if not isinstance(obj.list_max_show_all, int): return must_be( "an integer", option="list_max_show_all", obj=obj, id="admin.E119" ) else: return [] def _check_list_editable(self, obj): """Check that list_editable is a sequence of editable fields from list_display without first element.""" if not isinstance(obj.list_editable, (list, tuple)): return must_be( "a list or tuple", option="list_editable", obj=obj, id="admin.E120" ) else: return list( chain.from_iterable( self._check_list_editable_item( obj, item, "list_editable[%d]" % index ) for index, item in enumerate(obj.list_editable) ) ) def _check_list_editable_item(self, obj, field_name, label): try: field = obj.model._meta.get_field(field_name) except FieldDoesNotExist: return refer_to_missing_field( field=field_name, option=label, obj=obj, id="admin.E121" ) else: if field_name not in obj.list_display: return [ checks.Error( "The value of '%s' refers to '%s', which is not " "contained in 'list_display'." % (label, field_name), obj=obj.__class__, id="admin.E122", ) ] elif obj.list_display_links and field_name in obj.list_display_links: return [ checks.Error( "The value of '%s' cannot be in both 'list_editable' and " "'list_display_links'." % field_name, obj=obj.__class__, id="admin.E123", ) ] # If list_display[0] is in list_editable, check that # list_display_links is set. See #22792 and #26229 for use cases. elif ( obj.list_display[0] == field_name and not obj.list_display_links and obj.list_display_links is not None ): return [ checks.Error( "The value of '%s' refers to the first field in 'list_display' " "('%s'), which cannot be used unless 'list_display_links' is " "set." % (label, obj.list_display[0]), obj=obj.__class__, id="admin.E124", ) ] elif not field.editable or field.primary_key: return [ checks.Error( "The value of '%s' refers to '%s', which is not editable " "through the admin." % (label, field_name), obj=obj.__class__, id="admin.E125", ) ] else: return [] def _check_search_fields(self, obj): """Check search_fields is a sequence.""" if not isinstance(obj.search_fields, (list, tuple)): return must_be( "a list or tuple", option="search_fields", obj=obj, id="admin.E126" ) else: return [] def _check_date_hierarchy(self, obj): """Check that date_hierarchy refers to DateField or DateTimeField.""" if obj.date_hierarchy is None: return [] else: try: field = get_fields_from_path(obj.model, obj.date_hierarchy)[-1] except (NotRelationField, FieldDoesNotExist): return [ checks.Error( "The value of 'date_hierarchy' refers to '%s', which " "does not refer to a Field." % obj.date_hierarchy, obj=obj.__class__, id="admin.E127", ) ] else: if not isinstance(field, (models.DateField, models.DateTimeField)): return must_be( "a DateField or DateTimeField", option="date_hierarchy", obj=obj, id="admin.E128", ) else: return [] def _check_action_permission_methods(self, obj): """ Actions with an allowed_permission attribute require the ModelAdmin to implement a has__permission() method for each permission. """ actions = obj._get_base_actions() errors = [] for func, name, _ in actions: if not hasattr(func, "allowed_permissions"): continue for permission in func.allowed_permissions: method_name = "has_%s_permission" % permission if not hasattr(obj, method_name): errors.append( checks.Error( "%s must define a %s() method for the %s action." % ( obj.__class__.__name__, method_name, func.__name__, ), obj=obj.__class__, id="admin.E129", ) ) return errors def _check_actions_uniqueness(self, obj): """Check that every action has a unique __name__.""" errors = [] names = collections.Counter(name for _, name, _ in obj._get_base_actions()) for name, count in names.items(): if count > 1: errors.append( checks.Error( "__name__ attributes of actions defined in %s must be " "unique. Name %r is not unique." % ( obj.__class__.__name__, name, ), obj=obj.__class__, id="admin.E130", ) ) return errors class InlineModelAdminChecks(BaseModelAdminChecks): def check(self, inline_obj, **kwargs): parent_model = inline_obj.parent_model return [ *super().check(inline_obj), *self._check_relation(inline_obj, parent_model), *self._check_exclude_of_parent_model(inline_obj, parent_model), *self._check_extra(inline_obj), *self._check_max_num(inline_obj), *self._check_min_num(inline_obj), *self._check_formset(inline_obj), ] def _check_exclude_of_parent_model(self, obj, parent_model): # Do not perform more specific checks if the base checks result in an # error. errors = super()._check_exclude(obj) if errors: return [] # Skip if `fk_name` is invalid. if self._check_relation(obj, parent_model): return [] if obj.exclude is None: return [] fk = _get_foreign_key(parent_model, obj.model, fk_name=obj.fk_name) if fk.name in obj.exclude: return [ checks.Error( "Cannot exclude the field '%s', because it is the foreign key " "to the parent model '%s'." % ( fk.name, parent_model._meta.label, ), obj=obj.__class__, id="admin.E201", ) ] else: return [] def _check_relation(self, obj, parent_model): try: _get_foreign_key(parent_model, obj.model, fk_name=obj.fk_name) except ValueError as e: return [checks.Error(e.args[0], obj=obj.__class__, id="admin.E202")] else: return [] def _check_extra(self, obj): """Check that extra is an integer.""" if not isinstance(obj.extra, int): return must_be("an integer", option="extra", obj=obj, id="admin.E203") else: return [] def _check_max_num(self, obj): """Check that max_num is an integer.""" if obj.max_num is None: return [] elif not isinstance(obj.max_num, int): return must_be("an integer", option="max_num", obj=obj, id="admin.E204") else: return [] def _check_min_num(self, obj): """Check that min_num is an integer.""" if obj.min_num is None: return [] elif not isinstance(obj.min_num, int): return must_be("an integer", option="min_num", obj=obj, id="admin.E205") else: return [] def _check_formset(self, obj): """Check formset is a subclass of BaseModelFormSet.""" if not _issubclass(obj.formset, BaseModelFormSet): return must_inherit_from( parent="BaseModelFormSet", option="formset", obj=obj, id="admin.E206" ) else: return [] def must_be(type, option, obj, id): return [ checks.Error( "The value of '%s' must be %s." % (option, type), obj=obj.__class__, id=id, ), ] def must_inherit_from(parent, option, obj, id): return [ checks.Error( "The value of '%s' must inherit from '%s'." % (option, parent), obj=obj.__class__, id=id, ), ] def refer_to_missing_field(field, option, obj, id): return [ checks.Error( "The value of '%s' refers to '%s', which is not a field of '%s'." % (option, field, obj.model._meta.label), obj=obj.__class__, id=id, ), ]