diff --git a/django/core/management/validation.py b/django/core/management/validation.py
index f0b5b1c..56708e1 100644
a
|
b
|
|
1 | 1 | import sys |
2 | 2 | |
3 | 3 | from django.core.management.color import color_style |
| 4 | from django.db.models.sql.constants import LOOKUP_SEP |
4 | 5 | from django.utils.itercompat import is_iterable |
5 | 6 | |
| 7 | |
6 | 8 | class ModelErrorCollection: |
7 | 9 | def __init__(self, outfile=sys.stdout): |
8 | 10 | self.errors = [] |
… |
… |
def get_validation_errors(outfile, app=None):
|
32 | 34 | |
33 | 35 | for cls in models.get_models(app): |
34 | 36 | opts = cls._meta |
| 37 | # Keep record of used field names. Clashing names inside a single model |
| 38 | # (example: `book` and `book_id`) and shadowing field names in inheritance |
| 39 | # cases is not allowed as this will lead to buggy behavior. See ticket |
| 40 | # #17673 for details. |
| 41 | used_fields = {} # name or attname -> field |
| 42 | |
| 43 | # Check that multi-inheritance doesn't cause field name |
| 44 | # shadowing. |
| 45 | for parent in opts.parents: |
| 46 | for f in parent._meta.local_fields: |
| 47 | clashing_field = (used_fields.get(f.name) or |
| 48 | used_fields.get(f.attname) or |
| 49 | None) |
| 50 | if clashing_field: |
| 51 | e.add(opts, 'The field "%s" from parent model "%s" clashes with the field ' |
| 52 | '"%s" from another parent model "%s"' % ( |
| 53 | f.name, f.model._meta, clashing_field.name, |
| 54 | clashing_field.model._meta)) |
| 55 | used_fields[f.name] = f |
| 56 | used_fields[f.attname] = f |
35 | 57 | |
36 | 58 | # Do field-specific validation. |
37 | 59 | for f in opts.local_fields: |
| 60 | clashing_field = (used_fields.get(f.name) or |
| 61 | used_fields.get(f.attname) or |
| 62 | None) |
| 63 | if clashing_field: |
| 64 | e.add(opts, '"%s": This field clashes with field "%s" from "%s"' % ( |
| 65 | f.name, clashing_field.name, clashing_field.model._meta)) |
| 66 | used_fields[f.name] = f |
| 67 | used_fields[f.attname] = f |
| 68 | if f.name == 'pk': |
| 69 | e.add(opts, '"%s": You can\'t use "pk" as a field name. ' |
| 70 | 'It is a reserved name.' % f.name) |
| 71 | if LOOKUP_SEP in f.name: |
| 72 | e.add(opts, '"%s": Field\'s name must not contain "%s".' % ( |
| 73 | f.name, LOOKUP_SEP)) |
38 | 74 | if f.name == 'id' and not f.primary_key and opts.pk.name == 'id': |
39 | 75 | 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) |
40 | 76 | if f.name.endswith('_'): |
diff --git a/tests/modeltests/invalid_models/invalid_models/models.py b/tests/modeltests/invalid_models/invalid_models/models.py
index ed69fb6..9f9ae40 100644
a
|
b
|
class OrderByPKModel(models.Model):
|
243 | 243 | class Meta: |
244 | 244 | ordering = ('pk',) |
245 | 245 | |
| 246 | class InvalidFieldNames(models.Model): |
| 247 | pk = models.IntegerField() |
| 248 | some__field = models.IntegerField() |
| 249 | |
| 250 | class FirstParent(models.Model): |
| 251 | somef_id = models.IntegerField() |
| 252 | someotherf = models.IntegerField() |
| 253 | |
| 254 | class SecondParent(models.Model): |
| 255 | somef_id = models.IntegerField() |
| 256 | |
| 257 | class ChildShadowingField(FirstParent): |
| 258 | somef = models.ForeignKey(SecondParent) |
| 259 | |
| 260 | class MultiInheritanceClash(FirstParent, SecondParent): |
| 261 | # Here we have two clashed: id (automatic field) and somef, because |
| 262 | # both parents define these fields. |
| 263 | pass |
| 264 | |
| 265 | class InternalClashingNames(models.Model): |
| 266 | # fk.attname must not clash with fk_id.name |
| 267 | fk = models.ForeignKey(FirstParent) |
| 268 | fk_id = models.IntegerField() |
| 269 | |
246 | 270 | model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer. |
247 | 271 | invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer. |
248 | 272 | invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer. |
… |
… |
invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have
|
351 | 375 | invalid_models.nonexistingorderingwithsingleunderscore: "ordering" refers to "does_not_exist", a field that doesn't exist. |
352 | 376 | invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null. |
353 | 377 | invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value. |
| 378 | invalid_models.invalidfieldnames: "pk": You can't use "pk" as a field name. It is a reserved name. |
| 379 | invalid_models.invalidfieldnames: "some__field": Field's name must not contain "__". |
| 380 | invalid_models.childshadowingfield: "somef": This field clashes with field "somef_id" from "invalid_models.firstparent" |
| 381 | invalid_models.multiinheritanceclash: The field "id" from parent model "invalid_models.secondparent" clashes with the field "id" from another parent model "invalid_models.firstparent" |
| 382 | invalid_models.multiinheritanceclash: The field "somef_id" from parent model "invalid_models.secondparent" clashes with the field "somef_id" from another parent model "invalid_models.firstparent" |
| 383 | invalid_models.internalclashingnames: "fk_id": This field clashes with field "fk" from "invalid_models.internalclashingnames" |
354 | 384 | """ |
355 | 385 | |
356 | 386 | if not connection.features.interprets_empty_strings_as_nulls: |
diff --git a/tests/modeltests/model_inheritance/models.py b/tests/modeltests/model_inheritance/models.py
index a0fed8a..3435d6a 100644
a
|
b
|
class Student(CommonInfo):
|
38 | 38 | class Meta: |
39 | 39 | pass |
40 | 40 | |
41 | | class StudentWorker(Student, Worker): |
42 | | pass |
43 | | |
44 | 41 | # |
45 | 42 | # Abstract base classes with related models |
46 | 43 | # |
diff --git a/tests/modeltests/model_inheritance/tests.py b/tests/modeltests/model_inheritance/tests.py
index 2e1a7a5..0123a6b 100644
a
|
b
|
from django.core.exceptions import FieldError
|
6 | 6 | from django.test import TestCase |
7 | 7 | |
8 | 8 | from .models import (Chef, CommonInfo, ItalianRestaurant, ParkingLot, Place, |
9 | | Post, Restaurant, Student, StudentWorker, Supplier, Worker, MixinModel) |
| 9 | Post, Restaurant, Student, Supplier, Worker, MixinModel) |
10 | 10 | |
11 | 11 | |
12 | 12 | class ModelInheritanceTests(TestCase): |
… |
… |
class ModelInheritanceTests(TestCase):
|
43 | 43 | # doesn't exist as a model). |
44 | 44 | self.assertRaises(AttributeError, lambda: CommonInfo.objects.all()) |
45 | 45 | |
46 | | # A StudentWorker which does not exist is both a Student and Worker |
47 | | # which does not exist. |
48 | | self.assertRaises(Student.DoesNotExist, |
49 | | StudentWorker.objects.get, pk=12321321 |
50 | | ) |
51 | | self.assertRaises(Worker.DoesNotExist, |
52 | | StudentWorker.objects.get, pk=12321321 |
53 | | ) |
54 | | |
55 | | # MultipleObjectsReturned is also inherited. |
56 | | # This is written out "long form", rather than using __init__/create() |
57 | | # because of a bug with diamond inheritance (#10808) |
58 | | sw1 = StudentWorker() |
59 | | sw1.name = "Wilma" |
60 | | sw1.age = 35 |
61 | | sw1.save() |
62 | | sw2 = StudentWorker() |
63 | | sw2.name = "Betty" |
64 | | sw2.age = 24 |
65 | | sw2.save() |
66 | | |
67 | | self.assertRaises(Student.MultipleObjectsReturned, |
68 | | StudentWorker.objects.get, pk__lt=sw2.pk + 100 |
69 | | ) |
70 | | self.assertRaises(Worker.MultipleObjectsReturned, |
71 | | StudentWorker.objects.get, pk__lt=sw2.pk + 100 |
72 | | ) |
73 | | |
74 | 46 | def test_multiple_table(self): |
75 | 47 | post = Post.objects.create(title="Lorem Ipsum") |
76 | 48 | # The Post model has distinct accessors for the Comment and Link models. |