mirror of
https://github.com/django/django.git
synced 2024-12-22 09:05:43 +00:00
Fixed #16905 -- Added extensible checks (nee validation) framework
This is the result of Christopher Medrela's 2013 Summer of Code project. Thanks also to Preston Holmes, Tim Graham, Anssi Kääriäinen, Florian Apolloner, and Alex Gaynor for review notes along the way. Also: Fixes #8579, fixes #3055, fixes #19844.
This commit is contained in:
parent
6e7bd0b63b
commit
d818e0c9b2
@ -99,7 +99,7 @@ class Settings(BaseSettings):
|
||||
)
|
||||
|
||||
tuple_settings = ("INSTALLED_APPS", "TEMPLATE_DIRS")
|
||||
|
||||
self._explicit_settings = set()
|
||||
for setting in dir(mod):
|
||||
if setting.isupper():
|
||||
setting_value = getattr(mod, setting)
|
||||
@ -110,6 +110,7 @@ class Settings(BaseSettings):
|
||||
"Please fix your settings." % setting)
|
||||
|
||||
setattr(self, setting, setting_value)
|
||||
self._explicit_settings.add(setting)
|
||||
|
||||
if not self.SECRET_KEY:
|
||||
raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
|
||||
@ -126,6 +127,9 @@ class Settings(BaseSettings):
|
||||
os.environ['TZ'] = self.TIME_ZONE
|
||||
time.tzset()
|
||||
|
||||
def is_overridden(self, setting):
|
||||
return setting in self._explicit_settings
|
||||
|
||||
|
||||
class UserSettingsHolder(BaseSettings):
|
||||
"""
|
||||
@ -159,4 +163,10 @@ class UserSettingsHolder(BaseSettings):
|
||||
def __dir__(self):
|
||||
return list(self.__dict__) + dir(self.default_settings)
|
||||
|
||||
def is_overridden(self, setting):
|
||||
if setting in self._deleted:
|
||||
return False
|
||||
else:
|
||||
return self.default_settings.is_overridden(setting)
|
||||
|
||||
settings = LazySettings()
|
||||
|
@ -618,3 +618,13 @@ STATICFILES_FINDERS = (
|
||||
|
||||
# Migration module overrides for apps, by app label.
|
||||
MIGRATION_MODULES = {}
|
||||
|
||||
#################
|
||||
# SYSTEM CHECKS #
|
||||
#################
|
||||
|
||||
# List of all issues generated by system checks that should be silenced. Light
|
||||
# issues like warnings, infos or debugs will not generate a message. Silencing
|
||||
# serious issues like errors and criticals does not result in hiding the
|
||||
# message, but Django will not stop you from e.g. running server.
|
||||
SILENCED_SYSTEM_CHECKS = []
|
||||
|
@ -1,13 +1,15 @@
|
||||
# ACTION_CHECKBOX_NAME is unused, but should stay since its import from here
|
||||
# has been referenced in documentation.
|
||||
from django.contrib.admin.checks import check_admin_app
|
||||
from django.contrib.admin.decorators import register
|
||||
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
||||
from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
|
||||
from django.contrib.admin.options import StackedInline, TabularInline
|
||||
from django.contrib.admin.sites import AdminSite, site
|
||||
from django.contrib.admin.filters import (ListFilter, SimpleListFilter,
|
||||
FieldListFilter, BooleanFieldListFilter, RelatedFieldListFilter,
|
||||
ChoicesFieldListFilter, DateFieldListFilter, AllValuesFieldListFilter)
|
||||
from django.contrib.admin.sites import AdminSite, site
|
||||
from django.core import checks
|
||||
from django.utils.module_loading import autodiscover_modules
|
||||
|
||||
__all__ = [
|
||||
@ -21,3 +23,5 @@ __all__ = [
|
||||
|
||||
def autodiscover():
|
||||
autodiscover_modules('admin', register_to=site)
|
||||
|
||||
checks.register('admin')(check_admin_app)
|
||||
|
932
django/contrib/admin/checks.py
Normal file
932
django/contrib/admin/checks.py
Normal file
@ -0,0 +1,932 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from django.contrib.admin.util import get_fields_from_path, NotRelationField
|
||||
from django.core import checks
|
||||
from django.db import models
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.forms.models import BaseModelForm, _get_foreign_key, BaseModelFormSet
|
||||
|
||||
|
||||
def check_admin_app(**kwargs):
|
||||
from django.contrib.admin.sites import site
|
||||
|
||||
return list(chain.from_iterable(
|
||||
model_admin.check(model, **kwargs)
|
||||
for model, model_admin in site._registry.items()
|
||||
))
|
||||
|
||||
|
||||
class BaseModelAdminChecks(object):
|
||||
|
||||
def check(self, cls, model, **kwargs):
|
||||
errors = []
|
||||
errors.extend(self._check_raw_id_fields(cls, model))
|
||||
errors.extend(self._check_fields(cls, model))
|
||||
errors.extend(self._check_fieldsets(cls, model))
|
||||
errors.extend(self._check_exclude(cls, model))
|
||||
errors.extend(self._check_form(cls, model))
|
||||
errors.extend(self._check_filter_vertical(cls, model))
|
||||
errors.extend(self._check_filter_horizontal(cls, model))
|
||||
errors.extend(self._check_radio_fields(cls, model))
|
||||
errors.extend(self._check_prepopulated_fields(cls, model))
|
||||
errors.extend(self._check_view_on_site_url(cls, model))
|
||||
errors.extend(self._check_ordering(cls, model))
|
||||
errors.extend(self._check_readonly_fields(cls, model))
|
||||
return errors
|
||||
|
||||
def _check_raw_id_fields(self, cls, model):
|
||||
""" Check that `raw_id_fields` only contains field names that are listed
|
||||
on the model. """
|
||||
|
||||
if not isinstance(cls.raw_id_fields, (list, tuple)):
|
||||
return must_be('a list or tuple', option='raw_id_fields', obj=cls, id='admin.E001')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_raw_id_fields_item(cls, model, field_name, 'raw_id_fields[%d]' % index)
|
||||
for index, field_name in enumerate(cls.raw_id_fields)
|
||||
]))
|
||||
|
||||
def _check_raw_id_fields_item(self, cls, model, 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 = model._meta.get_field(field_name)
|
||||
except models.FieldDoesNotExist:
|
||||
return refer_to_missing_field(field=field_name, option=label,
|
||||
model=model, obj=cls, id='admin.E002')
|
||||
else:
|
||||
if not isinstance(field, (models.ForeignKey, models.ManyToManyField)):
|
||||
return must_be('a ForeignKey or ManyToManyField',
|
||||
option=label, obj=cls, id='admin.E003')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_fields(self, cls, model):
|
||||
""" Check that `fields` only refer to existing fields, doesn't contain
|
||||
duplicates. Check if at most one of `fields` and `fieldsets` is defined.
|
||||
"""
|
||||
|
||||
if cls.fields is None:
|
||||
return []
|
||||
elif not isinstance(cls.fields, (list, tuple)):
|
||||
return must_be('a list or tuple', option='fields', obj=cls, id='admin.E004')
|
||||
elif cls.fieldsets:
|
||||
return [
|
||||
checks.Error(
|
||||
'Both "fieldsets" and "fields" are specified.',
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E005',
|
||||
)
|
||||
]
|
||||
elif len(cls.fields) != len(set(cls.fields)):
|
||||
return [
|
||||
checks.Error(
|
||||
'There are duplicate field(s) in "fields".',
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E006',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_field_spec(cls, model, field_name, 'fields')
|
||||
for field_name in cls.fields
|
||||
]))
|
||||
|
||||
def _check_fieldsets(self, cls, model):
|
||||
""" Check that fieldsets is properly formatted and doesn't contain
|
||||
duplicates. """
|
||||
|
||||
if cls.fieldsets is None:
|
||||
return []
|
||||
elif not isinstance(cls.fieldsets, (list, tuple)):
|
||||
return must_be('a list or tuple', option='fieldsets', obj=cls, id='admin.E007')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_fieldsets_item(cls, model, fieldset, 'fieldsets[%d]' % index)
|
||||
for index, fieldset in enumerate(cls.fieldsets)
|
||||
]))
|
||||
|
||||
def _check_fieldsets_item(self, cls, model, fieldset, label):
|
||||
""" 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=cls, id='admin.E008')
|
||||
elif len(fieldset) != 2:
|
||||
return must_be('a pair', option=label, obj=cls, id='admin.E009')
|
||||
elif not isinstance(fieldset[1], dict):
|
||||
return must_be('a dictionary', option='%s[1]' % label, obj=cls, id='admin.E010')
|
||||
elif 'fields' not in fieldset[1]:
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s[1]" must contain "fields" key.' % label,
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E011',
|
||||
)
|
||||
]
|
||||
elif len(fieldset[1]['fields']) != len(set(fieldset[1]['fields'])):
|
||||
return [
|
||||
checks.Error(
|
||||
'There are duplicate field(s) in "%s[1]".' % label,
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E012',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_field_spec(cls, model, fields, '%s[1][\'fields\']' % label)
|
||||
for fields in fieldset[1]['fields']
|
||||
]))
|
||||
|
||||
def _check_field_spec(self, cls, model, 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(*[
|
||||
self._check_field_spec_item(cls, model, field_name, "%s[%d]" % (label, index))
|
||||
for index, field_name in enumerate(fields)
|
||||
]))
|
||||
else:
|
||||
return self._check_field_spec_item(cls, model, fields, label)
|
||||
|
||||
def _check_field_spec_item(self, cls, model, field_name, label):
|
||||
if field_name in cls.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 = model._meta.get_field(field_name)
|
||||
except models.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.rel.through._meta.auto_created):
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" cannot include the ManyToManyField "%s", '
|
||||
'because "%s" manually specifies relationship model.'
|
||||
% (label, field_name, field_name),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E013',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_exclude(self, cls, model):
|
||||
""" Check that exclude is a sequence without duplicates. """
|
||||
|
||||
if cls.exclude is None: # default value is None
|
||||
return []
|
||||
elif not isinstance(cls.exclude, (list, tuple)):
|
||||
return must_be('a list or tuple', option='exclude', obj=cls, id='admin.E014')
|
||||
elif len(cls.exclude) > len(set(cls.exclude)):
|
||||
return [
|
||||
checks.Error(
|
||||
'"exclude" contains duplicate field(s).',
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E015',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_form(self, cls, model):
|
||||
""" Check that form subclasses BaseModelForm. """
|
||||
|
||||
if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm):
|
||||
return must_inherit_from(parent='BaseModelForm', option='form',
|
||||
obj=cls, id='admin.E016')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_filter_vertical(self, cls, model):
|
||||
""" Check that filter_vertical is a sequence of field names. """
|
||||
|
||||
if not hasattr(cls, 'filter_vertical'):
|
||||
return []
|
||||
elif not isinstance(cls.filter_vertical, (list, tuple)):
|
||||
return must_be('a list or tuple', option='filter_vertical', obj=cls, id='admin.E017')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_filter_item(cls, model, field_name, "filter_vertical[%d]" % index)
|
||||
for index, field_name in enumerate(cls.filter_vertical)
|
||||
]))
|
||||
|
||||
def _check_filter_horizontal(self, cls, model):
|
||||
""" Check that filter_horizontal is a sequence of field names. """
|
||||
|
||||
if not hasattr(cls, 'filter_horizontal'):
|
||||
return []
|
||||
elif not isinstance(cls.filter_horizontal, (list, tuple)):
|
||||
return must_be('a list or tuple', option='filter_horizontal', obj=cls, id='admin.E018')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_filter_item(cls, model, field_name, "filter_horizontal[%d]" % index)
|
||||
for index, field_name in enumerate(cls.filter_horizontal)
|
||||
]))
|
||||
|
||||
def _check_filter_item(self, cls, model, 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 = model._meta.get_field(field_name)
|
||||
except models.FieldDoesNotExist:
|
||||
return refer_to_missing_field(field=field_name, option=label,
|
||||
model=model, obj=cls, id='admin.E019')
|
||||
else:
|
||||
if not isinstance(field, models.ManyToManyField):
|
||||
return must_be('a ManyToManyField', option=label, obj=cls, id='admin.E020')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_radio_fields(self, cls, model):
|
||||
""" Check that `radio_fields` is a dictionary. """
|
||||
|
||||
if not hasattr(cls, 'radio_fields'):
|
||||
return []
|
||||
elif not isinstance(cls.radio_fields, dict):
|
||||
return must_be('a dictionary', option='radio_fields', obj=cls, id='admin.E021')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_radio_fields_key(cls, model, field_name, 'radio_fields') +
|
||||
self._check_radio_fields_value(cls, model, val, 'radio_fields[\'%s\']' % field_name)
|
||||
for field_name, val in cls.radio_fields.items()
|
||||
]))
|
||||
|
||||
def _check_radio_fields_key(self, cls, model, 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 = model._meta.get_field(field_name)
|
||||
except models.FieldDoesNotExist:
|
||||
return refer_to_missing_field(field=field_name, option=label,
|
||||
model=model, obj=cls, id='admin.E022')
|
||||
else:
|
||||
if not (isinstance(field, models.ForeignKey) or field.choices):
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" refers to "%s", which is neither an instance of ForeignKey nor does have choices set.' % (
|
||||
label, field_name
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E023',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_radio_fields_value(self, cls, model, 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(
|
||||
'"%s" is neither admin.HORIZONTAL nor admin.VERTICAL.' % label,
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E024',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_view_on_site_url(self, cls, model):
|
||||
if hasattr(cls, 'view_on_site'):
|
||||
if not callable(cls.view_on_site) and not isinstance(cls.view_on_site, bool):
|
||||
return [
|
||||
checks.Error(
|
||||
'"view_on_site" is not a callable or a boolean value.',
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E025',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_prepopulated_fields(self, cls, model):
|
||||
""" Check that `prepopulated_fields` is a dictionary containing allowed
|
||||
field types. """
|
||||
|
||||
if not hasattr(cls, 'prepopulated_fields'):
|
||||
return []
|
||||
elif not isinstance(cls.prepopulated_fields, dict):
|
||||
return must_be('a dictionary', option='prepopulated_fields', obj=cls, id='admin.E026')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_prepopulated_fields_key(cls, model, field_name, 'prepopulated_fields') +
|
||||
self._check_prepopulated_fields_value(cls, model, val, 'prepopulated_fields[\'%s\']' % field_name)
|
||||
for field_name, val in cls.prepopulated_fields.items()
|
||||
]))
|
||||
|
||||
def _check_prepopulated_fields_key(self, cls, model, 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.
|
||||
"""
|
||||
|
||||
forbidden_field_types = (
|
||||
models.DateTimeField,
|
||||
models.ForeignKey,
|
||||
models.ManyToManyField
|
||||
)
|
||||
|
||||
try:
|
||||
field = model._meta.get_field(field_name)
|
||||
except models.FieldDoesNotExist:
|
||||
return refer_to_missing_field(field=field_name, option=label,
|
||||
model=model, obj=cls, id='admin.E027')
|
||||
else:
|
||||
if isinstance(field, forbidden_field_types):
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" refers to "%s", which must not be a DateTimeField, '
|
||||
'ForeignKey or ManyToManyField.' % (
|
||||
label, field_name
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E028',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_prepopulated_fields_value(self, cls, model, 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=cls, id='admin.E029')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_prepopulated_fields_value_item(cls, model, subfield_name, "%s[%r]" % (label, index))
|
||||
for index, subfield_name in enumerate(val)
|
||||
]))
|
||||
|
||||
def _check_prepopulated_fields_value_item(self, cls, model, field_name, label):
|
||||
""" For `prepopulated_fields` equal to {"slug": ("title",)},
|
||||
`field_name` is "title". """
|
||||
|
||||
try:
|
||||
model._meta.get_field(field_name)
|
||||
except models.FieldDoesNotExist:
|
||||
return refer_to_missing_field(field=field_name, option=label,
|
||||
model=model, obj=cls, id='admin.E030')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_ordering(self, cls, model):
|
||||
""" Check that ordering refers to existing fields or is random. """
|
||||
|
||||
# ordering = None
|
||||
if cls.ordering is None: # The default value is None
|
||||
return []
|
||||
elif not isinstance(cls.ordering, (list, tuple)):
|
||||
return must_be('a list or tuple', option='ordering', obj=cls, id='admin.E031')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_ordering_item(cls, model, field_name, 'ordering[%d]' % index)
|
||||
for index, field_name in enumerate(cls.ordering)
|
||||
]))
|
||||
|
||||
def _check_ordering_item(self, cls, model, field_name, label):
|
||||
""" Check that `ordering` refers to existing fields. """
|
||||
|
||||
if field_name == '?' and len(cls.ordering) != 1:
|
||||
return [
|
||||
checks.Error(
|
||||
'"ordering" has the random ordering marker "?", '
|
||||
'but contains other fields as well.',
|
||||
hint='Either remove the "?", or remove the other fields.',
|
||||
obj=cls,
|
||||
id='admin.E032',
|
||||
)
|
||||
]
|
||||
elif field_name == '?':
|
||||
return []
|
||||
elif '__' 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:]
|
||||
|
||||
try:
|
||||
model._meta.get_field(field_name)
|
||||
except models.FieldDoesNotExist:
|
||||
return refer_to_missing_field(field=field_name, option=label,
|
||||
model=model, obj=cls, id='admin.E033')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_readonly_fields(self, cls, model):
|
||||
""" Check that readonly_fields refers to proper attribute or field. """
|
||||
|
||||
if cls.readonly_fields == ():
|
||||
return []
|
||||
elif not isinstance(cls.readonly_fields, (list, tuple)):
|
||||
return must_be('a list or tuple', option='readonly_fields', obj=cls, id='admin.E034')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_readonly_fields_item(cls, model, field_name, "readonly_fields[%d]" % index)
|
||||
for index, field_name in enumerate(cls.readonly_fields)
|
||||
]))
|
||||
|
||||
def _check_readonly_fields_item(self, cls, model, field_name, label):
|
||||
if callable(field_name):
|
||||
return []
|
||||
elif hasattr(cls, field_name):
|
||||
return []
|
||||
elif hasattr(model, field_name):
|
||||
return []
|
||||
else:
|
||||
try:
|
||||
model._meta.get_field(field_name)
|
||||
except models.FieldDoesNotExist:
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" is neither a callable nor an attribute of "%s" nor found in the model %s.%s.' % (
|
||||
label, cls.__name__, model._meta.app_label, model._meta.object_name
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E035',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
class ModelAdminChecks(BaseModelAdminChecks):
|
||||
|
||||
def check(self, cls, model, **kwargs):
|
||||
errors = super(ModelAdminChecks, self).check(cls, model=model, **kwargs)
|
||||
errors.extend(self._check_save_as(cls, model))
|
||||
errors.extend(self._check_save_on_top(cls, model))
|
||||
errors.extend(self._check_inlines(cls, model))
|
||||
errors.extend(self._check_list_display(cls, model))
|
||||
errors.extend(self._check_list_display_links(cls, model))
|
||||
errors.extend(self._check_list_filter(cls, model))
|
||||
errors.extend(self._check_list_select_related(cls, model))
|
||||
errors.extend(self._check_list_per_page(cls, model))
|
||||
errors.extend(self._check_list_max_show_all(cls, model))
|
||||
errors.extend(self._check_list_editable(cls, model))
|
||||
errors.extend(self._check_search_fields(cls, model))
|
||||
errors.extend(self._check_date_hierarchy(cls, model))
|
||||
return errors
|
||||
|
||||
def _check_save_as(self, cls, model):
|
||||
""" Check save_as is a boolean. """
|
||||
|
||||
if not isinstance(cls.save_as, bool):
|
||||
return must_be('a boolean', option='save_as',
|
||||
obj=cls, id='admin.E101')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_save_on_top(self, cls, model):
|
||||
""" Check save_on_top is a boolean. """
|
||||
|
||||
if not isinstance(cls.save_on_top, bool):
|
||||
return must_be('a boolean', option='save_on_top',
|
||||
obj=cls, id='admin.E102')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_inlines(self, cls, model):
|
||||
""" Check all inline model admin classes. """
|
||||
|
||||
if not isinstance(cls.inlines, (list, tuple)):
|
||||
return must_be('a list or tuple', option='inlines', obj=cls, id='admin.E103')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_inlines_item(cls, model, item, "inlines[%d]" % index)
|
||||
for index, item in enumerate(cls.inlines)
|
||||
]))
|
||||
|
||||
def _check_inlines_item(self, cls, model, inline, label):
|
||||
""" Check one inline model admin. """
|
||||
|
||||
from django.contrib.admin.options import BaseModelAdmin
|
||||
|
||||
if not issubclass(inline, BaseModelAdmin):
|
||||
return must_inherit_from(parent='BaseModelAdmin', option=label,
|
||||
obj=cls, id='admin.E104')
|
||||
elif not inline.model:
|
||||
return [
|
||||
checks.Error(
|
||||
'"model" is a required attribute of "%s".' % label,
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E105',
|
||||
)
|
||||
]
|
||||
elif not issubclass(inline.model, models.Model):
|
||||
return must_be('a Model', option='%s.model' % label,
|
||||
obj=cls, id='admin.E106')
|
||||
else:
|
||||
return inline.check(model)
|
||||
|
||||
def _check_list_display(self, cls, model):
|
||||
""" Check that list_display only contains fields or usable attributes.
|
||||
"""
|
||||
|
||||
if not isinstance(cls.list_display, (list, tuple)):
|
||||
return must_be('a list or tuple', option='list_display', obj=cls, id='admin.E107')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_list_display_item(cls, model, item, "list_display[%d]" % index)
|
||||
for index, item in enumerate(cls.list_display)
|
||||
]))
|
||||
|
||||
def _check_list_display_item(self, cls, model, item, label):
|
||||
if callable(item):
|
||||
return []
|
||||
elif hasattr(cls, item):
|
||||
return []
|
||||
elif hasattr(model, item):
|
||||
# getattr(model, item) could be an X_RelatedObjectsDescriptor
|
||||
try:
|
||||
field = model._meta.get_field(item)
|
||||
except models.FieldDoesNotExist:
|
||||
try:
|
||||
field = getattr(model, item)
|
||||
except AttributeError:
|
||||
field = None
|
||||
|
||||
if field is None:
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" refers to "%s" that is neither a field, method nor a property of model %s.%s.' % (
|
||||
label, item, model._meta.app_label, model._meta.object_name
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E108',
|
||||
)
|
||||
]
|
||||
elif isinstance(field, models.ManyToManyField):
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" must not be a ManyToManyField.' % label,
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E109',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
try:
|
||||
model._meta.get_field(item)
|
||||
except models.FieldDoesNotExist:
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" is neither a callable nor an attribute of "%s" nor found in model %s.%s.' % (
|
||||
label, cls.__name__, model._meta.app_label, model._meta.object_name
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E110',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_list_display_links(self, cls, model):
|
||||
""" Check that list_display_links is a unique subset of list_display.
|
||||
"""
|
||||
|
||||
if cls.list_display_links is None:
|
||||
return []
|
||||
elif not isinstance(cls.list_display_links, (list, tuple)):
|
||||
return must_be('a list or tuple or None', option='list_display_links', obj=cls, id='admin.E111')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_list_display_links_item(cls, model, field_name, "list_display_links[%d]" % index)
|
||||
for index, field_name in enumerate(cls.list_display_links)
|
||||
]))
|
||||
|
||||
def _check_list_display_links_item(self, cls, model, field_name, label):
|
||||
if field_name not in cls.list_display:
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" refers to "%s", which is not defined in "list_display".' % (
|
||||
label, field_name
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E112',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_list_filter(self, cls, model):
|
||||
if not isinstance(cls.list_filter, (list, tuple)):
|
||||
return must_be('a list or tuple', option='list_filter', obj=cls, id='admin.E113')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_list_filter_item(cls, model, item, "list_filter[%d]" % index)
|
||||
for index, item in enumerate(cls.list_filter)
|
||||
]))
|
||||
|
||||
def _check_list_filter_item(self, cls, model, 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 ListFilter, FieldListFilter
|
||||
|
||||
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=cls, id='admin.E114')
|
||||
# ... but not a FieldListFilter.
|
||||
elif issubclass(item, FieldListFilter):
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" must not inherit from FieldListFilter.' % label,
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E115',
|
||||
)
|
||||
]
|
||||
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=cls, id='admin.E116')
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
# item is option #1
|
||||
field = item
|
||||
|
||||
# Validate the field string
|
||||
try:
|
||||
get_fields_from_path(model, field)
|
||||
except (NotRelationField, FieldDoesNotExist):
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" refers to "%s", which does not refer to a Field.' % (label, field),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E117',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_list_select_related(self, cls, model):
|
||||
""" Check that list_select_related is a boolean, a list or a tuple. """
|
||||
|
||||
if not isinstance(cls.list_select_related, (bool, list, tuple)):
|
||||
return must_be('a boolean, tuple or list', option='list_select_related',
|
||||
obj=cls, id='admin.E118')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_list_per_page(self, cls, model):
|
||||
""" Check that list_per_page is an integer. """
|
||||
|
||||
if not isinstance(cls.list_per_page, int):
|
||||
return must_be('an integer', option='list_per_page', obj=cls, id='admin.E119')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_list_max_show_all(self, cls, model):
|
||||
""" Check that list_max_show_all is an integer. """
|
||||
|
||||
if not isinstance(cls.list_max_show_all, int):
|
||||
return must_be('an integer', option='list_max_show_all', obj=cls, id='admin.E120')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_list_editable(self, cls, model):
|
||||
""" Check that list_editable is a sequence of editable fields from
|
||||
list_display without first element. """
|
||||
|
||||
if not isinstance(cls.list_editable, (list, tuple)):
|
||||
return must_be('a list or tuple', option='list_editable', obj=cls, id='admin.E121')
|
||||
else:
|
||||
return list(chain(*[
|
||||
self._check_list_editable_item(cls, model, item, "list_editable[%d]" % index)
|
||||
for index, item in enumerate(cls.list_editable)
|
||||
]))
|
||||
|
||||
def _check_list_editable_item(self, cls, model, field_name, label):
|
||||
try:
|
||||
field = model._meta.get_field_by_name(field_name)[0]
|
||||
except models.FieldDoesNotExist:
|
||||
return refer_to_missing_field(field=field_name, option=label,
|
||||
model=model, obj=cls, id='admin.E122')
|
||||
else:
|
||||
if field_name not in cls.list_display:
|
||||
return refer_to_missing_field(field=field_name, option=label,
|
||||
model=model, obj=cls, id='admin.E123')
|
||||
elif field_name in cls.list_display_links:
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" cannot be in both "list_editable" and "list_display_links".' % field_name,
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E124',
|
||||
)
|
||||
]
|
||||
elif not cls.list_display_links and cls.list_display[0] in cls.list_editable:
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" refers to the first field in list_display ("%s"), '
|
||||
'which cannot be used unless list_display_links is set.' % (
|
||||
label, cls.list_display[0]
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E125',
|
||||
)
|
||||
]
|
||||
elif not field.editable:
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" refers to field "%s", whih is not editable through the admin.' % (
|
||||
label, field_name
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E126',
|
||||
)
|
||||
]
|
||||
|
||||
def _check_search_fields(self, cls, model):
|
||||
""" Check search_fields is a sequence. """
|
||||
|
||||
if not isinstance(cls.search_fields, (list, tuple)):
|
||||
return must_be('a list or tuple', option='search_fields', obj=cls, id='admin.E127')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_date_hierarchy(self, cls, model):
|
||||
""" Check that date_hierarchy refers to DateField or DateTimeField. """
|
||||
|
||||
if cls.date_hierarchy is None:
|
||||
return []
|
||||
else:
|
||||
try:
|
||||
field = model._meta.get_field(cls.date_hierarchy)
|
||||
except models.FieldDoesNotExist:
|
||||
return refer_to_missing_field(option='date_hierarchy',
|
||||
field=cls.date_hierarchy,
|
||||
model=model, obj=cls, id='admin.E128')
|
||||
else:
|
||||
if not isinstance(field, (models.DateField, models.DateTimeField)):
|
||||
return must_be('a DateField or DateTimeField', option='date_hierarchy',
|
||||
obj=cls, id='admin.E129')
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
class InlineModelAdminChecks(BaseModelAdminChecks):
|
||||
|
||||
def check(self, cls, parent_model, **kwargs):
|
||||
errors = super(InlineModelAdminChecks, self).check(cls, model=cls.model, **kwargs)
|
||||
errors.extend(self._check_fk_name(cls, parent_model))
|
||||
errors.extend(self._check_exclude_of_parent_model(cls, parent_model))
|
||||
errors.extend(self._check_extra(cls))
|
||||
errors.extend(self._check_max_num(cls))
|
||||
errors.extend(self._check_formset(cls))
|
||||
return errors
|
||||
|
||||
def _check_exclude_of_parent_model(self, cls, parent_model):
|
||||
# Do not perform more specific checks if the base checks result in an
|
||||
# error.
|
||||
errors = super(InlineModelAdminChecks, self)._check_exclude(cls, parent_model)
|
||||
if errors:
|
||||
return []
|
||||
|
||||
# Skip if `fk_name` is invalid.
|
||||
if self._check_fk_name(cls, parent_model):
|
||||
return []
|
||||
|
||||
if cls.exclude is None:
|
||||
return []
|
||||
|
||||
fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name)
|
||||
if fk.name in cls.exclude:
|
||||
return [
|
||||
checks.Error(
|
||||
'Cannot exclude the field "%s", because it is the foreign key '
|
||||
'to the parent model %s.%s.' % (
|
||||
fk.name, parent_model._meta.app_label, parent_model._meta.object_name
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='admin.E201',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_fk_name(self, cls, parent_model):
|
||||
try:
|
||||
_get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name)
|
||||
except ValueError as e:
|
||||
return [checks.Error(e.args[0], hint=None, obj=cls, id='admin.E202')]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_extra(self, cls):
|
||||
""" Check that extra is an integer. """
|
||||
|
||||
if not isinstance(cls.extra, int):
|
||||
return must_be('an integer', option='extra', obj=cls, id='admin.E203')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_max_num(self, cls):
|
||||
""" Check that max_num is an integer. """
|
||||
|
||||
if cls.max_num is None:
|
||||
return []
|
||||
elif not isinstance(cls.max_num, int):
|
||||
return must_be('an integer', option='max_num', obj=cls, id='admin.E204')
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_formset(self, cls):
|
||||
""" Check formset is a subclass of BaseModelFormSet. """
|
||||
|
||||
if not issubclass(cls.formset, BaseModelFormSet):
|
||||
return must_inherit_from(parent='BaseModelFormSet', option='formset',
|
||||
obj=cls, id='admin.E205')
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def must_be(type, option, obj, id):
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" must be %s.' % (option, type),
|
||||
hint=None,
|
||||
obj=obj,
|
||||
id=id,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def must_inherit_from(parent, option, obj, id):
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" must inherit from %s.' % (option, parent),
|
||||
hint=None,
|
||||
obj=obj,
|
||||
id=id,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def refer_to_missing_field(field, option, model, obj, id):
|
||||
return [
|
||||
checks.Error(
|
||||
'"%s" refers to field "%s", which is missing from model %s.%s.' % (
|
||||
option, field, model._meta.app_label, model._meta.object_name
|
||||
),
|
||||
hint=None,
|
||||
obj=obj,
|
||||
id=id,
|
||||
),
|
||||
]
|
@ -8,14 +8,18 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin import widgets, helpers
|
||||
from django.contrib.admin.utils import (unquote, flatten_fieldsets, get_deleted_objects,
|
||||
model_format_dict, NestedObjects, lookup_needs_distinct)
|
||||
from django.contrib.admin import validation
|
||||
from django.contrib.admin.checks import (BaseModelAdminChecks, ModelAdminChecks,
|
||||
InlineModelAdminChecks)
|
||||
from django.contrib.admin.utils import (unquote, flatten_fieldsets,
|
||||
get_deleted_objects, model_format_dict, NestedObjects,
|
||||
lookup_needs_distinct)
|
||||
from django.contrib.admin.templatetags.admin_static import static
|
||||
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
||||
from django.contrib.auth import get_permission_codename
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied, ValidationError, FieldError
|
||||
from django.core import checks
|
||||
from django.core.exceptions import PermissionDenied, ValidationError, FieldError, ImproperlyConfigured
|
||||
from django.core.paginator import Paginator
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models, transaction, router
|
||||
@ -30,16 +34,17 @@ from django.http import Http404, HttpResponseRedirect
|
||||
from django.http.response import HttpResponseBase
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.response import SimpleTemplateResponse, TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.html import escape, escapejs
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils import six
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.deprecation import RenameMethodsBase
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.html import escape, escapejs
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.text import capfirst, get_text_list
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ungettext
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
|
||||
|
||||
@ -103,14 +108,42 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)):
|
||||
ordering = None
|
||||
view_on_site = True
|
||||
|
||||
# validation
|
||||
validator_class = validation.BaseValidator
|
||||
# Validation of ModelAdmin definitions
|
||||
# Old, deprecated style:
|
||||
validator_class = None
|
||||
default_validator_class = validation.BaseValidator
|
||||
# New style:
|
||||
checks_class = BaseModelAdminChecks
|
||||
|
||||
@classmethod
|
||||
def validate(cls, model):
|
||||
validator = cls.validator_class()
|
||||
warnings.warn(
|
||||
'ModelAdmin.validate() is deprecated. Use "check()" instead.',
|
||||
PendingDeprecationWarning)
|
||||
if cls.validator_class:
|
||||
validator = cls.validator_class()
|
||||
else:
|
||||
validator = cls.default_validator_class()
|
||||
validator.validate(cls, model)
|
||||
|
||||
@classmethod
|
||||
def check(cls, model, **kwargs):
|
||||
if cls.validator_class:
|
||||
warnings.warn(
|
||||
'ModelAdmin.validator_class is deprecated. '
|
||||
'ModeAdmin validators must be converted to use '
|
||||
'the system check framework.',
|
||||
PendingDeprecationWarning)
|
||||
validator = cls.validator_class()
|
||||
try:
|
||||
validator.validate(cls, model)
|
||||
except ImproperlyConfigured as e:
|
||||
return [checks.Error(e.args[0], hint=None, obj=cls)]
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
return cls.checks_class().check(cls, model, **kwargs)
|
||||
|
||||
def __init__(self):
|
||||
self._orig_formfield_overrides = self.formfield_overrides
|
||||
overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy()
|
||||
@ -435,6 +468,7 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)):
|
||||
return request.user.has_perm("%s.%s" % (opts.app_label, codename))
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ModelAdmin(BaseModelAdmin):
|
||||
"Encapsulates all admin options and functionality for a given model."
|
||||
|
||||
@ -469,7 +503,10 @@ class ModelAdmin(BaseModelAdmin):
|
||||
actions_selection_counter = True
|
||||
|
||||
# validation
|
||||
validator_class = validation.ModelAdminValidator
|
||||
# Old, deprecated style:
|
||||
default_validator_class = validation.ModelAdminValidator
|
||||
# New style:
|
||||
checks_class = ModelAdminChecks
|
||||
|
||||
def __init__(self, model, admin_site):
|
||||
self.model = model
|
||||
@ -477,6 +514,9 @@ class ModelAdmin(BaseModelAdmin):
|
||||
self.admin_site = admin_site
|
||||
super(ModelAdmin, self).__init__()
|
||||
|
||||
def __str__(self):
|
||||
return "%s.%s" % (self.model._meta.app_label, self.__class__.__name__)
|
||||
|
||||
def get_inline_instances(self, request, obj=None):
|
||||
inline_instances = []
|
||||
for inline_class in self.inlines:
|
||||
@ -1685,8 +1725,7 @@ class InlineModelAdmin(BaseModelAdmin):
|
||||
verbose_name_plural = None
|
||||
can_delete = True
|
||||
|
||||
# validation
|
||||
validator_class = validation.InlineValidator
|
||||
checks_class = InlineModelAdminChecks
|
||||
|
||||
def __init__(self, parent_model, admin_site):
|
||||
self.admin_site = admin_site
|
||||
|
@ -100,7 +100,7 @@ class AdminSite(object):
|
||||
admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)
|
||||
|
||||
if admin_class is not ModelAdmin and settings.DEBUG:
|
||||
admin_class.validate(model)
|
||||
admin_class.check(model)
|
||||
|
||||
# Instantiate the admin class to save in the registry
|
||||
self._registry[model] = admin_class(model, self)
|
||||
|
@ -2,6 +2,8 @@ import inspect
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.checks import check_user_model
|
||||
from django.core import checks
|
||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||
from django.utils.module_loading import import_by_path
|
||||
from django.middleware.csrf import rotate_token
|
||||
@ -13,6 +15,10 @@ BACKEND_SESSION_KEY = '_auth_user_backend'
|
||||
REDIRECT_FIELD_NAME = 'next'
|
||||
|
||||
|
||||
# Register the user model checks
|
||||
checks.register('models')(check_user_model)
|
||||
|
||||
|
||||
def load_backend(path):
|
||||
return import_by_path(path)()
|
||||
|
||||
|
69
django/contrib/auth/checks.py
Normal file
69
django/contrib/auth/checks.py
Normal file
@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import apps
|
||||
from django.core import checks
|
||||
|
||||
|
||||
def check_user_model(**kwargs):
|
||||
from django.conf import settings
|
||||
|
||||
errors = []
|
||||
app_name, model_name = settings.AUTH_USER_MODEL.split('.')
|
||||
|
||||
cls = apps.get_model(app_name, model_name)
|
||||
|
||||
# Check that REQUIRED_FIELDS is a list
|
||||
if not isinstance(cls.REQUIRED_FIELDS, (list, tuple)):
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'The REQUIRED_FIELDS must be a list or tuple.',
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='auth.E001',
|
||||
)
|
||||
)
|
||||
|
||||
# Check that the USERNAME FIELD isn't included in REQUIRED_FIELDS.
|
||||
if cls.USERNAME_FIELD in cls.REQUIRED_FIELDS:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
('The field named as the USERNAME_FIELD '
|
||||
'must not be included in REQUIRED_FIELDS '
|
||||
'on a custom user model.'),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='auth.E002',
|
||||
)
|
||||
)
|
||||
|
||||
# Check that the username field is unique
|
||||
if not cls._meta.get_field(cls.USERNAME_FIELD).unique:
|
||||
if (settings.AUTHENTICATION_BACKENDS ==
|
||||
('django.contrib.auth.backends.ModelBackend',)):
|
||||
errors.append(
|
||||
checks.Error(
|
||||
('The %s.%s field must be unique because it is '
|
||||
'pointed to by USERNAME_FIELD.') % (
|
||||
cls._meta.object_name, cls.USERNAME_FIELD
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='auth.E003',
|
||||
)
|
||||
)
|
||||
else:
|
||||
errors.append(
|
||||
checks.Warning(
|
||||
('The %s.%s field is pointed to by USERNAME_FIELD, '
|
||||
'but it is not unique.') % (
|
||||
cls._meta.object_name, cls.USERNAME_FIELD
|
||||
),
|
||||
hint=('Ensure that your authentication backend can handle '
|
||||
'non-unique usernames.'),
|
||||
obj=cls,
|
||||
id='auth.W004',
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
@ -15,7 +15,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
help = "Change a user's password for django.contrib.auth."
|
||||
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
def _get_pass(self, prompt="Password: "):
|
||||
p = getpass.getpass(prompt=prompt)
|
||||
|
@ -3,17 +3,18 @@ from datetime import date
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth import models, management
|
||||
from django.contrib.auth.checks import check_user_model
|
||||
from django.contrib.auth.management import create_permissions
|
||||
from django.contrib.auth.management.commands import changepassword
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.tests.custom_user import CustomUser
|
||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core import checks
|
||||
from django.core import exceptions
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from django.core.management.validation import get_validation_errors
|
||||
from django.test import TestCase, override_settings
|
||||
from django.test import TestCase, override_settings, override_system_checks
|
||||
from django.utils import six
|
||||
from django.utils.six import StringIO
|
||||
|
||||
@ -161,7 +162,7 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
|
||||
email="joe@somewhere.org",
|
||||
date_of_birth="1976-04-01",
|
||||
stdout=new_io,
|
||||
skip_validation=True
|
||||
skip_checks=True
|
||||
)
|
||||
command_output = new_io.getvalue().strip()
|
||||
self.assertEqual(command_output, 'Superuser created successfully.')
|
||||
@ -185,7 +186,7 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
|
||||
username="joe@somewhere.org",
|
||||
stdout=new_io,
|
||||
stderr=new_io,
|
||||
skip_validation=True
|
||||
skip_checks=True
|
||||
)
|
||||
|
||||
self.assertEqual(CustomUser._default_manager.count(), 0)
|
||||
@ -193,25 +194,81 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
|
||||
|
||||
class CustomUserModelValidationTestCase(TestCase):
|
||||
@override_settings(AUTH_USER_MODEL='auth.CustomUserNonListRequiredFields')
|
||||
@override_system_checks([check_user_model])
|
||||
def test_required_fields_is_list(self):
|
||||
"REQUIRED_FIELDS should be a list."
|
||||
new_io = StringIO()
|
||||
get_validation_errors(new_io, apps.get_app_config('auth'))
|
||||
self.assertIn("The REQUIRED_FIELDS must be a list or tuple.", new_io.getvalue())
|
||||
|
||||
from .custom_user import CustomUserNonListRequiredFields
|
||||
errors = checks.run_checks()
|
||||
expected = [
|
||||
checks.Error(
|
||||
'The REQUIRED_FIELDS must be a list or tuple.',
|
||||
hint=None,
|
||||
obj=CustomUserNonListRequiredFields,
|
||||
id='auth.E001',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
@override_settings(AUTH_USER_MODEL='auth.CustomUserBadRequiredFields')
|
||||
@override_system_checks([check_user_model])
|
||||
def test_username_not_in_required_fields(self):
|
||||
"USERNAME_FIELD should not appear in REQUIRED_FIELDS."
|
||||
new_io = StringIO()
|
||||
get_validation_errors(new_io, apps.get_app_config('auth'))
|
||||
self.assertIn("The field named as the USERNAME_FIELD should not be included in REQUIRED_FIELDS on a swappable User model.", new_io.getvalue())
|
||||
|
||||
from .custom_user import CustomUserBadRequiredFields
|
||||
errors = checks.run_checks()
|
||||
expected = [
|
||||
checks.Error(
|
||||
('The field named as the USERNAME_FIELD must not be included '
|
||||
'in REQUIRED_FIELDS on a custom user model.'),
|
||||
hint=None,
|
||||
obj=CustomUserBadRequiredFields,
|
||||
id='auth.E002',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
@override_settings(AUTH_USER_MODEL='auth.CustomUserNonUniqueUsername')
|
||||
@override_system_checks([check_user_model])
|
||||
def test_username_non_unique(self):
|
||||
"A non-unique USERNAME_FIELD should raise a model validation error."
|
||||
new_io = StringIO()
|
||||
get_validation_errors(new_io, apps.get_app_config('auth'))
|
||||
self.assertIn("The USERNAME_FIELD must be unique. Add unique=True to the field parameters.", new_io.getvalue())
|
||||
|
||||
from .custom_user import CustomUserNonUniqueUsername
|
||||
errors = checks.run_checks()
|
||||
expected = [
|
||||
checks.Error(
|
||||
('The CustomUserNonUniqueUsername.username field must be '
|
||||
'unique because it is pointed to by USERNAME_FIELD.'),
|
||||
hint=None,
|
||||
obj=CustomUserNonUniqueUsername,
|
||||
id='auth.E003',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
@override_settings(AUTH_USER_MODEL='auth.CustomUserNonUniqueUsername',
|
||||
AUTHENTICATION_BACKENDS=[
|
||||
'my.custom.backend',
|
||||
])
|
||||
@override_system_checks([check_user_model])
|
||||
def test_username_non_unique_with_custom_backend(self):
|
||||
""" A non-unique USERNAME_FIELD should raise an error only if we use the
|
||||
default authentication backend. Otherwise, an warning should be raised.
|
||||
"""
|
||||
|
||||
from .custom_user import CustomUserNonUniqueUsername
|
||||
errors = checks.run_checks()
|
||||
expected = [
|
||||
checks.Warning(
|
||||
('The CustomUserNonUniqueUsername.username field is pointed to '
|
||||
'by USERNAME_FIELD, but it is not unique.'),
|
||||
hint=('Ensure that your authentication backend can handle '
|
||||
'non-unique usernames.'),
|
||||
obj=CustomUserNonUniqueUsername,
|
||||
id='auth.W004',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
|
||||
class PermissionTestCase(TestCase):
|
||||
|
@ -0,0 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.checks import check_generic_foreign_keys
|
||||
from django.core import checks
|
||||
|
||||
|
||||
checks.register('models')(check_generic_foreign_keys)
|
18
django/contrib/contenttypes/checks.py
Normal file
18
django/contrib/contenttypes/checks.py
Normal file
@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils import six
|
||||
|
||||
|
||||
def check_generic_foreign_keys(**kwargs):
|
||||
from .generic import GenericForeignKey
|
||||
from django.db import models
|
||||
|
||||
errors = []
|
||||
fields = (obj
|
||||
for cls in models.get_models()
|
||||
for obj in six.itervalues(vars(cls))
|
||||
if isinstance(obj, GenericForeignKey))
|
||||
for field in fields:
|
||||
errors.extend(field.check())
|
||||
return errors
|
@ -6,10 +6,12 @@ from __future__ import unicode_literals
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
|
||||
from django.core import checks
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import connection
|
||||
from django.db import models, router, transaction, DEFAULT_DB_ALIAS
|
||||
from django.db.models import signals
|
||||
from django.db.models import signals, FieldDoesNotExist
|
||||
from django.db.models.base import ModelBase
|
||||
from django.db.models.fields.related import ForeignObject, ForeignObjectRel
|
||||
from django.db.models.related import PathInfo
|
||||
from django.db.models.sql.datastructures import Col
|
||||
@ -20,7 +22,7 @@ from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils import six
|
||||
from django.utils.deprecation import RenameMethodsBase
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.encoding import smart_text, python_2_unicode_compatible
|
||||
|
||||
|
||||
class RenameGenericForeignKeyMethods(RenameMethodsBase):
|
||||
@ -29,6 +31,7 @@ class RenameGenericForeignKeyMethods(RenameMethodsBase):
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)):
|
||||
"""
|
||||
Provides a generic relation to any object through content-type/object-id
|
||||
@ -53,6 +56,52 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)):
|
||||
|
||||
setattr(cls, name, self)
|
||||
|
||||
def __str__(self):
|
||||
model = self.model
|
||||
app = model._meta.app_label
|
||||
return '%s.%s.%s' % (app, model._meta.object_name, self.name)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = []
|
||||
errors.extend(self._check_content_type_field())
|
||||
errors.extend(self._check_object_id_field())
|
||||
errors.extend(self._check_field_name())
|
||||
return errors
|
||||
|
||||
def _check_content_type_field(self):
|
||||
return _check_content_type_field(
|
||||
model=self.model,
|
||||
field_name=self.ct_field,
|
||||
checked_object=self)
|
||||
|
||||
def _check_object_id_field(self):
|
||||
try:
|
||||
self.model._meta.get_field(self.fk_field)
|
||||
except FieldDoesNotExist:
|
||||
return [
|
||||
checks.Error(
|
||||
'The field refers to "%s" field which is missing.' % self.fk_field,
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='contenttypes.E001',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_field_name(self):
|
||||
if self.name.endswith("_"):
|
||||
return [
|
||||
checks.Error(
|
||||
'Field names must not end with underscores.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='contenttypes.E002',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def instance_pre_init(self, signal, sender, args, kwargs, **_kwargs):
|
||||
"""
|
||||
Handles initializing an object with the generic FK instead of
|
||||
@ -185,6 +234,72 @@ class GenericRelation(ForeignObject):
|
||||
to, to_fields=[],
|
||||
from_fields=[self.object_id_field_name], **kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(GenericRelation, self).check(**kwargs)
|
||||
errors.extend(self._check_content_type_field())
|
||||
errors.extend(self._check_object_id_field())
|
||||
errors.extend(self._check_generic_foreign_key_existence())
|
||||
return errors
|
||||
|
||||
def _check_content_type_field(self):
|
||||
target = self.rel.to
|
||||
if isinstance(target, ModelBase):
|
||||
return _check_content_type_field(
|
||||
model=target,
|
||||
field_name=self.content_type_field_name,
|
||||
checked_object=self)
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_object_id_field(self):
|
||||
target = self.rel.to
|
||||
if isinstance(target, ModelBase):
|
||||
opts = target._meta
|
||||
try:
|
||||
opts.get_field(self.object_id_field_name)
|
||||
except FieldDoesNotExist:
|
||||
return [
|
||||
checks.Error(
|
||||
'The field refers to %s.%s field which is missing.' % (
|
||||
opts.object_name, self.object_id_field_name
|
||||
),
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='contenttypes.E003',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_generic_foreign_key_existence(self):
|
||||
target = self.rel.to
|
||||
if isinstance(target, ModelBase):
|
||||
# Using `vars` is very ugly approach, but there is no better one,
|
||||
# because GenericForeignKeys are not considered as fields and,
|
||||
# therefore, are not included in `target._meta.local_fields`.
|
||||
fields = target._meta.virtual_fields
|
||||
if any(isinstance(field, GenericForeignKey) and
|
||||
field.ct_field == self.content_type_field_name and
|
||||
field.fk_field == self.object_id_field_name
|
||||
for field in fields):
|
||||
return []
|
||||
else:
|
||||
return [
|
||||
checks.Warning(
|
||||
('The field defines a generic relation with the model '
|
||||
'%s.%s, but the model lacks GenericForeignKey.') % (
|
||||
target._meta.app_label, target._meta.object_name
|
||||
),
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='contenttypes.E004',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
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],
|
||||
@ -252,6 +367,54 @@ class GenericRelation(ForeignObject):
|
||||
})
|
||||
|
||||
|
||||
def _check_content_type_field(model, field_name, checked_object):
|
||||
""" Check if field named `field_name` in model `model` exists and is
|
||||
valid content_type field (is a ForeignKey to ContentType). """
|
||||
|
||||
try:
|
||||
field = model._meta.get_field(field_name)
|
||||
except FieldDoesNotExist:
|
||||
return [
|
||||
checks.Error(
|
||||
'The field refers to %s.%s field which is missing.' % (
|
||||
model._meta.object_name, field_name
|
||||
),
|
||||
hint=None,
|
||||
obj=checked_object,
|
||||
id='contenttypes.E005',
|
||||
)
|
||||
]
|
||||
else:
|
||||
if not isinstance(field, models.ForeignKey):
|
||||
return [
|
||||
checks.Error(
|
||||
('"%s" field is used by a %s '
|
||||
'as content type field and therefore it must be '
|
||||
'a ForeignKey.') % (
|
||||
field_name, checked_object.__class__.__name__
|
||||
),
|
||||
hint=None,
|
||||
obj=checked_object,
|
||||
id='contenttypes.E006',
|
||||
)
|
||||
]
|
||||
elif field.rel.to != ContentType:
|
||||
return [
|
||||
checks.Error(
|
||||
('"%s" field is used by a %s '
|
||||
'as content type field and therefore it must be '
|
||||
'a ForeignKey to ContentType.') % (
|
||||
field_name, checked_object.__class__.__name__
|
||||
),
|
||||
hint=None,
|
||||
obj=checked_object,
|
||||
id='contenttypes.E007',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
class ReverseGenericRelatedObjectsDescriptor(object):
|
||||
"""
|
||||
This class provides the functionality that makes the related-object
|
||||
|
@ -74,7 +74,7 @@ class Command(LabelCommand):
|
||||
help='Generate mapping dictionary for use with `LayerMapping`.')
|
||||
)
|
||||
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
|
@ -1,42 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import checks
|
||||
from django.db import models
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
|
||||
|
||||
class CurrentSiteManager(models.Manager):
|
||||
"Use this to limit objects to those associated with the current site."
|
||||
|
||||
def __init__(self, field_name=None):
|
||||
super(CurrentSiteManager, self).__init__()
|
||||
self.__field_name = field_name
|
||||
self.__is_validated = False
|
||||
|
||||
def _validate_field_name(self):
|
||||
field_names = self.model._meta.get_all_field_names()
|
||||
def check(self, **kwargs):
|
||||
errors = super(CurrentSiteManager, self).check(**kwargs)
|
||||
errors.extend(self._check_field_name())
|
||||
return errors
|
||||
|
||||
# If a custom name is provided, make sure the field exists on the model
|
||||
if self.__field_name is not None and self.__field_name not in field_names:
|
||||
raise ValueError("%s couldn't find a field named %s in %s." %
|
||||
(self.__class__.__name__, self.__field_name, self.model._meta.object_name))
|
||||
|
||||
# Otherwise, see if there is a field called either 'site' or 'sites'
|
||||
else:
|
||||
for potential_name in ['site', 'sites']:
|
||||
if potential_name in field_names:
|
||||
self.__field_name = potential_name
|
||||
self.__is_validated = True
|
||||
break
|
||||
|
||||
# Now do a type check on the field (FK or M2M only)
|
||||
def _check_field_name(self):
|
||||
field_name = self._get_field_name()
|
||||
try:
|
||||
field = self.model._meta.get_field(self.__field_name)
|
||||
if not isinstance(field, (models.ForeignKey, models.ManyToManyField)):
|
||||
raise TypeError("%s must be a ForeignKey or ManyToManyField." % self.__field_name)
|
||||
field = self.model._meta.get_field(field_name)
|
||||
except FieldDoesNotExist:
|
||||
raise ValueError("%s couldn't find a field named %s in %s." %
|
||||
(self.__class__.__name__, self.__field_name, self.model._meta.object_name))
|
||||
self.__is_validated = True
|
||||
return [
|
||||
checks.Error(
|
||||
"CurrentSiteManager could not find a field named '%s'." % field_name,
|
||||
hint=('Ensure that you did not misspell the field name. '
|
||||
'Does the field exist?'),
|
||||
obj=self,
|
||||
id='sites.E001',
|
||||
)
|
||||
]
|
||||
|
||||
if not isinstance(field, (models.ForeignKey, models.ManyToManyField)):
|
||||
return [
|
||||
checks.Error(
|
||||
"CurrentSiteManager requires that '%s.%s' must be a "
|
||||
"ForeignKey or ManyToManyField." % (
|
||||
self.model._meta.object_name, field_name
|
||||
),
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='sites.E002',
|
||||
)
|
||||
]
|
||||
|
||||
return []
|
||||
|
||||
def _get_field_name(self):
|
||||
""" Return self.__field_name or 'site' or 'sites'. """
|
||||
|
||||
if not self.__field_name:
|
||||
try:
|
||||
self.model._meta.get_field('site')
|
||||
except FieldDoesNotExist:
|
||||
self.__field_name = 'sites'
|
||||
else:
|
||||
self.__field_name = 'site'
|
||||
return self.__field_name
|
||||
|
||||
def get_queryset(self):
|
||||
if not self.__is_validated:
|
||||
self._validate_field_name()
|
||||
return super(CurrentSiteManager, self).get_queryset().filter(**{self.__field_name + '__id': settings.SITE_ID})
|
||||
return super(CurrentSiteManager, self).get_queryset().filter(
|
||||
**{self._get_field_name() + '__id': settings.SITE_ID})
|
||||
|
@ -45,7 +45,7 @@ class Command(NoArgsCommand):
|
||||
"'.*' and '*~'."),
|
||||
)
|
||||
help = "Collect static files in a single location."
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NoArgsCommand, self).__init__(*args, **kwargs)
|
||||
|
@ -0,0 +1,18 @@
|
||||
# -*- coding: utf8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .messages import (CheckMessage,
|
||||
Debug, Info, Warning, Error, Critical,
|
||||
DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
from .registry import register, run_checks, tag_exists
|
||||
|
||||
# Import these to force registration of checks
|
||||
import django.core.checks.compatibility.django_1_6_0 # NOQA
|
||||
import django.core.checks.model_checks # NOQA
|
||||
|
||||
__all__ = [
|
||||
'CheckMessage',
|
||||
'Debug', 'Info', 'Warning', 'Error', 'Critical',
|
||||
'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL',
|
||||
'register', 'run_checks', 'tag_exists',
|
||||
]
|
@ -1,39 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
import warnings
|
||||
|
||||
from django.core.checks.compatibility import django_1_6_0
|
||||
|
||||
|
||||
COMPAT_CHECKS = [
|
||||
# Add new modules at the top, so we keep things in descending order.
|
||||
# After two-three minor releases, old versions should get dropped.
|
||||
django_1_6_0,
|
||||
]
|
||||
|
||||
|
||||
def check_compatibility():
|
||||
"""
|
||||
Runs through compatibility checks to warn the user with an existing install
|
||||
about changes in an up-to-date Django.
|
||||
|
||||
Modules should be located in ``django.core.compat_checks`` (typically one
|
||||
per release of Django) & must have a ``run_checks`` function that runs
|
||||
all the checks.
|
||||
|
||||
Returns a list of informational messages about incompatibilities.
|
||||
"""
|
||||
messages = []
|
||||
|
||||
for check_module in COMPAT_CHECKS:
|
||||
check = getattr(check_module, 'run_checks', None)
|
||||
|
||||
if check is None:
|
||||
warnings.warn(
|
||||
"The '%s' module lacks a " % check_module.__name__ +
|
||||
"'run_checks' method, which is needed to verify compatibility."
|
||||
)
|
||||
continue
|
||||
|
||||
messages.extend(check())
|
||||
|
||||
return messages
|
@ -1,10 +1,20 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
|
||||
from .. import Warning, register
|
||||
|
||||
|
||||
def check_test_runner():
|
||||
@register('compatibility')
|
||||
def check_1_6_compatibility(**kwargs):
|
||||
errors = []
|
||||
errors.extend(_check_test_runner(**kwargs))
|
||||
errors.extend(_check_boolean_field_default_value(**kwargs))
|
||||
return errors
|
||||
|
||||
|
||||
def _check_test_runner(app_configs=None, **kwargs):
|
||||
"""
|
||||
Checks if the user has *not* overridden the ``TEST_RUNNER`` setting &
|
||||
warns them about the default behavior changes.
|
||||
@ -13,53 +23,94 @@ def check_test_runner():
|
||||
doing & avoid generating a message.
|
||||
"""
|
||||
from django.conf import settings
|
||||
new_default = 'django.test.runner.DiscoverRunner'
|
||||
test_runner_setting = getattr(settings, 'TEST_RUNNER', new_default)
|
||||
|
||||
if test_runner_setting == new_default:
|
||||
message = [
|
||||
"Django 1.6 introduced a new default test runner ('%s')" % new_default,
|
||||
"You should ensure your tests are all running & behaving as expected. See",
|
||||
"https://docs.djangoproject.com/en/dev/releases/1.6/#discovery-of-tests-in-any-test-module",
|
||||
"for more information.",
|
||||
# We need to establish if this is a project defined on the 1.5 project template,
|
||||
# because if the project was generated on the 1.6 template, it will have be been
|
||||
# developed with the new TEST_RUNNER behavior in mind.
|
||||
|
||||
# There's no canonical way to do this; so we leverage off the fact that 1.6
|
||||
# also introduced a new project template, removing a bunch of settings from the
|
||||
# default that won't be in common usage.
|
||||
|
||||
# We make this determination on a balance of probabilities. Each of these factors
|
||||
# contributes a weight; if enough of them trigger, we've got a likely 1.6 project.
|
||||
weight = 0
|
||||
|
||||
# If TEST_RUNNER is explicitly set, it's all a moot point - if it's been explcitly set,
|
||||
# the user has opted into a specific set of behaviors, which won't change as the
|
||||
# default changes.
|
||||
if not settings.is_overridden('TEST_RUNNER'):
|
||||
# Strong markers:
|
||||
# SITE_ID = 1 is in 1.5 template, not defined in 1.6 template
|
||||
try:
|
||||
settings.SITE_ID
|
||||
weight += 2
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# BASE_DIR is not defined in 1.5 template, set in 1.6 template
|
||||
try:
|
||||
settings.BASE_DIR
|
||||
except AttributeError:
|
||||
weight += 2
|
||||
|
||||
# TEMPLATE_LOADERS defined in 1.5 template, not defined in 1.6 template
|
||||
if settings.is_overridden('TEMPLATE_LOADERS'):
|
||||
weight += 2
|
||||
|
||||
# MANAGERS defined in 1.5 template, not defined in 1.6 template
|
||||
if settings.is_overridden('MANAGERS'):
|
||||
weight += 2
|
||||
|
||||
# Weaker markers - These are more likely to have been added in common usage
|
||||
# ADMINS defined in 1.5 template, not defined in 1.6 template
|
||||
if settings.is_overridden('ADMINS'):
|
||||
weight += 1
|
||||
|
||||
# Clickjacking enabled by default in 1.6
|
||||
if 'django.middleware.clickjacking.XFrameOptionsMiddleware' not in set(settings.MIDDLEWARE_CLASSES):
|
||||
weight += 1
|
||||
|
||||
if weight >= 6:
|
||||
return [
|
||||
Warning(
|
||||
"Some project unittests may not execute as expected.",
|
||||
hint=("Django 1.6 introduced a new default test runner. It looks like "
|
||||
"this project was generated using Django 1.5 or earlier. You should "
|
||||
"ensure your tests are all running & behaving as expected. See "
|
||||
"https://docs.djangoproject.com/en/dev/releases/1.6/#discovery-of-tests-in-any-test-module "
|
||||
"for more information."),
|
||||
obj=None,
|
||||
id='1_6.W001',
|
||||
)
|
||||
]
|
||||
return ' '.join(message)
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def check_boolean_field_default_value():
|
||||
def _check_boolean_field_default_value(app_configs=None, **kwargs):
|
||||
"""
|
||||
Checks if there are any BooleanFields without a default value, &
|
||||
warns the user that the default has changed from False to Null.
|
||||
warns the user that the default has changed from False to None.
|
||||
"""
|
||||
fields = []
|
||||
for cls in apps.get_models():
|
||||
opts = cls._meta
|
||||
for f in opts.local_fields:
|
||||
if isinstance(f, models.BooleanField) and not f.has_default():
|
||||
fields.append(
|
||||
'%s.%s: "%s"' % (opts.app_label, opts.object_name, f.name)
|
||||
)
|
||||
if fields:
|
||||
fieldnames = ", ".join(fields)
|
||||
message = [
|
||||
"You have not set a default value for one or more BooleanFields:",
|
||||
"%s." % fieldnames,
|
||||
"In Django 1.6 the default value of BooleanField was changed from",
|
||||
"False to Null when Field.default isn't defined. See",
|
||||
"https://docs.djangoproject.com/en/1.6/ref/models/fields/#booleanfield"
|
||||
"for more information."
|
||||
]
|
||||
return ' '.join(message)
|
||||
from django.db import models
|
||||
|
||||
|
||||
def run_checks():
|
||||
"""
|
||||
Required by the ``check`` management command, this returns a list of
|
||||
messages from all the relevant check functions for this version of Django.
|
||||
"""
|
||||
checks = [
|
||||
check_test_runner(),
|
||||
check_boolean_field_default_value(),
|
||||
problem_fields = [
|
||||
field
|
||||
for model in apps.get_models(**kwargs)
|
||||
if app_configs is None or model._meta.app_config in app_configs
|
||||
for field in model._meta.local_fields
|
||||
if isinstance(field, models.BooleanField) and not field.has_default()
|
||||
]
|
||||
|
||||
return [
|
||||
Warning(
|
||||
"BooleanField does not have a default value. ",
|
||||
hint=("Django 1.6 changed the default value of BooleanField from False to None. "
|
||||
"See https://docs.djangoproject.com/en/1.6/ref/models/fields/#booleanfield "
|
||||
"for more information."),
|
||||
obj=field,
|
||||
id='1_6.W002',
|
||||
)
|
||||
for field in problem_fields
|
||||
]
|
||||
# Filter out the ``None`` or empty strings.
|
||||
return [output for output in checks if output]
|
||||
|
84
django/core/checks/messages.py
Normal file
84
django/core/checks/messages.py
Normal file
@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.encoding import python_2_unicode_compatible, force_str
|
||||
|
||||
|
||||
# Levels
|
||||
DEBUG = 10
|
||||
INFO = 20
|
||||
WARNING = 30
|
||||
ERROR = 40
|
||||
CRITICAL = 50
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class CheckMessage(object):
|
||||
|
||||
def __init__(self, level, msg, hint, obj=None, id=None):
|
||||
assert isinstance(level, int), "The first argument should be level."
|
||||
self.level = level
|
||||
self.msg = msg
|
||||
self.hint = hint
|
||||
self.obj = obj
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other):
|
||||
return all(getattr(self, attr) == getattr(other, attr)
|
||||
for attr in ['level', 'msg', 'hint', 'obj', 'id'])
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __str__(self):
|
||||
from django.db import models
|
||||
|
||||
if self.obj is None:
|
||||
obj = "?"
|
||||
elif isinstance(self.obj, models.base.ModelBase):
|
||||
# We need to hardcode ModelBase and Field cases because its __str__
|
||||
# method doesn't return "applabel.modellabel" and cannot be changed.
|
||||
model = self.obj
|
||||
app = model._meta.app_label
|
||||
obj = '%s.%s' % (app, model._meta.object_name)
|
||||
else:
|
||||
obj = force_str(self.obj)
|
||||
id = "(%s) " % self.id if self.id else ""
|
||||
hint = "\n\tHINT: %s" % self.hint if self.hint else ''
|
||||
return "%s: %s%s%s" % (obj, id, self.msg, hint)
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s: level=%r, msg=%r, hint=%r, obj=%r, id=%r>" % \
|
||||
(self.__class__.__name__, self.level, self.msg, self.hint, self.obj, self.id)
|
||||
|
||||
def is_serious(self):
|
||||
return self.level >= ERROR
|
||||
|
||||
def is_silenced(self):
|
||||
from django.conf import settings
|
||||
return self.id in settings.SILENCED_SYSTEM_CHECKS
|
||||
|
||||
|
||||
class Debug(CheckMessage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
return super(Debug, self).__init__(DEBUG, *args, **kwargs)
|
||||
|
||||
|
||||
class Info(CheckMessage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
return super(Info, self).__init__(INFO, *args, **kwargs)
|
||||
|
||||
|
||||
class Warning(CheckMessage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
return super(Warning, self).__init__(WARNING, *args, **kwargs)
|
||||
|
||||
|
||||
class Error(CheckMessage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
return super(Error, self).__init__(ERROR, *args, **kwargs)
|
||||
|
||||
|
||||
class Critical(CheckMessage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
return super(Critical, self).__init__(CRITICAL, *args, **kwargs)
|
49
django/core/checks/model_checks.py
Normal file
49
django/core/checks/model_checks.py
Normal file
@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from itertools import chain
|
||||
import types
|
||||
|
||||
from django.apps import apps
|
||||
|
||||
from . import Error, register
|
||||
|
||||
|
||||
@register('models')
|
||||
def check_all_models(app_configs=None, **kwargs):
|
||||
errors = [model.check(**kwargs)
|
||||
for model in apps.get_models()
|
||||
if app_configs is None or model._meta.app_config in app_configs]
|
||||
return list(chain(*errors))
|
||||
|
||||
|
||||
@register('models', 'signals')
|
||||
def check_model_signals(app_configs=None, **kwargs):
|
||||
"""Ensure lazily referenced model signals senders are installed."""
|
||||
from django.db import models
|
||||
errors = []
|
||||
|
||||
for name in dir(models.signals):
|
||||
obj = getattr(models.signals, name)
|
||||
if isinstance(obj, models.signals.ModelSignal):
|
||||
for reference, receivers in obj.unresolved_references.items():
|
||||
for receiver, _, _ in receivers:
|
||||
# The receiver is either a function or an instance of class
|
||||
# defining a `__call__` method.
|
||||
if isinstance(receiver, types.FunctionType):
|
||||
description = "The `%s` function" % receiver.__name__
|
||||
else:
|
||||
description = "An instance of the `%s` class" % receiver.__class__.__name__
|
||||
errors.append(
|
||||
Error(
|
||||
"%s was connected to the `%s` signal "
|
||||
"with a lazy reference to the '%s' sender, "
|
||||
"which has not been installed." % (
|
||||
description, name, '.'.join(reference)
|
||||
),
|
||||
obj=receiver.__module__,
|
||||
hint=None,
|
||||
id='E014'
|
||||
)
|
||||
)
|
||||
return errors
|
63
django/core/checks/registry.py
Normal file
63
django/core/checks/registry.py
Normal file
@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from django.utils.itercompat import is_iterable
|
||||
|
||||
|
||||
class CheckRegistry(object):
|
||||
|
||||
def __init__(self):
|
||||
self.registered_checks = []
|
||||
|
||||
def register(self, *tags):
|
||||
"""
|
||||
Decorator. Register given function `f` labeled with given `tags`. The
|
||||
function should receive **kwargs and return list of Errors and
|
||||
Warnings.
|
||||
|
||||
Example::
|
||||
|
||||
registry = CheckRegistry()
|
||||
@registry.register('mytag', 'anothertag')
|
||||
def my_check(apps, **kwargs):
|
||||
# ... perform checks and collect `errors` ...
|
||||
return errors
|
||||
|
||||
"""
|
||||
|
||||
def inner(check):
|
||||
check.tags = tags
|
||||
self.registered_checks.append(check)
|
||||
return check
|
||||
|
||||
return inner
|
||||
|
||||
def run_checks(self, app_configs=None, tags=None):
|
||||
""" Run all registered checks and return list of Errors and Warnings.
|
||||
"""
|
||||
errors = []
|
||||
if tags is not None:
|
||||
checks = [check for check in self.registered_checks
|
||||
if hasattr(check, 'tags') and set(check.tags) & set(tags)]
|
||||
else:
|
||||
checks = self.registered_checks
|
||||
|
||||
for check in checks:
|
||||
new_errors = check(app_configs=app_configs)
|
||||
assert is_iterable(new_errors), (
|
||||
"The function %r did not return a list. All functions registered "
|
||||
"with the checks registry must return a list." % check)
|
||||
errors.extend(new_errors)
|
||||
return errors
|
||||
|
||||
def tag_exists(self, tag):
|
||||
tags = chain(*[check.tags for check in self.registered_checks if hasattr(check, 'tags')])
|
||||
return tag in tags
|
||||
|
||||
|
||||
registry = CheckRegistry()
|
||||
register = registry.register
|
||||
run_checks = registry.run_checks
|
||||
tag_exists = registry.tag_exists
|
@ -1,3 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
"""
|
||||
Base classes for writing management commands (named commands which can
|
||||
be executed through ``django-admin.py`` or ``manage.py``).
|
||||
@ -12,9 +15,10 @@ import warnings
|
||||
from optparse import make_option, OptionParser
|
||||
|
||||
import django
|
||||
from django.core import checks
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.management.color import color_style, no_style
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.six import StringIO
|
||||
|
||||
|
||||
class CommandError(Exception):
|
||||
@ -136,7 +140,20 @@ class BaseCommand(object):
|
||||
wrapped with ``BEGIN;`` and ``COMMIT;``. Default value is
|
||||
``False``.
|
||||
|
||||
``requires_system_checks``
|
||||
A boolean; if ``True``, entire Django project will be checked for errors
|
||||
prior to executing the command. Default value is ``True``.
|
||||
To validate an individual application's models
|
||||
rather than all applications' models, call
|
||||
``self.check(app_configs)`` from ``handle()``, where ``app_configs``
|
||||
is the list of application's configuration provided by the
|
||||
app registry.
|
||||
|
||||
``requires_model_validation``
|
||||
DEPRECATED - This value will only be used if requires_system_checks
|
||||
has not been provided. Defining both ``requires_system_checks`` and
|
||||
``requires_model_validation`` will result in an error.
|
||||
|
||||
A boolean; if ``True``, validation of installed models will be
|
||||
performed prior to executing the command. Default value is
|
||||
``True``. To validate an individual application's models
|
||||
@ -181,13 +198,40 @@ class BaseCommand(object):
|
||||
|
||||
# Configuration shortcuts that alter various logic.
|
||||
can_import_settings = True
|
||||
requires_model_validation = True
|
||||
output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
|
||||
leave_locale_alone = False
|
||||
|
||||
# Uncomment the following line of code after deprecation plan for
|
||||
# requires_model_validation comes to completion:
|
||||
#
|
||||
# requires_system_checks = True
|
||||
|
||||
def __init__(self):
|
||||
self.style = color_style()
|
||||
|
||||
# `requires_model_validation` is deprecated in favour of
|
||||
# `requires_system_checks`. If both options are present, an error is
|
||||
# raised. Otherwise the present option is used. If none of them is
|
||||
# defined, the default value (True) is used.
|
||||
has_old_option = hasattr(self, 'requires_model_validation')
|
||||
has_new_option = hasattr(self, 'requires_system_checks')
|
||||
|
||||
if has_old_option:
|
||||
warnings.warn(
|
||||
'"requires_model_validation" is deprecated '
|
||||
'in favour of "requires_system_checks".',
|
||||
PendingDeprecationWarning)
|
||||
if has_old_option and has_new_option:
|
||||
raise ImproperlyConfigured(
|
||||
'Command %s defines both "requires_model_validation" '
|
||||
'and "requires_system_checks", which is illegal. Use only '
|
||||
'"requires_system_checks".' % self.__class__.__name__)
|
||||
|
||||
self.requires_system_checks = (
|
||||
self.requires_system_checks if has_new_option else
|
||||
self.requires_model_validation if has_old_option else
|
||||
True)
|
||||
|
||||
def get_version(self):
|
||||
"""
|
||||
Return the Django version, which should be correct for all
|
||||
@ -253,8 +297,8 @@ class BaseCommand(object):
|
||||
|
||||
def execute(self, *args, **options):
|
||||
"""
|
||||
Try to execute this command, performing model validation if
|
||||
needed (as controlled by the attribute
|
||||
Try to execute this command, performing system checks if needed (as
|
||||
controlled by attributes ``self.requires_system_checks`` and
|
||||
``self.requires_model_validation``, except if force-skipped).
|
||||
"""
|
||||
self.stdout = OutputWrapper(options.get('stdout', sys.stdout))
|
||||
@ -286,8 +330,10 @@ class BaseCommand(object):
|
||||
translation.activate('en-us')
|
||||
|
||||
try:
|
||||
if self.requires_model_validation and not options.get('skip_validation'):
|
||||
self.validate()
|
||||
if (self.requires_system_checks and
|
||||
not options.get('skip_validation') and # This will be removed at the end of deprecation proccess for `skip_validation`.
|
||||
not options.get('skip_checks')):
|
||||
self.check()
|
||||
output = self.handle(*args, **options)
|
||||
if output:
|
||||
if self.output_transaction:
|
||||
@ -305,21 +351,67 @@ class BaseCommand(object):
|
||||
translation.activate(saved_locale)
|
||||
|
||||
def validate(self, app_config=None, display_num_errors=False):
|
||||
"""
|
||||
Validates the given app, raising CommandError for any errors.
|
||||
""" Deprecated. Delegates to ``check``."""
|
||||
|
||||
If app_config is None, then this will validate all installed apps.
|
||||
if app_config is None:
|
||||
app_configs = None
|
||||
else:
|
||||
app_configs = [app_config]
|
||||
|
||||
return self.check(app_configs=app_configs, display_num_errors=display_num_errors)
|
||||
|
||||
def check(self, app_configs=None, tags=None, display_num_errors=False):
|
||||
"""
|
||||
Uses the system check framework to validate entire Django project.
|
||||
Raises CommandError for any serious message (error or critical errors).
|
||||
If there are only light messages (like warnings), they are printed to
|
||||
stderr and no exception is raised.
|
||||
|
||||
"""
|
||||
from django.core.management.validation import get_validation_errors
|
||||
s = StringIO()
|
||||
num_errors = get_validation_errors(s, app_config)
|
||||
if num_errors:
|
||||
s.seek(0)
|
||||
error_text = s.read()
|
||||
raise CommandError("One or more models did not validate:\n%s" % error_text)
|
||||
all_issues = checks.run_checks(app_configs=app_configs, tags=tags)
|
||||
|
||||
msg = ""
|
||||
if all_issues:
|
||||
debugs = [e for e in all_issues if e.level < checks.INFO and not e.is_silenced()]
|
||||
infos = [e for e in all_issues if checks.INFO <= e.level < checks.WARNING and not e.is_silenced()]
|
||||
warnings = [e for e in all_issues if checks.WARNING <= e.level < checks.ERROR and not e.is_silenced()]
|
||||
errors = [e for e in all_issues if checks.ERROR <= e.level < checks.CRITICAL]
|
||||
criticals = [e for e in all_issues if checks.CRITICAL <= e.level]
|
||||
sorted_issues = [
|
||||
(criticals, 'CRITICALS'),
|
||||
(errors, 'ERRORS'),
|
||||
(warnings, 'WARNINGS'),
|
||||
(infos, 'INFOS'),
|
||||
(debugs, 'DEBUGS'),
|
||||
]
|
||||
|
||||
for issues, group_name in sorted_issues:
|
||||
if issues:
|
||||
formatted = (
|
||||
color_style().ERROR(force_str(e))
|
||||
if e.is_serious()
|
||||
else color_style().WARNING(force_str(e))
|
||||
for e in issues)
|
||||
formatted = "\n".join(sorted(formatted))
|
||||
msg += '\n%s:\n%s\n' % (group_name, formatted)
|
||||
|
||||
msg = "System check identified some issues:\n%s" % msg
|
||||
|
||||
if display_num_errors:
|
||||
self.stdout.write("%s error%s found" % (num_errors, '' if num_errors == 1 else 's'))
|
||||
if msg:
|
||||
msg += '\n'
|
||||
msg += "System check identified %s." % (
|
||||
"no issues" if len(all_issues) == 0 else
|
||||
"1 issue" if len(all_issues) == 1 else
|
||||
"%s issues" % len(all_issues)
|
||||
)
|
||||
|
||||
if any(e.is_serious() and not e.is_silenced() for e in all_issues):
|
||||
raise CommandError(msg)
|
||||
elif msg and all_issues:
|
||||
self.stderr.write(msg)
|
||||
elif msg:
|
||||
self.stdout.write(msg)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
|
@ -1,14 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import warnings
|
||||
|
||||
from django.core.checks.compatibility.base import check_compatibility
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from optparse import make_option
|
||||
|
||||
from django.apps import apps
|
||||
from django.core import checks
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = "Checks your configuration's compatibility with this version " + \
|
||||
"of Django."
|
||||
class Command(BaseCommand):
|
||||
help = "Checks the entire Django project for potential problems."
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
for message in check_compatibility():
|
||||
warnings.warn(message)
|
||||
requires_system_checks = False
|
||||
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--tag', '-t', action='append', dest='tags',
|
||||
help='Run only checks labeled with given tag.'),
|
||||
)
|
||||
|
||||
def handle(self, *app_labels, **options):
|
||||
if app_labels:
|
||||
app_configs = [apps.get_app_config(app_label) for app_label in app_labels]
|
||||
else:
|
||||
app_configs = None
|
||||
|
||||
tags = options.get('tags', None)
|
||||
if tags and any(not checks.tag_exists(tag) for tag in tags):
|
||||
invalid_tag = next(tag for tag in tags if not checks.tag_exists(tag))
|
||||
raise CommandError('There is no system check with the "%s" tag.' % invalid_tag)
|
||||
|
||||
self.check(app_configs=app_configs, tags=tags, display_num_errors=True)
|
||||
|
@ -65,7 +65,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
help = 'Compiles .po files to .mo files for use with builtin gettext support.'
|
||||
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
leave_locale_alone = True
|
||||
|
||||
def handle(self, **options):
|
||||
|
@ -19,7 +19,7 @@ class Command(BaseCommand):
|
||||
'Defaults to the "default" database.'),
|
||||
)
|
||||
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
def handle(self, *tablenames, **options):
|
||||
db = options.get('database')
|
||||
|
@ -14,7 +14,7 @@ class Command(BaseCommand):
|
||||
'open a shell. Defaults to the "default" database.'),
|
||||
)
|
||||
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
def handle(self, **options):
|
||||
connection = connections[options.get('database')]
|
||||
|
@ -19,7 +19,7 @@ class Command(NoArgsCommand):
|
||||
'Default values are prefixed by "###".'),
|
||||
)
|
||||
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
# Inspired by Postfix's "postconf -n".
|
||||
|
@ -18,7 +18,7 @@ class Command(NoArgsCommand):
|
||||
'introspect. Defaults to using the "default" database.'),
|
||||
)
|
||||
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
db_module = 'django.db'
|
||||
|
||||
|
@ -203,7 +203,7 @@ class Command(NoArgsCommand):
|
||||
"applications) directory.\n\nYou must run this command with one of either the "
|
||||
"--locale or --all options.")
|
||||
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
leave_locale_alone = True
|
||||
|
||||
def handle_noargs(self, *args, **options):
|
||||
|
@ -37,7 +37,7 @@ class Command(BaseCommand):
|
||||
args = '[optional port number, or ipaddr:port]'
|
||||
|
||||
# Validation is called explicitly each time the server is reloaded.
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
def get_handler(self, *args, **options):
|
||||
"""
|
||||
@ -99,7 +99,7 @@ class Command(BaseCommand):
|
||||
shutdown_message = options.get('shutdown_message', '')
|
||||
quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'
|
||||
|
||||
self.stdout.write("Validating models...\n\n")
|
||||
self.stdout.write("Performing system checks...\n\n")
|
||||
self.validate(display_num_errors=True)
|
||||
self.check_migrations()
|
||||
now = datetime.now().strftime('%B %d, %Y - %X')
|
||||
|
@ -18,7 +18,7 @@ class Command(NoArgsCommand):
|
||||
|
||||
)
|
||||
help = "Runs a Python interactive interpreter. Tries to use IPython or bpython, if one of them is available."
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
def _ipython_pre_011(self):
|
||||
"""Start IPython pre-0.11"""
|
||||
|
@ -30,7 +30,7 @@ class Command(BaseCommand):
|
||||
help = ('Discover and run tests in the specified modules or the current directory.')
|
||||
args = '[path.to.modulename|path.to.modulename.TestCase|path.to.modulename.TestCase.test_method]...'
|
||||
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
def __init__(self):
|
||||
self.test_runner = None
|
||||
|
@ -16,7 +16,7 @@ class Command(BaseCommand):
|
||||
help = 'Runs a development server with data from the given fixture(s).'
|
||||
args = '[fixture ...]'
|
||||
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
def handle(self, *fixture_labels, **options):
|
||||
from django.core.management import call_command
|
||||
|
@ -1,10 +1,15 @@
|
||||
from django.core.management.base import NoArgsCommand
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import warnings
|
||||
|
||||
from django.core.management.commands.check import Command as CheckCommand
|
||||
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = "Validates all installed models."
|
||||
|
||||
requires_model_validation = False
|
||||
class Command(CheckCommand):
|
||||
help = 'Deprecated. Use "check" command instead. ' + CheckCommand.help
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
self.validate(display_num_errors=True)
|
||||
warnings.warn('"validate" has been deprecated in favour of "check".',
|
||||
PendingDeprecationWarning)
|
||||
super(Command, self).handle_noargs(**options)
|
||||
|
@ -52,7 +52,7 @@ class TemplateCommand(BaseCommand):
|
||||
'Separate multiple extensions with commas, or use '
|
||||
'-n multiple times.')
|
||||
)
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
# Can't import settings during this command, because they haven't
|
||||
# necessarily been created.
|
||||
can_import_settings = False
|
||||
|
@ -1,411 +0,0 @@
|
||||
import collections
|
||||
import sys
|
||||
import types
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.color import color_style
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.itercompat import is_iterable
|
||||
from django.utils import six
|
||||
|
||||
|
||||
class ModelErrorCollection:
|
||||
def __init__(self, outfile=sys.stdout):
|
||||
self.errors = []
|
||||
self.outfile = outfile
|
||||
self.style = color_style()
|
||||
|
||||
def add(self, context, error):
|
||||
self.errors.append((context, error))
|
||||
self.outfile.write(self.style.ERROR(force_str("%s: %s\n" % (context, error))))
|
||||
|
||||
|
||||
def get_validation_errors(outfile, app_config=None):
|
||||
"""
|
||||
Validates all models that are part of the specified app. If no app name is provided,
|
||||
validates all models of all installed apps. Writes errors, if any, to outfile.
|
||||
Returns number of errors.
|
||||
"""
|
||||
from django.apps import apps
|
||||
from django.db import connection, models
|
||||
from django.db.models.deletion import SET_NULL, SET_DEFAULT
|
||||
|
||||
e = ModelErrorCollection(outfile)
|
||||
|
||||
for cls in (app_config or apps).get_models(include_swapped=True):
|
||||
opts = cls._meta
|
||||
|
||||
# Check swappable attribute.
|
||||
if opts.swapped:
|
||||
try:
|
||||
app_label, model_name = opts.swapped.split('.')
|
||||
except ValueError:
|
||||
e.add(opts, "%s is not of the form 'app_label.app_name'." % opts.swappable)
|
||||
continue
|
||||
try:
|
||||
apps.get_model(app_label, model_name)
|
||||
except LookupError:
|
||||
e.add(opts, "Model has been swapped out for '%s' which has not been installed or is abstract." % opts.swapped)
|
||||
# No need to perform any other validation checks on a swapped model.
|
||||
continue
|
||||
|
||||
# If this is the current User model, check known validation problems with User models
|
||||
if settings.AUTH_USER_MODEL == '%s.%s' % (opts.app_label, opts.object_name):
|
||||
# Check that REQUIRED_FIELDS is a list
|
||||
if not isinstance(cls.REQUIRED_FIELDS, (list, tuple)):
|
||||
e.add(opts, 'The REQUIRED_FIELDS must be a list or tuple.')
|
||||
|
||||
# Check that the USERNAME FIELD isn't included in REQUIRED_FIELDS.
|
||||
if cls.USERNAME_FIELD in cls.REQUIRED_FIELDS:
|
||||
e.add(opts, 'The field named as the USERNAME_FIELD should not be included in REQUIRED_FIELDS on a swappable User model.')
|
||||
|
||||
# Check that the username field is unique
|
||||
if not opts.get_field(cls.USERNAME_FIELD).unique:
|
||||
e.add(opts, 'The USERNAME_FIELD must be unique. Add unique=True to the field parameters.')
|
||||
|
||||
# Store a list of column names which have already been used by other fields.
|
||||
used_column_names = []
|
||||
|
||||
# Model isn't swapped; do field-specific validation.
|
||||
for f in opts.local_fields:
|
||||
if f.name == 'id' and not f.primary_key and opts.pk.name == 'id':
|
||||
e.add(opts, '"%s": You can\'t use "id" as a field name, because each model automatically gets an "id" field if none of the fields have primary_key=True. You need to either remove/rename your "id" field or add primary_key=True to a field.' % f.name)
|
||||
if f.name.endswith('_'):
|
||||
e.add(opts, '"%s": Field names cannot end with underscores, because this would lead to ambiguous queryset filters.' % f.name)
|
||||
if (f.primary_key and f.null and
|
||||
not connection.features.interprets_empty_strings_as_nulls):
|
||||
# We cannot reliably check this for backends like Oracle which
|
||||
# consider NULL and '' to be equal (and thus set up
|
||||
# character-based fields a little differently).
|
||||
e.add(opts, '"%s": Primary key fields cannot have null=True.' % f.name)
|
||||
|
||||
# Column name validation.
|
||||
# Determine which column name this field wants to use.
|
||||
_, column_name = f.get_attname_column()
|
||||
|
||||
# Ensure the column name is not already in use.
|
||||
if column_name and column_name in used_column_names:
|
||||
e.add(opts, "Field '%s' has column name '%s' that is already used." % (f.name, column_name))
|
||||
else:
|
||||
used_column_names.append(column_name)
|
||||
|
||||
if isinstance(f, models.CharField):
|
||||
try:
|
||||
max_length = int(f.max_length)
|
||||
if max_length <= 0:
|
||||
e.add(opts, '"%s": CharFields require a "max_length" attribute that is a positive integer.' % f.name)
|
||||
except (ValueError, TypeError):
|
||||
e.add(opts, '"%s": CharFields require a "max_length" attribute that is a positive integer.' % f.name)
|
||||
if isinstance(f, models.DecimalField):
|
||||
decimalp_ok, mdigits_ok = False, False
|
||||
decimalp_msg = '"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.'
|
||||
try:
|
||||
decimal_places = int(f.decimal_places)
|
||||
if decimal_places < 0:
|
||||
e.add(opts, decimalp_msg % f.name)
|
||||
else:
|
||||
decimalp_ok = True
|
||||
except (ValueError, TypeError):
|
||||
e.add(opts, decimalp_msg % f.name)
|
||||
mdigits_msg = '"%s": DecimalFields require a "max_digits" attribute that is a positive integer.'
|
||||
try:
|
||||
max_digits = int(f.max_digits)
|
||||
if max_digits <= 0:
|
||||
e.add(opts, mdigits_msg % f.name)
|
||||
else:
|
||||
mdigits_ok = True
|
||||
except (ValueError, TypeError):
|
||||
e.add(opts, mdigits_msg % f.name)
|
||||
invalid_values_msg = '"%s": DecimalFields require a "max_digits" attribute value that is greater than or equal to the value of the "decimal_places" attribute.'
|
||||
if decimalp_ok and mdigits_ok:
|
||||
if decimal_places > max_digits:
|
||||
e.add(opts, invalid_values_msg % f.name)
|
||||
if isinstance(f, models.ImageField):
|
||||
try:
|
||||
from django.utils.image import Image # NOQA
|
||||
except ImportError:
|
||||
e.add(opts, '"%s": To use ImageFields, you need to install Pillow. Get it at https://pypi.python.org/pypi/Pillow.' % f.name)
|
||||
if isinstance(f, models.BooleanField) and getattr(f, 'null', False):
|
||||
e.add(opts, '"%s": BooleanFields do not accept null values. Use a NullBooleanField instead.' % f.name)
|
||||
if isinstance(f, models.FilePathField) and not (f.allow_files or f.allow_folders):
|
||||
e.add(opts, '"%s": FilePathFields must have either allow_files or allow_folders set to True.' % f.name)
|
||||
if isinstance(f, models.GenericIPAddressField) and not getattr(f, 'null', False) and getattr(f, 'blank', False):
|
||||
e.add(opts, '"%s": GenericIPAddressField can not accept blank values if null values are not allowed, as blank values are stored as null.' % f.name)
|
||||
if f.choices:
|
||||
if isinstance(f.choices, six.string_types) or not is_iterable(f.choices):
|
||||
e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name)
|
||||
else:
|
||||
for c in f.choices:
|
||||
if isinstance(c, six.string_types) or not is_iterable(c) or len(c) != 2:
|
||||
e.add(opts, '"%s": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples).' % f.name)
|
||||
if f.db_index not in (None, True, False):
|
||||
e.add(opts, '"%s": "db_index" should be either None, True or False.' % f.name)
|
||||
|
||||
# Perform any backend-specific field validation.
|
||||
connection.validation.validate_field(e, opts, f)
|
||||
|
||||
# Check if the on_delete behavior is sane
|
||||
if f.rel and hasattr(f.rel, 'on_delete'):
|
||||
if f.rel.on_delete == SET_NULL and not f.null:
|
||||
e.add(opts, "'%s' specifies on_delete=SET_NULL, but cannot be null." % f.name)
|
||||
elif f.rel.on_delete == SET_DEFAULT and not f.has_default():
|
||||
e.add(opts, "'%s' specifies on_delete=SET_DEFAULT, but has no default value." % f.name)
|
||||
|
||||
# Check to see if the related field will clash with any existing
|
||||
# fields, m2m fields, m2m related objects or related objects
|
||||
if f.rel:
|
||||
if f.rel.to not in apps.get_models():
|
||||
# If the related model is swapped, provide a hint;
|
||||
# otherwise, the model just hasn't been installed.
|
||||
if not isinstance(f.rel.to, six.string_types) and f.rel.to._meta.swapped:
|
||||
e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
|
||||
else:
|
||||
e.add(opts, "'%s' has a relation with model %s, which has either not been installed or is abstract." % (f.name, f.rel.to))
|
||||
# it is a string and we could not find the model it refers to
|
||||
# so skip the next section
|
||||
if isinstance(f.rel.to, six.string_types):
|
||||
continue
|
||||
|
||||
# Make sure the related field specified by a ForeignKey is unique
|
||||
if f.requires_unique_target:
|
||||
if len(f.foreign_related_fields) > 1:
|
||||
has_unique_field = False
|
||||
for rel_field in f.foreign_related_fields:
|
||||
has_unique_field = has_unique_field or rel_field.unique
|
||||
if not has_unique_field:
|
||||
e.add(opts, "Field combination '%s' under model '%s' must have a unique=True constraint" % (','.join(rel_field.name for rel_field in f.foreign_related_fields), f.rel.to.__name__))
|
||||
else:
|
||||
if not f.foreign_related_fields[0].unique:
|
||||
e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.foreign_related_fields[0].name, f.rel.to.__name__))
|
||||
|
||||
rel_opts = f.rel.to._meta
|
||||
rel_name = f.related.get_accessor_name()
|
||||
rel_query_name = f.related_query_name()
|
||||
if not f.rel.is_hidden():
|
||||
for r in rel_opts.fields:
|
||||
if r.name == rel_name:
|
||||
e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||
if r.name == rel_query_name:
|
||||
e.add(opts, "Reverse query name for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||
for r in rel_opts.local_many_to_many:
|
||||
if r.name == rel_name:
|
||||
e.add(opts, "Accessor for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||
if r.name == rel_query_name:
|
||||
e.add(opts, "Reverse query name for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||
for r in rel_opts.get_all_related_many_to_many_objects():
|
||||
if r.get_accessor_name() == rel_name:
|
||||
e.add(opts, "Accessor for field '%s' clashes with accessor for field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, r.model._meta.object_name, r.field.name, f.name))
|
||||
if r.get_accessor_name() == rel_query_name:
|
||||
e.add(opts, "Reverse query name for field '%s' clashes with accessor for field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, r.model._meta.object_name, r.field.name, f.name))
|
||||
for r in rel_opts.get_all_related_objects():
|
||||
if r.field is not f:
|
||||
if r.get_accessor_name() == rel_name:
|
||||
e.add(opts, "Accessor for field '%s' clashes with accessor for field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, r.model._meta.object_name, r.field.name, f.name))
|
||||
if r.get_accessor_name() == rel_query_name:
|
||||
e.add(opts, "Reverse query name for field '%s' clashes with accessor for field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, r.model._meta.object_name, r.field.name, f.name))
|
||||
|
||||
seen_intermediary_signatures = []
|
||||
for i, f in enumerate(opts.local_many_to_many):
|
||||
# Check to see if the related m2m field will clash with any
|
||||
# existing fields, m2m fields, m2m related objects or related
|
||||
# objects
|
||||
if f.rel.to not in apps.get_models():
|
||||
# If the related model is swapped, provide a hint;
|
||||
# otherwise, the model just hasn't been installed.
|
||||
if not isinstance(f.rel.to, six.string_types) and f.rel.to._meta.swapped:
|
||||
e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
|
||||
else:
|
||||
e.add(opts, "'%s' has an m2m relation with model %s, which has either not been installed or is abstract." % (f.name, f.rel.to))
|
||||
|
||||
# it is a string and we could not find the model it refers to
|
||||
# so skip the next section
|
||||
if isinstance(f.rel.to, six.string_types):
|
||||
continue
|
||||
|
||||
# Check that the field is not set to unique. ManyToManyFields do not support unique.
|
||||
if f.unique:
|
||||
e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name)
|
||||
|
||||
if f.rel.through is not None and not isinstance(f.rel.through, six.string_types):
|
||||
from_model, to_model = cls, f.rel.to
|
||||
if from_model == to_model and f.rel.symmetrical and not f.rel.through._meta.auto_created:
|
||||
e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.")
|
||||
seen_from, seen_to, seen_self = False, False, 0
|
||||
for inter_field in f.rel.through._meta.fields:
|
||||
rel_to = getattr(inter_field.rel, 'to', None)
|
||||
if from_model == to_model: # relation to self
|
||||
if rel_to == from_model:
|
||||
seen_self += 1
|
||||
if seen_self > 2:
|
||||
e.add(opts, "Intermediary model %s has more than "
|
||||
"two foreign keys to %s, which is ambiguous "
|
||||
"and is not permitted." % (
|
||||
f.rel.through._meta.object_name,
|
||||
from_model._meta.object_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
if rel_to == from_model:
|
||||
if seen_from:
|
||||
e.add(opts, "Intermediary model %s has more "
|
||||
"than one foreign key to %s, which is "
|
||||
"ambiguous and is not permitted." % (
|
||||
f.rel.through._meta.object_name,
|
||||
from_model._meta.object_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
seen_from = True
|
||||
elif rel_to == to_model:
|
||||
if seen_to:
|
||||
e.add(opts, "Intermediary model %s has more "
|
||||
"than one foreign key to %s, which is "
|
||||
"ambiguous and is not permitted." % (
|
||||
f.rel.through._meta.object_name,
|
||||
rel_to._meta.object_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
seen_to = True
|
||||
if f.rel.through not in apps.get_models(include_auto_created=True):
|
||||
e.add(opts, "'%s' specifies an m2m relation through model "
|
||||
"%s, which has not been installed." % (f.name, f.rel.through))
|
||||
signature = (f.rel.to, cls, f.rel.through)
|
||||
if signature in seen_intermediary_signatures:
|
||||
e.add(opts, "The model %s has two manually-defined m2m "
|
||||
"relations through the model %s, which is not "
|
||||
"permitted. Please consider using an extra field on "
|
||||
"your intermediary model instead." % (
|
||||
cls._meta.object_name,
|
||||
f.rel.through._meta.object_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
seen_intermediary_signatures.append(signature)
|
||||
if not f.rel.through._meta.auto_created:
|
||||
seen_related_fk, seen_this_fk = False, False
|
||||
for field in f.rel.through._meta.fields:
|
||||
if field.rel:
|
||||
if not seen_related_fk and field.rel.to == f.rel.to:
|
||||
seen_related_fk = True
|
||||
elif field.rel.to == cls:
|
||||
seen_this_fk = True
|
||||
if not seen_related_fk or not seen_this_fk:
|
||||
e.add(opts, "'%s' is a manually-defined m2m relation "
|
||||
"through model %s, which does not have foreign keys "
|
||||
"to %s and %s" % (
|
||||
f.name, f.rel.through._meta.object_name,
|
||||
f.rel.to._meta.object_name, cls._meta.object_name
|
||||
)
|
||||
)
|
||||
elif isinstance(f.rel.through, six.string_types):
|
||||
e.add(opts, "'%s' specifies an m2m relation through model %s, "
|
||||
"which has not been installed" % (f.name, f.rel.through))
|
||||
|
||||
rel_opts = f.rel.to._meta
|
||||
rel_name = f.related.get_accessor_name()
|
||||
rel_query_name = f.related_query_name()
|
||||
# If rel_name is none, there is no reverse accessor (this only
|
||||
# occurs for symmetrical m2m relations to self). If this is the
|
||||
# case, there are no clashes to check for this field, as there are
|
||||
# no reverse descriptors for this field.
|
||||
if not f.rel.is_hidden():
|
||||
for r in rel_opts.fields:
|
||||
if r.name == rel_name:
|
||||
e.add(opts, "Accessor for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||
if r.name == rel_query_name:
|
||||
e.add(opts, "Reverse query name for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||
for r in rel_opts.local_many_to_many:
|
||||
if r.name == rel_name:
|
||||
e.add(opts, "Accessor for m2m field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||
if r.name == rel_query_name:
|
||||
e.add(opts, "Reverse query name for m2m field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||
for r in rel_opts.get_all_related_many_to_many_objects():
|
||||
if r.field is not f:
|
||||
if r.get_accessor_name() == rel_name:
|
||||
e.add(opts, "Accessor for m2m field '%s' clashes with accessor for m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, r.model._meta.object_name, r.field.name, f.name))
|
||||
if r.get_accessor_name() == rel_query_name:
|
||||
e.add(opts, "Reverse query name for m2m field '%s' clashes with accessor for m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, r.model._meta.object_name, r.field.name, f.name))
|
||||
for r in rel_opts.get_all_related_objects():
|
||||
if r.get_accessor_name() == rel_name:
|
||||
e.add(opts, "Accessor for m2m field '%s' clashes with accessor for field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, r.model._meta.object_name, r.field.name, f.name))
|
||||
if r.get_accessor_name() == rel_query_name:
|
||||
e.add(opts, "Reverse query name for m2m field '%s' clashes with accessor for field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, r.model._meta.object_name, r.field.name, f.name))
|
||||
|
||||
# Check ordering attribute.
|
||||
if opts.ordering:
|
||||
for field_name in opts.ordering:
|
||||
if field_name == '?':
|
||||
continue
|
||||
if field_name.startswith('-'):
|
||||
field_name = field_name[1:]
|
||||
if opts.order_with_respect_to and field_name == '_order':
|
||||
continue
|
||||
# Skip ordering in the format field1__field2 (FIXME: checking
|
||||
# this format would be nice, but it's a little fiddly).
|
||||
if '__' in field_name:
|
||||
continue
|
||||
# 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.
|
||||
if field_name == 'pk':
|
||||
continue
|
||||
try:
|
||||
opts.get_field(field_name, many_to_many=False)
|
||||
except models.FieldDoesNotExist:
|
||||
e.add(opts, '"ordering" refers to "%s", a field that doesn\'t exist.' % field_name)
|
||||
|
||||
# Check unique_together.
|
||||
for ut in opts.unique_together:
|
||||
validate_local_fields(e, opts, "unique_together", ut)
|
||||
if not isinstance(opts.index_together, collections.Sequence):
|
||||
e.add(opts, '"index_together" must a sequence')
|
||||
else:
|
||||
for it in opts.index_together:
|
||||
validate_local_fields(e, opts, "index_together", it)
|
||||
|
||||
validate_model_signals(e)
|
||||
|
||||
return len(e.errors)
|
||||
|
||||
|
||||
def validate_local_fields(e, opts, field_name, fields):
|
||||
from django.db import models
|
||||
|
||||
if not isinstance(fields, collections.Sequence):
|
||||
e.add(opts, 'all %s elements must be sequences' % field_name)
|
||||
else:
|
||||
for field in fields:
|
||||
try:
|
||||
f = opts.get_field(field, many_to_many=True)
|
||||
except models.FieldDoesNotExist:
|
||||
e.add(opts, '"%s" refers to %s, a field that doesn\'t exist.' % (field_name, field))
|
||||
else:
|
||||
if isinstance(f.rel, models.ManyToManyRel):
|
||||
e.add(opts, '"%s" refers to %s. ManyToManyFields are not supported in %s.' % (field_name, f.name, field_name))
|
||||
if f not in opts.local_fields:
|
||||
e.add(opts, '"%s" refers to %s. This is not in the same model as the %s statement.' % (field_name, f.name, field_name))
|
||||
|
||||
|
||||
def validate_model_signals(e):
|
||||
"""Ensure lazily referenced model signals senders are installed."""
|
||||
from django.db import models
|
||||
|
||||
for name in dir(models.signals):
|
||||
obj = getattr(models.signals, name)
|
||||
if isinstance(obj, models.signals.ModelSignal):
|
||||
for reference, receivers in obj.unresolved_references.items():
|
||||
for receiver, _, _ in receivers:
|
||||
# The receiver is either a function or an instance of class
|
||||
# defining a `__call__` method.
|
||||
if isinstance(receiver, types.FunctionType):
|
||||
description = "The `%s` function" % receiver.__name__
|
||||
else:
|
||||
description = "An instance of the `%s` class" % receiver.__class__.__name__
|
||||
e.add(
|
||||
receiver.__module__,
|
||||
"%s was connected to the `%s` signal "
|
||||
"with a lazy reference to the '%s' sender, "
|
||||
"which has not been installed." % (
|
||||
description, name, '.'.join(reference)
|
||||
)
|
||||
)
|
@ -10,6 +10,7 @@ from contextlib import contextmanager
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import checks
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
from django.db.backends.signals import connection_created
|
||||
from django.db.backends import utils
|
||||
@ -1400,5 +1401,30 @@ class BaseDatabaseValidation(object):
|
||||
self.connection = connection
|
||||
|
||||
def validate_field(self, errors, opts, f):
|
||||
"By default, there is no backend-specific validation"
|
||||
"""
|
||||
By default, there is no backend-specific validation.
|
||||
|
||||
This method has been deprecated by the new checks framework. New
|
||||
backends should implement check_field instead.
|
||||
"""
|
||||
# This is deliberately commented out. It exists as a marker to
|
||||
# remind us to remove this method, and the check_field() shim,
|
||||
# when the time comes.
|
||||
# warnings.warn('"validate_field" has been deprecated", PendingDeprecationWarning)
|
||||
pass
|
||||
|
||||
def check_field(self, field, **kwargs):
|
||||
class ErrorList(list):
|
||||
"""A dummy list class that emulates API used by the older
|
||||
validate_field() method. When validate_field() is fully
|
||||
deprecated, this dummy can be removed too.
|
||||
"""
|
||||
def add(self, opts, error_message):
|
||||
self.append(checks.Error(error_message, hint=None, obj=field))
|
||||
|
||||
errors = ErrorList()
|
||||
# Some tests create fields in isolation -- the fields are not attached
|
||||
# to any model, so they have no `model` attribute.
|
||||
opts = field.model._meta if hasattr(field, 'model') else None
|
||||
self.validate_field(errors, field, opts)
|
||||
return list(errors)
|
||||
|
@ -1,17 +1,34 @@
|
||||
from django.core import checks
|
||||
from django.db.backends import BaseDatabaseValidation
|
||||
|
||||
|
||||
class DatabaseValidation(BaseDatabaseValidation):
|
||||
def validate_field(self, errors, opts, f):
|
||||
def check_field(self, field, **kwargs):
|
||||
"""
|
||||
MySQL has the following field length restriction:
|
||||
No character (varchar) fields can have a length exceeding 255
|
||||
characters if they have a unique index on them.
|
||||
"""
|
||||
from django.db import models
|
||||
varchar_fields = (models.CharField, models.CommaSeparatedIntegerField,
|
||||
models.SlugField)
|
||||
if (isinstance(f, varchar_fields) and f.unique
|
||||
and (f.max_length is None or int(f.max_length) > 255)):
|
||||
msg = '"%(name)s": %(cls)s cannot have a "max_length" greater than 255 when using "unique=True".'
|
||||
errors.add(opts, msg % {'name': f.name, 'cls': f.__class__.__name__})
|
||||
from django.db import connection
|
||||
|
||||
errors = super(DatabaseValidation, self).check_field(field, **kwargs)
|
||||
try:
|
||||
field_type = field.db_type(connection)
|
||||
except AttributeError:
|
||||
# If the field is a relative field and the target model is
|
||||
# missing, then field.rel.to is not a model and doesn't have
|
||||
# `_meta` attribute.
|
||||
field_type = ''
|
||||
|
||||
if (field_type.startswith('varchar') and field.unique
|
||||
and (field.max_length is None or int(field.max_length) > 255)):
|
||||
errors.append(
|
||||
checks.Error(
|
||||
('Under mysql backend, the field cannot have a "max_length" '
|
||||
'greated than 255 when it is unique.'),
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E047',
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
@ -9,6 +9,7 @@ from django.apps import apps
|
||||
from django.apps.base import MODELS_MODULE_NAME
|
||||
import django.db.models.manager # NOQA: Imported to register signal handler.
|
||||
from django.conf import settings
|
||||
from django.core import checks
|
||||
from django.core.exceptions import (ObjectDoesNotExist,
|
||||
MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS)
|
||||
from django.db.models.fields import AutoField, FieldDoesNotExist
|
||||
@ -1030,6 +1031,308 @@ class Model(six.with_metaclass(ModelBase)):
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
@classmethod
|
||||
def check(cls, **kwargs):
|
||||
errors = []
|
||||
errors.extend(cls._check_swappable())
|
||||
errors.extend(cls._check_managers(**kwargs))
|
||||
if not cls._meta.swapped:
|
||||
errors.extend(cls._check_fields(**kwargs))
|
||||
errors.extend(cls._check_m2m_through_same_relationship())
|
||||
errors.extend(cls._check_id_field())
|
||||
errors.extend(cls._check_column_name_clashes())
|
||||
errors.extend(cls._check_index_together())
|
||||
errors.extend(cls._check_unique_together())
|
||||
errors.extend(cls._check_ordering())
|
||||
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def _check_swappable(cls):
|
||||
""" Check if the swapped model exists. """
|
||||
|
||||
errors = []
|
||||
if cls._meta.swapped:
|
||||
try:
|
||||
app_label, model_name = cls._meta.swapped.split('.')
|
||||
except ValueError:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'"%s" is not of the form "app_label.app_name".' % cls._meta.swappable,
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='E002',
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
apps.get_model(app_label, model_name)
|
||||
except LookupError:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
('The model has been swapped out for %s.%s '
|
||||
'which has not been installed or is abstract.') % (
|
||||
app_label, model_name
|
||||
),
|
||||
hint=('Ensure that you did not misspell the model '
|
||||
'name and the app name as well as the model '
|
||||
'is not abstract. Does your INSTALLED_APPS '
|
||||
'setting contain the "%s" app?') % app_label,
|
||||
obj=cls,
|
||||
id='E003',
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def _check_managers(cls, **kwargs):
|
||||
""" Perform all manager checks. """
|
||||
|
||||
errors = []
|
||||
managers = cls._meta.concrete_managers + cls._meta.abstract_managers
|
||||
for (_, _, manager) in managers:
|
||||
errors.extend(manager.check(**kwargs))
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def _check_fields(cls, **kwargs):
|
||||
""" Perform all field checks. """
|
||||
|
||||
errors = []
|
||||
for field in cls._meta.local_fields:
|
||||
errors.extend(field.check(**kwargs))
|
||||
for field in cls._meta.local_many_to_many:
|
||||
errors.extend(field.check(from_model=cls, **kwargs))
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def _check_m2m_through_same_relationship(cls):
|
||||
""" Check if no relationship model is used by more than one m2m field.
|
||||
"""
|
||||
|
||||
errors = []
|
||||
seen_intermediary_signatures = []
|
||||
|
||||
fields = cls._meta.local_many_to_many
|
||||
|
||||
# Skip when the target model wasn't found.
|
||||
fields = (f for f in fields if isinstance(f.rel.to, ModelBase))
|
||||
|
||||
# Skip when the relationship model wasn't found.
|
||||
fields = (f for f in fields if isinstance(f.rel.through, ModelBase))
|
||||
|
||||
for f in fields:
|
||||
signature = (f.rel.to, cls, f.rel.through)
|
||||
if signature in seen_intermediary_signatures:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
('The model has two many-to-many relations through '
|
||||
'the intermediary %s model, which is not permitted.') % (
|
||||
f.rel.through._meta.object_name
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='E004',
|
||||
)
|
||||
)
|
||||
else:
|
||||
seen_intermediary_signatures.append(signature)
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def _check_id_field(cls):
|
||||
""" Check if `id` field is a primary key. """
|
||||
|
||||
fields = list(f for f in cls._meta.local_fields
|
||||
if f.name == 'id' and f != cls._meta.pk)
|
||||
# fields is empty or consists of the invalid "id" field
|
||||
if fields and not fields[0].primary_key and cls._meta.pk.name == 'id':
|
||||
return [
|
||||
checks.Error(
|
||||
('You cannot use "id" as a field name, because each model '
|
||||
'automatically gets an "id" field if none '
|
||||
'of the fields have primary_key=True.'),
|
||||
hint=('Remove or rename "id" field '
|
||||
'or add primary_key=True to a field.'),
|
||||
obj=cls,
|
||||
id='E005',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def _check_column_name_clashes(cls):
|
||||
# Store a list of column names which have already been used by other fields.
|
||||
used_column_names = []
|
||||
errors = []
|
||||
|
||||
for f in cls._meta.local_fields:
|
||||
_, column_name = f.get_attname_column()
|
||||
|
||||
# Ensure the column name is not already in use.
|
||||
if column_name and column_name in used_column_names:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'Field "%s" has column name "%s" that is already used.' % (f.name, column_name),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
)
|
||||
)
|
||||
else:
|
||||
used_column_names.append(column_name)
|
||||
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def _check_index_together(cls):
|
||||
""" Check the value of "index_together" option. """
|
||||
if not isinstance(cls._meta.index_together, (tuple, list)):
|
||||
return [
|
||||
checks.Error(
|
||||
'"index_together" must be a list or tuple.',
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='E006',
|
||||
)
|
||||
]
|
||||
|
||||
elif any(not isinstance(fields, (tuple, list))
|
||||
for fields in cls._meta.index_together):
|
||||
return [
|
||||
checks.Error(
|
||||
'All "index_together" elements must be lists or tuples.',
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='E007',
|
||||
)
|
||||
]
|
||||
|
||||
else:
|
||||
errors = []
|
||||
for fields in cls._meta.index_together:
|
||||
errors.extend(cls._check_local_fields(fields, "index_together"))
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def _check_unique_together(cls):
|
||||
""" Check the value of "unique_together" option. """
|
||||
if not isinstance(cls._meta.unique_together, (tuple, list)):
|
||||
return [
|
||||
checks.Error(
|
||||
'"unique_together" must be a list or tuple.',
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='E008',
|
||||
)
|
||||
]
|
||||
|
||||
elif any(not isinstance(fields, (tuple, list))
|
||||
for fields in cls._meta.unique_together):
|
||||
return [
|
||||
checks.Error(
|
||||
'All "unique_together" elements must be lists or tuples.',
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='E009',
|
||||
)
|
||||
]
|
||||
|
||||
else:
|
||||
errors = []
|
||||
for fields in cls._meta.unique_together:
|
||||
errors.extend(cls._check_local_fields(fields, "unique_together"))
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def _check_local_fields(cls, fields, option):
|
||||
from django.db import models
|
||||
|
||||
errors = []
|
||||
for field_name in fields:
|
||||
try:
|
||||
field = cls._meta.get_field(field_name,
|
||||
many_to_many=True)
|
||||
except models.FieldDoesNotExist:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'"%s" points to a missing field named "%s".' % (option, field_name),
|
||||
hint='Ensure that you did not misspell the field name.',
|
||||
obj=cls,
|
||||
id='E010',
|
||||
)
|
||||
)
|
||||
else:
|
||||
if isinstance(field.rel, models.ManyToManyRel):
|
||||
errors.append(
|
||||
checks.Error(
|
||||
('"%s" refers to a m2m "%s" field, but '
|
||||
'ManyToManyFields are not supported in "%s".') % (
|
||||
option, field_name, option
|
||||
),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='E011',
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
||||
@classmethod
|
||||
def _check_ordering(cls):
|
||||
""" Check "ordering" option -- is it a list of lists and do all fields
|
||||
exist? """
|
||||
|
||||
from django.db.models import FieldDoesNotExist
|
||||
|
||||
if not cls._meta.ordering:
|
||||
return []
|
||||
|
||||
if not isinstance(cls._meta.ordering, (list, tuple)):
|
||||
return [
|
||||
checks.Error(
|
||||
('"ordering" must be a tuple or list '
|
||||
'(even if you want to order by only one field).'),
|
||||
hint=None,
|
||||
obj=cls,
|
||||
id='E012',
|
||||
)
|
||||
]
|
||||
|
||||
errors = []
|
||||
|
||||
fields = cls._meta.ordering
|
||||
|
||||
# Skip '?' fields.
|
||||
fields = (f for f in fields if f != '?')
|
||||
|
||||
# Convert "-field" to "field".
|
||||
fields = ((f[1:] if f.startswith('-') else f) for f in fields)
|
||||
|
||||
fields = (f for f in fields if
|
||||
f != '_order' or not cls._meta.order_with_respect_to)
|
||||
|
||||
# Skip ordering in the format field1__field2 (FIXME: checking
|
||||
# this format would be nice, but it's a little fiddly).
|
||||
fields = (f for f in fields if '__' not in f)
|
||||
|
||||
# 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')
|
||||
|
||||
for field_name in fields:
|
||||
try:
|
||||
cls._meta.get_field(field_name, many_to_many=False)
|
||||
except FieldDoesNotExist:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'"ordering" pointing to a missing "%s" field.' % field_name,
|
||||
hint='Ensure that you did not misspell the field name.',
|
||||
obj=cls,
|
||||
id='E013',
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
############################################
|
||||
# HELPER FUNCTIONS (CURRIED MODEL METHODS) #
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
@ -15,16 +16,18 @@ from django.db.models.lookups import default_lookups, RegisterLookupMixin
|
||||
from django.db.models.query_utils import QueryWrapper
|
||||
from django.conf import settings
|
||||
from django import forms
|
||||
from django.core import exceptions, validators
|
||||
from django.core import exceptions, validators, checks
|
||||
from django.utils.datastructures import DictWrapper
|
||||
from django.utils.dateparse import parse_date, parse_datetime, parse_time
|
||||
from django.utils.functional import curry, total_ordering, Promise
|
||||
from django.utils.text import capfirst
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import smart_text, force_text, force_bytes
|
||||
from django.utils.encoding import (smart_text, force_text, force_bytes,
|
||||
python_2_unicode_compatible)
|
||||
from django.utils.ipv6 import clean_ipv6_address
|
||||
from django.utils import six
|
||||
from django.utils.itercompat import is_iterable
|
||||
|
||||
# Avoid "TypeError: Item in ``from list'' not a string" -- unicode_literals
|
||||
# makes these strings unicode
|
||||
@ -81,6 +84,7 @@ def _empty(of_cls):
|
||||
|
||||
|
||||
@total_ordering
|
||||
@python_2_unicode_compatible
|
||||
class Field(RegisterLookupMixin):
|
||||
"""Base class for all field types"""
|
||||
|
||||
@ -159,6 +163,111 @@ class Field(RegisterLookupMixin):
|
||||
self._error_messages = error_messages # Store for deconstruction later
|
||||
self.error_messages = messages
|
||||
|
||||
def __str__(self):
|
||||
""" Return "app_label.model_label.field_name". """
|
||||
model = self.model
|
||||
app = model._meta.app_label
|
||||
return '%s.%s.%s' % (app, model._meta.object_name, self.name)
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Displays the module, class and name of the field.
|
||||
"""
|
||||
path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
|
||||
name = getattr(self, 'name', None)
|
||||
if name is not None:
|
||||
return '<%s: %s>' % (path, name)
|
||||
return '<%s>' % path
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = []
|
||||
errors.extend(self._check_field_name())
|
||||
errors.extend(self._check_choices())
|
||||
errors.extend(self._check_db_index())
|
||||
errors.extend(self._check_null_allowed_for_primary_keys())
|
||||
errors.extend(self._check_backend_specific_checks(**kwargs))
|
||||
return errors
|
||||
|
||||
def _check_field_name(self):
|
||||
""" Check if field name is valid (i. e. not ending with an underscore).
|
||||
"""
|
||||
if self.name.endswith('_'):
|
||||
return [
|
||||
checks.Error(
|
||||
'Field names must not end with underscores.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E001',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_choices(self):
|
||||
if self.choices:
|
||||
if (isinstance(self.choices, six.string_types) or
|
||||
not is_iterable(self.choices)):
|
||||
return [
|
||||
checks.Error(
|
||||
'"choices" must be an iterable (e.g., a list or tuple).',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E033',
|
||||
)
|
||||
]
|
||||
elif any(isinstance(choice, six.string_types) or
|
||||
not is_iterable(choice) or len(choice) != 2
|
||||
for choice in self.choices):
|
||||
return [
|
||||
checks.Error(
|
||||
('All "choices" elements must be a tuple of two '
|
||||
'elements (the first one is the actual value '
|
||||
'to be stored and the second element is '
|
||||
'the human-readable name).'),
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E034',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_db_index(self):
|
||||
if self.db_index not in (None, True, False):
|
||||
return [
|
||||
checks.Error(
|
||||
'"db_index" must be either None, True or False.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E035',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_null_allowed_for_primary_keys(self):
|
||||
if (self.primary_key and self.null and
|
||||
not connection.features.interprets_empty_strings_as_nulls):
|
||||
# We cannot reliably check this for backends like Oracle which
|
||||
# consider NULL and '' to be equal (and thus set up
|
||||
# character-based fields a little differently).
|
||||
return [
|
||||
checks.Error(
|
||||
'Primary keys must not have null=True.',
|
||||
hint=('Set null=False on the field or '
|
||||
'remove primary_key=True argument.'),
|
||||
obj=self,
|
||||
id='E036',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_backend_specific_checks(self, **kwargs):
|
||||
return connection.validation.check_field(self, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
"""
|
||||
Returns enough information to recreate the field as a 4-tuple:
|
||||
@ -708,16 +817,6 @@ class Field(RegisterLookupMixin):
|
||||
"""
|
||||
return getattr(obj, self.attname)
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Displays the module, class and name of the field.
|
||||
"""
|
||||
path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
|
||||
name = getattr(self, 'name', None)
|
||||
if name is not None:
|
||||
return '<%s: %s>' % (path, name)
|
||||
return '<%s>' % path
|
||||
|
||||
|
||||
class AutoField(Field):
|
||||
description = _("Integer")
|
||||
@ -728,11 +827,27 @@ class AutoField(Field):
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
assert kwargs.get('primary_key', False) is True, \
|
||||
"%ss must have primary_key=True." % self.__class__.__name__
|
||||
kwargs['blank'] = True
|
||||
super(AutoField, self).__init__(*args, **kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(AutoField, self).check(**kwargs)
|
||||
errors.extend(self._check_primary_key())
|
||||
return errors
|
||||
|
||||
def _check_primary_key(self):
|
||||
if not self.primary_key:
|
||||
return [
|
||||
checks.Error(
|
||||
'The field must have primary_key=True, because it is an AutoField.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E048',
|
||||
),
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(AutoField, self).deconstruct()
|
||||
del kwargs['blank']
|
||||
@ -791,6 +906,24 @@ class BooleanField(Field):
|
||||
kwargs['blank'] = True
|
||||
super(BooleanField, self).__init__(*args, **kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(BooleanField, self).check(**kwargs)
|
||||
errors.extend(self._check_null(**kwargs))
|
||||
return errors
|
||||
|
||||
def _check_null(self, **kwargs):
|
||||
if getattr(self, 'null', False):
|
||||
return [
|
||||
checks.Error(
|
||||
'BooleanFields do not acceps null values.',
|
||||
hint='Use a NullBooleanField instead.',
|
||||
obj=self,
|
||||
id='E037',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(BooleanField, self).deconstruct()
|
||||
del kwargs['blank']
|
||||
@ -849,6 +982,37 @@ class CharField(Field):
|
||||
super(CharField, self).__init__(*args, **kwargs)
|
||||
self.validators.append(validators.MaxLengthValidator(self.max_length))
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(CharField, self).check(**kwargs)
|
||||
errors.extend(self._check_max_length_attibute(**kwargs))
|
||||
return errors
|
||||
|
||||
def _check_max_length_attibute(self, **kwargs):
|
||||
try:
|
||||
max_length = int(self.max_length)
|
||||
if max_length <= 0:
|
||||
raise ValueError()
|
||||
except TypeError:
|
||||
return [
|
||||
checks.Error(
|
||||
'The field must have "max_length" attribute.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E038',
|
||||
)
|
||||
]
|
||||
except ValueError:
|
||||
return [
|
||||
checks.Error(
|
||||
'"max_length" must be a positive integer.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E039',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_internal_type(self):
|
||||
return "CharField"
|
||||
|
||||
@ -1114,6 +1278,77 @@ class DecimalField(Field):
|
||||
self.max_digits, self.decimal_places = max_digits, decimal_places
|
||||
super(DecimalField, self).__init__(verbose_name, name, **kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(DecimalField, self).check(**kwargs)
|
||||
errors.extend(self._check_decimal_places_and_max_digits(**kwargs))
|
||||
return errors
|
||||
|
||||
def _check_decimal_places_and_max_digits(self, **kwargs):
|
||||
errors = self.__check_decimal_places()
|
||||
errors += self.__check_max_digits()
|
||||
if not errors and int(self.decimal_places) > int(self.max_digits):
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'"max_digits" must be greater or equal to "decimal_places".',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E040',
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
||||
def __check_decimal_places(self):
|
||||
try:
|
||||
decimal_places = int(self.decimal_places)
|
||||
if decimal_places < 0:
|
||||
raise ValueError()
|
||||
except TypeError:
|
||||
return [
|
||||
checks.Error(
|
||||
'The field requires a "decimal_places" attribute.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E041',
|
||||
)
|
||||
]
|
||||
except ValueError:
|
||||
return [
|
||||
checks.Error(
|
||||
'"decimal_places" attribute must be a non-negative integer.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E042',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def __check_max_digits(self):
|
||||
try:
|
||||
max_digits = int(self.max_digits)
|
||||
if max_digits <= 0:
|
||||
raise ValueError()
|
||||
except TypeError:
|
||||
return [
|
||||
checks.Error(
|
||||
'The field requires a "max_digits" attribute.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E043',
|
||||
)
|
||||
]
|
||||
except ValueError:
|
||||
return [
|
||||
checks.Error(
|
||||
'"max_digits" attribute must be a positive integer.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E044',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(DecimalField, self).deconstruct()
|
||||
if self.max_digits:
|
||||
@ -1212,6 +1447,23 @@ class FilePathField(Field):
|
||||
kwargs['max_length'] = kwargs.get('max_length', 100)
|
||||
super(FilePathField, self).__init__(verbose_name, name, **kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(FilePathField, self).check(**kwargs)
|
||||
errors.extend(self._check_allowing_files_or_folders(**kwargs))
|
||||
return errors
|
||||
|
||||
def _check_allowing_files_or_folders(self, **kwargs):
|
||||
if not self.allow_files and not self.allow_folders:
|
||||
return [
|
||||
checks.Error(
|
||||
'The field must have either "allow_files" or "allow_folders" set to True.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E045',
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(FilePathField, self).deconstruct()
|
||||
if self.path != '':
|
||||
@ -1373,6 +1625,24 @@ class GenericIPAddressField(Field):
|
||||
super(GenericIPAddressField, self).__init__(verbose_name, name, *args,
|
||||
**kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(GenericIPAddressField, self).check(**kwargs)
|
||||
errors.extend(self._check_blank_and_null_values(**kwargs))
|
||||
return errors
|
||||
|
||||
def _check_blank_and_null_values(self, **kwargs):
|
||||
if not getattr(self, 'null', False) and getattr(self, 'blank', False):
|
||||
return [
|
||||
checks.Error(
|
||||
('The field cannot accept blank values if null values '
|
||||
'are not allowed, as blank values are stored as null.'),
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E046',
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(GenericIPAddressField, self).deconstruct()
|
||||
if self.unpack_ipv4 is not False:
|
||||
|
@ -3,6 +3,8 @@ import os
|
||||
|
||||
from django import forms
|
||||
from django.db.models.fields import Field
|
||||
from django.core import checks
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.files.base import File
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.images import ImageFile
|
||||
@ -223,9 +225,8 @@ class FileField(Field):
|
||||
description = _("File")
|
||||
|
||||
def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs):
|
||||
for arg in ('primary_key', 'unique'):
|
||||
if arg in kwargs:
|
||||
raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__))
|
||||
self._primary_key_set_explicitly = 'primary_key' in kwargs
|
||||
self._unique_set_explicitly = 'unique' in kwargs
|
||||
|
||||
self.storage = storage or default_storage
|
||||
self.upload_to = upload_to
|
||||
@ -235,6 +236,52 @@ class FileField(Field):
|
||||
kwargs['max_length'] = kwargs.get('max_length', 100)
|
||||
super(FileField, self).__init__(verbose_name, name, **kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(FileField, self).check(**kwargs)
|
||||
#errors.extend(self._check_upload_to())
|
||||
errors.extend(self._check_unique())
|
||||
errors.extend(self._check_primary_key())
|
||||
return errors
|
||||
|
||||
def _check_upload_to(self):
|
||||
if not self.upload_to:
|
||||
return [
|
||||
checks.Error(
|
||||
'The field requires an "upload_to" attribute.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E031',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_unique(self):
|
||||
if self._unique_set_explicitly:
|
||||
return [
|
||||
checks.Error(
|
||||
'"unique" is not a valid argument for %s.' % self.__class__.__name__,
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E049',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _check_primary_key(self):
|
||||
if self._primary_key_set_explicitly:
|
||||
return [
|
||||
checks.Error(
|
||||
'"primary_key" is not a valid argument for %s.' % self.__class__.__name__,
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E050',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(FileField, self).deconstruct()
|
||||
if kwargs.get("max_length", None) != 100:
|
||||
@ -348,6 +395,27 @@ class ImageField(FileField):
|
||||
self.width_field, self.height_field = width_field, height_field
|
||||
super(ImageField, self).__init__(verbose_name, name, **kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(ImageField, self).check(**kwargs)
|
||||
errors.extend(self._check_image_library_installed())
|
||||
return errors
|
||||
|
||||
def _check_image_library_installed(self):
|
||||
try:
|
||||
from django.utils.image import Image # NOQA
|
||||
except ImproperlyConfigured:
|
||||
return [
|
||||
checks.Error(
|
||||
'To use ImageFields, Pillow must be installed.',
|
||||
hint=('Get Pillow at https://pypi.python.org/pypi/Pillow '
|
||||
'or run command "pip install pillow".'),
|
||||
obj=self,
|
||||
id='E032',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(ImageField, self).deconstruct()
|
||||
if self.width_field:
|
||||
|
@ -1,14 +1,16 @@
|
||||
from operator import attrgetter
|
||||
|
||||
from django.apps import apps
|
||||
from django.core import checks
|
||||
from django.db import connection, connections, router, transaction
|
||||
from django.db.backends import utils
|
||||
from django.db.models import signals, Q
|
||||
from django.db.models.deletion import SET_NULL, SET_DEFAULT, CASCADE
|
||||
from django.db.models.fields import (AutoField, Field, IntegerField,
|
||||
PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist)
|
||||
from django.db.models.lookups import IsNull
|
||||
from django.db.models.related import RelatedObject, PathInfo
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.models.deletion import CASCADE
|
||||
from django.db.models.sql.datastructures import Col
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils import six
|
||||
@ -93,6 +95,156 @@ signals.class_prepared.connect(do_pending_lookups)
|
||||
|
||||
|
||||
class RelatedField(Field):
|
||||
def check(self, **kwargs):
|
||||
errors = super(RelatedField, self).check(**kwargs)
|
||||
errors.extend(self._check_relation_model_exists())
|
||||
errors.extend(self._check_referencing_to_swapped_model())
|
||||
errors.extend(self._check_clashes())
|
||||
return errors
|
||||
|
||||
def _check_relation_model_exists(self):
|
||||
rel_is_missing = self.rel.to not in apps.get_models()
|
||||
rel_is_string = isinstance(self.rel.to, six.string_types)
|
||||
model_name = self.rel.to if rel_is_string else self.rel.to._meta.object_name
|
||||
if rel_is_missing and (rel_is_string or not self.rel.to._meta.swapped):
|
||||
return [
|
||||
checks.Error(
|
||||
('The field has a relation with model %s, which '
|
||||
'has either not been installed or is abstract.') % model_name,
|
||||
hint=('Ensure that you did not misspell the model name and '
|
||||
'the model is not abstract. Does your INSTALLED_APPS '
|
||||
'setting contain the app where %s is defined?') % model_name,
|
||||
obj=self,
|
||||
id='E030',
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
def _check_referencing_to_swapped_model(self):
|
||||
if (self.rel.to not in apps.get_models() and
|
||||
not isinstance(self.rel.to, six.string_types) and
|
||||
self.rel.to._meta.swapped):
|
||||
model = "%s.%s" % (
|
||||
self.rel.to._meta.app_label,
|
||||
self.rel.to._meta.object_name
|
||||
)
|
||||
return [
|
||||
checks.Error(
|
||||
('The field defines a relation with the model %s, '
|
||||
'which has been swapped out.') % model,
|
||||
hint='Update the relation to point at settings.%s' % self.rel.to._meta.swappable,
|
||||
obj=self,
|
||||
id='E029',
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
def _check_clashes(self):
|
||||
""" Check accessor and reverse query name clashes. """
|
||||
|
||||
from django.db.models.base import ModelBase
|
||||
|
||||
errors = []
|
||||
opts = self.model._meta
|
||||
|
||||
# `f.rel.to` may be a string instead of a model. Skip if model name is
|
||||
# not resolved.
|
||||
if not isinstance(self.rel.to, ModelBase):
|
||||
return []
|
||||
|
||||
# If the field doesn't install backward relation on the target model (so
|
||||
# `is_hidden` returns True), then there are no clashes to check and we
|
||||
# can skip these fields.
|
||||
if self.rel.is_hidden():
|
||||
return []
|
||||
|
||||
try:
|
||||
self.related
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
# Consider that we are checking field `Model.foreign` and the models
|
||||
# are:
|
||||
#
|
||||
# class Target(models.Model):
|
||||
# model = models.IntegerField()
|
||||
# model_set = models.IntegerField()
|
||||
#
|
||||
# class Model(models.Model):
|
||||
# foreign = models.ForeignKey(Target)
|
||||
# m2m = models.ManyToManyField(Target)
|
||||
|
||||
rel_opts = self.rel.to._meta
|
||||
# rel_opts.object_name == "Target"
|
||||
rel_name = self.related.get_accessor_name() # i. e. "model_set"
|
||||
rel_query_name = self.related_query_name() # i. e. "model"
|
||||
field_name = "%s.%s" % (opts.object_name,
|
||||
self.name) # i. e. "Model.field"
|
||||
|
||||
# Check clashes between accessor or reverse query name of `field`
|
||||
# and any other field name -- i. e. accessor for Model.foreign is
|
||||
# model_set and it clashes with Target.model_set.
|
||||
potential_clashes = rel_opts.fields + rel_opts.local_many_to_many
|
||||
for clash_field in potential_clashes:
|
||||
clash_name = "%s.%s" % (rel_opts.object_name,
|
||||
clash_field.name) # i. e. "Target.model_set"
|
||||
if clash_field.name == rel_name:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'Accessor for field %s clashes with field %s.' % (field_name, clash_name),
|
||||
hint=('Rename field %s or add/change a related_name '
|
||||
'argument to the definition for field %s.') % (clash_name, field_name),
|
||||
obj=self,
|
||||
id='E014',
|
||||
)
|
||||
)
|
||||
|
||||
if clash_field.name == rel_query_name:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'Reverse query name for field %s clashes with field %s.' % (field_name, clash_name),
|
||||
hint=('Rename field %s or add/change a related_name '
|
||||
'argument to the definition for field %s.') % (clash_name, field_name),
|
||||
obj=self,
|
||||
id='E015',
|
||||
)
|
||||
)
|
||||
|
||||
# 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)
|
||||
for clash_field in potential_clashes:
|
||||
clash_name = "%s.%s" % ( # i. e. "Model.m2m"
|
||||
clash_field.model._meta.object_name,
|
||||
clash_field.field.name)
|
||||
if clash_field.get_accessor_name() == rel_name:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'Clash between accessors for %s and %s.' % (field_name, clash_name),
|
||||
hint=('Add or change a related_name argument '
|
||||
'to the definition for %s or %s.') % (field_name, clash_name),
|
||||
obj=self,
|
||||
id='E016',
|
||||
)
|
||||
)
|
||||
|
||||
if clash_field.get_accessor_name() == rel_query_name:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'Clash between reverse query names for %s and %s.' % (field_name, clash_name),
|
||||
hint=('Add or change a related_name argument '
|
||||
'to the definition for %s or %s.') % (field_name, clash_name),
|
||||
obj=self,
|
||||
id='E017',
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
def db_type(self, connection):
|
||||
'''By default related field will not have a column
|
||||
as it relates columns to another table'''
|
||||
@ -1104,6 +1256,58 @@ class ForeignObject(RelatedField):
|
||||
|
||||
super(ForeignObject, self).__init__(**kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(ForeignObject, self).check(**kwargs)
|
||||
errors.extend(self._check_unique_target())
|
||||
return errors
|
||||
|
||||
def _check_unique_target(self):
|
||||
rel_is_string = isinstance(self.rel.to, six.string_types)
|
||||
if rel_is_string or not self.requires_unique_target:
|
||||
return []
|
||||
|
||||
# Skip if the
|
||||
try:
|
||||
self.foreign_related_fields
|
||||
except FieldDoesNotExist:
|
||||
return []
|
||||
|
||||
try:
|
||||
self.related
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
has_unique_field = any(rel_field.unique
|
||||
for rel_field in self.foreign_related_fields)
|
||||
if not has_unique_field and len(self.foreign_related_fields) > 1:
|
||||
field_combination = ','.join(rel_field.name
|
||||
for rel_field in self.foreign_related_fields)
|
||||
model_name = self.rel.to.__name__
|
||||
return [
|
||||
checks.Error(
|
||||
('No unique=True constraint '
|
||||
'on field combination "%s" under model %s.') % (field_combination, model_name),
|
||||
hint=('Set unique=True argument on any of the fields '
|
||||
'"%s" under model %s.') % (field_combination, model_name),
|
||||
obj=self,
|
||||
id='E018',
|
||||
)
|
||||
]
|
||||
elif not has_unique_field:
|
||||
field_name = self.foreign_related_fields[0].name
|
||||
model_name = self.rel.to.__name__
|
||||
return [
|
||||
checks.Error(
|
||||
('%s.%s must have unique=True '
|
||||
'because it is referenced by a foreign key.') % (model_name, field_name),
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E019',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(ForeignObject, self).deconstruct()
|
||||
kwargs['from_fields'] = self.from_fields
|
||||
@ -1331,7 +1535,6 @@ class ForeignKey(ForeignObject):
|
||||
except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
|
||||
assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT)
|
||||
else:
|
||||
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
|
||||
# For backwards compatibility purposes, we need to *try* and set
|
||||
# the to_field during FK construction. It won't be guaranteed to
|
||||
# be correct until contribute_to_class is called. Refs #12190.
|
||||
@ -1352,6 +1555,34 @@ class ForeignKey(ForeignObject):
|
||||
)
|
||||
super(ForeignKey, self).__init__(to, ['self'], [to_field], **kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(ForeignKey, self).check(**kwargs)
|
||||
errors.extend(self._check_on_delete())
|
||||
return errors
|
||||
|
||||
def _check_on_delete(self):
|
||||
on_delete = getattr(self.rel, 'on_delete', None)
|
||||
if on_delete == SET_NULL and not self.null:
|
||||
return [
|
||||
checks.Error(
|
||||
'The field specifies on_delete=SET_NULL, but cannot be null.',
|
||||
hint='Set null=True argument on the field.',
|
||||
obj=self,
|
||||
id='E020',
|
||||
)
|
||||
]
|
||||
elif on_delete == SET_DEFAULT and not self.has_default():
|
||||
return [
|
||||
checks.Error(
|
||||
'The field specifies on_delete=SET_DEFAULT, but has no default value.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E021',
|
||||
)
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(ForeignKey, self).deconstruct()
|
||||
del kwargs['to_fields']
|
||||
@ -1559,7 +1790,7 @@ class ManyToManyField(RelatedField):
|
||||
|
||||
def __init__(self, to, db_constraint=True, swappable=True, **kwargs):
|
||||
try:
|
||||
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
|
||||
to._meta
|
||||
except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
|
||||
assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ManyToManyField must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT)
|
||||
# Class names must be ASCII in Python 2.x, so we forcibly coerce it here to break early if there's a problem.
|
||||
@ -1582,6 +1813,136 @@ class ManyToManyField(RelatedField):
|
||||
|
||||
super(ManyToManyField, self).__init__(**kwargs)
|
||||
|
||||
def check(self, **kwargs):
|
||||
errors = super(ManyToManyField, self).check(**kwargs)
|
||||
errors.extend(self._check_unique(**kwargs))
|
||||
errors.extend(self._check_relationship_model(**kwargs))
|
||||
return errors
|
||||
|
||||
def _check_unique(self, **kwargs):
|
||||
if self.unique:
|
||||
return [
|
||||
checks.Error(
|
||||
'ManyToManyFields must not be unique.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E022',
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
def _check_relationship_model(self, from_model=None, **kwargs):
|
||||
errors = []
|
||||
|
||||
if self.rel.through not in apps.get_models(include_auto_created=True):
|
||||
# The relationship model is not installed.
|
||||
errors.append(
|
||||
checks.Error(
|
||||
('The field specifies a many-to-many relation through model '
|
||||
'%s, which has not been installed.') % self.rel.through,
|
||||
hint=('Ensure that you did not misspell the model name and '
|
||||
'the model is not abstract. Does your INSTALLED_APPS '
|
||||
'setting contain the app where %s is defined?') % self.rel.through,
|
||||
obj=self,
|
||||
id='E023',
|
||||
)
|
||||
)
|
||||
|
||||
elif not isinstance(self.rel.through, six.string_types):
|
||||
|
||||
assert from_model is not None, \
|
||||
"ManyToManyField with intermediate " \
|
||||
"tables cannot be checked if you don't pass the model " \
|
||||
"where the field is attached to."
|
||||
|
||||
# Set some useful local variables
|
||||
to_model = self.rel.to
|
||||
from_model_name = from_model._meta.object_name
|
||||
if isinstance(to_model, six.string_types):
|
||||
to_model_name = to_model
|
||||
else:
|
||||
to_model_name = to_model._meta.object_name
|
||||
relationship_model_name = self.rel.through._meta.object_name
|
||||
self_referential = from_model == to_model
|
||||
|
||||
# Check symmetrical attribute.
|
||||
if (self_referential and self.rel.symmetrical and
|
||||
not self.rel.through._meta.auto_created):
|
||||
errors.append(
|
||||
checks.Error(
|
||||
'Many-to-many fields with intermediate tables must not be symmetrical.',
|
||||
hint=None,
|
||||
obj=self,
|
||||
id='E024',
|
||||
)
|
||||
)
|
||||
|
||||
# Count foreign keys in intermediate model
|
||||
if self_referential:
|
||||
seen_self = sum(from_model == getattr(field.rel, 'to', None)
|
||||
for field in self.rel.through._meta.fields)
|
||||
|
||||
if seen_self > 2:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
('The model is used as an intermediary model by '
|
||||
'%s, but it has more than two foreign keys '
|
||||
'to %s, which is ambiguous and is not permitted.') % (self, from_model_name),
|
||||
hint=None,
|
||||
obj=self.rel.through,
|
||||
id='E025',
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
# Count foreign keys in relationship model
|
||||
seen_from = sum(from_model == getattr(field.rel, 'to', None)
|
||||
for field in self.rel.through._meta.fields)
|
||||
seen_to = sum(to_model == getattr(field.rel, 'to', None)
|
||||
for field in self.rel.through._meta.fields)
|
||||
|
||||
if seen_from > 1:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
('The model is used as an intermediary model by '
|
||||
'%s, but it has more than one foreign key '
|
||||
'to %s, which is ambiguous and is not permitted.') % (self, from_model_name),
|
||||
hint=('If you want to create a recursive relationship, '
|
||||
'use ForeignKey("self", symmetrical=False, '
|
||||
'through="%s").') % relationship_model_name,
|
||||
obj=self,
|
||||
id='E026',
|
||||
)
|
||||
)
|
||||
|
||||
if seen_to > 1:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
('The model is used as an intermediary model by '
|
||||
'%s, but it has more than one foreign key '
|
||||
'to %s, which is ambiguous and is not permitted.') % (self, to_model_name),
|
||||
hint=('If you want to create a recursive '
|
||||
'relationship, use ForeignKey("self", '
|
||||
'symmetrical=False, through="%s").') % relationship_model_name,
|
||||
obj=self,
|
||||
id='E027',
|
||||
)
|
||||
)
|
||||
|
||||
if seen_from == 0 or seen_to == 0:
|
||||
errors.append(
|
||||
checks.Error(
|
||||
('The model is used as an intermediary model by '
|
||||
'%s, but it misses a foreign key to %s or %s.') % (
|
||||
self, from_model_name, to_model_name
|
||||
),
|
||||
hint=None,
|
||||
obj=self.rel.through,
|
||||
id='E028',
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(ManyToManyField, self).deconstruct()
|
||||
# Handle the simpler arguments
|
||||
|
@ -7,6 +7,7 @@ from django.db.models import signals
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.utils import six
|
||||
from django.utils.deprecation import RenameMethodsBase
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
|
||||
def ensure_default_manager(sender, **kwargs):
|
||||
@ -58,6 +59,7 @@ class RenameManagerMethods(RenameMethodsBase):
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class BaseManager(six.with_metaclass(RenameManagerMethods)):
|
||||
# Tracks each time a Manager instance is created. Used to retain order.
|
||||
creation_counter = 0
|
||||
@ -70,6 +72,19 @@ class BaseManager(six.with_metaclass(RenameManagerMethods)):
|
||||
self._db = None
|
||||
self._hints = {}
|
||||
|
||||
def __str__(self):
|
||||
""" Return "app_label.model_label.manager_name". """
|
||||
model = self.model
|
||||
opts = model._meta
|
||||
app = model._meta.app_label
|
||||
manager_name = next(name for (_, name, manager)
|
||||
in opts.concrete_managers + opts.abstract_managers
|
||||
if manager == self)
|
||||
return '%s.%s.%s' % (app, model._meta.object_name, manager_name)
|
||||
|
||||
def check(self, **kwargs):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def _get_queryset_methods(cls, queryset_class):
|
||||
def create_method(name, method):
|
||||
@ -155,6 +170,10 @@ class BaseManager(six.with_metaclass(RenameManagerMethods)):
|
||||
def db(self):
|
||||
return self._db or router.db_for_read(self.model, **self._hints)
|
||||
|
||||
#######################
|
||||
# PROXIES TO QUERYSET #
|
||||
#######################
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Returns a new QuerySet object. Subclasses can override this method to
|
||||
|
@ -30,13 +30,18 @@ def normalize_unique_together(unique_together):
|
||||
tuple of two strings. Normalize it to a tuple of tuples, so that
|
||||
calling code can uniformly expect that.
|
||||
"""
|
||||
if not unique_together:
|
||||
return ()
|
||||
first_element = next(iter(unique_together))
|
||||
if not isinstance(first_element, (tuple, list)):
|
||||
unique_together = (unique_together,)
|
||||
# Normalize everything to tuples
|
||||
return tuple(tuple(ut) for ut in unique_together)
|
||||
try:
|
||||
if not unique_together:
|
||||
return ()
|
||||
first_element = next(iter(unique_together))
|
||||
if not isinstance(first_element, (tuple, list)):
|
||||
unique_together = (unique_together,)
|
||||
# Normalize everything to tuples
|
||||
return tuple(tuple(ut) for ut in unique_together)
|
||||
except TypeError:
|
||||
# If the value of unique_together isn't valid, return it
|
||||
# verbatim; this will be picked up by the check framework later.
|
||||
return unique_together
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
|
@ -927,9 +927,13 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False):
|
||||
if not isinstance(fk, ForeignKey) or \
|
||||
(fk.rel.to != parent_model and
|
||||
fk.rel.to not in parent_model._meta.get_parent_list()):
|
||||
raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
|
||||
raise ValueError(
|
||||
"fk_name '%s' is not a ForeignKey to '%s.%'."
|
||||
% (fk_name, parent_model._meta.app_label, parent_model._meta.object_name))
|
||||
elif len(fks_to_parent) == 0:
|
||||
raise Exception("%s has no field named '%s'" % (model, fk_name))
|
||||
raise ValueError(
|
||||
"'%s.%s' has no field named '%s'."
|
||||
% (model._meta.app_label, model._meta.object_name, fk_name))
|
||||
else:
|
||||
# Try to discover what the ForeignKey from model to parent_model is
|
||||
fks_to_parent = [
|
||||
@ -943,9 +947,13 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False):
|
||||
elif len(fks_to_parent) == 0:
|
||||
if can_fail:
|
||||
return
|
||||
raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
|
||||
raise ValueError(
|
||||
"'%s.%s' has no ForeignKey to '%s.%s'."
|
||||
% (model._meta.app_label, model._meta.object_name, parent_model._meta.app_label, parent_model._meta.object_name))
|
||||
else:
|
||||
raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
|
||||
raise ValueError(
|
||||
"'%s.%s' has more than one ForeignKey to '%s.%s'."
|
||||
% (model._meta.app_label, model._meta.object_name, parent_model._meta.app_label, parent_model._meta.object_name))
|
||||
return fk
|
||||
|
||||
|
||||
|
@ -8,10 +8,11 @@ from django.test.testcases import (
|
||||
SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
|
||||
skipUnlessDBFeature
|
||||
)
|
||||
from django.test.utils import modify_settings, override_settings
|
||||
from django.test.utils import modify_settings, override_settings, override_system_checks
|
||||
|
||||
__all__ = [
|
||||
'Client', 'RequestFactory', 'TestCase', 'TransactionTestCase',
|
||||
'SimpleTestCase', 'LiveServerTestCase', 'skipIfDBFeature',
|
||||
'skipUnlessDBFeature', 'modify_settings', 'override_settings',
|
||||
'override_system_checks'
|
||||
]
|
||||
|
@ -787,7 +787,7 @@ class TransactionTestCase(SimpleTestCase):
|
||||
# We have to use this slightly awkward syntax due to the fact
|
||||
# that we're using *args and **kwargs together.
|
||||
call_command('loaddata', *self.fixtures,
|
||||
**{'verbosity': 0, 'database': db_name, 'skip_validation': True})
|
||||
**{'verbosity': 0, 'database': db_name, 'skip_checks': True})
|
||||
|
||||
def _post_teardown(self):
|
||||
"""Performs any post-test things. This includes:
|
||||
@ -820,7 +820,7 @@ class TransactionTestCase(SimpleTestCase):
|
||||
# when flushing only a subset of the apps
|
||||
for db_name in self._databases_names(include_mirrors=False):
|
||||
call_command('flush', verbosity=0, interactive=False,
|
||||
database=db_name, skip_validation=True,
|
||||
database=db_name, skip_checks=True,
|
||||
reset_sequences=False,
|
||||
allow_cascade=self.available_apps is not None,
|
||||
inhibit_post_migrate=self.available_apps is not None)
|
||||
@ -887,7 +887,7 @@ class TestCase(TransactionTestCase):
|
||||
'verbosity': 0,
|
||||
'commit': False,
|
||||
'database': db_name,
|
||||
'skip_validation': True,
|
||||
'skip_checks': True,
|
||||
})
|
||||
except Exception:
|
||||
self._fixture_teardown()
|
||||
|
@ -300,6 +300,26 @@ class modify_settings(override_settings):
|
||||
super(modify_settings, self).enable()
|
||||
|
||||
|
||||
def override_system_checks(new_checks):
|
||||
""" Acts as a decorator. Overrides list of registered system checks.
|
||||
Useful when you override `INSTALLED_APPS`, e.g. if you exclude `auth` app,
|
||||
you also need to exclude its system checks. """
|
||||
|
||||
from django.core.checks.registry import registry
|
||||
|
||||
def outer(test_func):
|
||||
@wraps(test_func)
|
||||
def inner(*args, **kwargs):
|
||||
old_checks = registry.registered_checks
|
||||
registry.registered_checks = new_checks
|
||||
try:
|
||||
return test_func(*args, **kwargs)
|
||||
finally:
|
||||
registry.registered_checks = old_checks
|
||||
return inner
|
||||
return outer
|
||||
|
||||
|
||||
def compare_xml(want, got):
|
||||
"""Tries to do a 'xml-comparison' of want and got. Plain string
|
||||
comparison doesn't always work because, for example, attribute
|
||||
|
@ -76,6 +76,7 @@ LIGHT_PALETTE = 'light'
|
||||
PALETTES = {
|
||||
NOCOLOR_PALETTE: {
|
||||
'ERROR': {},
|
||||
'WARNING': {},
|
||||
'NOTICE': {},
|
||||
'SQL_FIELD': {},
|
||||
'SQL_COLTYPE': {},
|
||||
@ -95,6 +96,7 @@ PALETTES = {
|
||||
},
|
||||
DARK_PALETTE: {
|
||||
'ERROR': {'fg': 'red', 'opts': ('bold',)},
|
||||
'WARNING': {'fg': 'yellow', 'opts': ('bold',)},
|
||||
'NOTICE': {'fg': 'red'},
|
||||
'SQL_FIELD': {'fg': 'green', 'opts': ('bold',)},
|
||||
'SQL_COLTYPE': {'fg': 'green'},
|
||||
@ -114,6 +116,7 @@ PALETTES = {
|
||||
},
|
||||
LIGHT_PALETTE: {
|
||||
'ERROR': {'fg': 'red', 'opts': ('bold',)},
|
||||
'WARNING': {'fg': 'yellow', 'opts': ('bold',)},
|
||||
'NOTICE': {'fg': 'red'},
|
||||
'SQL_FIELD': {'fg': 'green', 'opts': ('bold',)},
|
||||
'SQL_COLTYPE': {'fg': 'green'},
|
||||
|
@ -227,8 +227,23 @@ All attributes can be set in your derived class and can be used in
|
||||
wrapped with ``BEGIN;`` and ``COMMIT;``. Default value is
|
||||
``False``.
|
||||
|
||||
.. attribute:: BaseCommand.requires_system_checks
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
A boolean; if ``True``, the entire Django project will be checked for
|
||||
potential problems prior to executing the command. If
|
||||
``requires_system_checks`` is missing, the value of
|
||||
``requires_model_validation`` is used. If the latter flag is missing
|
||||
as well, the default value (``True``) is used. Defining both
|
||||
``requires_system_checks`` and ``requires_model_validation`` will result
|
||||
in an error.
|
||||
|
||||
.. attribute:: BaseCommand.requires_model_validation
|
||||
|
||||
.. deprecated:: 1.7
|
||||
Replaced by ``requires_system_checks``
|
||||
|
||||
A boolean; if ``True``, validation of installed models will be
|
||||
performed prior to executing the command. Default value is
|
||||
``True``. To validate an individual application's models
|
||||
@ -299,12 +314,23 @@ the :meth:`~BaseCommand.handle` method must be implemented.
|
||||
|
||||
The actual logic of the command. Subclasses must implement this method.
|
||||
|
||||
.. method:: BaseCommand.check(app_configs=None, tags=None, display_num_errors=False)
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Uses the system check framework to inspect the entire Django project for
|
||||
potential problems. Serious problems are raised as a :class:`CommandError`;
|
||||
warnings are output to stderr; minor notifications are output to stdout.
|
||||
|
||||
If ``apps`` and ``tags`` are both None, all system checks are performed.
|
||||
``tags`` can be a list of check tags, like ``compatibility`` or ``models``.
|
||||
|
||||
.. method:: BaseCommand.validate(app=None, display_num_errors=False)
|
||||
|
||||
Validates the given app, raising :class:`CommandError` for any errors.
|
||||
|
||||
If ``app`` is None, then all installed apps are validated.
|
||||
.. deprecated:: 1.7
|
||||
Replaced with the :djadmin:`check` command
|
||||
|
||||
If ``app`` is None, then all installed apps are checked for errors.
|
||||
|
||||
.. _ref-basecommand-subclasses:
|
||||
|
||||
|
@ -291,6 +291,7 @@ Learn about some other core functionalities of the Django framework:
|
||||
* :doc:`Flatpages <ref/contrib/flatpages>`
|
||||
* :doc:`Redirects <ref/contrib/redirects>`
|
||||
* :doc:`Signals <topics/signals>`
|
||||
* :doc:`System check framework <ref/checks>`
|
||||
* :doc:`The sites framework <ref/contrib/sites>`
|
||||
* :doc:`Unicode in Django <ref/unicode>`
|
||||
|
||||
|
@ -234,6 +234,16 @@ these changes.
|
||||
|
||||
* Passing callable arguments to querysets will no longer be possible.
|
||||
|
||||
* ``BaseCommand.requires_model_validation`` will be removed in favor of
|
||||
``requires_system_checks``. Admin validators will be replaced by admin
|
||||
checks.
|
||||
|
||||
* ``ModelAdmin.validator`` will be removed in favor of the new ``checks``
|
||||
attribute.
|
||||
|
||||
* ``django.db.backends.DatabaseValidation.validate_field`` will be removed in
|
||||
favor of the ``check_field`` method.
|
||||
|
||||
2.0
|
||||
---
|
||||
|
||||
|
@ -140,7 +140,7 @@ You'll see the following output on the command line:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
Validating models...
|
||||
Performing system checks...
|
||||
|
||||
0 errors found
|
||||
|today| - 15:50:53
|
||||
@ -545,8 +545,8 @@ Note the following:
|
||||
changes.
|
||||
|
||||
If you're interested, you can also run
|
||||
:djadmin:`python manage.py validate <validate>`; this checks for any errors in
|
||||
your models without making migrations or touching the database.
|
||||
:djadmin:`python manage.py check <check>`; this checks for any problems in
|
||||
your project without making migrations or touching the database.
|
||||
|
||||
Now, run :djadmin:`migrate` again to create those model tables in your database:
|
||||
|
||||
|
208
docs/ref/checks.txt
Normal file
208
docs/ref/checks.txt
Normal file
@ -0,0 +1,208 @@
|
||||
======================
|
||||
System check framework
|
||||
======================
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
.. module:: django.core.checks
|
||||
|
||||
The system check framework is a set of static checks for validating Django
|
||||
projects. It detects common problems and provides hints for how to fix them.
|
||||
The framework is extensible so you can easily add your own checks.
|
||||
|
||||
Checks can be triggered explicitly via the :djadmin:`check` command. Checks are
|
||||
triggered implicitly before most commands, including :djadmin:`runserver` and
|
||||
:djadmin:`migrate`. For performance reasons, the checks are not performed if
|
||||
:setting:`DEBUG` is set to ``False``.
|
||||
|
||||
Serious errors will prevent Django commands (such as :djadmin:`runserver`) from
|
||||
running at all. Minor problems are reported to the console. If you have inspected
|
||||
the cause of a warning and are happy to ignore it, you can hide specific warnings
|
||||
using the :setting:`SILENCED_SYSTEM_CHECKS` setting in your project settings file.
|
||||
|
||||
Writing your own checks
|
||||
=======================
|
||||
|
||||
The framework is flexible and allows you to write functions that perform
|
||||
any other kind of check you may require. The following is an example stub
|
||||
check function::
|
||||
|
||||
from django.core.checks import register
|
||||
|
||||
@register()
|
||||
def example_check(app_configs, **kwargs):
|
||||
errors = []
|
||||
# ... your check logic here
|
||||
return errors
|
||||
|
||||
The check function *must* accept an ``app_configs`` argument; this argument is
|
||||
the list of applications that should be inspected. If None, the check must be
|
||||
run on *all* installed apps in the project. The ``**kwargs`` argument is required
|
||||
for future expansion.
|
||||
|
||||
Messages
|
||||
--------
|
||||
|
||||
The function must return a list of messages. If no problems are found as a result
|
||||
of the check, the check function must return an empty list.
|
||||
|
||||
.. class:: CheckMessage(level, msg, hint, obj=None, id=None)
|
||||
|
||||
The warnings and errors raised by the check method must be instances of
|
||||
:class:`~django.core.checks.CheckMessage`. An instance of
|
||||
:class:`~django.core.checks.CheckMessage` encapsulates a single reportable
|
||||
error or warning. It also provides context and hints applicable to the
|
||||
message, and a unique identifier that is used for filtering purposes.
|
||||
|
||||
The concept is very similar to messages from the :doc:`message
|
||||
framework </ref/contrib/messages>` or the :doc:`logging framework
|
||||
</topics/logging>`. Messages are tagged with a ``level`` indicating the
|
||||
severity of the message.
|
||||
|
||||
Constructor arguments are:
|
||||
|
||||
``level``
|
||||
The severity of the message. Use one of the
|
||||
predefined values: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``,
|
||||
``CRITICAL``. If the level is greater or equal to ``ERROR``, then Django
|
||||
will prevent management commands from executing. Messages with
|
||||
level lower than ``ERROR`` (i.e. warnings) are reported to the console,
|
||||
but can be silenced.
|
||||
|
||||
``msg``
|
||||
A short (less than 80 characters) string describing the problem. The string
|
||||
should *not* contain newlines.
|
||||
|
||||
``hint``
|
||||
A single-line string providing a hint for fixing the problem. If no hint
|
||||
can be provided, or the hint is self-evident from the error message, a
|
||||
value of ``None`` can be used::
|
||||
|
||||
Error('error message') # Will not work.
|
||||
Error('error message', None) # Good
|
||||
Error('error message', hint=None) # Better
|
||||
|
||||
``obj``
|
||||
Optional. An object providing context for the message (for example, the
|
||||
model where the problem was discovered). The object should be a model, field,
|
||||
or manager or any other object that defines ``__str__`` method (on
|
||||
Python 2 you need to define ``__unicode__`` method). The method is used while
|
||||
reporting all messages and its result precedes the message.
|
||||
|
||||
``id``
|
||||
Optional string. A unique identifier for the issue. Identifiers should
|
||||
follow the pattern ``applabel.X001``, where ``X`` is one of the letters
|
||||
``CEWID``, indicating the message severity (``C`` for criticals,
|
||||
``E`` for errors and so). The number can be allocated by the application,
|
||||
but should be unique within that application.
|
||||
|
||||
There are also shortcuts to make creating messages with common levels easier.
|
||||
When using these methods you can omit the ``level`` argument because it is
|
||||
implied by the class name.
|
||||
|
||||
.. class:: Debug(msg, hint, obj=None, id=None)
|
||||
.. class:: Info(msg, hint, obj=None, id=None)
|
||||
.. class:: Warning(msg, hint, obj=None, id=None)
|
||||
.. class:: Error(msg, hint, obj=None, id=None)
|
||||
.. class:: Critical(msg, hint, obj=None, id=None)
|
||||
|
||||
Messages are comparable. That allows you to easily write tests::
|
||||
|
||||
from django.core.checks import Error
|
||||
errors = checked_object.check()
|
||||
expected_errors = [
|
||||
Error(
|
||||
'an error',
|
||||
hint=None,
|
||||
obj=checked_object,
|
||||
id='myapp.E001',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected_errors)
|
||||
|
||||
Registering and labeling checks
|
||||
-------------------------------
|
||||
|
||||
Lastly, your check function must be registered explicitly with system check
|
||||
registry.
|
||||
|
||||
.. function:: register(*tags)(function)
|
||||
|
||||
You can pass as many tags to ``register`` as you want in order to label your
|
||||
check. Tagging checks is useful since it allows you to run only a certain
|
||||
group of checks. For example, to register a compatibility check, you would
|
||||
make the following call::
|
||||
|
||||
from django.core.checks import register
|
||||
|
||||
@register('compatibility')
|
||||
def my_check(app_configs, **kwargs):
|
||||
# ... perform compatibility checks and collect errors
|
||||
return errors
|
||||
|
||||
.. _field-checking:
|
||||
|
||||
Field, Model, and Manager checks
|
||||
--------------------------------
|
||||
|
||||
In some cases, you won't need to register your check function -- you can
|
||||
piggyback on an existing registration.
|
||||
|
||||
Fields, models, and model managers all implement a ``check()`` method that is
|
||||
already registered with the check framework. If you want to add extra checks,
|
||||
you can extend the implemenation on the base class, perform any extra
|
||||
checks you need, and append any messages to those generated by the base class.
|
||||
It's recommended the you delegate each check to a separate methods.
|
||||
|
||||
Consider an example where you are implementing a custom field named
|
||||
``RangedIntegerField``. This field adds ``min`` and ``max`` arguments to the
|
||||
constructor of ``IntegerField``. You may want to add a check to ensure that users
|
||||
provide a min value that is less than or equal to the max value. The following
|
||||
code snippet shows how you can implement this check::
|
||||
|
||||
from django.core import checks
|
||||
from django.db import models
|
||||
|
||||
class RangedIntegerField(models.IntegerField):
|
||||
def __init__(self, min=None, max=None, **kwargs):
|
||||
super(RangedIntegerField, self).__init__(**kwargs)
|
||||
self.min = min
|
||||
self.max = max
|
||||
|
||||
def check(self, **kwargs):
|
||||
# Call the superclass
|
||||
errors = super(RangedIntegerField, self).check(**kwargs)
|
||||
|
||||
# Do some custom checks and add messages to `errors`:
|
||||
errors.extend(self._check_min_max_values(**kwargs))
|
||||
|
||||
# Return all errors and warnings
|
||||
return errors
|
||||
|
||||
def _check_min_max_values(self, **kwargs):
|
||||
if (self.min is not None and
|
||||
self.max is not None and
|
||||
self.min > self.max):
|
||||
return [
|
||||
checks.Error(
|
||||
'min greated than max.',
|
||||
hint='Decrease min or increase max.',
|
||||
obj=self,
|
||||
id='myapp.E001',
|
||||
)
|
||||
]
|
||||
# When no error, return an empty list
|
||||
return []
|
||||
|
||||
If you wanted to add checks to a model manager, you would take the same
|
||||
approach on your sublass of :class:`~django.db.models.Manager`.
|
||||
|
||||
If you want to add a check to a model class, the approach is *almost* the same:
|
||||
the only difference is that the check is a classmethod, not an instance method::
|
||||
|
||||
class MyModel(models.Model):
|
||||
@classmethod
|
||||
def check(cls, **kwargs):
|
||||
errors = super(MyModel, cls).check(**kwargs)
|
||||
# ... your own checks ...
|
||||
return errors
|
@ -95,18 +95,36 @@ documentation for the :djadminopt:`--verbosity` option.
|
||||
Available commands
|
||||
==================
|
||||
|
||||
check
|
||||
-----
|
||||
check <appname appname ...>
|
||||
---------------------------
|
||||
|
||||
.. django-admin:: check
|
||||
|
||||
.. versionadded:: 1.6
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
Performs a series of checks to verify a given setup (settings/application code)
|
||||
is compatible with the current version of Django.
|
||||
Uses the :doc:`system check framework </ref/checks>` to inspect
|
||||
the entire Django project for common problems.
|
||||
|
||||
Upon finding things that are incompatible or require notifying the user, it
|
||||
issues a series of warnings.
|
||||
The system check framework will confirm that there aren't any problems with
|
||||
your installed models or your admin registrations. It will also provide warnings
|
||||
of common compatibility problems introduced by upgrading Django to a new version.
|
||||
Custom checks may be introduced by other libraries and applications.
|
||||
|
||||
By default, all apps will be checkd. You can check a subset of apps by providing
|
||||
a list of app labels as arguments::
|
||||
|
||||
python manage.py auth admin myapp
|
||||
|
||||
If you do not specify any app, all apps will be checked.
|
||||
|
||||
.. django-admin-option:: --tag <tagname>
|
||||
|
||||
The :doc:`system check framework </ref/checks>` performs many different
|
||||
types of checks. These check types are categorized with tags. You can use these tags
|
||||
to restrict the checks performed to just those in a particular category. For example,
|
||||
to perform only security and compatibility checks, you would run::
|
||||
|
||||
python manage.py --tag security -tag compatibility
|
||||
|
||||
compilemessages
|
||||
---------------
|
||||
@ -810,9 +828,9 @@ reduction.
|
||||
``pyinotify`` support was added.
|
||||
|
||||
When you start the server, and each time you change Python code while the
|
||||
server is running, the server will validate all of your installed models. (See
|
||||
the :djadmin:`validate` command below.) If the validator finds errors, it will
|
||||
print them to standard output, but it won't stop the server.
|
||||
server is running, the server will check your entire Django project for errors (see
|
||||
the :djadmin:`check` command). If any errors are found, they will be printed
|
||||
to standard output, but it won't stop the server.
|
||||
|
||||
You can run as many servers as you want, as long as they're on separate ports.
|
||||
Just execute ``django-admin.py runserver`` more than once.
|
||||
@ -1310,6 +1328,9 @@ validate
|
||||
|
||||
.. django-admin:: validate
|
||||
|
||||
.. deprecated:: 1.7
|
||||
Replaced by the :djadmin:`check` command.
|
||||
|
||||
Validates all installed models (according to the :setting:`INSTALLED_APPS`
|
||||
setting) and prints validation errors to standard output.
|
||||
|
||||
|
@ -6,6 +6,7 @@ API Reference
|
||||
:maxdepth: 1
|
||||
|
||||
applications
|
||||
checks
|
||||
class-based-views/index
|
||||
clickjacking
|
||||
contrib/index
|
||||
|
@ -1766,6 +1766,22 @@ The backend used for signing cookies and other data.
|
||||
|
||||
See also the :doc:`/topics/signing` documentation.
|
||||
|
||||
.. setting:: SILENCED_SYSTEM_CHECKS
|
||||
|
||||
SILENCED_SYSTEM_CHECKS
|
||||
----------------------
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Default: ``[]``
|
||||
|
||||
A list of identifers of messages generated by the system check framework
|
||||
(i.e. ``["models.W001"]``) that you wish to permanently acknowledge and ignore.
|
||||
Silenced warnings will no longer be output to the console; silenced errors
|
||||
will still be printed, but will not prevent management commands from running.
|
||||
|
||||
See also the :doc:`/ref/checks` documentation.
|
||||
|
||||
.. setting:: TEMPLATE_CONTEXT_PROCESSORS
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS
|
||||
@ -2737,6 +2753,7 @@ Error reporting
|
||||
* :setting:`IGNORABLE_404_URLS`
|
||||
* :setting:`MANAGERS`
|
||||
* :setting:`SEND_BROKEN_LINK_EMAILS`
|
||||
* :setting:`SILENCED_SYSTEM_CHECKS`
|
||||
|
||||
File uploads
|
||||
------------
|
||||
|
@ -140,6 +140,17 @@ Using a custom manager when traversing reverse relations
|
||||
It is now possible to :ref:`specify a custom manager
|
||||
<using-custom-reverse-manager>` when traversing a reverse relationship.
|
||||
|
||||
New system check framework
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
We've added a new :doc:`System check framework </ref/checks>` for
|
||||
detecting common problems (like invalid models) and providing hints for
|
||||
resolving those problems. The framework is extensible so you can add your
|
||||
own checks for your own apps and libraries.
|
||||
|
||||
To perform system checks, you use the :djadmin:`check` management command.
|
||||
This command replaces the older :djadmin:`validate` management command.
|
||||
|
||||
New ``Prefetch`` object for advanced ``prefetch_related`` operations.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -993,7 +1004,7 @@ considered a bug and has been addressed.
|
||||
``syncdb``
|
||||
~~~~~~~~~~
|
||||
|
||||
The ``syncdb`` command has been deprecated in favour of the new ``migrate``
|
||||
The :djadmin:`syncdb` command has been deprecated in favor of the new :djadmin:`migrate`
|
||||
command. ``migrate`` takes the same arguments as ``syncdb`` used to plus a few
|
||||
more, so it's safe to just change the name you're calling and nothing else.
|
||||
|
||||
@ -1103,3 +1114,32 @@ remove the setting from your configuration at your convenience.
|
||||
``SplitDateTimeWidget`` support in :class:`~django.forms.DateTimeField` is
|
||||
deprecated, use ``SplitDateTimeWidget`` with
|
||||
:class:`~django.forms.SplitDateTimeField` instead.
|
||||
|
||||
``validate``
|
||||
~~~~~~~~~~~~
|
||||
|
||||
:djadmin:`validate` command is deprecated in favor of :djadmin:`check` command.
|
||||
|
||||
``django.core.management.BaseCommand``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``requires_model_validation`` is deprecated in favor of a new
|
||||
``requires_system_checks`` flag. If the latter flag is missing, then the
|
||||
value of the former flag is used. Defining both ``requires_system_checks`` and
|
||||
``requires_model_validation`` results in an error.
|
||||
|
||||
The ``check()`` method has replaced the old ``validate()`` method.
|
||||
|
||||
``ModelAdmin.validator``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``ModelAdmin.validator`` is deprecated in favor of new ``checks`` attribute.
|
||||
|
||||
``django.db.backends.DatabaseValidation.validate_field``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This method is deprecated in favor of a new ``check_field`` method.
|
||||
The functionality required by ``check_field()`` is the same as that provided
|
||||
by ``validate_field()``, but the output format is different. Third-party database
|
||||
backends needing this functionality should modify their backends to provide an
|
||||
implementation of ``check_field()``.
|
||||
|
@ -960,7 +960,7 @@ The reverse name of the ``common.ChildA.m2m`` field will be
|
||||
reverse name of the ``rare.ChildB.m2m`` field will be ``rare_childb_related``.
|
||||
It is up to you how you use the ``'%(class)s'`` and ``'%(app_label)s`` portion
|
||||
to construct your related name, but if you forget to use it, Django will raise
|
||||
errors when you validate your models (or run :djadmin:`migrate`).
|
||||
errors when you perform system checks (or run :djadmin:`migrate`).
|
||||
|
||||
If you don't specify a :attr:`~django.db.models.ForeignKey.related_name`
|
||||
attribute for a field in an abstract base class, the default reverse name will
|
||||
@ -1053,7 +1053,7 @@ are putting those types of relations on a subclass of another model,
|
||||
you **must** specify the
|
||||
:attr:`~django.db.models.ForeignKey.related_name` attribute on each
|
||||
such field. If you forget, Django will raise an error when you run
|
||||
:djadmin:`validate` or :djadmin:`migrate`.
|
||||
:djadmin:`check` or :djadmin:`migrate`.
|
||||
|
||||
For example, using the above ``Place`` class again, let's create another
|
||||
subclass with a :class:`~django.db.models.ManyToManyField`::
|
||||
|
57
tests/admin_checks/models.py
Normal file
57
tests/admin_checks/models.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""
|
||||
Tests of ModelAdmin system checks logic.
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
|
||||
class Album(models.Model):
|
||||
title = models.CharField(max_length=150)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Song(models.Model):
|
||||
title = models.CharField(max_length=150)
|
||||
album = models.ForeignKey(Album)
|
||||
original_release = models.DateField(editable=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ('title',)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def readonly_method_on_model(self):
|
||||
# does nothing
|
||||
pass
|
||||
|
||||
|
||||
class TwoAlbumFKAndAnE(models.Model):
|
||||
album1 = models.ForeignKey(Album, related_name="album1_set")
|
||||
album2 = models.ForeignKey(Album, related_name="album2_set")
|
||||
e = models.CharField(max_length=1)
|
||||
|
||||
|
||||
class Author(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class Book(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
subtitle = models.CharField(max_length=100)
|
||||
price = models.FloatField()
|
||||
authors = models.ManyToManyField(Author, through='AuthorsBooks')
|
||||
|
||||
|
||||
class AuthorsBooks(models.Model):
|
||||
author = models.ForeignKey(Author)
|
||||
book = models.ForeignKey(Book)
|
||||
|
||||
|
||||
class State(models.Model):
|
||||
name = models.CharField(max_length=15)
|
||||
|
||||
|
||||
class City(models.Model):
|
||||
state = models.ForeignKey(State)
|
436
tests/admin_checks/tests.py
Normal file
436
tests/admin_checks/tests.py
Normal file
@ -0,0 +1,436 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.core import checks
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import TestCase
|
||||
|
||||
from .models import Song, Book, Album, TwoAlbumFKAndAnE, City, State
|
||||
|
||||
|
||||
class SongForm(forms.ModelForm):
|
||||
pass
|
||||
|
||||
|
||||
class ValidFields(admin.ModelAdmin):
|
||||
form = SongForm
|
||||
fields = ['title']
|
||||
|
||||
|
||||
class ValidFormFieldsets(admin.ModelAdmin):
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
class ExtraFieldForm(SongForm):
|
||||
name = forms.CharField(max_length=50)
|
||||
return ExtraFieldForm
|
||||
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('name',),
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
class SystemChecksTestCase(TestCase):
|
||||
|
||||
def test_checks_are_performed(self):
|
||||
class MyAdmin(admin.ModelAdmin):
|
||||
@classmethod
|
||||
def check(self, model, **kwargs):
|
||||
return ['error!']
|
||||
|
||||
admin.site.register(Song, MyAdmin)
|
||||
try:
|
||||
errors = checks.run_checks()
|
||||
expected = ['error!']
|
||||
self.assertEqual(errors, expected)
|
||||
finally:
|
||||
admin.site.unregister(Song)
|
||||
|
||||
def test_readonly_and_editable(self):
|
||||
class SongAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ["original_release"]
|
||||
fieldsets = [
|
||||
(None, {
|
||||
"fields": ["title", "original_release"],
|
||||
}),
|
||||
]
|
||||
|
||||
errors = SongAdmin.check(model=Song)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_custom_modelforms_with_fields_fieldsets(self):
|
||||
"""
|
||||
# Regression test for #8027: custom ModelForms with fields/fieldsets
|
||||
"""
|
||||
|
||||
errors = ValidFields.check(model=Song)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_custom_get_form_with_fieldsets(self):
|
||||
"""
|
||||
Ensure that the fieldsets checks are skipped when the ModelAdmin.get_form() method
|
||||
is overridden.
|
||||
Refs #19445.
|
||||
"""
|
||||
|
||||
errors = ValidFormFieldsets.check(model=Song)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_exclude_values(self):
|
||||
"""
|
||||
Tests for basic system checks of 'exclude' option values (#12689)
|
||||
"""
|
||||
|
||||
class ExcludedFields1(admin.ModelAdmin):
|
||||
exclude = 'foo'
|
||||
|
||||
errors = ExcludedFields1.check(model=Book)
|
||||
expected = [
|
||||
checks.Error(
|
||||
'"exclude" must be a list or tuple.',
|
||||
hint=None,
|
||||
obj=ExcludedFields1,
|
||||
id='admin.E014',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_exclude_duplicate_values(self):
|
||||
class ExcludedFields2(admin.ModelAdmin):
|
||||
exclude = ('name', 'name')
|
||||
|
||||
errors = ExcludedFields2.check(model=Book)
|
||||
expected = [
|
||||
checks.Error(
|
||||
'"exclude" contains duplicate field(s).',
|
||||
hint=None,
|
||||
obj=ExcludedFields2,
|
||||
id='admin.E015',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_exclude_in_inline(self):
|
||||
class ExcludedFieldsInline(admin.TabularInline):
|
||||
model = Song
|
||||
exclude = 'foo'
|
||||
|
||||
class ExcludedFieldsAlbumAdmin(admin.ModelAdmin):
|
||||
model = Album
|
||||
inlines = [ExcludedFieldsInline]
|
||||
|
||||
errors = ExcludedFieldsAlbumAdmin.check(model=Album)
|
||||
expected = [
|
||||
checks.Error(
|
||||
'"exclude" must be a list or tuple.',
|
||||
hint=None,
|
||||
obj=ExcludedFieldsInline,
|
||||
id='admin.E014',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_exclude_inline_model_admin(self):
|
||||
"""
|
||||
Regression test for #9932 - exclude in InlineModelAdmin should not
|
||||
contain the ForeignKey field used in ModelAdmin.model
|
||||
"""
|
||||
|
||||
class SongInline(admin.StackedInline):
|
||||
model = Song
|
||||
exclude = ['album']
|
||||
|
||||
class AlbumAdmin(admin.ModelAdmin):
|
||||
model = Album
|
||||
inlines = [SongInline]
|
||||
|
||||
errors = AlbumAdmin.check(model=Album)
|
||||
expected = [
|
||||
checks.Error(
|
||||
('Cannot exclude the field "album", because it is the foreign key '
|
||||
'to the parent model admin_checks.Album.'),
|
||||
hint=None,
|
||||
obj=SongInline,
|
||||
id='admin.E201',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_app_label_in_admin_checks(self):
|
||||
"""
|
||||
Regression test for #15669 - Include app label in admin system check messages
|
||||
"""
|
||||
|
||||
class RawIdNonexistingAdmin(admin.ModelAdmin):
|
||||
raw_id_fields = ('nonexisting',)
|
||||
|
||||
errors = RawIdNonexistingAdmin.check(model=Album)
|
||||
expected = [
|
||||
checks.Error(
|
||||
('"raw_id_fields[0]" refers to field "nonexisting", which is '
|
||||
'missing from model admin_checks.Album.'),
|
||||
hint=None,
|
||||
obj=RawIdNonexistingAdmin,
|
||||
id='admin.E002',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_fk_exclusion(self):
|
||||
"""
|
||||
Regression test for #11709 - when testing for fk excluding (when exclude is
|
||||
given) make sure fk_name is honored or things blow up when there is more
|
||||
than one fk to the parent model.
|
||||
"""
|
||||
|
||||
class TwoAlbumFKAndAnEInline(admin.TabularInline):
|
||||
model = TwoAlbumFKAndAnE
|
||||
exclude = ("e",)
|
||||
fk_name = "album1"
|
||||
|
||||
class MyAdmin(admin.ModelAdmin):
|
||||
inlines = [TwoAlbumFKAndAnEInline]
|
||||
|
||||
errors = MyAdmin.check(model=Album)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_inline_self_check(self):
|
||||
class TwoAlbumFKAndAnEInline(admin.TabularInline):
|
||||
model = TwoAlbumFKAndAnE
|
||||
|
||||
class MyAdmin(admin.ModelAdmin):
|
||||
inlines = [TwoAlbumFKAndAnEInline]
|
||||
|
||||
errors = MyAdmin.check(model=Album)
|
||||
expected = [
|
||||
checks.Error(
|
||||
"'admin_checks.TwoAlbumFKAndAnE' has more than one ForeignKey to 'admin_checks.Album'.",
|
||||
hint=None,
|
||||
obj=TwoAlbumFKAndAnEInline,
|
||||
id='admin.E202',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_inline_with_specified(self):
|
||||
class TwoAlbumFKAndAnEInline(admin.TabularInline):
|
||||
model = TwoAlbumFKAndAnE
|
||||
fk_name = "album1"
|
||||
|
||||
class MyAdmin(admin.ModelAdmin):
|
||||
inlines = [TwoAlbumFKAndAnEInline]
|
||||
|
||||
errors = MyAdmin.check(model=Album)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_readonly(self):
|
||||
class SongAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ("title",)
|
||||
|
||||
errors = SongAdmin.check(model=Song)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_readonly_on_method(self):
|
||||
def my_function(obj):
|
||||
pass
|
||||
|
||||
class SongAdmin(admin.ModelAdmin):
|
||||
readonly_fields = (my_function,)
|
||||
|
||||
errors = SongAdmin.check(model=Song)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_readonly_on_modeladmin(self):
|
||||
class SongAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ("readonly_method_on_modeladmin",)
|
||||
|
||||
def readonly_method_on_modeladmin(self, obj):
|
||||
pass
|
||||
|
||||
errors = SongAdmin.check(model=Song)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_readonly_method_on_model(self):
|
||||
class SongAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ("readonly_method_on_model",)
|
||||
|
||||
errors = SongAdmin.check(model=Song)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_nonexistant_field(self):
|
||||
class SongAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ("title", "nonexistant")
|
||||
|
||||
errors = SongAdmin.check(model=Song)
|
||||
expected = [
|
||||
checks.Error(
|
||||
('"readonly_fields[1]" is neither a callable nor an attribute '
|
||||
'of "SongAdmin" nor found in the model admin_checks.Song.'),
|
||||
hint=None,
|
||||
obj=SongAdmin,
|
||||
id='admin.E035',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_nonexistant_field_on_inline(self):
|
||||
class CityInline(admin.TabularInline):
|
||||
model = City
|
||||
readonly_fields = ['i_dont_exist'] # Missing attribute
|
||||
|
||||
errors = CityInline.check(State)
|
||||
expected = [
|
||||
checks.Error(
|
||||
('"readonly_fields[0]" is neither a callable nor an attribute '
|
||||
'of "CityInline" nor found in the model admin_checks.City.'),
|
||||
hint=None,
|
||||
obj=CityInline,
|
||||
id='admin.E035',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_extra(self):
|
||||
class SongAdmin(admin.ModelAdmin):
|
||||
def awesome_song(self, instance):
|
||||
if instance.title == "Born to Run":
|
||||
return "Best Ever!"
|
||||
return "Status unknown."
|
||||
|
||||
errors = SongAdmin.check(model=Song)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_readonly_lambda(self):
|
||||
class SongAdmin(admin.ModelAdmin):
|
||||
readonly_fields = (lambda obj: "test",)
|
||||
|
||||
errors = SongAdmin.check(model=Song)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_graceful_m2m_fail(self):
|
||||
"""
|
||||
Regression test for #12203/#12237 - Fail more gracefully when a M2M field that
|
||||
specifies the 'through' option is included in the 'fields' or the 'fieldsets'
|
||||
ModelAdmin options.
|
||||
"""
|
||||
|
||||
class BookAdmin(admin.ModelAdmin):
|
||||
fields = ['authors']
|
||||
|
||||
errors = BookAdmin.check(model=Book)
|
||||
expected = [
|
||||
checks.Error(
|
||||
('"fields" cannot include the ManyToManyField "authors", '
|
||||
'because "authors" manually specifies relationship model.'),
|
||||
hint=None,
|
||||
obj=BookAdmin,
|
||||
id='admin.E013',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_cannot_include_through(self):
|
||||
class FieldsetBookAdmin(admin.ModelAdmin):
|
||||
fieldsets = (
|
||||
('Header 1', {'fields': ('name',)}),
|
||||
('Header 2', {'fields': ('authors',)}),
|
||||
)
|
||||
|
||||
errors = FieldsetBookAdmin.check(model=Book)
|
||||
expected = [
|
||||
checks.Error(
|
||||
('"fieldsets[1][1][\'fields\']" cannot include the ManyToManyField '
|
||||
'"authors", because "authors" manually specifies relationship model.'),
|
||||
hint=None,
|
||||
obj=FieldsetBookAdmin,
|
||||
id='admin.E013',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_nested_fields(self):
|
||||
class NestedFieldsAdmin(admin.ModelAdmin):
|
||||
fields = ('price', ('name', 'subtitle'))
|
||||
|
||||
errors = NestedFieldsAdmin.check(model=Book)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_nested_fieldsets(self):
|
||||
class NestedFieldsetAdmin(admin.ModelAdmin):
|
||||
fieldsets = (
|
||||
('Main', {'fields': ('price', ('name', 'subtitle'))}),
|
||||
)
|
||||
|
||||
errors = NestedFieldsetAdmin.check(model=Book)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_explicit_through_override(self):
|
||||
"""
|
||||
Regression test for #12209 -- If the explicitly provided through model
|
||||
is specified as a string, the admin should still be able use
|
||||
Model.m2m_field.through
|
||||
"""
|
||||
|
||||
class AuthorsInline(admin.TabularInline):
|
||||
model = Book.authors.through
|
||||
|
||||
class BookAdmin(admin.ModelAdmin):
|
||||
inlines = [AuthorsInline]
|
||||
|
||||
errors = BookAdmin.check(model=Book)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_non_model_fields(self):
|
||||
"""
|
||||
Regression for ensuring ModelAdmin.fields can contain non-model fields
|
||||
that broke with r11737
|
||||
"""
|
||||
|
||||
class SongForm(forms.ModelForm):
|
||||
extra_data = forms.CharField()
|
||||
|
||||
class FieldsOnFormOnlyAdmin(admin.ModelAdmin):
|
||||
form = SongForm
|
||||
fields = ['title', 'extra_data']
|
||||
|
||||
errors = FieldsOnFormOnlyAdmin.check(model=Song)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_non_model_first_field(self):
|
||||
"""
|
||||
Regression for ensuring ModelAdmin.field can handle first elem being a
|
||||
non-model field (test fix for UnboundLocalError introduced with r16225).
|
||||
"""
|
||||
|
||||
class SongForm(forms.ModelForm):
|
||||
extra_data = forms.CharField()
|
||||
|
||||
class Meta:
|
||||
model = Song
|
||||
fields = '__all__'
|
||||
|
||||
class FieldsOnFormOnlyAdmin(admin.ModelAdmin):
|
||||
form = SongForm
|
||||
fields = ['extra_data', 'title']
|
||||
|
||||
errors = FieldsOnFormOnlyAdmin.check(model=Song)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_validator_compatibility(self):
|
||||
class MyValidator(object):
|
||||
def validate(self, cls, model):
|
||||
raise ImproperlyConfigured("error!")
|
||||
|
||||
class MyModelAdmin(admin.ModelAdmin):
|
||||
validator_class = MyValidator
|
||||
|
||||
errors = MyModelAdmin.check(model=Song)
|
||||
expected = [
|
||||
checks.Error(
|
||||
'error!',
|
||||
hint=None,
|
||||
obj=MyModelAdmin,
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
27
tests/admin_scripts/app_raising_messages/models.py
Normal file
27
tests/admin_scripts/app_raising_messages/models.py
Normal file
@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core import checks
|
||||
from django.db import models
|
||||
|
||||
|
||||
class ModelRaisingMessages(models.Model):
|
||||
@classmethod
|
||||
def check(self, **kwargs):
|
||||
return [
|
||||
checks.Warning(
|
||||
'First warning',
|
||||
hint='Hint',
|
||||
obj='obj'
|
||||
),
|
||||
checks.Warning(
|
||||
'Second warning',
|
||||
hint=None,
|
||||
obj='a'
|
||||
),
|
||||
checks.Error(
|
||||
'An error',
|
||||
hint='Error hint',
|
||||
obj=None,
|
||||
)
|
||||
]
|
0
tests/admin_scripts/app_raising_warning/__init__.py
Normal file
0
tests/admin_scripts/app_raising_warning/__init__.py
Normal file
16
tests/admin_scripts/app_raising_warning/models.py
Normal file
16
tests/admin_scripts/app_raising_warning/models.py
Normal file
@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core import checks
|
||||
from django.db import models
|
||||
|
||||
|
||||
class ModelRaisingMessages(models.Model):
|
||||
@classmethod
|
||||
def check(self, **kwargs):
|
||||
return [
|
||||
checks.Warning(
|
||||
'A warning',
|
||||
hint=None,
|
||||
),
|
||||
]
|
@ -3,7 +3,7 @@ from django.core.management.base import AppCommand
|
||||
|
||||
class Command(AppCommand):
|
||||
help = 'Test Application-based commands'
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
args = '[app_label ...]'
|
||||
|
||||
def handle_app_config(self, app_config, **options):
|
||||
|
@ -10,7 +10,7 @@ class Command(BaseCommand):
|
||||
make_option('--option_c', '-c', action='store', dest='option_c', default='3'),
|
||||
)
|
||||
help = 'Test basic commands'
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
args = '[labels ...]'
|
||||
|
||||
def handle(self, *labels, **options):
|
||||
|
@ -3,7 +3,7 @@ from django.core.management.base import LabelCommand
|
||||
|
||||
class Command(LabelCommand):
|
||||
help = "Test Label-based commands"
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
args = '<label>'
|
||||
|
||||
def handle_label(self, label, **options):
|
||||
|
@ -3,7 +3,7 @@ from django.core.management.base import NoArgsCommand
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = "Test No-args commands"
|
||||
requires_model_validation = False
|
||||
requires_system_checks = False
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
print('EXECUTE:NoArgsCommand options=%s' % sorted(options.items()))
|
||||
|
@ -0,0 +1,11 @@
|
||||
from django.core.management.base import NoArgsCommand
|
||||
|
||||
|
||||
class InvalidCommand(NoArgsCommand):
|
||||
help = ("Test raising an error if both requires_system_checks "
|
||||
"and requires_model_validation are defined.")
|
||||
requires_system_checks = True
|
||||
requires_model_validation = True
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
pass
|
@ -1,4 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
"""
|
||||
A series of tests to establish that the command-line managment tools work as
|
||||
advertised - especially with regards to the handling of the DJANGO_SETTINGS_MODULE
|
||||
@ -18,14 +20,15 @@ import unittest
|
||||
import django
|
||||
from django import conf, get_version
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.management import BaseCommand, CommandError, call_command
|
||||
from django.db import connection
|
||||
from django.test.runner import DiscoverRunner
|
||||
from django.test.utils import str_prefix
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils._os import upath
|
||||
from django.utils.six import StringIO
|
||||
from django.test import LiveServerTestCase, TestCase
|
||||
from django.test.runner import DiscoverRunner
|
||||
from django.test.utils import str_prefix
|
||||
|
||||
|
||||
test_dir = os.path.realpath(os.path.join(os.environ['DJANGO_TEST_TEMP_DIR'], 'test_project'))
|
||||
@ -52,6 +55,7 @@ class AdminScriptTestCase(unittest.TestCase):
|
||||
'DATABASES',
|
||||
'ROOT_URLCONF',
|
||||
'SECRET_KEY',
|
||||
'TEST_RUNNER', # We need to include TEST_RUNNER, otherwise we get a compatibility warning.
|
||||
]
|
||||
for s in exports:
|
||||
if hasattr(settings, s):
|
||||
@ -1072,49 +1076,125 @@ class ManageSettingsWithSettingsErrors(AdminScriptTestCase):
|
||||
self.assertNoOutput(err)
|
||||
|
||||
|
||||
class ManageValidate(AdminScriptTestCase):
|
||||
class ManageCheck(AdminScriptTestCase):
|
||||
def tearDown(self):
|
||||
self.remove_settings('settings.py')
|
||||
|
||||
def test_nonexistent_app(self):
|
||||
"manage.py validate reports an error on a non-existent app in INSTALLED_APPS"
|
||||
self.write_settings('settings.py', apps=['admin_scriptz.broken_app'], sdict={'USE_I18N': False})
|
||||
args = ['validate']
|
||||
""" manage.py check reports an error on a non-existent app in
|
||||
INSTALLED_APPS """
|
||||
|
||||
self.write_settings('settings.py',
|
||||
apps=['admin_scriptz.broken_app'],
|
||||
sdict={'USE_I18N': False})
|
||||
args = ['check']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(out)
|
||||
self.assertOutput(err, 'ImportError')
|
||||
self.assertOutput(err, 'No module named')
|
||||
self.assertOutput(err, 'admin_scriptz')
|
||||
|
||||
def test_broken_app(self):
|
||||
"manage.py validate reports an ImportError if an app's models.py raises one on import"
|
||||
""" manage.py check reports an ImportError if an app's models.py
|
||||
raises one on import """
|
||||
|
||||
self.write_settings('settings.py', apps=['admin_scripts.broken_app'])
|
||||
args = ['validate']
|
||||
args = ['check']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(out)
|
||||
self.assertOutput(err, 'ImportError')
|
||||
|
||||
def test_complex_app(self):
|
||||
"manage.py validate does not raise an ImportError validating a complex app"
|
||||
self.write_settings('settings.py',
|
||||
apps=['admin_scripts.complex_app', 'admin_scripts.simple_app'],
|
||||
sdict={'DEBUG': True})
|
||||
args = ['validate']
|
||||
""" manage.py check does not raise an ImportError validating a
|
||||
complex app with nested calls to load_app """
|
||||
|
||||
self.write_settings(
|
||||
'settings.py',
|
||||
apps=[
|
||||
'admin_scripts.complex_app',
|
||||
'admin_scripts.simple_app',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
],
|
||||
sdict={
|
||||
'DEBUG': True
|
||||
}
|
||||
)
|
||||
args = ['check']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, '0 errors found')
|
||||
self.assertEqual(out, 'System check identified no issues.\n')
|
||||
|
||||
def test_app_with_import(self):
|
||||
"manage.py validate does not raise errors when an app imports a base class that itself has an abstract base"
|
||||
""" manage.py check does not raise errors when an app imports a base
|
||||
class that itself has an abstract base. """
|
||||
|
||||
self.write_settings('settings.py',
|
||||
apps=['admin_scripts.app_with_import',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sites'],
|
||||
sdict={'DEBUG': True})
|
||||
args = ['validate']
|
||||
args = ['check']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, '0 errors found')
|
||||
self.assertEqual(out, 'System check identified no issues.\n')
|
||||
|
||||
def test_output_format(self):
|
||||
""" All errors/warnings should be sorted by level and by message. """
|
||||
|
||||
self.write_settings('settings.py',
|
||||
apps=['admin_scripts.app_raising_messages',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes'],
|
||||
sdict={'DEBUG': True})
|
||||
args = ['check']
|
||||
out, err = self.run_manage(args)
|
||||
expected_err = (
|
||||
"CommandError: System check identified some issues:\n"
|
||||
"\n"
|
||||
"ERRORS:\n"
|
||||
"?: An error\n"
|
||||
"\tHINT: Error hint\n"
|
||||
"\n"
|
||||
"WARNINGS:\n"
|
||||
"a: Second warning\n"
|
||||
"obj: First warning\n"
|
||||
"\tHINT: Hint\n"
|
||||
"\n"
|
||||
"System check identified 3 issues.\n"
|
||||
)
|
||||
self.assertEqual(err, expected_err)
|
||||
self.assertNoOutput(out)
|
||||
|
||||
def test_warning_does_not_halt(self):
|
||||
"""
|
||||
When there are only warnings or less serious messages, then Django
|
||||
should not prevent user from launching their project, so `check`
|
||||
command should not raise `CommandError` exception.
|
||||
|
||||
In this test we also test output format.
|
||||
|
||||
"""
|
||||
|
||||
self.write_settings('settings.py',
|
||||
apps=['admin_scripts.app_raising_warning',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes'],
|
||||
sdict={'DEBUG': True})
|
||||
args = ['check']
|
||||
out, err = self.run_manage(args)
|
||||
expected_err = (
|
||||
"System check identified some issues:\n" # No "CommandError: " part
|
||||
"\n"
|
||||
"WARNINGS:\n"
|
||||
"?: A warning\n"
|
||||
"\n"
|
||||
"System check identified 1 issue.\n"
|
||||
)
|
||||
self.assertEqual(err, expected_err)
|
||||
self.assertNoOutput(out)
|
||||
|
||||
|
||||
class CustomTestRunner(DiscoverRunner):
|
||||
@ -1311,37 +1391,44 @@ class CommandTypes(AdminScriptTestCase):
|
||||
def test_base_command(self):
|
||||
"User BaseCommands can execute when a label is provided"
|
||||
args = ['base_command', 'testlabel']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
expected_labels = "('testlabel',)"
|
||||
self._test_base_command(args, expected_labels)
|
||||
|
||||
def test_base_command_no_label(self):
|
||||
"User BaseCommands can execute when no labels are provided"
|
||||
args = ['base_command']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=(), options=[('no_color', False), ('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
expected_labels = "()"
|
||||
self._test_base_command(args, expected_labels)
|
||||
|
||||
def test_base_command_multiple_label(self):
|
||||
"User BaseCommands can execute when no labels are provided"
|
||||
args = ['base_command', 'testlabel', 'anotherlabel']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel', 'anotherlabel'), options=[('no_color', False), ('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
expected_labels = "('testlabel', 'anotherlabel')"
|
||||
self._test_base_command(args, expected_labels)
|
||||
|
||||
def test_base_command_with_option(self):
|
||||
"User BaseCommands can execute with options when a label is provided"
|
||||
args = ['base_command', 'testlabel', '--option_a=x']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
expected_labels = "('testlabel',)"
|
||||
self._test_base_command(args, expected_labels, option_a="'x'")
|
||||
|
||||
def test_base_command_with_options(self):
|
||||
"User BaseCommands can execute with multiple options when a label is provided"
|
||||
args = ['base_command', 'testlabel', '-a', 'x', '--option_b=y']
|
||||
expected_labels = "('testlabel',)"
|
||||
self._test_base_command(args, expected_labels, option_a="'x'", option_b="'y'")
|
||||
|
||||
def _test_base_command(self, args, labels, option_a="'1'", option_b="'2'"):
|
||||
out, err = self.run_manage(args)
|
||||
|
||||
expected_out = str_prefix(
|
||||
("EXECUTE:BaseCommand labels=%%s, "
|
||||
"options=[('no_color', False), ('option_a', %%s), ('option_b', %%s), "
|
||||
"('option_c', '3'), ('pythonpath', None), ('settings', None), "
|
||||
"('traceback', None), ('verbosity', %(_)s'1')]")
|
||||
) % (labels, option_a, option_b)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
self.assertOutput(out, expected_out)
|
||||
|
||||
def test_base_run_from_argv(self):
|
||||
"""
|
||||
@ -1468,6 +1555,10 @@ class CommandTypes(AdminScriptTestCase):
|
||||
self.assertOutput(out, str_prefix("EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
self.assertOutput(out, str_prefix("EXECUTE:LabelCommand label=anotherlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
|
||||
def test_requires_model_validation_and_requires_system_checks_both_defined(self):
|
||||
from .management.commands.validation_command import InvalidCommand
|
||||
self.assertRaises(ImproperlyConfigured, InvalidCommand)
|
||||
|
||||
|
||||
class Discovery(TestCase):
|
||||
|
||||
@ -1476,12 +1567,16 @@ class Discovery(TestCase):
|
||||
Apps listed first in INSTALLED_APPS have precendence.
|
||||
"""
|
||||
with self.settings(INSTALLED_APPS=['admin_scripts.complex_app',
|
||||
'admin_scripts.simple_app']):
|
||||
'admin_scripts.simple_app',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes']):
|
||||
out = StringIO()
|
||||
call_command('duplicate', stdout=out)
|
||||
self.assertEqual(out.getvalue().strip(), 'complex_app')
|
||||
with self.settings(INSTALLED_APPS=['admin_scripts.simple_app',
|
||||
'admin_scripts.complex_app']):
|
||||
'admin_scripts.complex_app',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes']):
|
||||
out = StringIO()
|
||||
call_command('duplicate', stdout=out)
|
||||
self.assertEqual(out.getvalue().strip(), 'simple_app')
|
||||
@ -1505,39 +1600,35 @@ class ArgumentOrder(AdminScriptTestCase):
|
||||
self.remove_settings('alternate_settings.py')
|
||||
|
||||
def test_setting_then_option(self):
|
||||
"Options passed after settings are correctly handled"
|
||||
""" Options passed after settings are correctly handled. """
|
||||
args = ['base_command', 'testlabel', '--settings=alternate_settings', '--option_a=x']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
self._test(args)
|
||||
|
||||
def test_setting_then_short_option(self):
|
||||
"Short options passed after settings are correctly handled"
|
||||
args = ['base_command', 'testlabel', '--settings=alternate_settings', '--option_a=x']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
""" Short options passed after settings are correctly handled. """
|
||||
args = ['base_command', 'testlabel', '--settings=alternate_settings', '-a', 'x']
|
||||
self._test(args)
|
||||
|
||||
def test_option_then_setting(self):
|
||||
"Options passed before settings are correctly handled"
|
||||
""" Options passed before settings are correctly handled. """
|
||||
args = ['base_command', 'testlabel', '--option_a=x', '--settings=alternate_settings']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
self._test(args)
|
||||
|
||||
def test_short_option_then_setting(self):
|
||||
"Short options passed before settings are correctly handled"
|
||||
""" Short options passed before settings are correctly handled. """
|
||||
args = ['base_command', 'testlabel', '-a', 'x', '--settings=alternate_settings']
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
self._test(args)
|
||||
|
||||
def test_option_then_setting_then_option(self):
|
||||
"Options are correctly handled when they are passed before and after a setting"
|
||||
""" Options are correctly handled when they are passed before and after
|
||||
a setting. """
|
||||
args = ['base_command', 'testlabel', '--option_a=x', '--settings=alternate_settings', '--option_b=y']
|
||||
self._test(args, option_b="'y'")
|
||||
|
||||
def _test(self, args, option_b="'2'"):
|
||||
out, err = self.run_manage(args)
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]"))
|
||||
self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', %%s), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]") % option_b)
|
||||
|
||||
|
||||
class StartProject(LiveServerTestCase, AdminScriptTestCase):
|
||||
|
@ -142,8 +142,8 @@ class ValidationTestCase(TestCase):
|
||||
class MyAdmin(admin.ModelAdmin):
|
||||
inlines = [TwoAlbumFKAndAnEInline]
|
||||
|
||||
self.assertRaisesMessage(Exception,
|
||||
"<class 'admin_validation.models.TwoAlbumFKAndAnE'> has more than 1 ForeignKey to <class 'admin_validation.models.Album'>",
|
||||
self.assertRaisesMessage(ValueError,
|
||||
"'admin_validation.TwoAlbumFKAndAnE' has more than one ForeignKey to 'admin_validation.Album'.",
|
||||
MyAdmin.validate, Album)
|
||||
|
||||
def test_inline_with_specified(self):
|
||||
|
@ -8,6 +8,7 @@ import unittest
|
||||
|
||||
from django.conf import settings, global_settings
|
||||
from django.core import mail
|
||||
from django.core.checks import Error
|
||||
from django.core.files import temp as tempfile
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
@ -4675,17 +4676,27 @@ class AdminViewOnSiteTests(TestCase):
|
||||
self.assertEqual(['Children must share a family name with their parents in this contrived test case'],
|
||||
error_set.get('__all__'))
|
||||
|
||||
def test_validate(self):
|
||||
def test_check(self):
|
||||
"Ensure that the view_on_site value is either a boolean or a callable"
|
||||
CityAdmin.view_on_site = True
|
||||
CityAdmin.validate(City)
|
||||
CityAdmin.view_on_site = False
|
||||
CityAdmin.validate(City)
|
||||
CityAdmin.view_on_site = lambda obj: obj.get_absolute_url()
|
||||
CityAdmin.validate(City)
|
||||
CityAdmin.view_on_site = []
|
||||
with self.assertRaisesMessage(ImproperlyConfigured, 'CityAdmin.view_on_site is not a callable or a boolean value.'):
|
||||
CityAdmin.validate(City)
|
||||
try:
|
||||
CityAdmin.view_on_site = True
|
||||
self.assertEqual(CityAdmin.check(City), [])
|
||||
CityAdmin.view_on_site = False
|
||||
self.assertEqual(CityAdmin.check(City), [])
|
||||
CityAdmin.view_on_site = lambda obj: obj.get_absolute_url()
|
||||
self.assertEqual(CityAdmin.check(City), [])
|
||||
CityAdmin.view_on_site = []
|
||||
self.assertEqual(CityAdmin.check(City), [
|
||||
Error(
|
||||
'"view_on_site" is not a callable or a boolean value.',
|
||||
hint=None,
|
||||
obj=CityAdmin,
|
||||
id='admin.E025',
|
||||
),
|
||||
])
|
||||
finally:
|
||||
# Restore the original values for the benefit of other tests.
|
||||
CityAdmin.view_on_site = True
|
||||
|
||||
def test_false(self):
|
||||
"Ensure that the 'View on site' button is not displayed if view_on_site is False"
|
||||
|
@ -1,128 +0,0 @@
|
||||
from django.core.checks.compatibility import base
|
||||
from django.core.checks.compatibility import django_1_6_0
|
||||
from django.core.management.commands import check
|
||||
from django.core.management import call_command
|
||||
from django.db.models.fields import NOT_PROVIDED
|
||||
from django.test import TestCase
|
||||
|
||||
from .models import Book
|
||||
|
||||
|
||||
class StubCheckModule(object):
|
||||
# Has no ``run_checks`` attribute & will trigger a warning.
|
||||
__name__ = 'StubCheckModule'
|
||||
|
||||
|
||||
class FakeWarnings(object):
|
||||
def __init__(self):
|
||||
self._warnings = []
|
||||
|
||||
def warn(self, message):
|
||||
self._warnings.append(message)
|
||||
|
||||
|
||||
class CompatChecksTestCase(TestCase):
|
||||
def setUp(self):
|
||||
super(CompatChecksTestCase, self).setUp()
|
||||
|
||||
# We're going to override the list of checks to perform for test
|
||||
# consistency in the future.
|
||||
self.old_compat_checks = base.COMPAT_CHECKS
|
||||
base.COMPAT_CHECKS = [
|
||||
django_1_6_0,
|
||||
]
|
||||
|
||||
def tearDown(self):
|
||||
# Restore what's supposed to be in ``COMPAT_CHECKS``.
|
||||
base.COMPAT_CHECKS = self.old_compat_checks
|
||||
super(CompatChecksTestCase, self).tearDown()
|
||||
|
||||
def test_check_test_runner_new_default(self):
|
||||
with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'):
|
||||
result = django_1_6_0.check_test_runner()
|
||||
self.assertTrue("Django 1.6 introduced a new default test runner" in result)
|
||||
|
||||
def test_check_test_runner_overridden(self):
|
||||
with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
|
||||
self.assertEqual(django_1_6_0.check_test_runner(), None)
|
||||
|
||||
def test_run_checks_new_default(self):
|
||||
with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'):
|
||||
result = django_1_6_0.run_checks()
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertTrue("Django 1.6 introduced a new default test runner" in result[0])
|
||||
|
||||
def test_run_checks_overridden(self):
|
||||
with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
|
||||
self.assertEqual(len(django_1_6_0.run_checks()), 0)
|
||||
|
||||
def test_boolean_field_default_value(self):
|
||||
with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
|
||||
# We patch the field's default value to trigger the warning
|
||||
boolean_field = Book._meta.get_field('is_published')
|
||||
old_default = boolean_field.default
|
||||
try:
|
||||
boolean_field.default = NOT_PROVIDED
|
||||
result = django_1_6_0.run_checks()
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertTrue("You have not set a default value for one or more BooleanFields" in result[0])
|
||||
self.assertTrue('check.Book: "is_published"' in result[0])
|
||||
# We did not patch the BlogPost.is_published field so
|
||||
# there should not be a warning about it
|
||||
self.assertFalse('check.BlogPost' in result[0])
|
||||
finally:
|
||||
# Restore the ``default``
|
||||
boolean_field.default = old_default
|
||||
|
||||
def test_check_compatibility(self):
|
||||
with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'):
|
||||
result = base.check_compatibility()
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertTrue("Django 1.6 introduced a new default test runner" in result[0])
|
||||
|
||||
with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
|
||||
self.assertEqual(len(base.check_compatibility()), 0)
|
||||
|
||||
def test_check_compatibility_warning(self):
|
||||
# First, we're patching over the ``COMPAT_CHECKS`` with a stub which
|
||||
# will trigger the warning.
|
||||
base.COMPAT_CHECKS = [
|
||||
StubCheckModule(),
|
||||
]
|
||||
|
||||
# Next, we unfortunately have to patch out ``warnings``.
|
||||
old_warnings = base.warnings
|
||||
base.warnings = FakeWarnings()
|
||||
|
||||
self.assertEqual(len(base.warnings._warnings), 0)
|
||||
|
||||
with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
|
||||
self.assertEqual(len(base.check_compatibility()), 0)
|
||||
|
||||
self.assertEqual(len(base.warnings._warnings), 1)
|
||||
self.assertTrue("The 'StubCheckModule' module lacks a 'run_checks'" in base.warnings._warnings[0])
|
||||
|
||||
# Restore the ``warnings``.
|
||||
base.warnings = old_warnings
|
||||
|
||||
def test_management_command(self):
|
||||
# Again, we unfortunately have to patch out ``warnings``. Different
|
||||
old_warnings = check.warnings
|
||||
check.warnings = FakeWarnings()
|
||||
|
||||
self.assertEqual(len(check.warnings._warnings), 0)
|
||||
|
||||
# Should not produce any warnings.
|
||||
with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
|
||||
call_command('check')
|
||||
|
||||
self.assertEqual(len(check.warnings._warnings), 0)
|
||||
|
||||
with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'):
|
||||
call_command('check')
|
||||
|
||||
self.assertEqual(len(check.warnings._warnings), 1)
|
||||
self.assertTrue("Django 1.6 introduced a new default test runner" in check.warnings._warnings[0])
|
||||
|
||||
# Restore the ``warnings``.
|
||||
base.warnings = old_warnings
|
0
tests/check_framework/__init__.py
Normal file
0
tests/check_framework/__init__.py
Normal file
@ -1,6 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class SimpleModel(models.Model):
|
||||
field = models.IntegerField()
|
||||
manager = models.manager.Manager()
|
||||
|
||||
|
||||
class Book(models.Model):
|
||||
title = models.CharField(max_length=250)
|
||||
is_published = models.BooleanField(default=False)
|
219
tests/check_framework/tests.py
Normal file
219
tests/check_framework/tests.py
Normal file
@ -0,0 +1,219 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.six import StringIO
|
||||
import sys
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core import checks
|
||||
from django.core.checks import Error
|
||||
from django.core.checks.registry import CheckRegistry
|
||||
from django.core.checks.compatibility.django_1_6_0 import check_1_6_compatibility
|
||||
from django.core.management.base import CommandError
|
||||
from django.core.management import call_command
|
||||
from django.db.models.fields import NOT_PROVIDED
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings, override_system_checks
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from .models import SimpleModel, Book
|
||||
|
||||
|
||||
class DummyObj(object):
|
||||
def __repr__(self):
|
||||
return "obj"
|
||||
|
||||
|
||||
class SystemCheckFrameworkTests(TestCase):
|
||||
|
||||
def test_register_and_run_checks(self):
|
||||
calls = [0]
|
||||
|
||||
registry = CheckRegistry()
|
||||
|
||||
@registry.register()
|
||||
def f(**kwargs):
|
||||
calls[0] += 1
|
||||
return [1, 2, 3]
|
||||
errors = registry.run_checks()
|
||||
self.assertEqual(errors, [1, 2, 3])
|
||||
self.assertEqual(calls[0], 1)
|
||||
|
||||
|
||||
class MessageTests(TestCase):
|
||||
|
||||
def test_printing(self):
|
||||
e = Error("Message", hint="Hint", obj=DummyObj())
|
||||
expected = "obj: Message\n\tHINT: Hint"
|
||||
self.assertEqual(force_text(e), expected)
|
||||
|
||||
def test_printing_no_hint(self):
|
||||
e = Error("Message", hint=None, obj=DummyObj())
|
||||
expected = "obj: Message"
|
||||
self.assertEqual(force_text(e), expected)
|
||||
|
||||
def test_printing_no_object(self):
|
||||
e = Error("Message", hint="Hint", obj=None)
|
||||
expected = "?: Message\n\tHINT: Hint"
|
||||
self.assertEqual(force_text(e), expected)
|
||||
|
||||
def test_printing_with_given_id(self):
|
||||
e = Error("Message", hint="Hint", obj=DummyObj(), id="ID")
|
||||
expected = "obj: (ID) Message\n\tHINT: Hint"
|
||||
self.assertEqual(force_text(e), expected)
|
||||
|
||||
def test_printing_field_error(self):
|
||||
field = SimpleModel._meta.get_field('field')
|
||||
e = Error("Error", hint=None, obj=field)
|
||||
expected = "check_framework.SimpleModel.field: Error"
|
||||
self.assertEqual(force_text(e), expected)
|
||||
|
||||
def test_printing_model_error(self):
|
||||
e = Error("Error", hint=None, obj=SimpleModel)
|
||||
expected = "check_framework.SimpleModel: Error"
|
||||
self.assertEqual(force_text(e), expected)
|
||||
|
||||
def test_printing_manager_error(self):
|
||||
manager = SimpleModel.manager
|
||||
e = Error("Error", hint=None, obj=manager)
|
||||
expected = "check_framework.SimpleModel.manager: Error"
|
||||
self.assertEqual(force_text(e), expected)
|
||||
|
||||
|
||||
class Django_1_6_0_CompatibilityChecks(TestCase):
|
||||
|
||||
@override_settings(TEST_RUNNER='django.test.runner.DiscoverRunner')
|
||||
def test_test_runner_new_default(self):
|
||||
errors = check_1_6_compatibility()
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
@override_settings(TEST_RUNNER='myapp.test.CustomRunner')
|
||||
def test_test_runner_overriden(self):
|
||||
errors = check_1_6_compatibility()
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_test_runner_not_set_explicitly(self):
|
||||
# We remove some settings to make this look like a project generated under Django 1.5.
|
||||
old_test_runner = settings._wrapped.TEST_RUNNER
|
||||
del settings._wrapped.TEST_RUNNER
|
||||
settings._wrapped._explicit_settings.add('MANAGERS')
|
||||
settings._wrapped._explicit_settings.add('ADMINS')
|
||||
try:
|
||||
errors = check_1_6_compatibility()
|
||||
expected = [
|
||||
checks.Warning(
|
||||
"Some project unittests may not execute as expected.",
|
||||
hint=("Django 1.6 introduced a new default test runner. It looks like "
|
||||
"this project was generated using Django 1.5 or earlier. You should "
|
||||
"ensure your tests are all running & behaving as expected. See "
|
||||
"https://docs.djangoproject.com/en/dev/releases/1.6/#discovery-of-tests-in-any-test-module "
|
||||
"for more information."),
|
||||
obj=None,
|
||||
id='1_6.W001',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
finally:
|
||||
# Restore settings value
|
||||
settings._wrapped.TEST_RUNNER = old_test_runner
|
||||
settings._wrapped._explicit_settings.remove('MANAGERS')
|
||||
settings._wrapped._explicit_settings.remove('ADMINS')
|
||||
|
||||
def test_boolean_field_default_value(self):
|
||||
with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
|
||||
# We patch the field's default value to trigger the warning
|
||||
boolean_field = Book._meta.get_field('is_published')
|
||||
old_default = boolean_field.default
|
||||
try:
|
||||
boolean_field.default = NOT_PROVIDED
|
||||
errors = check_1_6_compatibility()
|
||||
expected = [
|
||||
checks.Warning(
|
||||
'BooleanField does not have a default value. ',
|
||||
hint=('Django 1.6 changed the default value of BooleanField from False to None. '
|
||||
'See https://docs.djangoproject.com/en/1.6/ref/models/fields/#booleanfield '
|
||||
'for more information.'),
|
||||
obj=boolean_field,
|
||||
id='1_6.W002',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
finally:
|
||||
# Restore the ``default``
|
||||
boolean_field.default = old_default
|
||||
|
||||
|
||||
def simple_system_check(**kwargs):
|
||||
simple_system_check.kwargs = kwargs
|
||||
return []
|
||||
|
||||
|
||||
def tagged_system_check(**kwargs):
|
||||
tagged_system_check.kwargs = kwargs
|
||||
return []
|
||||
tagged_system_check.tags = ['simpletag']
|
||||
|
||||
|
||||
class CheckCommandTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
simple_system_check.kwargs = None
|
||||
tagged_system_check.kwargs = None
|
||||
self.old_stdout, self.old_stderr = sys.stdout, sys.stderr
|
||||
sys.stdout, sys.stderr = StringIO(), StringIO()
|
||||
|
||||
def tearDown(self):
|
||||
sys.stdout, sys.stderr = self.old_stdout, self.old_stderr
|
||||
|
||||
@override_system_checks([simple_system_check, tagged_system_check])
|
||||
def test_simple_call(self):
|
||||
call_command('check')
|
||||
self.assertEqual(simple_system_check.kwargs, {'app_configs': None})
|
||||
self.assertEqual(tagged_system_check.kwargs, {'app_configs': None})
|
||||
|
||||
@override_system_checks([simple_system_check, tagged_system_check])
|
||||
def test_given_app(self):
|
||||
call_command('check', 'auth', 'admin')
|
||||
auth_config = apps.get_app_config('auth')
|
||||
admin_config = apps.get_app_config('admin')
|
||||
self.assertEqual(simple_system_check.kwargs, {'app_configs': [auth_config, admin_config]})
|
||||
self.assertEqual(tagged_system_check.kwargs, {'app_configs': [auth_config, admin_config]})
|
||||
|
||||
@override_system_checks([simple_system_check, tagged_system_check])
|
||||
def test_given_tag(self):
|
||||
call_command('check', tags=['simpletag'])
|
||||
self.assertEqual(simple_system_check.kwargs, None)
|
||||
self.assertEqual(tagged_system_check.kwargs, {'app_configs': None})
|
||||
|
||||
@override_system_checks([simple_system_check, tagged_system_check])
|
||||
def test_invalid_tag(self):
|
||||
self.assertRaises(CommandError, call_command, 'check', tags=['missingtag'])
|
||||
|
||||
|
||||
def custom_system_check(app_configs, **kwargs):
|
||||
return [
|
||||
Error(
|
||||
'Error',
|
||||
hint=None,
|
||||
id='mycheck.E001',
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class SilencingCheckTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.old_stdout, self.old_stderr = sys.stdout, sys.stderr
|
||||
sys.stdout, sys.stderr = StringIO(), StringIO()
|
||||
|
||||
def tearDown(self):
|
||||
sys.stdout, sys.stderr = self.old_stdout, self.old_stderr
|
||||
|
||||
@override_settings(SILENCED_SYSTEM_CHECKS=['mycheck.E001'])
|
||||
@override_system_checks([custom_system_check])
|
||||
def test_simple(self):
|
||||
try:
|
||||
call_command('check')
|
||||
except CommandError:
|
||||
self.fail("The mycheck.E001 check should be silenced.")
|
@ -1,9 +1,14 @@
|
||||
from __future__ import unicode_literals
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.apps.registry import Apps, apps
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core import checks
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from .models import Author, Article
|
||||
|
||||
@ -67,3 +72,297 @@ class ContentTypesViewsTests(TestCase):
|
||||
self.assertEqual(ct.app_label, 'my_great_app')
|
||||
self.assertEqual(ct.model, 'modelcreatedonthefly')
|
||||
self.assertEqual(ct.name, 'a model created on the fly')
|
||||
|
||||
|
||||
class IsolatedModelsTestCase(TestCase):
|
||||
def setUp(self):
|
||||
# The unmanaged models need to be removed after the test in order to
|
||||
# prevent bad interactions with the flush operation in other tests.
|
||||
self._old_models = apps.app_configs['contenttypes_tests'].models.copy()
|
||||
|
||||
def tearDown(self):
|
||||
apps.app_configs['contenttypes_tests'].models = self._old_models
|
||||
apps.all_models['contenttypes_tests'] = self._old_models
|
||||
apps.clear_cache()
|
||||
|
||||
|
||||
class GenericForeignKeyTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_str(self):
|
||||
class Model(models.Model):
|
||||
field = generic.GenericForeignKey()
|
||||
expected = "contenttypes_tests.Model.field"
|
||||
actual = force_str(Model.field)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_missing_content_type_field(self):
|
||||
class TaggedItem(models.Model):
|
||||
# no content_type field
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey()
|
||||
|
||||
errors = TaggedItem.content_object.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
'The field refers to TaggedItem.content_type field which is missing.',
|
||||
hint=None,
|
||||
obj=TaggedItem.content_object,
|
||||
id='contenttypes.E005',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_invalid_content_type_field(self):
|
||||
class Model(models.Model):
|
||||
content_type = models.IntegerField() # should be ForeignKey
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey(
|
||||
'content_type', 'object_id')
|
||||
|
||||
errors = Model.content_object.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
('"content_type" field is used by a GenericForeignKey '
|
||||
'as content type field and therefore it must be '
|
||||
'a ForeignKey.'),
|
||||
hint=None,
|
||||
obj=Model.content_object,
|
||||
id='contenttypes.E006',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_content_type_field_pointing_to_wrong_model(self):
|
||||
class Model(models.Model):
|
||||
content_type = models.ForeignKey('self') # should point to ContentType
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey(
|
||||
'content_type', 'object_id')
|
||||
|
||||
errors = Model.content_object.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
('"content_type" field is used by a GenericForeignKey '
|
||||
'as content type field and therefore it must be '
|
||||
'a ForeignKey to ContentType.'),
|
||||
hint=None,
|
||||
obj=Model.content_object,
|
||||
id='contenttypes.E007',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_missing_object_id_field(self):
|
||||
class TaggedItem(models.Model):
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
# missing object_id field
|
||||
content_object = generic.GenericForeignKey()
|
||||
|
||||
errors = TaggedItem.content_object.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
'The field refers to "object_id" field which is missing.',
|
||||
hint=None,
|
||||
obj=TaggedItem.content_object,
|
||||
id='contenttypes.E001',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_field_name_ending_with_underscore(self):
|
||||
class Model(models.Model):
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object_ = generic.GenericForeignKey(
|
||||
'content_type', 'object_id')
|
||||
|
||||
errors = Model.content_object_.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
'Field names must not end with underscores.',
|
||||
hint=None,
|
||||
obj=Model.content_object_,
|
||||
id='contenttypes.E002',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_generic_foreign_key_checks_are_performed(self):
|
||||
class MyGenericForeignKey(generic.GenericForeignKey):
|
||||
def check(self, **kwargs):
|
||||
return ['performed!']
|
||||
|
||||
class Model(models.Model):
|
||||
content_object = MyGenericForeignKey()
|
||||
|
||||
errors = checks.run_checks()
|
||||
self.assertEqual(errors, ['performed!'])
|
||||
|
||||
|
||||
class GenericRelationshipTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_valid_generic_relationship(self):
|
||||
class TaggedItem(models.Model):
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey()
|
||||
|
||||
class Bookmark(models.Model):
|
||||
tags = generic.GenericRelation('TaggedItem')
|
||||
|
||||
errors = Bookmark.tags.field.check()
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_valid_generic_relationship_with_explicit_fields(self):
|
||||
class TaggedItem(models.Model):
|
||||
custom_content_type = models.ForeignKey(ContentType)
|
||||
custom_object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey(
|
||||
'custom_content_type', 'custom_object_id')
|
||||
|
||||
class Bookmark(models.Model):
|
||||
tags = generic.GenericRelation('TaggedItem',
|
||||
content_type_field='custom_content_type',
|
||||
object_id_field='custom_object_id')
|
||||
|
||||
errors = Bookmark.tags.field.check()
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_pointing_to_missing_model(self):
|
||||
class Model(models.Model):
|
||||
rel = generic.GenericRelation('MissingModel')
|
||||
|
||||
errors = Model.rel.field.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
('The field has a relation with model MissingModel, '
|
||||
'which has either not been installed or is abstract.'),
|
||||
hint=('Ensure that you did not misspell the model name and '
|
||||
'the model is not abstract. Does your INSTALLED_APPS '
|
||||
'setting contain the app where MissingModel is defined?'),
|
||||
obj=Model.rel.field,
|
||||
id='E030',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_valid_self_referential_generic_relationship(self):
|
||||
class Model(models.Model):
|
||||
rel = generic.GenericRelation('Model')
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey(
|
||||
'content_type', 'object_id')
|
||||
|
||||
errors = Model.rel.field.check()
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_missing_content_type_field(self):
|
||||
class TaggedItem(models.Model):
|
||||
# no content_type field
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey()
|
||||
|
||||
class Bookmark(models.Model):
|
||||
tags = generic.GenericRelation('TaggedItem')
|
||||
|
||||
errors = Bookmark.tags.field.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
'The field refers to TaggedItem.content_type field which is missing.',
|
||||
hint=None,
|
||||
obj=Bookmark.tags.field,
|
||||
id='contenttypes.E005',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_missing_object_id_field(self):
|
||||
class TaggedItem(models.Model):
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
# missing object_id field
|
||||
content_object = generic.GenericForeignKey()
|
||||
|
||||
class Bookmark(models.Model):
|
||||
tags = generic.GenericRelation('TaggedItem')
|
||||
|
||||
errors = Bookmark.tags.field.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
'The field refers to TaggedItem.object_id field which is missing.',
|
||||
hint=None,
|
||||
obj=Bookmark.tags.field,
|
||||
id='contenttypes.E003',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_missing_generic_foreign_key(self):
|
||||
class TaggedItem(models.Model):
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
|
||||
class Bookmark(models.Model):
|
||||
tags = generic.GenericRelation('TaggedItem')
|
||||
|
||||
errors = Bookmark.tags.field.check()
|
||||
expected = [
|
||||
checks.Warning(
|
||||
('The field defines a generic relation with the model '
|
||||
'contenttypes_tests.TaggedItem, but the model lacks '
|
||||
'GenericForeignKey.'),
|
||||
hint=None,
|
||||
obj=Bookmark.tags.field,
|
||||
id='contenttypes.E004',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
@override_settings(TEST_SWAPPED_MODEL='contenttypes_tests.Replacement')
|
||||
def test_pointing_to_swapped_model(self):
|
||||
class Replacement(models.Model):
|
||||
pass
|
||||
|
||||
class SwappedModel(models.Model):
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey()
|
||||
|
||||
class Meta:
|
||||
swappable = 'TEST_SWAPPED_MODEL'
|
||||
|
||||
class Model(models.Model):
|
||||
rel = generic.GenericRelation('SwappedModel')
|
||||
|
||||
errors = Model.rel.field.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
('The field defines a relation with the model '
|
||||
'contenttypes_tests.SwappedModel, '
|
||||
'which has been swapped out.'),
|
||||
hint='Update the relation to point at settings.TEST_SWAPPED_MODEL',
|
||||
obj=Model.rel.field,
|
||||
id='E029',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_field_name_ending_with_underscore(self):
|
||||
class TaggedItem(models.Model):
|
||||
content_type = models.ForeignKey(ContentType)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = generic.GenericForeignKey()
|
||||
|
||||
class InvalidBookmark(models.Model):
|
||||
tags_ = generic.GenericRelation('TaggedItem')
|
||||
|
||||
errors = InvalidBookmark.tags_.field.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
'Field names must not end with underscores.',
|
||||
hint=None,
|
||||
obj=InvalidBookmark.tags_.field,
|
||||
id='E001',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
@ -5,6 +5,7 @@ import warnings
|
||||
from django.core import management
|
||||
from django.db import transaction
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
from django.test.utils import override_system_checks
|
||||
from django.utils.six import StringIO
|
||||
|
||||
from .models import Article, Book
|
||||
@ -30,6 +31,7 @@ class TestNoInitialDataLoading(TransactionTestCase):
|
||||
|
||||
available_apps = ['fixtures_model_package']
|
||||
|
||||
@override_system_checks([])
|
||||
def test_migrate(self):
|
||||
with transaction.atomic():
|
||||
Book.objects.all().delete()
|
||||
@ -41,6 +43,7 @@ class TestNoInitialDataLoading(TransactionTestCase):
|
||||
)
|
||||
self.assertQuerysetEqual(Book.objects.all(), [])
|
||||
|
||||
@override_system_checks([])
|
||||
def test_flush(self):
|
||||
# Test presence of fixture (flush called by TransactionTestCase)
|
||||
self.assertQuerysetEqual(
|
||||
|
@ -124,8 +124,9 @@ class InlineFormsetFactoryTest(TestCase):
|
||||
to use for the inline formset, we should get an exception.
|
||||
"""
|
||||
six.assertRaisesRegex(
|
||||
self, Exception,
|
||||
"<class 'inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'inline_formsets.models.Parent'>",
|
||||
self,
|
||||
ValueError,
|
||||
"'inline_formsets.Child' has more than one ForeignKey to 'inline_formsets.Parent'.",
|
||||
inlineformset_factory, Parent, Child
|
||||
)
|
||||
|
||||
@ -146,8 +147,8 @@ class InlineFormsetFactoryTest(TestCase):
|
||||
exception.
|
||||
"""
|
||||
six.assertRaisesRegex(
|
||||
self, Exception,
|
||||
"<class 'inline_formsets.models.Child'> has no field named 'test'",
|
||||
self, ValueError,
|
||||
"'inline_formsets.Child' has no field named 'test'.",
|
||||
inlineformset_factory, Parent, Child, fk_name='test'
|
||||
)
|
||||
|
||||
|
18
tests/invalid_models_tests/base.py
Normal file
18
tests/invalid_models_tests/base.py
Normal file
@ -0,0 +1,18 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import apps
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class IsolatedModelsTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# The unmanaged models need to be removed after the test in order to
|
||||
# prevent bad interactions with the flush operation in other tests.
|
||||
self._old_models = apps.app_configs['invalid_models_tests'].models.copy()
|
||||
|
||||
def tearDown(self):
|
||||
apps.app_configs['invalid_models_tests'].models = self._old_models
|
||||
apps.all_models['invalid_models_tests'] = self._old_models
|
||||
apps.clear_cache()
|
@ -1,535 +0,0 @@
|
||||
# encoding=utf-8
|
||||
"""
|
||||
26. Invalid models
|
||||
|
||||
This example exists purely to point out errors in models.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import connection, models
|
||||
|
||||
|
||||
class FieldErrors(models.Model):
|
||||
charfield = models.CharField()
|
||||
charfield2 = models.CharField(max_length=-1)
|
||||
charfield3 = models.CharField(max_length="bad")
|
||||
decimalfield = models.DecimalField()
|
||||
decimalfield2 = models.DecimalField(max_digits=-1, decimal_places=-1)
|
||||
decimalfield3 = models.DecimalField(max_digits="bad", decimal_places="bad")
|
||||
decimalfield4 = models.DecimalField(max_digits=9, decimal_places=10)
|
||||
decimalfield5 = models.DecimalField(max_digits=10, decimal_places=10)
|
||||
choices = models.CharField(max_length=10, choices='bad')
|
||||
choices2 = models.CharField(max_length=10, choices=[(1, 2, 3), (1, 2, 3)])
|
||||
index = models.CharField(max_length=10, db_index='bad')
|
||||
field_ = models.CharField(max_length=10)
|
||||
nullbool = models.BooleanField(null=True)
|
||||
generic_ip_notnull_blank = models.GenericIPAddressField(null=False, blank=True)
|
||||
|
||||
|
||||
class Target(models.Model):
|
||||
tgt_safe = models.CharField(max_length=10)
|
||||
clash1 = models.CharField(max_length=10)
|
||||
clash2 = models.CharField(max_length=10)
|
||||
|
||||
clash1_set = models.CharField(max_length=10)
|
||||
|
||||
|
||||
class Clash1(models.Model):
|
||||
src_safe = models.CharField(max_length=10)
|
||||
|
||||
foreign = models.ForeignKey(Target)
|
||||
m2m = models.ManyToManyField(Target)
|
||||
|
||||
|
||||
class Clash2(models.Model):
|
||||
src_safe = models.CharField(max_length=10)
|
||||
|
||||
foreign_1 = models.ForeignKey(Target, related_name='id')
|
||||
foreign_2 = models.ForeignKey(Target, related_name='src_safe')
|
||||
|
||||
m2m_1 = models.ManyToManyField(Target, related_name='id')
|
||||
m2m_2 = models.ManyToManyField(Target, related_name='src_safe')
|
||||
|
||||
|
||||
class Target2(models.Model):
|
||||
clash3 = models.CharField(max_length=10)
|
||||
foreign_tgt = models.ForeignKey(Target)
|
||||
clashforeign_set = models.ForeignKey(Target)
|
||||
|
||||
m2m_tgt = models.ManyToManyField(Target)
|
||||
clashm2m_set = models.ManyToManyField(Target)
|
||||
|
||||
|
||||
class Clash3(models.Model):
|
||||
src_safe = models.CharField(max_length=10)
|
||||
|
||||
foreign_1 = models.ForeignKey(Target2, related_name='foreign_tgt')
|
||||
foreign_2 = models.ForeignKey(Target2, related_name='m2m_tgt')
|
||||
|
||||
m2m_1 = models.ManyToManyField(Target2, related_name='foreign_tgt')
|
||||
m2m_2 = models.ManyToManyField(Target2, related_name='m2m_tgt')
|
||||
|
||||
|
||||
class ClashForeign(models.Model):
|
||||
foreign = models.ForeignKey(Target2)
|
||||
|
||||
|
||||
class ClashM2M(models.Model):
|
||||
m2m = models.ManyToManyField(Target2)
|
||||
|
||||
|
||||
class SelfClashForeign(models.Model):
|
||||
src_safe = models.CharField(max_length=10)
|
||||
selfclashforeign = models.CharField(max_length=10)
|
||||
|
||||
selfclashforeign_set = models.ForeignKey("SelfClashForeign")
|
||||
foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id')
|
||||
foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe')
|
||||
|
||||
|
||||
class ValidM2M(models.Model):
|
||||
src_safe = models.CharField(max_length=10)
|
||||
validm2m = models.CharField(max_length=10)
|
||||
|
||||
# M2M fields are symmetrical by default. Symmetrical M2M fields
|
||||
# on self don't require a related accessor, so many potential
|
||||
# clashes are avoided.
|
||||
validm2m_set = models.ManyToManyField("self")
|
||||
|
||||
m2m_1 = models.ManyToManyField("self", related_name='id')
|
||||
m2m_2 = models.ManyToManyField("self", related_name='src_safe')
|
||||
|
||||
m2m_3 = models.ManyToManyField('self')
|
||||
m2m_4 = models.ManyToManyField('self')
|
||||
|
||||
|
||||
class SelfClashM2M(models.Model):
|
||||
src_safe = models.CharField(max_length=10)
|
||||
selfclashm2m = models.CharField(max_length=10)
|
||||
|
||||
# Non-symmetrical M2M fields _do_ have related accessors, so
|
||||
# there is potential for clashes.
|
||||
selfclashm2m_set = models.ManyToManyField("self", symmetrical=False)
|
||||
|
||||
m2m_1 = models.ManyToManyField("self", related_name='id', symmetrical=False)
|
||||
m2m_2 = models.ManyToManyField("self", related_name='src_safe', symmetrical=False)
|
||||
|
||||
m2m_3 = models.ManyToManyField('self', symmetrical=False)
|
||||
m2m_4 = models.ManyToManyField('self', symmetrical=False)
|
||||
|
||||
|
||||
class Model(models.Model):
|
||||
"But it's valid to call a model Model."
|
||||
year = models.PositiveIntegerField() # 1960
|
||||
make = models.CharField(max_length=10) # Aston Martin
|
||||
name = models.CharField(max_length=10) # DB 4 GT
|
||||
|
||||
|
||||
class Car(models.Model):
|
||||
colour = models.CharField(max_length=5)
|
||||
model = models.ForeignKey(Model)
|
||||
|
||||
|
||||
class MissingRelations(models.Model):
|
||||
rel1 = models.ForeignKey("Rel1")
|
||||
rel2 = models.ManyToManyField("Rel2")
|
||||
|
||||
|
||||
class MissingManualM2MModel(models.Model):
|
||||
name = models.CharField(max_length=5)
|
||||
missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
|
||||
|
||||
|
||||
class Person(models.Model):
|
||||
name = models.CharField(max_length=5)
|
||||
|
||||
|
||||
class Group(models.Model):
|
||||
name = models.CharField(max_length=5)
|
||||
primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
|
||||
secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
|
||||
tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary")
|
||||
|
||||
|
||||
class GroupTwo(models.Model):
|
||||
name = models.CharField(max_length=5)
|
||||
primary = models.ManyToManyField(Person, through="Membership")
|
||||
secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
|
||||
|
||||
|
||||
class Membership(models.Model):
|
||||
person = models.ForeignKey(Person)
|
||||
group = models.ForeignKey(Group)
|
||||
not_default_or_null = models.CharField(max_length=5)
|
||||
|
||||
|
||||
class MembershipMissingFK(models.Model):
|
||||
person = models.ForeignKey(Person)
|
||||
|
||||
|
||||
class PersonSelfRefM2M(models.Model):
|
||||
name = models.CharField(max_length=5)
|
||||
friends = models.ManyToManyField('self', through="Relationship")
|
||||
too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK")
|
||||
|
||||
|
||||
class PersonSelfRefM2MExplicit(models.Model):
|
||||
name = models.CharField(max_length=5)
|
||||
friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True)
|
||||
|
||||
|
||||
class Relationship(models.Model):
|
||||
first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
|
||||
second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
|
||||
date_added = models.DateTimeField()
|
||||
|
||||
|
||||
class ExplicitRelationship(models.Model):
|
||||
first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set")
|
||||
second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set")
|
||||
date_added = models.DateTimeField()
|
||||
|
||||
|
||||
class RelationshipTripleFK(models.Model):
|
||||
first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2")
|
||||
second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2")
|
||||
third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far")
|
||||
date_added = models.DateTimeField()
|
||||
|
||||
|
||||
class RelationshipDoubleFK(models.Model):
|
||||
first = models.ForeignKey(Person, related_name="first_related_name")
|
||||
second = models.ForeignKey(Person, related_name="second_related_name")
|
||||
third = models.ForeignKey(Group, related_name="rel_to_set")
|
||||
date_added = models.DateTimeField()
|
||||
|
||||
|
||||
class AbstractModel(models.Model):
|
||||
name = models.CharField(max_length=10)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class AbstractRelationModel(models.Model):
|
||||
fk1 = models.ForeignKey('AbstractModel')
|
||||
fk2 = models.ManyToManyField('AbstractModel')
|
||||
|
||||
|
||||
class UniqueM2M(models.Model):
|
||||
""" Model to test for unique ManyToManyFields, which are invalid. """
|
||||
unique_people = models.ManyToManyField(Person, unique=True)
|
||||
|
||||
|
||||
class NonUniqueFKTarget1(models.Model):
|
||||
""" Model to test for non-unique FK target in yet-to-be-defined model: expect an error """
|
||||
tgt = models.ForeignKey('FKTarget', to_field='bad')
|
||||
|
||||
|
||||
class UniqueFKTarget1(models.Model):
|
||||
""" Model to test for unique FK target in yet-to-be-defined model: expect no error """
|
||||
tgt = models.ForeignKey('FKTarget', to_field='good')
|
||||
|
||||
|
||||
class FKTarget(models.Model):
|
||||
bad = models.IntegerField()
|
||||
good = models.IntegerField(unique=True)
|
||||
|
||||
|
||||
class NonUniqueFKTarget2(models.Model):
|
||||
""" Model to test for non-unique FK target in previously seen model: expect an error """
|
||||
tgt = models.ForeignKey(FKTarget, to_field='bad')
|
||||
|
||||
|
||||
class UniqueFKTarget2(models.Model):
|
||||
""" Model to test for unique FK target in previously seen model: expect no error """
|
||||
tgt = models.ForeignKey(FKTarget, to_field='good')
|
||||
|
||||
|
||||
class NonExistingOrderingWithSingleUnderscore(models.Model):
|
||||
class Meta:
|
||||
ordering = ("does_not_exist",)
|
||||
|
||||
|
||||
class InvalidSetNull(models.Model):
|
||||
fk = models.ForeignKey('self', on_delete=models.SET_NULL)
|
||||
|
||||
|
||||
class InvalidSetDefault(models.Model):
|
||||
fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT)
|
||||
|
||||
|
||||
class UnicodeForeignKeys(models.Model):
|
||||
"""Foreign keys which can translate to ascii should be OK, but fail if
|
||||
they're not."""
|
||||
good = models.ForeignKey('FKTarget')
|
||||
also_good = models.ManyToManyField('FKTarget', related_name='unicode2')
|
||||
|
||||
# In Python 3 this should become legal, but currently causes unicode errors
|
||||
# when adding the errors in core/management/validation.py
|
||||
#bad = models.ForeignKey('★')
|
||||
|
||||
|
||||
class PrimaryKeyNull(models.Model):
|
||||
my_pk_field = models.IntegerField(primary_key=True, null=True)
|
||||
|
||||
|
||||
class OrderByPKModel(models.Model):
|
||||
"""
|
||||
Model to test that ordering by pk passes validation.
|
||||
Refs #8291
|
||||
"""
|
||||
name = models.CharField(max_length=100, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('pk',)
|
||||
|
||||
|
||||
class SwappableModel(models.Model):
|
||||
"""A model that can be, but isn't swapped out.
|
||||
|
||||
References to this model *shoudln't* raise any validation error.
|
||||
"""
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
swappable = 'TEST_SWAPPABLE_MODEL'
|
||||
|
||||
|
||||
class SwappedModel(models.Model):
|
||||
"""A model that is swapped out.
|
||||
|
||||
References to this model *should* raise a validation error.
|
||||
Requires TEST_SWAPPED_MODEL to be defined in the test environment;
|
||||
this is guaranteed by the test runner using @override_settings.
|
||||
|
||||
The foreign keys and m2m relations on this model *shouldn't*
|
||||
install related accessors, so there shouldn't be clashes with
|
||||
the equivalent names on the replacement.
|
||||
"""
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
foreign = models.ForeignKey(Target, related_name='swappable_fk_set')
|
||||
m2m = models.ManyToManyField(Target, related_name='swappable_m2m_set')
|
||||
|
||||
class Meta:
|
||||
swappable = 'TEST_SWAPPED_MODEL'
|
||||
|
||||
|
||||
class ReplacementModel(models.Model):
|
||||
"""A replacement model for swapping purposes."""
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
foreign = models.ForeignKey(Target, related_name='swappable_fk_set')
|
||||
m2m = models.ManyToManyField(Target, related_name='swappable_m2m_set')
|
||||
|
||||
|
||||
class BadSwappableValue(models.Model):
|
||||
"""A model that can be swapped out; during testing, the swappable
|
||||
value is not of the format app.model
|
||||
"""
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
swappable = 'TEST_SWAPPED_MODEL_BAD_VALUE'
|
||||
|
||||
|
||||
class BadSwappableModel(models.Model):
|
||||
"""A model that can be swapped out; during testing, the swappable
|
||||
value references an unknown model.
|
||||
"""
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
swappable = 'TEST_SWAPPED_MODEL_BAD_MODEL'
|
||||
|
||||
|
||||
class HardReferenceModel(models.Model):
|
||||
fk_1 = models.ForeignKey(SwappableModel, related_name='fk_hardref1')
|
||||
fk_2 = models.ForeignKey('invalid_models.SwappableModel', related_name='fk_hardref2')
|
||||
fk_3 = models.ForeignKey(SwappedModel, related_name='fk_hardref3')
|
||||
fk_4 = models.ForeignKey('invalid_models.SwappedModel', related_name='fk_hardref4')
|
||||
m2m_1 = models.ManyToManyField(SwappableModel, related_name='m2m_hardref1')
|
||||
m2m_2 = models.ManyToManyField('invalid_models.SwappableModel', related_name='m2m_hardref2')
|
||||
m2m_3 = models.ManyToManyField(SwappedModel, related_name='m2m_hardref3')
|
||||
m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4')
|
||||
|
||||
|
||||
class BadIndexTogether1(models.Model):
|
||||
class Meta:
|
||||
index_together = [
|
||||
["field_that_does_not_exist"],
|
||||
]
|
||||
|
||||
|
||||
class DuplicateColumnNameModel1(models.Model):
|
||||
"""
|
||||
A field (bar) attempts to use a column name which is already auto-assigned
|
||||
earlier in the class. This should raise a validation error.
|
||||
"""
|
||||
foo = models.IntegerField()
|
||||
bar = models.IntegerField(db_column='foo')
|
||||
|
||||
class Meta:
|
||||
db_table = 'foobar'
|
||||
|
||||
|
||||
class DuplicateColumnNameModel2(models.Model):
|
||||
"""
|
||||
A field (foo) attempts to use a column name which is already auto-assigned
|
||||
later in the class. This should raise a validation error.
|
||||
"""
|
||||
foo = models.IntegerField(db_column='bar')
|
||||
bar = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
db_table = 'foobar'
|
||||
|
||||
|
||||
class DuplicateColumnNameModel3(models.Model):
|
||||
"""Two fields attempt to use each others' names.
|
||||
|
||||
This is not a desirable scenario but valid nonetheless.
|
||||
|
||||
It should not raise a validation error.
|
||||
"""
|
||||
foo = models.IntegerField(db_column='bar')
|
||||
bar = models.IntegerField(db_column='foo')
|
||||
|
||||
class Meta:
|
||||
db_table = 'foobar3'
|
||||
|
||||
|
||||
class DuplicateColumnNameModel4(models.Model):
|
||||
"""Two fields attempt to use the same db_column value.
|
||||
|
||||
This should raise a validation error.
|
||||
"""
|
||||
foo = models.IntegerField(db_column='baz')
|
||||
bar = models.IntegerField(db_column='baz')
|
||||
|
||||
class Meta:
|
||||
db_table = 'foobar'
|
||||
|
||||
|
||||
model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer.
|
||||
invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer.
|
||||
invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer.
|
||||
invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute that is a non-negative integer.
|
||||
invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute that is a positive integer.
|
||||
invalid_models.fielderrors: "decimalfield2": DecimalFields require a "decimal_places" attribute that is a non-negative integer.
|
||||
invalid_models.fielderrors: "decimalfield2": DecimalFields require a "max_digits" attribute that is a positive integer.
|
||||
invalid_models.fielderrors: "decimalfield3": DecimalFields require a "decimal_places" attribute that is a non-negative integer.
|
||||
invalid_models.fielderrors: "decimalfield3": DecimalFields require a "max_digits" attribute that is a positive integer.
|
||||
invalid_models.fielderrors: "decimalfield4": DecimalFields require a "max_digits" attribute value that is greater than or equal to the value of the "decimal_places" attribute.
|
||||
invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list).
|
||||
invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples).
|
||||
invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples).
|
||||
invalid_models.fielderrors: "index": "db_index" should be either None, True or False.
|
||||
invalid_models.fielderrors: "field_": Field names cannot end with underscores, because this would lead to ambiguous queryset filters.
|
||||
invalid_models.fielderrors: "nullbool": BooleanFields do not accept null values. Use a NullBooleanField instead.
|
||||
invalid_models.fielderrors: "generic_ip_notnull_blank": GenericIPAddressField can not accept blank values if null values are not allowed, as blank values are stored as null.
|
||||
invalid_models.clash1: Accessor for field 'foreign' clashes with field 'Target.clash1_set'. Add a related_name argument to the definition for 'foreign'.
|
||||
invalid_models.clash1: Accessor for field 'foreign' clashes with accessor for field 'Clash1.m2m'. Add a related_name argument to the definition for 'foreign'.
|
||||
invalid_models.clash1: Reverse query name for field 'foreign' clashes with field 'Target.clash1'. Add a related_name argument to the definition for 'foreign'.
|
||||
invalid_models.clash1: Accessor for m2m field 'm2m' clashes with field 'Target.clash1_set'. Add a related_name argument to the definition for 'm2m'.
|
||||
invalid_models.clash1: Accessor for m2m field 'm2m' clashes with accessor for field 'Clash1.foreign'. Add a related_name argument to the definition for 'm2m'.
|
||||
invalid_models.clash1: Reverse query name for m2m field 'm2m' clashes with field 'Target.clash1'. Add a related_name argument to the definition for 'm2m'.
|
||||
invalid_models.clash2: Accessor for field 'foreign_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'foreign_1'.
|
||||
invalid_models.clash2: Accessor for field 'foreign_1' clashes with accessor for field 'Clash2.m2m_1'. Add a related_name argument to the definition for 'foreign_1'.
|
||||
invalid_models.clash2: Reverse query name for field 'foreign_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'foreign_1'.
|
||||
invalid_models.clash2: Reverse query name for field 'foreign_1' clashes with accessor for field 'Clash2.m2m_1'. Add a related_name argument to the definition for 'foreign_1'.
|
||||
invalid_models.clash2: Accessor for field 'foreign_2' clashes with accessor for field 'Clash2.m2m_2'. Add a related_name argument to the definition for 'foreign_2'.
|
||||
invalid_models.clash2: Reverse query name for field 'foreign_2' clashes with accessor for field 'Clash2.m2m_2'. Add a related_name argument to the definition for 'foreign_2'.
|
||||
invalid_models.clash2: Accessor for m2m field 'm2m_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'm2m_1'.
|
||||
invalid_models.clash2: Accessor for m2m field 'm2m_1' clashes with accessor for field 'Clash2.foreign_1'. Add a related_name argument to the definition for 'm2m_1'.
|
||||
invalid_models.clash2: Reverse query name for m2m field 'm2m_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'm2m_1'.
|
||||
invalid_models.clash2: Reverse query name for m2m field 'm2m_1' clashes with accessor for field 'Clash2.foreign_1'. Add a related_name argument to the definition for 'm2m_1'.
|
||||
invalid_models.clash2: Accessor for m2m field 'm2m_2' clashes with accessor for field 'Clash2.foreign_2'. Add a related_name argument to the definition for 'm2m_2'.
|
||||
invalid_models.clash2: Reverse query name for m2m field 'm2m_2' clashes with accessor for field 'Clash2.foreign_2'. Add a related_name argument to the definition for 'm2m_2'.
|
||||
invalid_models.clash3: Accessor for field 'foreign_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'foreign_1'.
|
||||
invalid_models.clash3: Accessor for field 'foreign_1' clashes with accessor for field 'Clash3.m2m_1'. Add a related_name argument to the definition for 'foreign_1'.
|
||||
invalid_models.clash3: Reverse query name for field 'foreign_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'foreign_1'.
|
||||
invalid_models.clash3: Reverse query name for field 'foreign_1' clashes with accessor for field 'Clash3.m2m_1'. Add a related_name argument to the definition for 'foreign_1'.
|
||||
invalid_models.clash3: Accessor for field 'foreign_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'foreign_2'.
|
||||
invalid_models.clash3: Accessor for field 'foreign_2' clashes with accessor for field 'Clash3.m2m_2'. Add a related_name argument to the definition for 'foreign_2'.
|
||||
invalid_models.clash3: Reverse query name for field 'foreign_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'foreign_2'.
|
||||
invalid_models.clash3: Reverse query name for field 'foreign_2' clashes with accessor for field 'Clash3.m2m_2'. Add a related_name argument to the definition for 'foreign_2'.
|
||||
invalid_models.clash3: Accessor for m2m field 'm2m_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'm2m_1'.
|
||||
invalid_models.clash3: Accessor for m2m field 'm2m_1' clashes with accessor for field 'Clash3.foreign_1'. Add a related_name argument to the definition for 'm2m_1'.
|
||||
invalid_models.clash3: Reverse query name for m2m field 'm2m_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'm2m_1'.
|
||||
invalid_models.clash3: Reverse query name for m2m field 'm2m_1' clashes with accessor for field 'Clash3.foreign_1'. Add a related_name argument to the definition for 'm2m_1'.
|
||||
invalid_models.clash3: Accessor for m2m field 'm2m_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'm2m_2'.
|
||||
invalid_models.clash3: Accessor for m2m field 'm2m_2' clashes with accessor for field 'Clash3.foreign_2'. Add a related_name argument to the definition for 'm2m_2'.
|
||||
invalid_models.clash3: Reverse query name for m2m field 'm2m_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'm2m_2'.
|
||||
invalid_models.clash3: Reverse query name for m2m field 'm2m_2' clashes with accessor for field 'Clash3.foreign_2'. Add a related_name argument to the definition for 'm2m_2'.
|
||||
invalid_models.clashforeign: Accessor for field 'foreign' clashes with field 'Target2.clashforeign_set'. Add a related_name argument to the definition for 'foreign'.
|
||||
invalid_models.clashm2m: Accessor for m2m field 'm2m' clashes with m2m field 'Target2.clashm2m_set'. Add a related_name argument to the definition for 'm2m'.
|
||||
invalid_models.target2: Accessor for field 'foreign_tgt' clashes with accessor for field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'foreign_tgt'.
|
||||
invalid_models.target2: Accessor for field 'foreign_tgt' clashes with accessor for field 'Target2.clashm2m_set'. Add a related_name argument to the definition for 'foreign_tgt'.
|
||||
invalid_models.target2: Accessor for field 'foreign_tgt' clashes with accessor for field 'Target2.clashforeign_set'. Add a related_name argument to the definition for 'foreign_tgt'.
|
||||
invalid_models.target2: Accessor for field 'clashforeign_set' clashes with accessor for field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'clashforeign_set'.
|
||||
invalid_models.target2: Accessor for field 'clashforeign_set' clashes with accessor for field 'Target2.clashm2m_set'. Add a related_name argument to the definition for 'clashforeign_set'.
|
||||
invalid_models.target2: Accessor for field 'clashforeign_set' clashes with accessor for field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'clashforeign_set'.
|
||||
invalid_models.target2: Accessor for m2m field 'm2m_tgt' clashes with accessor for m2m field 'Target2.clashm2m_set'. Add a related_name argument to the definition for 'm2m_tgt'.
|
||||
invalid_models.target2: Accessor for m2m field 'm2m_tgt' clashes with accessor for field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'm2m_tgt'.
|
||||
invalid_models.target2: Accessor for m2m field 'm2m_tgt' clashes with accessor for field 'Target2.clashforeign_set'. Add a related_name argument to the definition for 'm2m_tgt'.
|
||||
invalid_models.target2: Accessor for m2m field 'clashm2m_set' clashes with accessor for m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'clashm2m_set'.
|
||||
invalid_models.target2: Accessor for m2m field 'clashm2m_set' clashes with accessor for field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'clashm2m_set'.
|
||||
invalid_models.target2: Accessor for m2m field 'clashm2m_set' clashes with accessor for field 'Target2.clashforeign_set'. Add a related_name argument to the definition for 'clashm2m_set'.
|
||||
invalid_models.selfclashforeign: Accessor for field 'selfclashforeign_set' clashes with field 'SelfClashForeign.selfclashforeign_set'. Add a related_name argument to the definition for 'selfclashforeign_set'.
|
||||
invalid_models.selfclashforeign: Reverse query name for field 'selfclashforeign_set' clashes with field 'SelfClashForeign.selfclashforeign'. Add a related_name argument to the definition for 'selfclashforeign_set'.
|
||||
invalid_models.selfclashforeign: Accessor for field 'foreign_1' clashes with field 'SelfClashForeign.id'. Add a related_name argument to the definition for 'foreign_1'.
|
||||
invalid_models.selfclashforeign: Reverse query name for field 'foreign_1' clashes with field 'SelfClashForeign.id'. Add a related_name argument to the definition for 'foreign_1'.
|
||||
invalid_models.selfclashforeign: Accessor for field 'foreign_2' clashes with field 'SelfClashForeign.src_safe'. Add a related_name argument to the definition for 'foreign_2'.
|
||||
invalid_models.selfclashforeign: Reverse query name for field 'foreign_2' clashes with field 'SelfClashForeign.src_safe'. Add a related_name argument to the definition for 'foreign_2'.
|
||||
invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'selfclashm2m_set'.
|
||||
invalid_models.selfclashm2m: Reverse query name for m2m field 'selfclashm2m_set' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'selfclashm2m_set'.
|
||||
invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with accessor for m2m field 'SelfClashM2M.m2m_3'. Add a related_name argument to the definition for 'selfclashm2m_set'.
|
||||
invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with accessor for m2m field 'SelfClashM2M.m2m_4'. Add a related_name argument to the definition for 'selfclashm2m_set'.
|
||||
invalid_models.selfclashm2m: Accessor for m2m field 'm2m_1' clashes with field 'SelfClashM2M.id'. Add a related_name argument to the definition for 'm2m_1'.
|
||||
invalid_models.selfclashm2m: Accessor for m2m field 'm2m_2' clashes with field 'SelfClashM2M.src_safe'. Add a related_name argument to the definition for 'm2m_2'.
|
||||
invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_1' clashes with field 'SelfClashM2M.id'. Add a related_name argument to the definition for 'm2m_1'.
|
||||
invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_2' clashes with field 'SelfClashM2M.src_safe'. Add a related_name argument to the definition for 'm2m_2'.
|
||||
invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'.
|
||||
invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with accessor for m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'.
|
||||
invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with accessor for m2m field 'SelfClashM2M.m2m_4'. Add a related_name argument to the definition for 'm2m_3'.
|
||||
invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'.
|
||||
invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with accessor for m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'.
|
||||
invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with accessor for m2m field 'SelfClashM2M.m2m_3'. Add a related_name argument to the definition for 'm2m_4'.
|
||||
invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_3'.
|
||||
invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_4' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_4'.
|
||||
invalid_models.missingrelations: 'rel1' has a relation with model Rel1, which has either not been installed or is abstract.
|
||||
invalid_models.missingrelations: 'rel2' has an m2m relation with model Rel2, which has either not been installed or is abstract.
|
||||
invalid_models.grouptwo: 'primary' is a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo
|
||||
invalid_models.grouptwo: 'secondary' is a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo
|
||||
invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed
|
||||
invalid_models.group: The model Group has two manually-defined m2m relations through the model Membership, which is not permitted. Please consider using an extra field on your intermediary model instead.
|
||||
invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to Person, which is ambiguous and is not permitted.
|
||||
invalid_models.personselfrefm2m: Many-to-many fields with intermediate tables cannot be symmetrical.
|
||||
invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys to PersonSelfRefM2M, which is ambiguous and is not permitted.
|
||||
invalid_models.personselfrefm2mexplicit: Many-to-many fields with intermediate tables cannot be symmetrical.
|
||||
invalid_models.abstractrelationmodel: 'fk1' has a relation with model AbstractModel, which has either not been installed or is abstract.
|
||||
invalid_models.abstractrelationmodel: 'fk2' has an m2m relation with model AbstractModel, which has either not been installed or is abstract.
|
||||
invalid_models.uniquem2m: ManyToManyFields cannot be unique. Remove the unique argument on 'unique_people'.
|
||||
invalid_models.nonuniquefktarget1: Field 'bad' under model 'FKTarget' must have a unique=True constraint.
|
||||
invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have a unique=True constraint.
|
||||
invalid_models.nonexistingorderingwithsingleunderscore: "ordering" refers to "does_not_exist", a field that doesn't exist.
|
||||
invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null.
|
||||
invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value.
|
||||
invalid_models.hardreferencemodel: 'fk_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL.
|
||||
invalid_models.hardreferencemodel: 'fk_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL.
|
||||
invalid_models.hardreferencemodel: 'm2m_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL.
|
||||
invalid_models.hardreferencemodel: 'm2m_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL.
|
||||
invalid_models.badswappablevalue: TEST_SWAPPED_MODEL_BAD_VALUE is not of the form 'app_label.app_name'.
|
||||
invalid_models.badswappablemodel: Model has been swapped out for 'not_an_app.Target' which has not been installed or is abstract.
|
||||
invalid_models.badindextogether1: "index_together" refers to field_that_does_not_exist, a field that doesn't exist.
|
||||
invalid_models.duplicatecolumnnamemodel1: Field 'bar' has column name 'foo' that is already used.
|
||||
invalid_models.duplicatecolumnnamemodel2: Field 'bar' has column name 'bar' that is already used.
|
||||
invalid_models.duplicatecolumnnamemodel4: Field 'bar' has column name 'baz' that is already used.
|
||||
"""
|
||||
|
||||
if not connection.features.interprets_empty_strings_as_nulls:
|
||||
model_errors += """invalid_models.primarykeynull: "my_pk_field": Primary key fields cannot have null=True.
|
||||
"""
|
68
tests/invalid_models_tests/test_backend_specific.py
Normal file
68
tests/invalid_models_tests/test_backend_specific.py
Normal file
@ -0,0 +1,68 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from types import MethodType
|
||||
|
||||
from django.core.checks import Error
|
||||
from django.db import connection, models
|
||||
|
||||
from .base import IsolatedModelsTestCase
|
||||
|
||||
|
||||
class BackendSpecificChecksTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_check_field(self):
|
||||
""" Test if backend specific checks are performed. """
|
||||
|
||||
error = Error('an error', hint=None)
|
||||
|
||||
def mock(self, field, **kwargs):
|
||||
return [error]
|
||||
|
||||
class Model(models.Model):
|
||||
field = models.IntegerField()
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
|
||||
# Mock connection.validation.check_field method.
|
||||
v = connection.validation
|
||||
old_check_field = v.check_field
|
||||
v.check_field = MethodType(mock, v)
|
||||
try:
|
||||
errors = field.check()
|
||||
finally:
|
||||
# Unmock connection.validation.check_field method.
|
||||
v.check_field = old_check_field
|
||||
|
||||
self.assertEqual(errors, [error])
|
||||
|
||||
def test_validate_field(self):
|
||||
""" Errors raised by deprecated `validate_field` method should be
|
||||
collected. """
|
||||
|
||||
def mock(self, errors, opts, field):
|
||||
errors.add(opts, "An error!")
|
||||
|
||||
class Model(models.Model):
|
||||
field = models.IntegerField()
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
expected = [
|
||||
Error(
|
||||
"An error!",
|
||||
hint=None,
|
||||
obj=field,
|
||||
)
|
||||
]
|
||||
|
||||
# Mock connection.validation.validate_field method.
|
||||
v = connection.validation
|
||||
old_validate_field = v.validate_field
|
||||
v.validate_field = MethodType(mock, v)
|
||||
try:
|
||||
errors = field.check()
|
||||
finally:
|
||||
# Unmock connection.validation.validate_field method.
|
||||
v.validate_field = old_validate_field
|
||||
|
||||
self.assertEqual(errors, expected)
|
334
tests/invalid_models_tests/test_models.py
Normal file
334
tests/invalid_models_tests/test_models.py
Normal file
@ -0,0 +1,334 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.checks import Error
|
||||
from django.db import models
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from .base import IsolatedModelsTestCase
|
||||
|
||||
|
||||
class IndexTogetherTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_non_iterable(self):
|
||||
class Model(models.Model):
|
||||
class Meta:
|
||||
index_together = 42
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"index_together" must be a list or tuple.',
|
||||
hint=None,
|
||||
obj=Model,
|
||||
id='E006',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_non_list(self):
|
||||
class Model(models.Model):
|
||||
class Meta:
|
||||
index_together = 'not-a-list'
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"index_together" must be a list or tuple.',
|
||||
hint=None,
|
||||
obj=Model,
|
||||
id='E006',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_list_containing_non_iterable(self):
|
||||
class Model(models.Model):
|
||||
class Meta:
|
||||
index_together = [
|
||||
'non-iterable',
|
||||
'second-non-iterable',
|
||||
]
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
'All "index_together" elements must be lists or tuples.',
|
||||
hint=None,
|
||||
obj=Model,
|
||||
id='E007',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_pointing_to_missing_field(self):
|
||||
class Model(models.Model):
|
||||
class Meta:
|
||||
index_together = [
|
||||
["missing_field"],
|
||||
]
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"index_together" points to a missing field named "missing_field".',
|
||||
hint='Ensure that you did not misspell the field name.',
|
||||
obj=Model,
|
||||
id='E010',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_pointing_to_m2m_field(self):
|
||||
class Model(models.Model):
|
||||
m2m = models.ManyToManyField('self')
|
||||
|
||||
class Meta:
|
||||
index_together = [
|
||||
["m2m"],
|
||||
]
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
('"index_together" refers to a m2m "m2m" field, but '
|
||||
'ManyToManyFields are not supported in "index_together".'),
|
||||
hint=None,
|
||||
obj=Model,
|
||||
id='E011',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
|
||||
# unique_together tests are very similar to index_together tests.
|
||||
class UniqueTogetherTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_non_iterable(self):
|
||||
class Model(models.Model):
|
||||
class Meta:
|
||||
unique_together = 42
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"unique_together" must be a list or tuple.',
|
||||
hint=None,
|
||||
obj=Model,
|
||||
id='E008',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_list_containing_non_iterable(self):
|
||||
class Model(models.Model):
|
||||
one = models.IntegerField()
|
||||
two = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
unique_together = [('a', 'b'), 42]
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
'All "unique_together" elements must be lists or tuples.',
|
||||
hint=None,
|
||||
obj=Model,
|
||||
id='E009',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_valid_model(self):
|
||||
class Model(models.Model):
|
||||
one = models.IntegerField()
|
||||
two = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
# unique_together can be a simple tuple
|
||||
unique_together = ('one', 'two')
|
||||
|
||||
errors = Model.check()
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_pointing_to_missing_field(self):
|
||||
class Model(models.Model):
|
||||
class Meta:
|
||||
unique_together = [
|
||||
["missing_field"],
|
||||
]
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"unique_together" points to a missing field named "missing_field".',
|
||||
hint='Ensure that you did not misspell the field name.',
|
||||
obj=Model,
|
||||
id='E010',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_pointing_to_m2m(self):
|
||||
class Model(models.Model):
|
||||
m2m = models.ManyToManyField('self')
|
||||
|
||||
class Meta:
|
||||
unique_together = [
|
||||
["m2m"],
|
||||
]
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
('"unique_together" refers to a m2m "m2m" field, but '
|
||||
'ManyToManyFields are not supported in "unique_together".'),
|
||||
hint=None,
|
||||
obj=Model,
|
||||
id='E011',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
|
||||
class OtherModelTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_unique_primary_key(self):
|
||||
class Model(models.Model):
|
||||
id = models.IntegerField(primary_key=False)
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
('You cannot use "id" as a field name, because each model '
|
||||
'automatically gets an "id" field if none of the fields '
|
||||
'have primary_key=True.'),
|
||||
hint='Remove or rename "id" field or add primary_key=True to a field.',
|
||||
obj=Model,
|
||||
id='E005',
|
||||
),
|
||||
Error(
|
||||
'Field "id" has column name "id" that is already used.',
|
||||
hint=None,
|
||||
obj=Model,
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_field_names_ending_with_underscore(self):
|
||||
class Model(models.Model):
|
||||
field_ = models.CharField(max_length=10)
|
||||
m2m_ = models.ManyToManyField('self')
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
'Field names must not end with underscores.',
|
||||
hint=None,
|
||||
obj=Model._meta.get_field('field_'),
|
||||
id='E001',
|
||||
),
|
||||
Error(
|
||||
'Field names must not end with underscores.',
|
||||
hint=None,
|
||||
obj=Model._meta.get_field('m2m_'),
|
||||
id='E001',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_ordering_non_iterable(self):
|
||||
class Model(models.Model):
|
||||
class Meta:
|
||||
ordering = "missing_field"
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
('"ordering" must be a tuple or list '
|
||||
'(even if you want to order by only one field).'),
|
||||
hint=None,
|
||||
obj=Model,
|
||||
id='E012',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_ordering_pointing_to_missing_field(self):
|
||||
class Model(models.Model):
|
||||
class Meta:
|
||||
ordering = ("missing_field",)
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"ordering" pointing to a missing "missing_field" field.',
|
||||
hint='Ensure that you did not misspell the field name.',
|
||||
obj=Model,
|
||||
id='E013',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
@override_settings(TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model')
|
||||
def test_swappable_missing_app_name(self):
|
||||
class Model(models.Model):
|
||||
class Meta:
|
||||
swappable = 'TEST_SWAPPED_MODEL_BAD_VALUE'
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"TEST_SWAPPED_MODEL_BAD_VALUE" is not of the form "app_label.app_name".',
|
||||
hint=None,
|
||||
obj=Model,
|
||||
id='E002',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
@override_settings(TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target')
|
||||
def test_swappable_missing_app(self):
|
||||
class Model(models.Model):
|
||||
class Meta:
|
||||
swappable = 'TEST_SWAPPED_MODEL_BAD_MODEL'
|
||||
|
||||
errors = Model.check()
|
||||
expected = [
|
||||
Error(
|
||||
('The model has been swapped out for not_an_app.Target '
|
||||
'which has not been installed or is abstract.'),
|
||||
hint=('Ensure that you did not misspell the model name and '
|
||||
'the app name as well as the model is not abstract. Does '
|
||||
'your INSTALLED_APPS setting contain the "not_an_app" app?'),
|
||||
obj=Model,
|
||||
id='E003',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_two_m2m_through_same_relationship(self):
|
||||
class Person(models.Model):
|
||||
pass
|
||||
|
||||
class Group(models.Model):
|
||||
primary = models.ManyToManyField(Person,
|
||||
through="Membership", related_name="primary")
|
||||
secondary = models.ManyToManyField(Person, through="Membership",
|
||||
related_name="secondary")
|
||||
|
||||
class Membership(models.Model):
|
||||
person = models.ForeignKey(Person)
|
||||
group = models.ForeignKey(Group)
|
||||
|
||||
errors = Group.check()
|
||||
expected = [
|
||||
Error(
|
||||
('The model has two many-to-many relations through '
|
||||
'the intermediary Membership model, which is not permitted.'),
|
||||
hint=None,
|
||||
obj=Group,
|
||||
id='E004',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
417
tests/invalid_models_tests/test_ordinary_fields.py
Normal file
417
tests/invalid_models_tests/test_ordinary_fields.py
Normal file
@ -0,0 +1,417 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.checks import Error
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
|
||||
from .base import IsolatedModelsTestCase
|
||||
|
||||
|
||||
class AutoFieldTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_valid_case(self):
|
||||
class Model(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
field = Model._meta.get_field('id')
|
||||
errors = field.check()
|
||||
expected = []
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_primary_key(self):
|
||||
# primary_key must be True. Refs #12467.
|
||||
class Model(models.Model):
|
||||
field = models.AutoField(primary_key=False)
|
||||
|
||||
# Prevent Django from autocreating `id` AutoField, which would
|
||||
# result in an error, because a model must have exactly one
|
||||
# AutoField.
|
||||
another = models.IntegerField(primary_key=True)
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'The field must have primary_key=True, because it is an AutoField.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E048',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
|
||||
class BooleanFieldTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_nullable_boolean_field(self):
|
||||
class Model(models.Model):
|
||||
field = models.BooleanField(null=True)
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'BooleanFields do not acceps null values.',
|
||||
hint='Use a NullBooleanField instead.',
|
||||
obj=field,
|
||||
id='E037',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
|
||||
class CharFieldTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_valid_field(self):
|
||||
class Model(models.Model):
|
||||
field = models.CharField(
|
||||
max_length=255,
|
||||
choices=[
|
||||
('1', 'item1'),
|
||||
('2', 'item2'),
|
||||
],
|
||||
db_index=True)
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = []
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_missing_max_length(self):
|
||||
class Model(models.Model):
|
||||
field = models.CharField()
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'The field must have "max_length" attribute.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E038',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_negative_max_length(self):
|
||||
class Model(models.Model):
|
||||
field = models.CharField(max_length=-1)
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"max_length" must be a positive integer.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E039',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_bad_max_length_value(self):
|
||||
class Model(models.Model):
|
||||
field = models.CharField(max_length="bad")
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"max_length" must be a positive integer.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E039',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_non_iterable_choices(self):
|
||||
class Model(models.Model):
|
||||
field = models.CharField(max_length=10, choices='bad')
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"choices" must be an iterable (e.g., a list or tuple).',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E033',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_choices_containing_non_pairs(self):
|
||||
class Model(models.Model):
|
||||
field = models.CharField(max_length=10, choices=[(1, 2, 3), (1, 2, 3)])
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
('All "choices" elements must be a tuple of two elements '
|
||||
'(the first one is the actual value to be stored '
|
||||
'and the second element is the human-readable name).'),
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E034',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_bad_db_index_value(self):
|
||||
class Model(models.Model):
|
||||
field = models.CharField(max_length=10, db_index='bad')
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"db_index" must be either None, True or False.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E035',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_too_long_char_field_under_mysql(self):
|
||||
from django.db.backends.mysql.validation import DatabaseValidation
|
||||
|
||||
class Model(models.Model):
|
||||
field = models.CharField(unique=True, max_length=256)
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
validator = DatabaseValidation(connection=None)
|
||||
errors = validator.check_field(field)
|
||||
expected = [
|
||||
Error(
|
||||
('Under mysql backend, the field cannot have a "max_length" '
|
||||
'greated than 255 when it is unique.'),
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E047',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
|
||||
class DecimalFieldTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_required_attributes(self):
|
||||
class Model(models.Model):
|
||||
field = models.DecimalField()
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'The field requires a "decimal_places" attribute.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E041',
|
||||
),
|
||||
Error(
|
||||
'The field requires a "max_digits" attribute.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E043',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_negative_max_digits_and_decimal_places(self):
|
||||
class Model(models.Model):
|
||||
field = models.DecimalField(max_digits=-1, decimal_places=-1)
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"decimal_places" attribute must be a non-negative integer.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E042',
|
||||
),
|
||||
Error(
|
||||
'"max_digits" attribute must be a positive integer.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E044',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_bad_values_of_max_digits_and_decimal_places(self):
|
||||
class Model(models.Model):
|
||||
field = models.DecimalField(max_digits="bad", decimal_places="bad")
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"decimal_places" attribute must be a non-negative integer.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E042',
|
||||
),
|
||||
Error(
|
||||
'"max_digits" attribute must be a positive integer.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E044',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_decimal_places_greater_than_max_digits(self):
|
||||
class Model(models.Model):
|
||||
field = models.DecimalField(max_digits=9, decimal_places=10)
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"max_digits" must be greater or equal to "decimal_places".',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E040',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_valid_field(self):
|
||||
class Model(models.Model):
|
||||
field = models.DecimalField(max_digits=10, decimal_places=10)
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = []
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
|
||||
class FileFieldTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_valid_case(self):
|
||||
class Model(models.Model):
|
||||
field = models.FileField(upload_to='somewhere')
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = []
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
# def test_missing_upload_to(self):
|
||||
# class Model(models.Model):
|
||||
# field = models.FileField()
|
||||
|
||||
# field = Model._meta.get_field('field')
|
||||
# errors = field.check()
|
||||
# expected = [
|
||||
# Error(
|
||||
# 'The field requires an "upload_to" attribute.',
|
||||
# hint=None,
|
||||
# obj=field,
|
||||
# id='E031',
|
||||
# ),
|
||||
# ]
|
||||
# self.assertEqual(errors, expected)
|
||||
|
||||
def test_unique(self):
|
||||
class Model(models.Model):
|
||||
field = models.FileField(unique=False, upload_to='somewhere')
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"unique" is not a valid argument for FileField.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E049',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_primary_key(self):
|
||||
class Model(models.Model):
|
||||
field = models.FileField(primary_key=False, upload_to='somewhere')
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'"primary_key" is not a valid argument for FileField.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E050',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
|
||||
class FilePathFieldTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_forbidden_files_and_folders(self):
|
||||
class Model(models.Model):
|
||||
field = models.FilePathField(allow_files=False, allow_folders=False)
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
'The field must have either "allow_files" or "allow_folders" set to True.',
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E045',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
|
||||
class GenericIPAddressFieldTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_non_nullable_blank(self):
|
||||
class Model(models.Model):
|
||||
field = models.GenericIPAddressField(null=False, blank=True)
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [
|
||||
Error(
|
||||
('The field cannot accept blank values if null values '
|
||||
'are not allowed, as blank values are stored as null.'),
|
||||
hint=None,
|
||||
obj=field,
|
||||
id='E046',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
|
||||
class ImageFieldTests(IsolatedModelsTestCase):
|
||||
|
||||
def test_pillow_installed(self):
|
||||
try:
|
||||
import django.utils.image # NOQA
|
||||
except ImproperlyConfigured:
|
||||
pillow_installed = False
|
||||
else:
|
||||
pillow_installed = True
|
||||
|
||||
class Model(models.Model):
|
||||
field = models.ImageField(upload_to='somewhere')
|
||||
|
||||
field = Model._meta.get_field('field')
|
||||
errors = field.check()
|
||||
expected = [] if pillow_installed else [
|
||||
Error(
|
||||
'To use ImageFields, Pillow must be installed.',
|
||||
hint=('Get Pillow at https://pypi.python.org/pypi/Pillow '
|
||||
'or run command "pip install pillow".'),
|
||||
obj=field,
|
||||
id='E032',
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
1037
tests/invalid_models_tests/test_relative_fields.py
Normal file
1037
tests/invalid_models_tests/test_relative_fields.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,46 +0,0 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management.validation import get_validation_errors
|
||||
from django.test import override_settings
|
||||
from django.utils.six import StringIO
|
||||
|
||||
|
||||
class InvalidModelTestCase(unittest.TestCase):
|
||||
"""Import an appliation with invalid models and test the exceptions."""
|
||||
|
||||
def setUp(self):
|
||||
# Make sure sys.stdout is not a tty so that we get errors without
|
||||
# coloring attached (makes matching the results easier). We restore
|
||||
# sys.stderr afterwards.
|
||||
self.old_stdout = sys.stdout
|
||||
self.stdout = StringIO()
|
||||
sys.stdout = self.stdout
|
||||
|
||||
def tearDown(self):
|
||||
sys.stdout = self.old_stdout
|
||||
|
||||
# Technically, this isn't an override -- TEST_SWAPPED_MODEL must be
|
||||
# set to *something* in order for the test to work. However, it's
|
||||
# easier to set this up as an override than to require every developer
|
||||
# to specify a value in their test settings.
|
||||
@override_settings(
|
||||
INSTALLED_APPS=['invalid_models_tests.invalid_models'],
|
||||
TEST_SWAPPED_MODEL='invalid_models.ReplacementModel',
|
||||
TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model',
|
||||
TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target',
|
||||
)
|
||||
def test_invalid_models(self):
|
||||
app_config = apps.get_app_config("invalid_models")
|
||||
get_validation_errors(self.stdout, app_config)
|
||||
|
||||
self.stdout.seek(0)
|
||||
error_log = self.stdout.read()
|
||||
actual = error_log.split('\n')
|
||||
expected = app_config.models_module.model_errors.split('\n')
|
||||
|
||||
unexpected = [err for err in actual if err not in expected]
|
||||
missing = [err for err in expected if err not in actual]
|
||||
self.assertFalse(unexpected, "Unexpected Errors: " + '\n'.join(unexpected))
|
||||
self.assertFalse(missing, "Missing Errors: " + '\n'.join(missing))
|
@ -333,7 +333,7 @@ class SettingsConfigTest(AdminScriptTestCase):
|
||||
# validate is just an example command to trigger settings configuration
|
||||
out, err = self.run_manage(['validate'])
|
||||
self.assertNoOutput(err)
|
||||
self.assertOutput(out, "0 errors found")
|
||||
self.assertOutput(out, "System check identified no issues.")
|
||||
|
||||
|
||||
def dictConfig(config):
|
||||
|
@ -1,7 +1,8 @@
|
||||
from django.apps import apps
|
||||
from django.core import management
|
||||
from django.db.models import signals
|
||||
from django.test import TestCase
|
||||
from django.core import management
|
||||
from django.test.utils import override_system_checks
|
||||
from django.utils import six
|
||||
|
||||
|
||||
@ -61,6 +62,9 @@ class MigrateSignalTests(TestCase):
|
||||
def test_pre_migrate_call_time(self):
|
||||
self.assertEqual(pre_migrate_receiver.call_counter, 1)
|
||||
|
||||
# `auth` app is imported, but not installed in this test, so we need to
|
||||
# exclude checks registered by this app.
|
||||
@override_system_checks([])
|
||||
def test_pre_migrate_args(self):
|
||||
r = PreMigrateReceiver()
|
||||
signals.pre_migrate.connect(r, sender=APP_CONFIG)
|
||||
|
@ -7,7 +7,7 @@ import shutil
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management import call_command, CommandError
|
||||
from django.test import override_settings
|
||||
from django.test import override_settings, override_system_checks
|
||||
from django.utils import six
|
||||
from django.utils._os import upath
|
||||
from django.utils.encoding import force_text
|
||||
@ -21,6 +21,10 @@ class MigrateTests(MigrationTestBase):
|
||||
Tests running the migrate command.
|
||||
"""
|
||||
|
||||
# `auth` app is imported, but not installed in these tests (thanks to
|
||||
# MigrationTestBase), so we need to exclude checks registered by this app.
|
||||
|
||||
@override_system_checks([])
|
||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
|
||||
def test_migrate(self):
|
||||
"""
|
||||
@ -49,6 +53,7 @@ class MigrateTests(MigrationTestBase):
|
||||
self.assertTableNotExists("migrations_tribble")
|
||||
self.assertTableNotExists("migrations_book")
|
||||
|
||||
@override_system_checks([])
|
||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
|
||||
def test_migrate_list(self):
|
||||
"""
|
||||
@ -71,6 +76,7 @@ class MigrateTests(MigrationTestBase):
|
||||
# Cleanup by unmigrating everything
|
||||
call_command("migrate", "migrations", "zero", verbosity=0)
|
||||
|
||||
@override_system_checks([])
|
||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"})
|
||||
def test_migrate_conflict_exit(self):
|
||||
"""
|
||||
@ -79,6 +85,7 @@ class MigrateTests(MigrationTestBase):
|
||||
with self.assertRaises(CommandError):
|
||||
call_command("migrate", "migrations")
|
||||
|
||||
@override_system_checks([])
|
||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"})
|
||||
def test_makemigrations_conflict_exit(self):
|
||||
"""
|
||||
@ -87,6 +94,7 @@ class MigrateTests(MigrationTestBase):
|
||||
with self.assertRaises(CommandError):
|
||||
call_command("makemigrations")
|
||||
|
||||
@override_system_checks([])
|
||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"})
|
||||
def test_makemigrations_merge_basic(self):
|
||||
"""
|
||||
@ -99,6 +107,7 @@ class MigrateTests(MigrationTestBase):
|
||||
except CommandError:
|
||||
self.fail("Makemigrations errored in merge mode with conflicts")
|
||||
|
||||
@override_system_checks([])
|
||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
|
||||
def test_sqlmigrate(self):
|
||||
"""
|
||||
@ -150,6 +159,9 @@ class MakeMigrationsTests(MigrationTestBase):
|
||||
return
|
||||
shutil.rmtree(dname)
|
||||
|
||||
# `auth` app is imported, but not installed in this test (thanks to
|
||||
# MigrationTestBase), so we need to exclude checks registered by this app.
|
||||
@override_system_checks([])
|
||||
def test_files_content(self):
|
||||
self.assertTableNotExists("migrations_unicodemodel")
|
||||
apps.register_model('migrations', UnicodeModel)
|
||||
@ -186,6 +198,9 @@ class MakeMigrationsTests(MigrationTestBase):
|
||||
self.assertTrue('\\xda\\xd1\\xcd\\xa2\\xd3\\xd0\\xc9' in content) # title.verbose_name
|
||||
self.assertTrue('\\u201c\\xd0j\\xe1\\xf1g\\xf3\\u201d' in content) # title.default
|
||||
|
||||
# `auth` app is imported, but not installed in this test (thanks to
|
||||
# MigrationTestBase), so we need to exclude checks registered by this app.
|
||||
@override_system_checks([])
|
||||
def test_failing_migration(self):
|
||||
#21280 - If a migration fails to serialize, it shouldn't generate an empty file.
|
||||
apps.register_model('migrations', UnserializableModel)
|
||||
|
@ -85,6 +85,11 @@ class BasicFieldTests(test.TestCase):
|
||||
klass = forms.TypedMultipleChoiceField
|
||||
self.assertIsInstance(field.formfield(choices_form_class=klass), klass)
|
||||
|
||||
def test_field_str(self):
|
||||
from django.utils.encoding import force_str
|
||||
f = Foo._meta.get_field('a')
|
||||
self.assertEqual(force_str(f), "model_fields.Foo.a")
|
||||
|
||||
|
||||
class DecimalFieldTests(test.TestCase):
|
||||
def test_to_python(self):
|
||||
|
@ -1,7 +1,5 @@
|
||||
from django.core import management
|
||||
from django.core.management.validation import (
|
||||
ModelErrorCollection, validate_model_signals
|
||||
)
|
||||
from django.core.checks import run_checks, Error
|
||||
from django.db.models.signals import post_init
|
||||
from django.test import TestCase
|
||||
from django.utils import six
|
||||
@ -24,26 +22,32 @@ class ModelValidationTest(TestCase):
|
||||
# See: https://code.djangoproject.com/ticket/20430
|
||||
# * related_name='+' doesn't clash with another '+'
|
||||
# See: https://code.djangoproject.com/ticket/21375
|
||||
management.call_command("validate", stdout=six.StringIO())
|
||||
management.call_command("check", stdout=six.StringIO())
|
||||
|
||||
def test_model_signal(self):
|
||||
unresolved_references = post_init.unresolved_references.copy()
|
||||
post_init.connect(on_post_init, sender='missing-app.Model')
|
||||
post_init.connect(OnPostInit(), sender='missing-app.Model')
|
||||
e = ModelErrorCollection(six.StringIO())
|
||||
validate_model_signals(e)
|
||||
self.assertSetEqual(
|
||||
set(e.errors),
|
||||
{(
|
||||
'model_validation.tests',
|
||||
|
||||
errors = run_checks()
|
||||
expected = [
|
||||
Error(
|
||||
"The `on_post_init` function was connected to the `post_init` "
|
||||
"signal with a lazy reference to the 'missing-app.Model' "
|
||||
"sender, which has not been installed."
|
||||
), (
|
||||
'model_validation.tests',
|
||||
"sender, which has not been installed.",
|
||||
hint=None,
|
||||
obj='model_validation.tests',
|
||||
id='E014',
|
||||
),
|
||||
Error(
|
||||
"An instance of the `OnPostInit` class was connected to "
|
||||
"the `post_init` signal with a lazy reference to the "
|
||||
"'missing-app.Model' sender, which has not been installed."
|
||||
)}
|
||||
)
|
||||
"'missing-app.Model' sender, which has not been installed.",
|
||||
hint=None,
|
||||
obj='model_validation.tests',
|
||||
id='E014',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
post_init.unresolved_references = unresolved_references
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ import sys
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
from django.test.utils import override_system_checks
|
||||
from django.utils._os import upath
|
||||
|
||||
from .models import (ConcreteModel, ConcreteModelSubclass,
|
||||
@ -26,6 +27,9 @@ class ProxyModelInheritanceTests(TransactionTestCase):
|
||||
def tearDown(self):
|
||||
sys.path = self.old_sys_path
|
||||
|
||||
# `auth` app is imported, but not installed in this test, so we need to
|
||||
# exclude checks registered by this app.
|
||||
@override_system_checks([])
|
||||
def test_table_exists(self):
|
||||
with self.modify_settings(INSTALLED_APPS={'append': ['app1', 'app2']}):
|
||||
call_command('migrate', verbosity=0)
|
||||
|
@ -31,14 +31,3 @@ class CustomArticle(AbstractArticle):
|
||||
|
||||
objects = models.Manager()
|
||||
on_site = CurrentSiteManager("places_this_article_should_appear")
|
||||
|
||||
|
||||
class InvalidArticle(AbstractArticle):
|
||||
site = models.ForeignKey(Site)
|
||||
|
||||
objects = models.Manager()
|
||||
on_site = CurrentSiteManager("places_this_article_should_appear")
|
||||
|
||||
|
||||
class ConfusedArticle(AbstractArticle):
|
||||
site = models.IntegerField()
|
||||
|
@ -1,9 +1,13 @@
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.managers import CurrentSiteManager
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core import checks
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
|
||||
from .models import (SyndicatedArticle, ExclusiveArticle, CustomArticle,
|
||||
InvalidArticle, ConfusedArticle)
|
||||
AbstractArticle)
|
||||
|
||||
|
||||
class SitesFrameworkTestCase(TestCase):
|
||||
@ -11,6 +15,13 @@ class SitesFrameworkTestCase(TestCase):
|
||||
Site.objects.get_or_create(id=settings.SITE_ID, domain="example.com", name="example.com")
|
||||
Site.objects.create(id=settings.SITE_ID + 1, domain="example2.com", name="example2.com")
|
||||
|
||||
self._old_models = apps.app_configs['sites_framework'].models.copy()
|
||||
|
||||
def tearDown(self):
|
||||
apps.app_configs['sites_framework'].models = self._old_models
|
||||
apps.all_models['sites_framework'] = self._old_models
|
||||
apps.clear_cache()
|
||||
|
||||
def test_site_fk(self):
|
||||
article = ExclusiveArticle.objects.create(title="Breaking News!", site_id=settings.SITE_ID)
|
||||
self.assertEqual(ExclusiveArticle.on_site.all().get(), article)
|
||||
@ -28,9 +39,38 @@ class SitesFrameworkTestCase(TestCase):
|
||||
self.assertEqual(CustomArticle.on_site.all().get(), article)
|
||||
|
||||
def test_invalid_name(self):
|
||||
InvalidArticle.objects.create(title="Bad News!", site_id=settings.SITE_ID)
|
||||
self.assertRaises(ValueError, InvalidArticle.on_site.all)
|
||||
|
||||
class InvalidArticle(AbstractArticle):
|
||||
site = models.ForeignKey(Site)
|
||||
|
||||
objects = models.Manager()
|
||||
on_site = CurrentSiteManager("places_this_article_should_appear")
|
||||
|
||||
errors = InvalidArticle.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
("CurrentSiteManager could not find a field named "
|
||||
"'places_this_article_should_appear'."),
|
||||
hint=('Ensure that you did not misspell the field name. '
|
||||
'Does the field exist?'),
|
||||
obj=InvalidArticle.on_site,
|
||||
id='sites.E001',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
||||
def test_invalid_field_type(self):
|
||||
ConfusedArticle.objects.create(title="More Bad News!", site=settings.SITE_ID)
|
||||
self.assertRaises(TypeError, ConfusedArticle.on_site.all)
|
||||
|
||||
class ConfusedArticle(AbstractArticle):
|
||||
site = models.IntegerField()
|
||||
|
||||
errors = ConfusedArticle.check()
|
||||
expected = [
|
||||
checks.Error(
|
||||
"CurrentSiteManager requires that 'ConfusedArticle.site' must be a ForeignKey or ManyToManyField.",
|
||||
hint=None,
|
||||
obj=ConfusedArticle.on_site,
|
||||
id='sites.E002',
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected)
|
||||
|
@ -12,7 +12,7 @@ from django.core.management import call_command
|
||||
from django import db
|
||||
from django.test import runner, TestCase, TransactionTestCase, skipUnlessDBFeature
|
||||
from django.test.testcases import connections_support_transactions
|
||||
from django.test.utils import IgnoreAllDeprecationWarningsMixin
|
||||
from django.test.utils import IgnoreAllDeprecationWarningsMixin, override_system_checks
|
||||
|
||||
from admin_scripts.tests import AdminScriptTestCase
|
||||
from .models import Person
|
||||
@ -245,6 +245,9 @@ class Sqlite3InMemoryTestDbs(TestCase):
|
||||
|
||||
available_apps = []
|
||||
|
||||
# `setup_databases` triggers system check framework, but we do not want to
|
||||
# perform checks.
|
||||
@override_system_checks([])
|
||||
@unittest.skipUnless(all(db.connections[conn].vendor == 'sqlite' for conn in db.connections),
|
||||
"This is an sqlite-specific issue")
|
||||
def test_transaction_support(self):
|
||||
|
@ -6,7 +6,7 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
class Command(BaseCommand):
|
||||
help = "Dance around like a madman."
|
||||
args = ''
|
||||
requires_model_validation = True
|
||||
requires_system_checks = True
|
||||
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option("-s", "--style", default="Rock'n'Roll"),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user