| | 1 | # Possible improvements: gather all errors and raise in the end instead of |
|---|
| | 2 | # bailing out on the first one (like core/management/validation.py does) |
|---|
| | 3 | # --- |
|---|
| | 4 | # Another issue brought up by jkocherhans: |
|---|
| | 5 | # <jkocherhans> mrts: we're probably not going to be able to design a nice |
|---|
| | 6 | # system for validation before 1.0, so a function that does the validation is |
|---|
| | 7 | # probably fine... no need to over-engieer anything or provide hook for custom |
|---|
| | 8 | # classes to allow their own validation. |
|---|
| | 9 | # <mrts> it doesn't hurt though... |
|---|
| | 10 | # <jkocherhans> I'd argue that it *does* hurt though, cause then we're |
|---|
| | 11 | # committing to an api even if it isn't documented, people will use it |
|---|
| | 12 | # <mrts> should I remove the calls to _call_validation_hook(cls, model) then? |
|---|
| | 13 | # <jkocherhans> mrts: I'd say yes, but you should probably get a second opinion |
|---|
| | 14 | |
|---|
| | 15 | from django.core.exceptions import ImproperlyConfigured |
|---|
| | 16 | from django.db import models |
|---|
| | 17 | from django.newforms.forms import BaseForm |
|---|
| | 18 | from django.newforms.formsets import BaseFormSet |
|---|
| | 19 | from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin |
|---|
| | 20 | from django.contrib.admin.options import HORIZONTAL, VERTICAL |
|---|
| | 21 | |
|---|
| | 22 | def validate(cls, model): |
|---|
| | 23 | """ |
|---|
| | 24 | Does basic ModelAdmin option validation. Calls custom validation |
|---|
| | 25 | classmethod in the end if it is provided in cls. The signature of the |
|---|
| | 26 | custom validation classmethod should be: def validate(cls, model). |
|---|
| | 27 | """ |
|---|
| | 28 | opts = model._meta |
|---|
| | 29 | _validate_base(cls, model) |
|---|
| | 30 | |
|---|
| | 31 | # currying is expensive, use wrappers instead |
|---|
| | 32 | def _check_istuplew(label, obj): |
|---|
| | 33 | _check_istuple(cls, label, obj) |
|---|
| | 34 | |
|---|
| | 35 | def _check_isdictw(label, obj): |
|---|
| | 36 | _check_isdict(cls, label, obj) |
|---|
| | 37 | |
|---|
| | 38 | def _check_field_existsw(label, field): |
|---|
| | 39 | return _check_field_exists(cls, model, opts, label, field) |
|---|
| | 40 | |
|---|
| | 41 | def _check_attr_existsw(label, field): |
|---|
| | 42 | _check_attr_exists(cls, model, opts, label, field) |
|---|
| | 43 | |
|---|
| | 44 | # list_display |
|---|
| | 45 | if not cls.list_display: |
|---|
| | 46 | raise ImproperlyConfigured("%s.list_display can not be empty." % |
|---|
| | 47 | cls.__name__) |
|---|
| | 48 | _check_istuplew('list_display', cls.list_display) |
|---|
| | 49 | for idx, field in enumerate(cls.list_display): |
|---|
| | 50 | f = _check_attr_existsw("list_display['%d']" % idx, field) |
|---|
| | 51 | if isinstance(f, models.ManyToManyField): |
|---|
| | 52 | raise ImproperlyConfigured("`%s.list_display['%d']`, `%s` is a " |
|---|
| | 53 | "ManyToManyField which is not supported." |
|---|
| | 54 | % (cls.__name__, idx, field)) |
|---|
| | 55 | |
|---|
| | 56 | # list_display_links |
|---|
| | 57 | if cls.list_display_links: |
|---|
| | 58 | _check_istuplew('list_display_links', cls.list_display_links) |
|---|
| | 59 | for idx, field in enumerate(cls.list_display_links): |
|---|
| | 60 | _check_attr_existsw('list_display_links[%d]' % idx, field) |
|---|
| | 61 | if field not in cls.list_display: |
|---|
| | 62 | raise ImproperlyConfigured("`%s.list_display_links['%d']`" |
|---|
| | 63 | "refers to `%s` which is not defined in `list_display`." |
|---|
| | 64 | % (cls.__name__, idx, field)) |
|---|
| | 65 | |
|---|
| | 66 | # list_filter |
|---|
| | 67 | if cls.list_filter: |
|---|
| | 68 | _check_istuplew('list_filter', cls.list_filter) |
|---|
| | 69 | for idx, field in enumerate(cls.list_filter): |
|---|
| | 70 | _check_field_existsw('list_filter[%d]' % idx, field) |
|---|
| | 71 | |
|---|
| | 72 | # list_per_page = 100 |
|---|
| | 73 | if not isinstance(cls.list_per_page, int): |
|---|
| | 74 | raise ImproperlyConfigured("`%s.list_per_page` should be a integer." |
|---|
| | 75 | % cls.__name__) |
|---|
| | 76 | |
|---|
| | 77 | # search_fields = () |
|---|
| | 78 | if cls.search_fields: |
|---|
| | 79 | _check_istuplew('search_fields', cls.search_fields) |
|---|
| | 80 | # TODO: search field validation is quite complex (restrictions, |
|---|
| | 81 | # follow fields etc), will skip it as of now |
|---|
| | 82 | # for idx, field in enumerate(cls.search_fields): |
|---|
| | 83 | # _check_field_existsw('search_fields[%d]' % idx, field) |
|---|
| | 84 | |
|---|
| | 85 | # date_hierarchy = None |
|---|
| | 86 | if cls.date_hierarchy: |
|---|
| | 87 | f = _check_field_existsw('date_hierarchy', cls.date_hierarchy) |
|---|
| | 88 | if not isinstance(f, (models.DateField, models.DateTimeField)): |
|---|
| | 89 | raise ImproperlyConfigured("`%s.date_hierarchy is " |
|---|
| | 90 | "neither an instance of DateField nor DateTimeField." |
|---|
| | 91 | % cls.__name__) |
|---|
| | 92 | |
|---|
| | 93 | # ordering = None |
|---|
| | 94 | if cls.ordering: |
|---|
| | 95 | _check_istuplew('ordering', cls.ordering) |
|---|
| | 96 | for idx, field in enumerate(cls.ordering): |
|---|
| | 97 | if field == '?' and len(cls.ordering) != 1: |
|---|
| | 98 | raise ImproperlyConfigured("`%s.ordering` has the random " |
|---|
| | 99 | "ordering marker `?`, but contains other fields as " |
|---|
| | 100 | "well. Please either remove `?` or the other fields." |
|---|
| | 101 | % cls.__name__) |
|---|
| | 102 | if field.startswith('-'): |
|---|
| | 103 | field = field[1:] |
|---|
| | 104 | _check_field_existsw('ordering[%d]' % idx, field) |
|---|
| | 105 | |
|---|
| | 106 | # list_select_related = False |
|---|
| | 107 | # save_as = False |
|---|
| | 108 | # save_on_top = False |
|---|
| | 109 | for attr in ('list_select_related', 'save_as', 'save_on_top'): |
|---|
| | 110 | if not isinstance(getattr(cls, attr), bool): |
|---|
| | 111 | raise ImproperlyConfigured("`%s.%s` should be a boolean." |
|---|
| | 112 | % (cls.__name__, attr)) |
|---|
| | 113 | |
|---|
| | 114 | # inlines = [] |
|---|
| | 115 | if cls.inlines: |
|---|
| | 116 | _check_istuplew('inlines', cls.inlines) |
|---|
| | 117 | for idx, inline in enumerate(cls.inlines): |
|---|
| | 118 | if not issubclass(inline, BaseModelAdmin): |
|---|
| | 119 | raise ImproperlyConfigured("`%s.inlines[%d]` does not inherit " |
|---|
| | 120 | "from BaseModelAdmin." % (cls.__name__, idx)) |
|---|
| | 121 | if not inline.model: |
|---|
| | 122 | raise ImproperlyConfigured("`model` is a required attribute " |
|---|
| | 123 | "of `%s.inlines[%d]`." % (cls.__name__, idx)) |
|---|
| | 124 | if not issubclass(inline.model, models.Model): |
|---|
| | 125 | raise ImproperlyConfigured("`%s.inlines[%d].model` does not " |
|---|
| | 126 | "inherit from models.Model." % (cls.__name__, idx)) |
|---|
| | 127 | _validate_base(inline, inline.model) |
|---|
| | 128 | _validate_inline(inline) |
|---|
| | 129 | |
|---|
| | 130 | # TODO: check that the templates exist if given |
|---|
| | 131 | # change_form_template = None |
|---|
| | 132 | # change_list_template = None |
|---|
| | 133 | # delete_confirmation_template = None |
|---|
| | 134 | # object_history_template = None |
|---|
| | 135 | |
|---|
| | 136 | # hook for custom validation |
|---|
| | 137 | _call_validation_hook(cls, model) |
|---|
| | 138 | |
|---|
| | 139 | def _validate_inline(cls): |
|---|
| | 140 | # model is already verified to exist and be a Model |
|---|
| | 141 | if cls.fk_name: |
|---|
| | 142 | f = _check_field_exists(cls, cls.model, cls.model._meta, |
|---|
| | 143 | 'fk_name', cls.fk_name) |
|---|
| | 144 | if not isinstance(f, models.ForeignKey): |
|---|
| | 145 | raise ImproperlyConfigured("`%s.fk_name is not an instance of " |
|---|
| | 146 | "models.ForeignKey." % cls.__name__) |
|---|
| | 147 | # extra = 3 |
|---|
| | 148 | # max_num = 0 |
|---|
| | 149 | for attr in ('extra', 'max_num'): |
|---|
| | 150 | if not isinstance(getattr(cls, attr), int): |
|---|
| | 151 | raise ImproperlyConfigured("`%s.%s` should be a integer." |
|---|
| | 152 | % (cls.__name__, attr)) |
|---|
| | 153 | |
|---|
| | 154 | # formset |
|---|
| | 155 | if cls.formset and not issubclass(cls.formset, BaseFormSet): |
|---|
| | 156 | raise ImproperlyConfigured("`%s.formset` does not inherit from " |
|---|
| | 157 | "BaseFormSet." % cls.__name__) |
|---|
| | 158 | |
|---|
| | 159 | # TODO: check the following as other templates above |
|---|
| | 160 | # template = None |
|---|
| | 161 | |
|---|
| | 162 | # TODO: is there a need to validate the following? |
|---|
| | 163 | # verbose_name = None |
|---|
| | 164 | # verbose_name_plural = None |
|---|
| | 165 | |
|---|
| | 166 | # hook for custom validation |
|---|
| | 167 | _call_validation_hook(cls, cls.model) |
|---|
| | 168 | |
|---|
| | 169 | def _validate_base(cls, model): |
|---|
| | 170 | opts = model._meta |
|---|
| | 171 | # currying is expensive, use wrappers instead |
|---|
| | 172 | def _check_istuplew(label, obj): |
|---|
| | 173 | _check_istuple(cls, label, obj) |
|---|
| | 174 | |
|---|
| | 175 | def _check_isdictw(label, obj): |
|---|
| | 176 | _check_isdict(cls, label, obj) |
|---|
| | 177 | |
|---|
| | 178 | def _check_field_existsw(label, field): |
|---|
| | 179 | return _check_field_exists(cls, model, opts, label, field) |
|---|
| | 180 | |
|---|
| | 181 | # raw_id_fields |
|---|
| | 182 | if cls.raw_id_fields: |
|---|
| | 183 | _check_istuplew('raw_id_fields', cls.raw_id_fields) |
|---|
| | 184 | for field in cls.raw_id_fields: |
|---|
| | 185 | _check_field_existsw('raw_id_fields', field) |
|---|
| | 186 | |
|---|
| | 187 | # fields |
|---|
| | 188 | if cls.fields: |
|---|
| | 189 | for field in cls.fields: |
|---|
| | 190 | _check_field_existsw('fields', field) |
|---|
| | 191 | |
|---|
| | 192 | # fieldsets |
|---|
| | 193 | if cls.fieldsets: |
|---|
| | 194 | _check_istuplew('fieldsets', cls.fieldsets) |
|---|
| | 195 | for idx, fieldset in enumerate(cls.fieldsets): |
|---|
| | 196 | _check_istuplew('fieldsets[%d]' % idx, fieldset) |
|---|
| | 197 | if len(fieldset) != 2: |
|---|
| | 198 | raise ImproperlyConfigured("`%s.fieldsets[%d]` does not " |
|---|
| | 199 | "have exactly two elements." % (cls.__name__, idx)) |
|---|
| | 200 | _check_isdictw('fieldsets[%d][1]' % idx, fieldset[1]) |
|---|
| | 201 | if 'fields' not in fieldset[1]: |
|---|
| | 202 | raise ImproperlyConfigured("`fields` key is required in " |
|---|
| | 203 | "%s.fieldsets[%d][1] field options dict." |
|---|
| | 204 | % (cls.__name__, idx)) |
|---|
| | 205 | for field in flatten_fieldsets(cls.fieldsets): |
|---|
| | 206 | _check_field_existsw("fieldsets[%d][1]['fields']" % idx, field) |
|---|
| | 207 | |
|---|
| | 208 | # form |
|---|
| | 209 | if cls.form and not issubclass(cls.form, BaseForm): |
|---|
| | 210 | raise ImproperlyConfigured("%s.form does not inherit from BaseForm." |
|---|
| | 211 | % cls.__name__) |
|---|
| | 212 | |
|---|
| | 213 | # filter_vertical |
|---|
| | 214 | if cls.filter_vertical: |
|---|
| | 215 | _check_istuplew('filter_vertical', cls.filter_vertical) |
|---|
| | 216 | for field in cls.filter_vertical: |
|---|
| | 217 | _check_field_existsw('filter_vertical', field) |
|---|
| | 218 | |
|---|
| | 219 | # filter_horizontal |
|---|
| | 220 | if cls.filter_horizontal: |
|---|
| | 221 | _check_istuplew('filter_horizontal', cls.filter_horizontal) |
|---|
| | 222 | for field in cls.filter_horizontal: |
|---|
| | 223 | _check_field_existsw('filter_horizontal', field) |
|---|
| | 224 | |
|---|
| | 225 | # radio_fields |
|---|
| | 226 | if cls.radio_fields: |
|---|
| | 227 | _check_isdictw('radio_fields', cls.radio_fields) |
|---|
| | 228 | for field, val in cls.radio_fields.items(): |
|---|
| | 229 | f = _check_field_existsw('radio_fields', field) |
|---|
| | 230 | if not (isinstance(f, models.ForeignKey) or f.choices): |
|---|
| | 231 | raise ImproperlyConfigured("`%s.radio_fields['%s']` " |
|---|
| | 232 | "is neither an instance of ForeignKey nor does " |
|---|
| | 233 | "have choices set." % (cls.__name__, field)) |
|---|
| | 234 | if not val in (HORIZONTAL, VERTICAL): |
|---|
| | 235 | raise ImproperlyConfigured("`%s.radio_fields['%s']` " |
|---|
| | 236 | "is neither admin.HORIZONTAL nor admin.VERTICAL." |
|---|
| | 237 | % (cls.__name__, field)) |
|---|
| | 238 | |
|---|
| | 239 | # prepopulated_fields |
|---|
| | 240 | if cls.prepopulated_fields: |
|---|
| | 241 | _check_isdictw('prepopulated_fields', cls.prepopulated_fields) |
|---|
| | 242 | for field, val in cls.prepopulated_fields.items(): |
|---|
| | 243 | f = _check_field_existsw('prepopulated_fields', field) |
|---|
| | 244 | if isinstance(f, (models.DateTimeField, models.ForeignKey, |
|---|
| | 245 | models.ManyToManyField)): |
|---|
| | 246 | raise ImproperlyConfigured("`%s.prepopulated_fields['%s']` " |
|---|
| | 247 | "is either a DateTimeField, ForeignKey or " |
|---|
| | 248 | "ManyToManyField. This isn't allowed." |
|---|
| | 249 | % (cls.__name__, field)) |
|---|
| | 250 | _check_istuplew("prepopulated_fields['%s']" % field, val) |
|---|
| | 251 | for idx, f in enumerate(val): |
|---|
| | 252 | _check_field_existsw("prepopulated_fields['%s'][%d]" |
|---|
| | 253 | % (f, idx), f) |
|---|
| | 254 | |
|---|
| | 255 | def _call_validation_hook(cls, model): |
|---|
| | 256 | if hasattr(cls, 'validate'): |
|---|
| | 257 | if not callable(cls.validate): |
|---|
| | 258 | raise ImproperlyConfigured("`%s.validate` should be a callable " |
|---|
| | 259 | "(class method)." % cls.__name__) |
|---|
| | 260 | cls.validate(model) |
|---|
| | 261 | |
|---|
| | 262 | def _check_istuple(cls, label, obj): |
|---|
| | 263 | if not isinstance(obj, (list, tuple)): |
|---|
| | 264 | raise ImproperlyConfigured("`%s.%s` must be a " |
|---|
| | 265 | "list or tuple." % (cls.__name__, label)) |
|---|
| | 266 | |
|---|
| | 267 | def _check_isdict(cls, label, obj): |
|---|
| | 268 | if not isinstance(obj, dict): |
|---|
| | 269 | raise ImproperlyConfigured("`%s.%s` must be a dictionary." |
|---|
| | 270 | % (cls.__name__, label)) |
|---|
| | 271 | |
|---|
| | 272 | def _check_field_exists(cls, model, opts, label, field): |
|---|
| | 273 | try: |
|---|
| | 274 | return opts.get_field(field) |
|---|
| | 275 | except models.FieldDoesNotExist: |
|---|
| | 276 | raise ImproperlyConfigured("`%s.%s` refers to " |
|---|
| | 277 | "field `%s` that is missing from model `%s`." |
|---|
| | 278 | % (cls.__name__, label, field, model.__name__)) |
|---|
| | 279 | |
|---|
| | 280 | def _check_attr_exists(cls, model, opts, label, field): |
|---|
| | 281 | try: |
|---|
| | 282 | return opts.get_field(field) |
|---|
| | 283 | except models.FieldDoesNotExist: |
|---|
| | 284 | if not hasattr(model, field): |
|---|
| | 285 | raise ImproperlyConfigured("`%s.%s` refers to " |
|---|
| | 286 | "`%s` that is neither a field, method or property " |
|---|
| | 287 | "of model `%s`." |
|---|
| | 288 | % (cls.__name__, label, field, model.__name__)) |
|---|
| | 289 | return getattr(model, field) |