~ubuntu-branches/debian/sid/python-django/sid

« back to all changes in this revision

Viewing changes to django/core/management/validation.py

  • Committer: Package Import Robot
  • Author(s): Raphaël Hertzog
  • Date: 2014-09-17 14:15:11 UTC
  • mfrom: (1.3.17) (6.2.18 experimental)
  • Revision ID: package-import@ubuntu.com-20140917141511-icneokthe9ww5sk4
Tags: 1.7-2
* Release to unstable.
* Add a migrate-south sample script to help users apply their South
  migrations. Thanks to Brian May.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
import collections
2
 
import sys
3
 
 
4
 
from django.conf import settings
5
 
from django.core.management.color import color_style
6
 
from django.utils.encoding import force_str
7
 
from django.utils.itercompat import is_iterable
8
 
from django.utils import six
9
 
 
10
 
 
11
 
class ModelErrorCollection:
12
 
    def __init__(self, outfile=sys.stdout):
13
 
        self.errors = []
14
 
        self.outfile = outfile
15
 
        self.style = color_style()
16
 
 
17
 
    def add(self, context, error):
18
 
        self.errors.append((context, error))
19
 
        self.outfile.write(self.style.ERROR(force_str("%s: %s\n" % (context, error))))
20
 
 
21
 
 
22
 
def get_validation_errors(outfile, app=None):
23
 
    """
24
 
    Validates all models that are part of the specified app. If no app name is provided,
25
 
    validates all models of all installed apps. Writes errors, if any, to outfile.
26
 
    Returns number of errors.
27
 
    """
28
 
    from django.db import models, connection
29
 
    from django.db.models.loading import get_app_errors
30
 
    from django.db.models.deletion import SET_NULL, SET_DEFAULT
31
 
 
32
 
    e = ModelErrorCollection(outfile)
33
 
 
34
 
    for (app_name, error) in get_app_errors().items():
35
 
        e.add(app_name, error)
36
 
 
37
 
    for cls in models.get_models(app, include_swapped=True):
38
 
        opts = cls._meta
39
 
 
40
 
        # Check swappable attribute.
41
 
        if opts.swapped:
42
 
            try:
43
 
                app_label, model_name = opts.swapped.split('.')
44
 
            except ValueError:
45
 
                e.add(opts, "%s is not of the form 'app_label.app_name'." % opts.swappable)
46
 
                continue
47
 
            if not models.get_model(app_label, model_name):
48
 
                e.add(opts, "Model has been swapped out for '%s' which has not been installed or is abstract." % opts.swapped)
49
 
            # No need to perform any other validation checks on a swapped model.
50
 
            continue
51
 
 
52
 
        # If this is the current User model, check known validation problems with User models
53
 
        if settings.AUTH_USER_MODEL == '%s.%s' % (opts.app_label, opts.object_name):
54
 
            # Check that REQUIRED_FIELDS is a list
55
 
            if not isinstance(cls.REQUIRED_FIELDS, (list, tuple)):
56
 
                e.add(opts, 'The REQUIRED_FIELDS must be a list or tuple.')
57
 
 
58
 
            # Check that the USERNAME FIELD isn't included in REQUIRED_FIELDS.
59
 
            if cls.USERNAME_FIELD in cls.REQUIRED_FIELDS:
60
 
                e.add(opts, 'The field named as the USERNAME_FIELD should not be included in REQUIRED_FIELDS on a swappable User model.')
61
 
 
62
 
            # Check that the username field is unique
63
 
            if not opts.get_field(cls.USERNAME_FIELD).unique:
64
 
                e.add(opts, 'The USERNAME_FIELD must be unique. Add unique=True to the field parameters.')
65
 
 
66
 
        # Model isn't swapped; do field-specific validation.
67
 
        for f in opts.local_fields:
68
 
            if f.name == 'id' and not f.primary_key and opts.pk.name == 'id':
69
 
                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)
70
 
            if f.name.endswith('_'):
