django1/django/contrib/admin/checks.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1351 lines
49 KiB
Python
Raw Normal View History

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.
2016-04-04 08:37:32 +08:00
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_<perm>_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,
),
]