| 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) |