71
 
                e.add(opts, '"%s": Field names cannot end with underscores, because this would lead to ambiguous queryset filters.' % f.name)
72
 
            if (f.primary_key and f.null and
73
 
                    not connection.features.interprets_empty_strings_as_nulls):
74
 
                # We cannot reliably check this for backends like Oracle which
75
 
                # consider NULL and '' to be equal (and thus set up
76
 
                # character-based fields a little differently).
77
 
                e.add(opts, '"%s": Primary key fields cannot have null=True.' % f.name)
78
 
            if isinstance(f, models.CharField):
79
 
                try:
80
 
                    max_length = int(f.max_length)
81
 
                    if max_length <= 0:
82
 
                        e.add(opts, '"%s": CharFields require a "max_length" attribute that is a positive integer.' % f.name)
83
 
                except (ValueError, TypeError):
84
 
                    e.add(opts, '"%s": CharFields require a "max_length" attribute that is a positive integer.' % f.name)
85
 
            if isinstance(f, models.DecimalField):
86
 
                decimalp_ok, mdigits_ok = False, False
87
 
                decimalp_msg = '"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.'
88
 
                try:
89
 
                    decimal_places = int(f.decimal_places)
90
 
                    if decimal_places < 0:
91
 
                        e.add(opts, decimalp_msg % f.name)
92
 
                    else:
93
 
                        decimalp_ok = True
94
 
                except (ValueError, TypeError):
95
 
                    e.add(opts, decimalp_msg % f.name)
96
 
                mdigits_msg = '"%s": DecimalFields require a "max_digits" attribute that is a positive integer.'
97
 
                try:
98
 
                    max_digits = int(f.max_digits)
99
 
                    if max_digits <= 0:
100
 
                        e.add(opts,  mdigits_msg % f.name)
101
 
                    else:
102
 
                        mdigits_ok = True
103
 
                except (ValueError, TypeError):
104
 
                    e.add(opts, mdigits_msg % f.name)
105
 
                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.'
106
 
                if decimalp_ok and mdigits_ok:
107
 
                    if decimal_places > max_digits:
108
 
                        e.add(opts, invalid_values_msg % f.name)
109
 
            if isinstance(f, models.FileField) and not f.upload_to:
110
 
                e.add(opts, '"%s": FileFields require an "upload_to" attribute.' % f.name)
111
 
            if isinstance(f, models.ImageField):
112
 
                try:
113
 
                    from django.utils.image import Image
114
 
                except ImportError:
115
 
                    e.add(opts, '"%s": To use ImageFields, you need to install Pillow. Get it at https://pypi.python.org/pypi/Pillow.' % f.name)
116
 
            if isinstance(f, models.BooleanField) and getattr(f, 'null', False):
117
 
                e.add(opts, '"%s": BooleanFields do not accept null values. Use a NullBooleanField instead.' % f.name)
118
 
            if isinstance(f, models.FilePathField) and not (f.allow_files or f.allow_folders):
119
 
                e.add(opts, '"%s": FilePathFields must have either allow_files or allow_folders set to True.' % f.name)
120
 
            if isinstance(f, models.GenericIPAddressField) and not getattr(f, 'null', False) and getattr(f, 'blank', False):
121
 
                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)
122
 
            if f.choices:
123
 
                if isinstance(f.choices, six.string_types) or not is_iterable(f.choices):
124
 
                    e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name)
125
 
                else:
126
 
                    for c in f.choices:
127
 
                        if isinstance(c, six.string_types) or not is_iterable(c) or len(c) != 2:
128
 
                            e.add(opts, '"%s": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples).' % f.name)
129
 
            if f.db_index not in (None, True, False):
130
 
                e.add(opts, '"%s": "db_index" should be either None, True or False.' % f.name)
131
 
 
132
 
            # Perform any backend-specific field validation.
133
 
            connection.validation.validate_field(e, opts, f)
134
 
 
135
 
            # Check if the on_delete behavior is sane
136
 
            if f.rel and hasattr(f.rel, 'on_delete'):
