| 1 |
try: |
|---|
| 2 |
set |
|---|
| 3 |
except NameError: |
|---|
| 4 |
from sets import Set as set # Python 2.3 fallback |
|---|
| 5 |
|
|---|
| 6 |
from django.core.exceptions import ImproperlyConfigured |
|---|
| 7 |
from django.db import models |
|---|
| 8 |
from django.forms.models import BaseModelForm, BaseModelFormSet, fields_for_model |
|---|
| 9 |
from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin |
|---|
| 10 |
from django.contrib.admin.options import HORIZONTAL, VERTICAL |
|---|
| 11 |
|
|---|
| 12 |
__all__ = ['validate'] |
|---|
| 13 |
|
|---|
| 14 |
def validate(cls, model): |
|---|
| 15 |
""" |
|---|
| 16 |
Does basic ModelAdmin option validation. Calls custom validation |
|---|
| 17 |
classmethod in the end if it is provided in cls. The signature of the |
|---|
| 18 |
custom validation classmethod should be: def validate(cls, model). |
|---|
| 19 |
""" |
|---|
| 20 |
# Before we can introspect models, they need to be fully loaded so that |
|---|
| 21 |
# inter-relations are set up correctly. We force that here. |
|---|
| 22 |
models.get_apps() |
|---|
| 23 |
|
|---|
| 24 |
opts = model._meta |
|---|
| 25 |
validate_base(cls, model) |
|---|
| 26 |
|
|---|
| 27 |
# list_display |
|---|
| 28 |
if hasattr(cls, 'list_display'): |
|---|
| 29 |
check_isseq(cls, 'list_display', cls.list_display) |
|---|
| 30 |
for idx, field in enumerate(cls.list_display): |
|---|
| 31 |
if not callable(field): |
|---|
| 32 |
if not hasattr(cls, field): |
|---|
| 33 |
if not hasattr(model, field): |
|---|
| 34 |
try: |
|---|
| 35 |
opts.get_field(field) |
|---|
| 36 |
except models.FieldDoesNotExist: |
|---|
| 37 |
raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r." |
|---|
| 38 |
% (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) |
|---|
| 39 |
f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field) |
|---|
| 40 |
if isinstance(f, models.ManyToManyField): |
|---|
| 41 |
raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported." |
|---|
| 42 |
% (cls.__name__, idx, field)) |
|---|
| 43 |
|
|---|
| 44 |
# list_display_links |
|---|
| 45 |
if hasattr(cls, 'list_display_links'): |
|---|
| 46 |
check_isseq(cls, 'list_display_links', cls.list_display_links) |
|---|
| 47 |
for idx, field in enumerate(cls.list_display_links): |
|---|
| 48 |
fetch_attr(cls, model, opts, 'list_display_links[%d]' % idx, field) |
|---|
| 49 |
if field not in cls.list_display: |
|---|
| 50 |
raise ImproperlyConfigured("'%s.list_display_links[%d]'" |
|---|
| 51 |
"refers to '%s' which is not defined in 'list_display'." |
|---|
| 52 |
% (cls.__name__, idx, field)) |
|---|
| 53 |
|
|---|
| 54 |
# list_filter |
|---|
| 55 |
if hasattr(cls, 'list_filter'): |
|---|
| 56 |
check_isseq(cls, 'list_filter', cls.list_filter) |
|---|
| 57 |
for idx, field in enumerate(cls.list_filter): |
|---|
| 58 |
get_field(cls, model, opts, 'list_filter[%d]' % idx, field) |
|---|
| 59 |
|
|---|
| 60 |
# list_per_page = 100 |
|---|
| 61 |
if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int): |
|---|
| 62 |
raise ImproperlyConfigured("'%s.list_per_page' should be a integer." |
|---|
| 63 |
% cls.__name__) |
|---|
| 64 |
|
|---|
| 65 |
# search_fields = () |
|---|
| 66 |
if hasattr(cls, 'search_fields'): |
|---|
| 67 |
check_isseq(cls, 'search_fields', cls.search_fields) |
|---|
| 68 |
|
|---|
| 69 |
# date_hierarchy = None |
|---|
| 70 |
if cls.date_hierarchy: |
|---|
| 71 |
f = get_field(cls, model, opts, 'date_hierarchy', cls.date_hierarchy) |
|---|
| 72 |
if not isinstance(f, (models.DateField, models.DateTimeField)): |
|---|
| 73 |
raise ImproperlyConfigured("'%s.date_hierarchy is " |
|---|
| 74 |
"neither an instance of DateField nor DateTimeField." |
|---|
| 75 |
% cls.__name__) |
|---|
| 76 |
|
|---|
| 77 |
# ordering = None |
|---|
| 78 |
if cls.ordering: |
|---|
| 79 |
check_isseq(cls, 'ordering', cls.ordering) |
|---|
| 80 |
for idx, field in enumerate(cls.ordering): |
|---|
| 81 |
if field == '?' and len(cls.ordering) != 1: |
|---|
| 82 |
raise ImproperlyConfigured("'%s.ordering' has the random " |
|---|
| 83 |
"ordering marker '?', but contains other fields as " |
|---|
| 84 |
"well. Please either remove '?' or the other fields." |
|---|
| 85 |
% cls.__name__) |
|---|
| 86 |
if field == '?': |
|---|
| 87 |
continue |
|---|
| 88 |
if field.startswith('-'): |
|---|
| 89 |
field = field[1:] |
|---|
| 90 |
# Skip ordering in the format field1__field2 (FIXME: checking |
|---|
| 91 |
# this format would be nice, but it's a little fiddly). |
|---|
| 92 |
if '__' in field: |
|---|
| 93 |
continue |
|---|
| 94 |
get_field(cls, model, opts, 'ordering[%d]' % idx, field) |
|---|
| 95 |
|
|---|
| 96 |
# list_select_related = False |
|---|
| 97 |
# save_as = False |
|---|
| 98 |
# save_on_top = False |
|---|
| 99 |
for attr in ('list_select_related', 'save_as', 'save_on_top'): |
|---|
| 100 |
if not isinstance(getattr(cls, attr), bool): |
|---|
| 101 |
raise ImproperlyConfigured("'%s.%s' should be a boolean." |
|---|
| 102 |
% (cls.__name__, attr)) |
|---|
| 103 |
|
|---|
| 104 |
# inlines = [] |
|---|
| 105 |
if hasattr(cls, 'inlines'): |
|---|
| 106 |
check_isseq(cls, 'inlines', cls.inlines) |
|---|
| 107 |
for idx, inline in enumerate(cls.inlines): |
|---|
| 108 |
if not issubclass(inline, BaseModelAdmin): |
|---|
| 109 |
raise ImproperlyConfigured("'%s.inlines[%d]' does not inherit " |
|---|
| 110 |
"from BaseModelAdmin." % (cls.__name__, idx)) |
|---|
| 111 |
if not inline.model: |
|---|
| 112 |
raise ImproperlyConfigured("'model' is a required attribute " |
|---|
| 113 |
"of '%s.inlines[%d]'." % (cls.__name__, idx)) |
|---|
| 114 |
if not issubclass(inline.model, models.Model): |
|---|
| 115 |
raise ImproperlyConfigured("'%s.inlines[%d].model' does not " |
|---|
| 116 |
"inherit from models.Model." % (cls.__name__, idx)) |
|---|
| 117 |
validate_base(inline, inline.model) |
|---|
| 118 |
validate_inline(inline) |
|---|
| 119 |
|
|---|
| 120 |
def validate_inline(cls): |
|---|
| 121 |
# model is already verified to exist and be a Model |
|---|
| 122 |
if cls.fk_name: # default value is None |
|---|
| 123 |
f = get_field(cls, cls.model, cls.model._meta, 'fk_name', cls.fk_name) |
|---|
| 124 |
if not isinstance(f, models.ForeignKey): |
|---|
| 125 |
raise ImproperlyConfigured("'%s.fk_name is not an instance of " |
|---|
| 126 |
"models.ForeignKey." % cls.__name__) |
|---|
| 127 |
# extra = 3 |
|---|
| 128 |
# max_num = 0 |
|---|
| 129 |
for attr in ('extra', 'max_num'): |
|---|
| 130 |
if not isinstance(getattr(cls, attr), int): |
|---|
| 131 |
raise ImproperlyConfigured("'%s.%s' should be a integer." |
|---|
| 132 |
% (cls.__name__, attr)) |
|---|
| 133 |
|
|---|
| 134 |
# formset |
|---|
| 135 |
if hasattr(cls, 'formset') and not issubclass(cls.formset, BaseModelFormSet): |
|---|
| 136 |
raise ImproperlyConfigured("'%s.formset' does not inherit from " |
|---|
| 137 |
"BaseModelFormSet." % cls.__name__) |
|---|
| 138 |
|
|---|
| 139 |
def validate_base(cls, model): |
|---|
| 140 |
opts = model._meta |
|---|
| 141 |
|
|---|
| 142 |
# raw_id_fields |
|---|
| 143 |
if hasattr(cls, 'raw_id_fields'): |
|---|
| 144 |
check_isseq(cls, 'raw_id_fields', cls.raw_id_fields) |
|---|
| 145 |
for idx, field in enumerate(cls.raw_id_fields): |
|---|
| 146 |
f = get_field(cls, model, opts, 'raw_id_fields', field) |
|---|
| 147 |
if not isinstance(f, (models.ForeignKey, models.ManyToManyField)): |
|---|
| 148 |
raise ImproperlyConfigured("'%s.raw_id_fields[%d]', '%s' must " |
|---|
| 149 |
"be either a ForeignKey or ManyToManyField." |
|---|
| 150 |
% (cls.__name__, idx, field)) |
|---|
| 151 |
|
|---|
| 152 |
# fields |
|---|
| 153 |
if cls.fields: # default value is None |
|---|
| 154 |
check_isseq(cls, 'fields', cls.fields) |
|---|
| 155 |
for field in cls.fields: |
|---|
| 156 |
check_formfield(cls, model, opts, 'fields', field) |
|---|
| 157 |
if cls.fieldsets: |
|---|
| 158 |
raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) |
|---|
| 159 |
if len(cls.fields) > len(set(cls.fields)): |
|---|
| 160 |
raise ImproperlyConfigured('There are duplicate field(s) in %s.fields' % cls.__name__) |
|---|
| 161 |
|
|---|
| 162 |
# fieldsets |
|---|
| 163 |
if cls.fieldsets: # default value is None |
|---|
| 164 |
check_isseq(cls, 'fieldsets', cls.fieldsets) |
|---|
| 165 |
for idx, fieldset in enumerate(cls.fieldsets): |
|---|
| 166 |
check_isseq(cls, 'fieldsets[%d]' % idx, fieldset) |
|---|
| 167 |
if len(fieldset) != 2: |
|---|
| 168 |
raise ImproperlyConfigured("'%s.fieldsets[%d]' does not " |
|---|
| 169 |
"have exactly two elements." % (cls.__name__, idx)) |
|---|
| 170 |
check_isdict(cls, 'fieldsets[%d][1]' % idx, fieldset[1]) |
|---|
| 171 |
if 'fields' not in fieldset[1]: |
|---|
| 172 |
raise ImproperlyConfigured("'fields' key is required in " |
|---|
| 173 |
"%s.fieldsets[%d][1] field options dict." |
|---|
| 174 |
% (cls.__name__, idx)) |
|---|
| 175 |
flattened_fieldsets = flatten_fieldsets(cls.fieldsets) |
|---|
| 176 |
if len(flattened_fieldsets) > len(set(flattened_fieldsets)): |
|---|
| 177 |
raise ImproperlyConfigured('There are duplicate field(s) in %s.fieldsets' % cls.__name__) |
|---|
| 178 |
for field in flattened_fieldsets: |
|---|
| 179 |
check_formfield(cls, model, opts, "fieldsets[%d][1]['fields']" % idx, field) |
|---|
| 180 |
|
|---|
| 181 |
# form |
|---|
| 182 |
if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm): |
|---|
| 183 |
raise ImproperlyConfigured("%s.form does not inherit from " |
|---|
| 184 |
"BaseModelForm." % cls.__name__) |
|---|
| 185 |
|
|---|
| 186 |
# filter_vertical |
|---|
| 187 |
if hasattr(cls, 'filter_vertical'): |
|---|
| 188 |
check_isseq(cls, 'filter_vertical', cls.filter_vertical) |
|---|
| 189 |
for idx, field in enumerate(cls.filter_vertical): |
|---|
| 190 |
f = get_field(cls, model, opts, 'filter_vertical', field) |
|---|
| 191 |
if not isinstance(f, models.ManyToManyField): |
|---|
| 192 |
raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be " |
|---|
| 193 |
"a ManyToManyField." % (cls.__name__, idx)) |
|---|
| 194 |
|
|---|
| 195 |
# filter_horizontal |
|---|
| 196 |
if hasattr(cls, 'filter_horizontal'): |
|---|
| 197 |
check_isseq(cls, 'filter_horizontal', cls.filter_horizontal) |
|---|
| 198 |
for idx, field in enumerate(cls.filter_horizontal): |
|---|
| 199 |
f = get_field(cls, model, opts, 'filter_horizontal', field) |
|---|
| 200 |
if not isinstance(f, models.ManyToManyField): |
|---|
| 201 |
raise ImproperlyConfigured("'%s.filter_horizontal[%d]' must be " |
|---|
| 202 |
"a ManyToManyField." % (cls.__name__, idx)) |
|---|
| 203 |
|
|---|
| 204 |
# radio_fields |
|---|
| 205 |
if hasattr(cls, 'radio_fields'): |
|---|
| 206 |
check_isdict(cls, 'radio_fields', cls.radio_fields) |
|---|
| 207 |
for field, val in cls.radio_fields.items(): |
|---|
| 208 |
f = get_field(cls, model, opts, 'radio_fields', field) |
|---|
| 209 |
if not (isinstance(f, models.ForeignKey) or f.choices): |
|---|
| 210 |
raise ImproperlyConfigured("'%s.radio_fields['%s']' " |
|---|
| 211 |
"is neither an instance of ForeignKey nor does " |
|---|
| 212 |
"have choices set." % (cls.__name__, field)) |
|---|
| 213 |
if not val in (HORIZONTAL, VERTICAL): |
|---|
| 214 |
raise ImproperlyConfigured("'%s.radio_fields['%s']' " |
|---|
| 215 |
"is neither admin.HORIZONTAL nor admin.VERTICAL." |
|---|
| 216 |
% (cls.__name__, field)) |
|---|
| 217 |
|
|---|
| 218 |
# prepopulated_fields |
|---|
| 219 |
if hasattr(cls, 'prepopulated_fields'): |
|---|
| 220 |
check_isdict(cls, 'prepopulated_fields', cls.prepopulated_fields) |
|---|
| 221 |
for field, val in cls.prepopulated_fields.items(): |
|---|
| 222 |
f = get_field(cls, model, opts, 'prepopulated_fields', field) |
|---|
| 223 |
if isinstance(f, (models.DateTimeField, models.ForeignKey, |
|---|
| 224 |
models.ManyToManyField)): |
|---|
| 225 |
raise ImproperlyConfigured("'%s.prepopulated_fields['%s']' " |
|---|
| 226 |
"is either a DateTimeField, ForeignKey or " |
|---|
| 227 |
"ManyToManyField. This isn't allowed." |
|---|
| 228 |
% (cls.__name__, field)) |
|---|
| 229 |
check_isseq(cls, "prepopulated_fields['%s']" % field, val) |
|---|
| 230 |
for idx, f in enumerate(val): |
|---|
| 231 |
get_field(cls, model, opts, "prepopulated_fields['%s'][%d]" % (field, idx), f) |
|---|
| 232 |
|
|---|
| 233 |
def check_isseq(cls, label, obj): |
|---|
| 234 |
if not isinstance(obj, (list, tuple)): |
|---|
| 235 |
raise ImproperlyConfigured("'%s.%s' must be a list or tuple." % (cls.__name__, label)) |
|---|
| 236 |
|
|---|
| 237 |
def check_isdict(cls, label, obj): |
|---|
| 238 |
if not isinstance(obj, dict): |
|---|
| 239 |
raise ImproperlyConfigured("'%s.%s' must be a dictionary." % (cls.__name__, label)) |
|---|
| 240 |
|
|---|
| 241 |
def get_field(cls, model, opts, label, field): |
|---|
| 242 |
try: |
|---|
| 243 |
return opts.get_field(field) |
|---|
| 244 |
except models.FieldDoesNotExist: |
|---|
| 245 |
raise ImproperlyConfigured("'%s.%s' refers to field '%s' that is missing from model '%s'." |
|---|
| 246 |
% (cls.__name__, label, field, model.__name__)) |
|---|
| 247 |
|
|---|
| 248 |
def check_formfield(cls, model, opts, label, field): |
|---|
| 249 |
if getattr(cls.form, 'base_fields', None): |
|---|
| 250 |
try: |
|---|
| 251 |
cls.form.base_fields[field] |
|---|
| 252 |
except KeyError: |
|---|
| 253 |
raise ImproperlyConfigured("'%s.%s' refers to field '%s' that " |
|---|
| 254 |
"is missing from the form." % (cls.__name__, label, field)) |
|---|
| 255 |
else: |
|---|
| 256 |
fields = fields_for_model(model) |
|---|
| 257 |
try: |
|---|
| 258 |
fields[field] |
|---|
| 259 |
except KeyError: |
|---|
| 260 |
raise ImproperlyConfigured("'%s.%s' refers to field '%s' that " |
|---|
| 261 |
"is missing from the form." % (cls.__name__, label, field)) |
|---|
| 262 |
|
|---|
| 263 |
def fetch_attr(cls, model, opts, label, field): |
|---|
| 264 |
try: |
|---|
| 265 |
return opts.get_field(field) |
|---|
| 266 |
except models.FieldDoesNotExist: |
|---|
| 267 |
pass |
|---|
| 268 |
try: |
|---|
| 269 |
return getattr(model, field) |
|---|
| 270 |
except AttributeError: |
|---|
| 271 |
raise ImproperlyConfigured("'%s.%s' refers to '%s' that is neither a field, method or property of model '%s'." |
|---|
| 272 |
% (cls.__name__, label, field, model.__name__)) |
|---|