137
 
                if f.rel.on_delete == SET_NULL and not f.null:
138
 
                    e.add(opts, "'%s' specifies on_delete=SET_NULL, but cannot be null." % f.name)
139
 
                elif f.rel.on_delete == SET_DEFAULT and not f.has_default():
140
 
                    e.add(opts, "'%s' specifies on_delete=SET_DEFAULT, but has no default value." % f.name)
141
 
 
142
 
            # Check to see if the related field will clash with any existing
143
 
            # fields, m2m fields, m2m related objects or related objects
144
 
            if f.rel:
145
 
                if f.rel.to not in models.get_models():
146
 
                    # If the related model is swapped, provide a hint;
147
 
                    # otherwise, the model just hasn't been installed.
148
 
                    if not isinstance(f.rel.to, six.string_types) and f.rel.to._meta.swapped:
149
 
                        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))
150
 
                    else:
151
 
                        e.add(opts, "'%s' has a relation with model %s, which has either not been installed or is abstract." % (f.name, f.rel.to))
152
 
                # it is a string and we could not find the model it refers to
153
 
                # so skip the next section
154
 
                if isinstance(f.rel.to, six.string_types):
155
 
                    continue
156
 
 
157
 
                # Make sure the related field specified by a ForeignKey is unique
158
 
                if f.requires_unique_target:
159
 
                    if len(f.foreign_related_fields) > 1:
160
 
                        has_unique_field = False
161
 
                        for rel_field in f.foreign_related_fields:
162
 
                            has_unique_field = has_unique_field or rel_field.unique
163
 
                        if not has_unique_field:
164
 
                            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__))
165
 
                    else:
166
 
                        if not f.foreign_related_fields[0].unique:
167
 
                            e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.foreign_related_fields[0].name, f.rel.to.__name__))
168
 
 
169
 
                rel_opts = f.rel.to._meta
170
 
                rel_name = f.related.get_accessor_name()
171
 
                rel_query_name = f.related_query_name()
172
 
                if not f.rel.is_hidden():
173
 
                    for r in rel_opts.fields:
174
 
                        if r.name == rel_name:
175
 
                            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))
176
 
                        if r.name == rel_query_name:
177
 
                            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))
178
 
                    for r in rel_opts.local_many_to_many:
179
 
                        if r.name == rel_name:
180
 
                            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))
181
 
                        if r.name == rel_query_name:
182
 
                            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))
183
 
                    for r in rel_opts.get_all_related_many_to_many_objects():
184
 
                        if r.get_accessor_name() == rel_name:
185
 
                            e.add(opts, "Accessor for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
186
 
                        if r.get_accessor_name() == rel_query_name:
187
 
                            e.add(opts, "Reverse query name for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
188
 
                    for r in rel_opts.get_all_related_objects():
189
 
                        if r.field is not f:
190
 
                            if r.get_accessor_name() == rel_name:
191
 
                                e.add(opts, "Accessor for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
192
 
                            if r.get_accessor_name() == rel_query_name:
193
 
                                e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
194
 
 
195
 
        seen_intermediary_signatures = []
196
 
        for i, f in enumerate(opts.local_many_to_many):
197
 
            # Check to see if the related m2m field will clash with any
198
 
            # existing fields, m2m fields, m2m related objects or related
199
 
            # objects
200
 
            if f.rel.to not in models.get_models():
201
 
                # If the related model is swapped, provide a hint;
202
 
                # otherwise, the model just hasn't been installed.
203
 
                if not isinstance(f.rel.to, six.string_types) and f.rel.to._meta.swapped:
204
 
                    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))
205
 
                else:
206
 
                    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))
207
 
 
208
 
                # it is a string and we could not find the model it refers to
209
 
                # so skip the next section
210
 
                if isinstance(f.rel.to, six.string_types):
211
 
                    continue
212
 
 
213
 
            # Check that the field is not set to unique.  ManyToManyFields do not support unique.
214
 
            if f.unique:
215
 
                e.add(opts, "ManyToManyFields cannot be unique.  Remove the unique argument on '%s'." % f.name)
216
 
 
217
 
            if f.rel.through is not None and not isinstance(f.rel.through, six.string_types):
218
 
                from_model, to_model = cls, f.rel.to
219
 
                if from_model == to_model and f.rel.symmetrical and not f.rel.through._meta.auto_created:
220
 
                    e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.")
221
 
                seen_from, seen_to, seen_self = False, False, 0
222
 
                for inter_field in f.rel.through._meta.fields:
223
 
                    rel_to = getattr(inter_field.rel, 'to', None)
224
 
                    if from_model == to_model:  # relation to self
225
 
                        if rel_to == from_model:
226
 
                            seen_self += 1
227
 
                        if seen_self > 2:
228
 
                            e.add(opts, "Intermediary model %s has more than "
229
 
                                "two foreign keys to %s, which is ambiguous "
230
 
                                "and is not permitted." % (
231
 
                                    f.rel.through._meta.object_name,
232
 
                                    from_model._meta.object_name
233
 
                                )
234
 
                            )
235
 
                    else:
236
 
                        if rel_to == from_model:
237
 
                            if seen_from:
238
 
                                e.add(opts, "Intermediary model %s has more "
239
 
                                    "than one foreign key to %s, which is "
240
 
                                    "ambiguous and is not permitted." % (
241
 
                                        f.rel.through._meta.object_name,
242
 
                                         from_model._meta.object_name
243
 
                                     )
244
 
                                 )
245
 
                            else:
246
 
                                seen_from = True
247
 
                        elif rel_to == to_model:
248
 
                            if seen_to:
249
 
                                e.add(opts, "Intermediary model %s has more "
250
 
                                    "than one foreign key to %s, which is "
251
 
                                    "ambiguous and is not permitted." % (
252
 
                                        f.rel.through._meta.object_name,
253
 
                                        rel_to._meta.object_name
254
 
                                    )
255
 
                                )
256
 
                            else:
257
 
                                seen_to = True
258
 
                if f.rel.through not in models.get_models(include_auto_created=True):
259
 
                    e.add(opts, "'%s' specifies an m2m relation through model "
260
 
                        "%s, which has not been installed." % (f.name, f.rel.through)
261
 
                    )
262
 
                signature = (f.rel.to, cls, f.rel.through)
263
 
                if signature in seen_intermediary_signatures:
264
 
                    e.add(opts, "The model %s has two manually-defined m2m "
265
 
                        "relations through the model %s, which is not "
266
 
                        "permitted. Please consider using an extra field on "
267
 
                        "your intermediary model instead." % (
268
 
                            cls._meta.object_name,
269
 
                            f.rel.through._meta.object_name
270
 
                        )
271
 
                    )
272
 
                else:
273
 
                    seen_intermediary_signatures.append(signature)
274
 
                if not f.rel.through._meta.auto_created:
275
 
                    seen_related_fk, seen_this_fk = False, False
276
 
                    for field in f.rel.through._meta.fields:
277
 
                        if field.rel:
278
 
                            if not seen_related_fk and field.rel.to == f.rel.to:
279
 
                                seen_related_fk = True
280
 
                            elif field.rel.to == cls:
281
 
                                seen_this_fk = True
282
 
                    if not seen_related_fk or not seen_this_fk:
283
 
                        e.add(opts, "'%s' is a manually-defined m2m relation "
284
 
                            "through model %s, which does not have foreign keys "
285
 
                            "to %s and %s" % (f.name, f.rel.through._meta.object_name,
286
 
                                f.rel.to._meta.object_name, cls._meta.object_name)
287
 
                        )
288
 
            elif isinstance(f.rel.through, six.string_types):
289
 
                e.add(opts, "'%s' specifies an m2m relation through model %s, "
290
 
                    "which has not been installed" % (f.name, f.rel.through)
291
 
                )
292
 
 
293
 
            rel_opts = f.rel.to._meta
294
 
            rel_name = f.related.get_accessor_name()
295
 
            rel_query_name = f.related_query_name()
296
 
            # If rel_name is none, there is no reverse accessor (this only
297
 
            # occurs for symmetrical m2m relations to self). If this is the
298
 
            # case, there are no clashes to check for this field, as there are
299
 
            # no reverse descriptors for this field.
300
 
            if rel_name is not None:
301
 
                for r in rel_opts.fields:
302
 
                    if r.name == rel_name:
303
 
                        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))
304
 
                    if r.name == rel_query_name:
305
 
                        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))
306
 
                for r in rel_opts.local_many_to_many:
307
 
                    if r.name == rel_name:
308
 
                        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))
309
 
                    if r.name == rel_query_name:
310
 
                        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))
311
 
                for r in rel_opts.get_all_related_many_to_many_objects():
312
 
                    if r.field is not f:
313
 
                        if r.get_accessor_name() == rel_name:
314
 
                            e.add(opts, "Accessor for m2m field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
315
 
                        if r.get_accessor_name() == rel_query_name:
316
 
                            e.add(opts, "Reverse query name for m2m field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
317
 
                for r in rel_opts.get_all_related_objects():
318
 
                    if r.get_accessor_name() == rel_name:
319
 
                        e.add(opts, "Accessor for m2m field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
320
 
                    if r.get_accessor_name() == rel_query_name:
321
 
                        e.add(opts, "Reverse query name for m2m field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
322
 
 
323
 
        # Check ordering attribute.
324
 
        if opts.ordering:
325
 
            for field_name in opts.ordering:
326
 
                if field_name == '?':
327
 
                    continue
328
 
                if field_name.startswith('-'):
329
 
                    field_name = field_name[1:]
330
 
                if opts.order_with_respect_to and field_name == '_order':
331
 
                    continue
332
 
                # Skip ordering in the format field1__field2 (FIXME: checking
333
 
                # this format would be nice, but it's a little fiddly).
334
 
                if '__' in field_name:
335
 
                    continue
336
 
                # Skip ordering on pk. This is always a valid order_by field
337
 
                # but is an alias and therefore won't be found by opts.get_field.
338
 
                if field_name == 'pk':
339
 
                    continue
340
 
                try:
341
 
                    opts.get_field(field_name, many_to_many=False)
342
 
                except models.FieldDoesNotExist:
343
 
                    e.add(opts, '"ordering" refers to "%s", a field that doesn\'t exist.' % field_name)
344
 
 
345
 
        # Check unique_together.
346
 
        for ut in opts.unique_together:
347
 
            validate_local_fields(e, opts, "unique_together", ut)
348
 
        if not isinstance(opts.index_together, collections.Sequence):
349
 
            e.add(opts, '"index_together" must a sequence')
350
 
        else:
351
 
            for it in opts.index_together:
352
 
                validate_local_fields(e, opts, "index_together", it)
353
 
 
354
 
    return len(e.errors)
355
 
 
356
 
 
357
 
def validate_local_fields(e, opts, field_name, fields):
358
 
    from django.db import models
359
 
 
360
 
    if not isinstance(fields, collections.Sequence):
361
 
        e.add(opts, 'all %s elements must be sequences' % field_name)
362
 
    else:
363
 
        for field in fields:
364
 
            try:
365
 
                f = opts.get_field(field, many_to_many=True)
366
 
            except models.FieldDoesNotExist:
367
 
                e.add(opts, '"%s" refers to %s, a field that doesn\'t exist.' % (field_name, field))
368
 
            else:
369
 
                if isinstance(f.rel, models.ManyToManyRel):
370
 
                    e.add(opts, '"%s" refers to %s. ManyToManyFields are not supported in %s.' % (field_name, f.name, field_name))
371
 
                if f not in opts.local_fields:
372
 
                    e.add(opts, '"%s" refers to %s. This is not in the same model as the %s statement.' % (field_name, f.name, field_name))