diff options
| author | Russell Keith-Magee <russell@keith-magee.com> | 2014-01-20 10:45:21 +0800 |
|---|---|---|
| committer | Russell Keith-Magee <russell@keith-magee.com> | 2014-01-20 10:45:21 +0800 |
| commit | d818e0c9b2b88276cc499974f9eee893170bf0a8 (patch) | |
| tree | 13ef631f7ba50bf81fa36f484abf925ba8172651 /tests/invalid_models_tests | |
| parent | 6e7bd0b63bd01949ac4fd647f2597639bed0c3a2 (diff) | |
Fixed #16905 -- Added extensible checks (nee validation) framework
This is the result of Christopher Medrela's 2013 Summer of Code project.
Thanks also to Preston Holmes, Tim Graham, Anssi Kääriäinen, Florian
Apolloner, and Alex Gaynor for review notes along the way.
Also: Fixes #8579, fixes #3055, fixes #19844.
Diffstat (limited to 'tests/invalid_models_tests')
| -rw-r--r-- | tests/invalid_models_tests/base.py | 18 | ||||
| -rw-r--r-- | tests/invalid_models_tests/invalid_models/__init__.py | 0 | ||||
| -rw-r--r-- | tests/invalid_models_tests/invalid_models/models.py | 535 | ||||
| -rw-r--r-- | tests/invalid_models_tests/test_backend_specific.py | 68 | ||||
| -rw-r--r-- | tests/invalid_models_tests/test_models.py | 334 | ||||
| -rw-r--r-- | tests/invalid_models_tests/test_ordinary_fields.py | 417 | ||||
| -rw-r--r-- | tests/invalid_models_tests/test_relative_fields.py | 1037 | ||||
| -rw-r--r-- | tests/invalid_models_tests/tests.py | 46 |
8 files changed, 1874 insertions, 581 deletions
diff --git a/tests/invalid_models_tests/base.py b/tests/invalid_models_tests/base.py new file mode 100644 index 0000000000..a180eec6e2 --- /dev/null +++ b/tests/invalid_models_tests/base.py @@ -0,0 +1,18 @@ +# -*- encoding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import apps +from django.test import TestCase + + +class IsolatedModelsTestCase(TestCase): + + def setUp(self): + # The unmanaged models need to be removed after the test in order to + # prevent bad interactions with the flush operation in other tests. + self._old_models = apps.app_configs['invalid_models_tests'].models.copy() + + def tearDown(self): + apps.app_configs['invalid_models_tests'].models = self._old_models + apps.all_models['invalid_models_tests'] = self._old_models + apps.clear_cache() diff --git a/tests/invalid_models_tests/invalid_models/__init__.py b/tests/invalid_models_tests/invalid_models/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/tests/invalid_models_tests/invalid_models/__init__.py +++ /dev/null diff --git a/tests/invalid_models_tests/invalid_models/models.py b/tests/invalid_models_tests/invalid_models/models.py deleted file mode 100644 index 0c991dcf13..0000000000 --- a/tests/invalid_models_tests/invalid_models/models.py +++ /dev/null @@ -1,535 +0,0 @@ -# encoding=utf-8 -""" -26. Invalid models - -This example exists purely to point out errors in models. -""" - -from __future__ import unicode_literals - -from django.db import connection, models - - -class FieldErrors(models.Model): - charfield = models.CharField() - charfield2 = models.CharField(max_length=-1) - charfield3 = models.CharField(max_length="bad") - decimalfield = models.DecimalField() - decimalfield2 = models.DecimalField(max_digits=-1, decimal_places=-1) - decimalfield3 = models.DecimalField(max_digits="bad", decimal_places="bad") - decimalfield4 = models.DecimalField(max_digits=9, decimal_places=10) - decimalfield5 = models.DecimalField(max_digits=10, decimal_places=10) - choices = models.CharField(max_length=10, choices='bad') - choices2 = models.CharField(max_length=10, choices=[(1, 2, 3), (1, 2, 3)]) - index = models.CharField(max_length=10, db_index='bad') - field_ = models.CharField(max_length=10) - nullbool = models.BooleanField(null=True) - generic_ip_notnull_blank = models.GenericIPAddressField(null=False, blank=True) - - -class Target(models.Model): - tgt_safe = models.CharField(max_length=10) - clash1 = models.CharField(max_length=10) - clash2 = models.CharField(max_length=10) - - clash1_set = models.CharField(max_length=10) - - -class Clash1(models.Model): - src_safe = models.CharField(max_length=10) - - foreign = models.ForeignKey(Target) - m2m = models.ManyToManyField(Target) - - -class Clash2(models.Model): - src_safe = models.CharField(max_length=10) - - foreign_1 = models.ForeignKey(Target, related_name='id') - foreign_2 = models.ForeignKey(Target, related_name='src_safe') - - m2m_1 = models.ManyToManyField(Target, related_name='id') - m2m_2 = models.ManyToManyField(Target, related_name='src_safe') - - -class Target2(models.Model): - clash3 = models.CharField(max_length=10) - foreign_tgt = models.ForeignKey(Target) - clashforeign_set = models.ForeignKey(Target) - - m2m_tgt = models.ManyToManyField(Target) - clashm2m_set = models.ManyToManyField(Target) - - -class Clash3(models.Model): - src_safe = models.CharField(max_length=10) - - foreign_1 = models.ForeignKey(Target2, related_name='foreign_tgt') - foreign_2 = models.ForeignKey(Target2, related_name='m2m_tgt') - - m2m_1 = models.ManyToManyField(Target2, related_name='foreign_tgt') - m2m_2 = models.ManyToManyField(Target2, related_name='m2m_tgt') - - -class ClashForeign(models.Model): - foreign = models.ForeignKey(Target2) - - -class ClashM2M(models.Model): - m2m = models.ManyToManyField(Target2) - - -class SelfClashForeign(models.Model): - src_safe = models.CharField(max_length=10) - selfclashforeign = models.CharField(max_length=10) - - selfclashforeign_set = models.ForeignKey("SelfClashForeign") - foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id') - foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe') - - -class ValidM2M(models.Model): - src_safe = models.CharField(max_length=10) - validm2m = models.CharField(max_length=10) - - # M2M fields are symmetrical by default. Symmetrical M2M fields - # on self don't require a related accessor, so many potential - # clashes are avoided. - validm2m_set = models.ManyToManyField("self") - - m2m_1 = models.ManyToManyField("self", related_name='id') - m2m_2 = models.ManyToManyField("self", related_name='src_safe') - - m2m_3 = models.ManyToManyField('self') - m2m_4 = models.ManyToManyField('self') - - -class SelfClashM2M(models.Model): - src_safe = models.CharField(max_length=10) - selfclashm2m = models.CharField(max_length=10) - - # Non-symmetrical M2M fields _do_ have related accessors, so - # there is potential for clashes. - selfclashm2m_set = models.ManyToManyField("self", symmetrical=False) - - m2m_1 = models.ManyToManyField("self", related_name='id', symmetrical=False) - m2m_2 = models.ManyToManyField("self", related_name='src_safe', symmetrical=False) - - m2m_3 = models.ManyToManyField('self', symmetrical=False) - m2m_4 = models.ManyToManyField('self', symmetrical=False) - - -class Model(models.Model): - "But it's valid to call a model Model." - year = models.PositiveIntegerField() # 1960 - make = models.CharField(max_length=10) # Aston Martin - name = models.CharField(max_length=10) # DB 4 GT - - -class Car(models.Model): - colour = models.CharField(max_length=5) - model = models.ForeignKey(Model) - - -class MissingRelations(models.Model): - rel1 = models.ForeignKey("Rel1") - rel2 = models.ManyToManyField("Rel2") - - -class MissingManualM2MModel(models.Model): - name = models.CharField(max_length=5) - missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") - - -class Person(models.Model): - name = models.CharField(max_length=5) - - -class Group(models.Model): - name = models.CharField(max_length=5) - primary = models.ManyToManyField(Person, through="Membership", related_name="primary") - secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") - tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary") - - -class GroupTwo(models.Model): - name = models.CharField(max_length=5) - primary = models.ManyToManyField(Person, through="Membership") - secondary = models.ManyToManyField(Group, through="MembershipMissingFK") - - -class Membership(models.Model): - person = models.ForeignKey(Person) - group = models.ForeignKey(Group) - not_default_or_null = models.CharField(max_length=5) - - -class MembershipMissingFK(models.Model): - person = models.ForeignKey(Person) - - -class PersonSelfRefM2M(models.Model): - name = models.CharField(max_length=5) - friends = models.ManyToManyField('self', through="Relationship") - too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") - - -class PersonSelfRefM2MExplicit(models.Model): - name = models.CharField(max_length=5) - friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True) - - -class Relationship(models.Model): - first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") - second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") - date_added = models.DateTimeField() - - -class ExplicitRelationship(models.Model): - first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set") - second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set") - date_added = models.DateTimeField() - - -class RelationshipTripleFK(models.Model): - first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") - second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") - third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") - date_added = models.DateTimeField() - - -class RelationshipDoubleFK(models.Model): - first = models.ForeignKey(Person, related_name="first_related_name") - second = models.ForeignKey(Person, related_name="second_related_name") - third = models.ForeignKey(Group, related_name="rel_to_set") - date_added = models.DateTimeField() - - -class AbstractModel(models.Model): - name = models.CharField(max_length=10) - - class Meta: - abstract = True - - -class AbstractRelationModel(models.Model): - fk1 = models.ForeignKey('AbstractModel') - fk2 = models.ManyToManyField('AbstractModel') - - -class UniqueM2M(models.Model): - """ Model to test for unique ManyToManyFields, which are invalid. """ - unique_people = models.ManyToManyField(Person, unique=True) - - -class NonUniqueFKTarget1(models.Model): - """ Model to test for non-unique FK target in yet-to-be-defined model: expect an error """ - tgt = models.ForeignKey('FKTarget', to_field='bad') - - -class UniqueFKTarget1(models.Model): - """ Model to test for unique FK target in yet-to-be-defined model: expect no error """ - tgt = models.ForeignKey('FKTarget', to_field='good') - - -class FKTarget(models.Model): - bad = models.IntegerField() - good = models.IntegerField(unique=True) - - -class NonUniqueFKTarget2(models.Model): - """ Model to test for non-unique FK target in previously seen model: expect an error """ - tgt = models.ForeignKey(FKTarget, to_field='bad') - - -class UniqueFKTarget2(models.Model): - """ Model to test for unique FK target in previously seen model: expect no error """ - tgt = models.ForeignKey(FKTarget, to_field='good') - - -class NonExistingOrderingWithSingleUnderscore(models.Model): - class Meta: - ordering = ("does_not_exist",) - - -class InvalidSetNull(models.Model): - fk = models.ForeignKey('self', on_delete=models.SET_NULL) - - -class InvalidSetDefault(models.Model): - fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT) - - -class UnicodeForeignKeys(models.Model): - """Foreign keys which can translate to ascii should be OK, but fail if - they're not.""" - good = models.ForeignKey('FKTarget') - also_good = models.ManyToManyField('FKTarget', related_name='unicode2') - - # In Python 3 this should become legal, but currently causes unicode errors - # when adding the errors in core/management/validation.py - #bad = models.ForeignKey('★') - - -class PrimaryKeyNull(models.Model): - my_pk_field = models.IntegerField(primary_key=True, null=True) - - -class OrderByPKModel(models.Model): - """ - Model to test that ordering by pk passes validation. - Refs #8291 - """ - name = models.CharField(max_length=100, blank=True) - - class Meta: - ordering = ('pk',) - - -class SwappableModel(models.Model): - """A model that can be, but isn't swapped out. - - References to this model *shoudln't* raise any validation error. - """ - name = models.CharField(max_length=100) - - class Meta: - swappable = 'TEST_SWAPPABLE_MODEL' - - -class SwappedModel(models.Model): - """A model that is swapped out. - - References to this model *should* raise a validation error. - Requires TEST_SWAPPED_MODEL to be defined in the test environment; - this is guaranteed by the test runner using @override_settings. - - The foreign keys and m2m relations on this model *shouldn't* - install related accessors, so there shouldn't be clashes with - the equivalent names on the replacement. - """ - name = models.CharField(max_length=100) - - foreign = models.ForeignKey(Target, related_name='swappable_fk_set') - m2m = models.ManyToManyField(Target, related_name='swappable_m2m_set') - - class Meta: - swappable = 'TEST_SWAPPED_MODEL' - - -class ReplacementModel(models.Model): - """A replacement model for swapping purposes.""" - name = models.CharField(max_length=100) - - foreign = models.ForeignKey(Target, related_name='swappable_fk_set') - m2m = models.ManyToManyField(Target, related_name='swappable_m2m_set') - - -class BadSwappableValue(models.Model): - """A model that can be swapped out; during testing, the swappable - value is not of the format app.model - """ - name = models.CharField(max_length=100) - - class Meta: - swappable = 'TEST_SWAPPED_MODEL_BAD_VALUE' - - -class BadSwappableModel(models.Model): - """A model that can be swapped out; during testing, the swappable - value references an unknown model. - """ - name = models.CharField(max_length=100) - - class Meta: - swappable = 'TEST_SWAPPED_MODEL_BAD_MODEL' - - -class HardReferenceModel(models.Model): - fk_1 = models.ForeignKey(SwappableModel, related_name='fk_hardref1') - fk_2 = models.ForeignKey('invalid_models.SwappableModel', related_name='fk_hardref2') - fk_3 = models.ForeignKey(SwappedModel, related_name='fk_hardref3') - fk_4 = models.ForeignKey('invalid_models.SwappedModel', related_name='fk_hardref4') - m2m_1 = models.ManyToManyField(SwappableModel, related_name='m2m_hardref1') - m2m_2 = models.ManyToManyField('invalid_models.SwappableModel', related_name='m2m_hardref2') - m2m_3 = models.ManyToManyField(SwappedModel, related_name='m2m_hardref3') - m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4') - - -class BadIndexTogether1(models.Model): - class Meta: - index_together = [ - ["field_that_does_not_exist"], - ] - - -class DuplicateColumnNameModel1(models.Model): - """ - A field (bar) attempts to use a column name which is already auto-assigned - earlier in the class. This should raise a validation error. - """ - foo = models.IntegerField() - bar = models.IntegerField(db_column='foo') - - class Meta: - db_table = 'foobar' - - -class DuplicateColumnNameModel2(models.Model): - """ - A field (foo) attempts to use a column name which is already auto-assigned - later in the class. This should raise a validation error. - """ - foo = models.IntegerField(db_column='bar') - bar = models.IntegerField() - - class Meta: - db_table = 'foobar' - - -class DuplicateColumnNameModel3(models.Model): - """Two fields attempt to use each others' names. - - This is not a desirable scenario but valid nonetheless. - - It should not raise a validation error. - """ - foo = models.IntegerField(db_column='bar') - bar = models.IntegerField(db_column='foo') - - class Meta: - db_table = 'foobar3' - - -class DuplicateColumnNameModel4(models.Model): - """Two fields attempt to use the same db_column value. - - This should raise a validation error. - """ - foo = models.IntegerField(db_column='baz') - bar = models.IntegerField(db_column='baz') - - class Meta: - db_table = 'foobar' - - -model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer. -invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer. -invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer. -invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute that is a non-negative integer. -invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute that is a positive integer. -invalid_models.fielderrors: "decimalfield2": DecimalFields require a "decimal_places" attribute that is a non-negative integer. -invalid_models.fielderrors: "decimalfield2": DecimalFields require a "max_digits" attribute that is a positive integer. -invalid_models.fielderrors: "decimalfield3": DecimalFields require a "decimal_places" attribute that is a non-negative integer. -invalid_models.fielderrors: "decimalfield3": DecimalFields require a "max_digits" attribute that is a positive integer. -invalid_models.fielderrors: "decimalfield4": DecimalFields require a "max_digits" attribute value that is greater than or equal to the value of the "decimal_places" attribute. -invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list). -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). -invalid_models.fielderrors: "index": "db_index" should be either None, True or False. -invalid_models.fielderrors: "field_": Field names cannot end with underscores, because this would lead to ambiguous queryset filters. -invalid_models.fielderrors: "nullbool": BooleanFields do not accept null values. Use a NullBooleanField instead. -invalid_models.fielderrors: "generic_ip_notnull_blank": GenericIPAddressField can not accept blank values if null values are not allowed, as blank values are stored as null. -invalid_models.clash1: Accessor for field 'foreign' clashes with field 'Target.clash1_set'. Add a related_name argument to the definition for 'foreign'. -invalid_models.clash1: Accessor for field 'foreign' clashes with accessor for field 'Clash1.m2m'. Add a related_name argument to the definition for 'foreign'. -invalid_models.clash1: Reverse query name for field 'foreign' clashes with field 'Target.clash1'. Add a related_name argument to the definition for 'foreign'. -invalid_models.clash1: Accessor for m2m field 'm2m' clashes with field 'Target.clash1_set'. Add a related_name argument to the definition for 'm2m'. -invalid_models.clash1: Accessor for m2m field 'm2m' clashes with accessor for field 'Clash1.foreign'. Add a related_name argument to the definition for 'm2m'. -invalid_models.clash1: Reverse query name for m2m field 'm2m' clashes with field 'Target.clash1'. Add a related_name argument to the definition for 'm2m'. -invalid_models.clash2: Accessor for field 'foreign_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'foreign_1'. -invalid_models.clash2: Accessor for field 'foreign_1' clashes with accessor for field 'Clash2.m2m_1'. Add a related_name argument to the definition for 'foreign_1'. -invalid_models.clash2: Reverse query name for field 'foreign_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'foreign_1'. -invalid_models.clash2: Reverse query name for field 'foreign_1' clashes with accessor for field 'Clash2.m2m_1'. Add a related_name argument to the definition for 'foreign_1'. -invalid_models.clash2: Accessor for field 'foreign_2' clashes with accessor for field 'Clash2.m2m_2'. Add a related_name argument to the definition for 'foreign_2'. -invalid_models.clash2: Reverse query name for field 'foreign_2' clashes with accessor for field 'Clash2.m2m_2'. Add a related_name argument to the definition for 'foreign_2'. -invalid_models.clash2: Accessor for m2m field 'm2m_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'm2m_1'. -invalid_models.clash2: Accessor for m2m field 'm2m_1' clashes with accessor for field 'Clash2.foreign_1'. Add a related_name argument to the definition for 'm2m_1'. -invalid_models.clash2: Reverse query name for m2m field 'm2m_1' clashes with field 'Target.id'. Add a related_name argument to the definition for 'm2m_1'. -invalid_models.clash2: Reverse query name for m2m field 'm2m_1' clashes with accessor for field 'Clash2.foreign_1'. Add a related_name argument to the definition for 'm2m_1'. -invalid_models.clash2: Accessor for m2m field 'm2m_2' clashes with accessor for field 'Clash2.foreign_2'. Add a related_name argument to the definition for 'm2m_2'. -invalid_models.clash2: Reverse query name for m2m field 'm2m_2' clashes with accessor for field 'Clash2.foreign_2'. Add a related_name argument to the definition for 'm2m_2'. -invalid_models.clash3: Accessor for field 'foreign_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'foreign_1'. -invalid_models.clash3: Accessor for field 'foreign_1' clashes with accessor for field 'Clash3.m2m_1'. Add a related_name argument to the definition for 'foreign_1'. -invalid_models.clash3: Reverse query name for field 'foreign_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'foreign_1'. -invalid_models.clash3: Reverse query name for field 'foreign_1' clashes with accessor for field 'Clash3.m2m_1'. Add a related_name argument to the definition for 'foreign_1'. -invalid_models.clash3: Accessor for field 'foreign_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'foreign_2'. -invalid_models.clash3: Accessor for field 'foreign_2' clashes with accessor for field 'Clash3.m2m_2'. Add a related_name argument to the definition for 'foreign_2'. -invalid_models.clash3: Reverse query name for field 'foreign_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'foreign_2'. -invalid_models.clash3: Reverse query name for field 'foreign_2' clashes with accessor for field 'Clash3.m2m_2'. Add a related_name argument to the definition for 'foreign_2'. -invalid_models.clash3: Accessor for m2m field 'm2m_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'm2m_1'. -invalid_models.clash3: Accessor for m2m field 'm2m_1' clashes with accessor for field 'Clash3.foreign_1'. Add a related_name argument to the definition for 'm2m_1'. -invalid_models.clash3: Reverse query name for m2m field 'm2m_1' clashes with field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'm2m_1'. -invalid_models.clash3: Reverse query name for m2m field 'm2m_1' clashes with accessor for field 'Clash3.foreign_1'. Add a related_name argument to the definition for 'm2m_1'. -invalid_models.clash3: Accessor for m2m field 'm2m_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'm2m_2'. -invalid_models.clash3: Accessor for m2m field 'm2m_2' clashes with accessor for field 'Clash3.foreign_2'. Add a related_name argument to the definition for 'm2m_2'. -invalid_models.clash3: Reverse query name for m2m field 'm2m_2' clashes with m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'm2m_2'. -invalid_models.clash3: Reverse query name for m2m field 'm2m_2' clashes with accessor for field 'Clash3.foreign_2'. Add a related_name argument to the definition for 'm2m_2'. -invalid_models.clashforeign: Accessor for field 'foreign' clashes with field 'Target2.clashforeign_set'. Add a related_name argument to the definition for 'foreign'. -invalid_models.clashm2m: Accessor for m2m field 'm2m' clashes with m2m field 'Target2.clashm2m_set'. Add a related_name argument to the definition for 'm2m'. -invalid_models.target2: Accessor for field 'foreign_tgt' clashes with accessor for field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'foreign_tgt'. -invalid_models.target2: Accessor for field 'foreign_tgt' clashes with accessor for field 'Target2.clashm2m_set'. Add a related_name argument to the definition for 'foreign_tgt'. -invalid_models.target2: Accessor for field 'foreign_tgt' clashes with accessor for field 'Target2.clashforeign_set'. Add a related_name argument to the definition for 'foreign_tgt'. -invalid_models.target2: Accessor for field 'clashforeign_set' clashes with accessor for field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'clashforeign_set'. -invalid_models.target2: Accessor for field 'clashforeign_set' clashes with accessor for field 'Target2.clashm2m_set'. Add a related_name argument to the definition for 'clashforeign_set'. -invalid_models.target2: Accessor for field 'clashforeign_set' clashes with accessor for field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'clashforeign_set'. -invalid_models.target2: Accessor for m2m field 'm2m_tgt' clashes with accessor for m2m field 'Target2.clashm2m_set'. Add a related_name argument to the definition for 'm2m_tgt'. -invalid_models.target2: Accessor for m2m field 'm2m_tgt' clashes with accessor for field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'm2m_tgt'. -invalid_models.target2: Accessor for m2m field 'm2m_tgt' clashes with accessor for field 'Target2.clashforeign_set'. Add a related_name argument to the definition for 'm2m_tgt'. -invalid_models.target2: Accessor for m2m field 'clashm2m_set' clashes with accessor for m2m field 'Target2.m2m_tgt'. Add a related_name argument to the definition for 'clashm2m_set'. -invalid_models.target2: Accessor for m2m field 'clashm2m_set' clashes with accessor for field 'Target2.foreign_tgt'. Add a related_name argument to the definition for 'clashm2m_set'. -invalid_models.target2: Accessor for m2m field 'clashm2m_set' clashes with accessor for field 'Target2.clashforeign_set'. Add a related_name argument to the definition for 'clashm2m_set'. -invalid_models.selfclashforeign: Accessor for field 'selfclashforeign_set' clashes with field 'SelfClashForeign.selfclashforeign_set'. Add a related_name argument to the definition for 'selfclashforeign_set'. -invalid_models.selfclashforeign: Reverse query name for field 'selfclashforeign_set' clashes with field 'SelfClashForeign.selfclashforeign'. Add a related_name argument to the definition for 'selfclashforeign_set'. -invalid_models.selfclashforeign: Accessor for field 'foreign_1' clashes with field 'SelfClashForeign.id'. Add a related_name argument to the definition for 'foreign_1'. -invalid_models.selfclashforeign: Reverse query name for field 'foreign_1' clashes with field 'SelfClashForeign.id'. Add a related_name argument to the definition for 'foreign_1'. -invalid_models.selfclashforeign: Accessor for field 'foreign_2' clashes with field 'SelfClashForeign.src_safe'. Add a related_name argument to the definition for 'foreign_2'. -invalid_models.selfclashforeign: Reverse query name for field 'foreign_2' clashes with field 'SelfClashForeign.src_safe'. Add a related_name argument to the definition for 'foreign_2'. -invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'selfclashm2m_set'. -invalid_models.selfclashm2m: Reverse query name for m2m field 'selfclashm2m_set' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'selfclashm2m_set'. -invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with accessor for m2m field 'SelfClashM2M.m2m_3'. Add a related_name argument to the definition for 'selfclashm2m_set'. -invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with accessor for m2m field 'SelfClashM2M.m2m_4'. Add a related_name argument to the definition for 'selfclashm2m_set'. -invalid_models.selfclashm2m: Accessor for m2m field 'm2m_1' clashes with field 'SelfClashM2M.id'. Add a related_name argument to the definition for 'm2m_1'. -invalid_models.selfclashm2m: Accessor for m2m field 'm2m_2' clashes with field 'SelfClashM2M.src_safe'. Add a related_name argument to the definition for 'm2m_2'. -invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_1' clashes with field 'SelfClashM2M.id'. Add a related_name argument to the definition for 'm2m_1'. -invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_2' clashes with field 'SelfClashM2M.src_safe'. Add a related_name argument to the definition for 'm2m_2'. -invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'. -invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with accessor for m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'. -invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with accessor for m2m field 'SelfClashM2M.m2m_4'. Add a related_name argument to the definition for 'm2m_3'. -invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'. -invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with accessor for m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'. -invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with accessor for m2m field 'SelfClashM2M.m2m_3'. Add a related_name argument to the definition for 'm2m_4'. -invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_3'. -invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_4' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_4'. -invalid_models.missingrelations: 'rel1' has a relation with model Rel1, which has either not been installed or is abstract. -invalid_models.missingrelations: 'rel2' has an m2m relation with model Rel2, which has either not been installed or is abstract. -invalid_models.grouptwo: 'primary' is a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo -invalid_models.grouptwo: 'secondary' is a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo -invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed -invalid_models.group: The model Group has two manually-defined m2m relations through the model Membership, which is not permitted. Please consider using an extra field on your intermediary model instead. -invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to Person, which is ambiguous and is not permitted. -invalid_models.personselfrefm2m: Many-to-many fields with intermediate tables cannot be symmetrical. -invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys to PersonSelfRefM2M, which is ambiguous and is not permitted. -invalid_models.personselfrefm2mexplicit: Many-to-many fields with intermediate tables cannot be symmetrical. -invalid_models.abstractrelationmodel: 'fk1' has a relation with model AbstractModel, which has either not been installed or is abstract. -invalid_models.abstractrelationmodel: 'fk2' has an m2m relation with model AbstractModel, which has either not been installed or is abstract. -invalid_models.uniquem2m: ManyToManyFields cannot be unique. Remove the unique argument on 'unique_people'. -invalid_models.nonuniquefktarget1: Field 'bad' under model 'FKTarget' must have a unique=True constraint. -invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have a unique=True constraint. -invalid_models.nonexistingorderingwithsingleunderscore: "ordering" refers to "does_not_exist", a field that doesn't exist. -invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null. -invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value. -invalid_models.hardreferencemodel: 'fk_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. -invalid_models.hardreferencemodel: 'fk_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. -invalid_models.hardreferencemodel: 'm2m_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. -invalid_models.hardreferencemodel: 'm2m_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. -invalid_models.badswappablevalue: TEST_SWAPPED_MODEL_BAD_VALUE is not of the form 'app_label.app_name'. -invalid_models.badswappablemodel: Model has been swapped out for 'not_an_app.Target' which has not been installed or is abstract. -invalid_models.badindextogether1: "index_together" refers to field_that_does_not_exist, a field that doesn't exist. -invalid_models.duplicatecolumnnamemodel1: Field 'bar' has column name 'foo' that is already used. -invalid_models.duplicatecolumnnamemodel2: Field 'bar' has column name 'bar' that is already used. -invalid_models.duplicatecolumnnamemodel4: Field 'bar' has column name 'baz' that is already used. -""" - -if not connection.features.interprets_empty_strings_as_nulls: - model_errors += """invalid_models.primarykeynull: "my_pk_field": Primary key fields cannot have null=True. -""" diff --git a/tests/invalid_models_tests/test_backend_specific.py b/tests/invalid_models_tests/test_backend_specific.py new file mode 100644 index 0000000000..7e60fb7567 --- /dev/null +++ b/tests/invalid_models_tests/test_backend_specific.py @@ -0,0 +1,68 @@ +# -*- encoding: utf-8 -*- +from __future__ import unicode_literals + +from types import MethodType + +from django.core.checks import Error +from django.db import connection, models + +from .base import IsolatedModelsTestCase + + +class BackendSpecificChecksTests(IsolatedModelsTestCase): + + def test_check_field(self): + """ Test if backend specific checks are performed. """ + + error = Error('an error', hint=None) + + def mock(self, field, **kwargs): + return [error] + + class Model(models.Model): + field = models.IntegerField() + + field = Model._meta.get_field('field') + + # Mock connection.validation.check_field method. + v = connection.validation + old_check_field = v.check_field + v.check_field = MethodType(mock, v) + try: + errors = field.check() + finally: + # Unmock connection.validation.check_field method. + v.check_field = old_check_field + + self.assertEqual(errors, [error]) + + def test_validate_field(self): + """ Errors raised by deprecated `validate_field` method should be + collected. """ + + def mock(self, errors, opts, field): + errors.add(opts, "An error!") + + class Model(models.Model): + field = models.IntegerField() + + field = Model._meta.get_field('field') + expected = [ + Error( + "An error!", + hint=None, + obj=field, + ) + ] + + # Mock connection.validation.validate_field method. + v = connection.validation + old_validate_field = v.validate_field + v.validate_field = MethodType(mock, v) + try: + errors = field.check() + finally: + # Unmock connection.validation.validate_field method. + v.validate_field = old_validate_field + + self.assertEqual(errors, expected) diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py new file mode 100644 index 0000000000..8515cc8070 --- /dev/null +++ b/tests/invalid_models_tests/test_models.py @@ -0,0 +1,334 @@ +# -*- encoding: utf-8 -*- +from __future__ import unicode_literals + +from django.core.checks import Error +from django.db import models +from django.test.utils import override_settings + +from .base import IsolatedModelsTestCase + + +class IndexTogetherTests(IsolatedModelsTestCase): + + def test_non_iterable(self): + class Model(models.Model): + class Meta: + index_together = 42 + + errors = Model.check() + expected = [ + Error( + '"index_together" must be a list or tuple.', + hint=None, + obj=Model, + id='E006', + ), + ] + self.assertEqual(errors, expected) + + def test_non_list(self): + class Model(models.Model): + class Meta: + index_together = 'not-a-list' + + errors = Model.check() + expected = [ + Error( + '"index_together" must be a list or tuple.', + hint=None, + obj=Model, + id='E006', + ), + ] + self.assertEqual(errors, expected) + + def test_list_containing_non_iterable(self): + class Model(models.Model): + class Meta: + index_together = [ + 'non-iterable', + 'second-non-iterable', + ] + + errors = Model.check() + expected = [ + Error( + 'All "index_together" elements must be lists or tuples.', + hint=None, + obj=Model, + id='E007', + ), + ] + self.assertEqual(errors, expected) + + def test_pointing_to_missing_field(self): + class Model(models.Model): + class Meta: + index_together = [ + ["missing_field"], + ] + + errors = Model.check() + expected = [ + Error( + '"index_together" points to a missing field named "missing_field".', + hint='Ensure that you did not misspell the field name.', + obj=Model, + id='E010', + ), + ] + self.assertEqual(errors, expected) + + def test_pointing_to_m2m_field(self): + class Model(models.Model): + m2m = models.ManyToManyField('self') + + class Meta: + index_together = [ + ["m2m"], + ] + + errors = Model.check() + expected = [ + Error( + ('"index_together" refers to a m2m "m2m" field, but ' + 'ManyToManyFields are not supported in "index_together".'), + hint=None, + obj=Model, + id='E011', + ), + ] + self.assertEqual(errors, expected) + + +# unique_together tests are very similar to index_together tests. +class UniqueTogetherTests(IsolatedModelsTestCase): + + def test_non_iterable(self): + class Model(models.Model): + class Meta: + unique_together = 42 + + errors = Model.check() + expected = [ + Error( + '"unique_together" must be a list or tuple.', + hint=None, + obj=Model, + id='E008', + ), + ] + self.assertEqual(errors, expected) + + def test_list_containing_non_iterable(self): + class Model(models.Model): + one = models.IntegerField() + two = models.IntegerField() + + class Meta: + unique_together = [('a', 'b'), 42] + + errors = Model.check() + expected = [ + Error( + 'All "unique_together" elements must be lists or tuples.', + hint=None, + obj=Model, + id='E009', + ), + ] + self.assertEqual(errors, expected) + + def test_valid_model(self): + class Model(models.Model): + one = models.IntegerField() + two = models.IntegerField() + + class Meta: + # unique_together can be a simple tuple + unique_together = ('one', 'two') + + errors = Model.check() + self.assertEqual(errors, []) + + def test_pointing_to_missing_field(self): + class Model(models.Model): + class Meta: + unique_together = [ + ["missing_field"], + ] + + errors = Model.check() + expected = [ + Error( + '"unique_together" points to a missing field named "missing_field".', + hint='Ensure that you did not misspell the field name.', + obj=Model, + id='E010', + ), + ] + self.assertEqual(errors, expected) + + def test_pointing_to_m2m(self): + class Model(models.Model): + m2m = models.ManyToManyField('self') + + class Meta: + unique_together = [ + ["m2m"], + ] + + errors = Model.check() + expected = [ + Error( + ('"unique_together" refers to a m2m "m2m" field, but ' + 'ManyToManyFields are not supported in "unique_together".'), + hint=None, + obj=Model, + id='E011', + ), + ] + self.assertEqual(errors, expected) + + +class OtherModelTests(IsolatedModelsTestCase): + + def test_unique_primary_key(self): + class Model(models.Model): + id = models.IntegerField(primary_key=False) + + errors = Model.check() + expected = [ + Error( + ('You cannot use "id" as a field name, because each model ' + 'automatically gets an "id" field if none of the fields ' + 'have primary_key=True.'), + hint='Remove or rename "id" field or add primary_key=True to a field.', + obj=Model, + id='E005', + ), + Error( + 'Field "id" has column name "id" that is already used.', + hint=None, + obj=Model, + ) + ] + self.assertEqual(errors, expected) + + def test_field_names_ending_with_underscore(self): + class Model(models.Model): + field_ = models.CharField(max_length=10) + m2m_ = models.ManyToManyField('self') + + errors = Model.check() + expected = [ + Error( + 'Field names must not end with underscores.', + hint=None, + obj=Model._meta.get_field('field_'), + id='E001', + ), + Error( + 'Field names must not end with underscores.', + hint=None, + obj=Model._meta.get_field('m2m_'), + id='E001', + ), + ] + self.assertEqual(errors, expected) + + def test_ordering_non_iterable(self): + class Model(models.Model): + class Meta: + ordering = "missing_field" + + errors = Model.check() + expected = [ + Error( + ('"ordering" must be a tuple or list ' + '(even if you want to order by only one field).'), + hint=None, + obj=Model, + id='E012', + ), + ] + self.assertEqual(errors, expected) + + def test_ordering_pointing_to_missing_field(self): + class Model(models.Model): + class Meta: + ordering = ("missing_field",) + + errors = Model.check() + expected = [ + Error( + '"ordering" pointing to a missing "missing_field" field.', + hint='Ensure that you did not misspell the field name.', + obj=Model, + id='E013', + ) + ] + self.assertEqual(errors, expected) + + @override_settings(TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model') + def test_swappable_missing_app_name(self): + class Model(models.Model): + class Meta: + swappable = 'TEST_SWAPPED_MODEL_BAD_VALUE' + + errors = Model.check() + expected = [ + Error( + '"TEST_SWAPPED_MODEL_BAD_VALUE" is not of the form "app_label.app_name".', + hint=None, + obj=Model, + id='E002', + ), + ] + self.assertEqual(errors, expected) + + @override_settings(TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target') + def test_swappable_missing_app(self): + class Model(models.Model): + class Meta: + swappable = 'TEST_SWAPPED_MODEL_BAD_MODEL' + + errors = Model.check() + expected = [ + Error( + ('The model has been swapped out for not_an_app.Target ' + 'which has not been installed or is abstract.'), + hint=('Ensure that you did not misspell the model name and ' + 'the app name as well as the model is not abstract. Does ' + 'your INSTALLED_APPS setting contain the "not_an_app" app?'), + obj=Model, + id='E003', + ), + ] + self.assertEqual(errors, expected) + + def test_two_m2m_through_same_relationship(self): + class Person(models.Model): + pass + + class Group(models.Model): + primary = models.ManyToManyField(Person, + through="Membership", related_name="primary") + secondary = models.ManyToManyField(Person, through="Membership", + related_name="secondary") + + class Membership(models.Model): + person = models.ForeignKey(Person) + group = models.ForeignKey(Group) + + errors = Group.check() + expected = [ + Error( + ('The model has two many-to-many relations through ' + 'the intermediary Membership model, which is not permitted.'), + hint=None, + obj=Group, + id='E004', + ) + ] + self.assertEqual(errors, expected) diff --git a/tests/invalid_models_tests/test_ordinary_fields.py b/tests/invalid_models_tests/test_ordinary_fields.py new file mode 100644 index 0000000000..9db8c95ce7 --- /dev/null +++ b/tests/invalid_models_tests/test_ordinary_fields.py @@ -0,0 +1,417 @@ +# -*- encoding: utf-8 -*- +from __future__ import unicode_literals + +from django.core.checks import Error +from django.core.exceptions import ImproperlyConfigured +from django.db import models + +from .base import IsolatedModelsTestCase + + +class AutoFieldTests(IsolatedModelsTestCase): + + def test_valid_case(self): + class Model(models.Model): + id = models.AutoField(primary_key=True) + + field = Model._meta.get_field('id') + errors = field.check() + expected = [] + self.assertEqual(errors, expected) + + def test_primary_key(self): + # primary_key must be True. Refs #12467. + class Model(models.Model): + field = models.AutoField(primary_key=False) + + # Prevent Django from autocreating `id` AutoField, which would + # result in an error, because a model must have exactly one + # AutoField. + another = models.IntegerField(primary_key=True) + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + 'The field must have primary_key=True, because it is an AutoField.', + hint=None, + obj=field, + id='E048', + ), + ] + self.assertEqual(errors, expected) + + +class BooleanFieldTests(IsolatedModelsTestCase): + + def test_nullable_boolean_field(self): + class Model(models.Model): + field = models.BooleanField(null=True) + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + 'BooleanFields do not acceps null values.', + hint='Use a NullBooleanField instead.', + obj=field, + id='E037', + ), + ] + self.assertEqual(errors, expected) + + +class CharFieldTests(IsolatedModelsTestCase): + + def test_valid_field(self): + class Model(models.Model): + field = models.CharField( + max_length=255, + choices=[ + ('1', 'item1'), + ('2', 'item2'), + ], + db_index=True) + + field = Model._meta.get_field('field') + errors = field.check() + expected = [] + self.assertEqual(errors, expected) + + def test_missing_max_length(self): + class Model(models.Model): + field = models.CharField() + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + 'The field must have "max_length" attribute.', + hint=None, + obj=field, + id='E038', + ), + ] + self.assertEqual(errors, expected) + + def test_negative_max_length(self): + class Model(models.Model): + field = models.CharField(max_length=-1) + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + '"max_length" must be a positive integer.', + hint=None, + obj=field, + id='E039', + ), + ] + self.assertEqual(errors, expected) + + def test_bad_max_length_value(self): + class Model(models.Model): + field = models.CharField(max_length="bad") + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + '"max_length" must be a positive integer.', + hint=None, + obj=field, + id='E039', + ), + ] + self.assertEqual(errors, expected) + + def test_non_iterable_choices(self): + class Model(models.Model): + field = models.CharField(max_length=10, choices='bad') + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + '"choices" must be an iterable (e.g., a list or tuple).', + hint=None, + obj=field, + id='E033', + ), + ] + self.assertEqual(errors, expected) + + def test_choices_containing_non_pairs(self): + class Model(models.Model): + field = models.CharField(max_length=10, choices=[(1, 2, 3), (1, 2, 3)]) + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + ('All "choices" elements must be a tuple of two elements ' + '(the first one is the actual value to be stored ' + 'and the second element is the human-readable name).'), + hint=None, + obj=field, + id='E034', + ), + ] + self.assertEqual(errors, expected) + + def test_bad_db_index_value(self): + class Model(models.Model): + field = models.CharField(max_length=10, db_index='bad') + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + '"db_index" must be either None, True or False.', + hint=None, + obj=field, + id='E035', + ), + ] + self.assertEqual(errors, expected) + + def test_too_long_char_field_under_mysql(self): + from django.db.backends.mysql.validation import DatabaseValidation + + class Model(models.Model): + field = models.CharField(unique=True, max_length=256) + + field = Model._meta.get_field('field') + validator = DatabaseValidation(connection=None) + errors = validator.check_field(field) + expected = [ + Error( + ('Under mysql backend, the field cannot have a "max_length" ' + 'greated than 255 when it is unique.'), + hint=None, + obj=field, + id='E047', + ) + ] + self.assertEqual(errors, expected) + + +class DecimalFieldTests(IsolatedModelsTestCase): + + def test_required_attributes(self): + class Model(models.Model): + field = models.DecimalField() + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + 'The field requires a "decimal_places" attribute.', + hint=None, + obj=field, + id='E041', + ), + Error( + 'The field requires a "max_digits" attribute.', + hint=None, + obj=field, + id='E043', + ), + ] + self.assertEqual(errors, expected) + + def test_negative_max_digits_and_decimal_places(self): + class Model(models.Model): + field = models.DecimalField(max_digits=-1, decimal_places=-1) + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + '"decimal_places" attribute must be a non-negative integer.', + hint=None, + obj=field, + id='E042', + ), + Error( + '"max_digits" attribute must be a positive integer.', + hint=None, + obj=field, + id='E044', + ), + ] + self.assertEqual(errors, expected) + + def test_bad_values_of_max_digits_and_decimal_places(self): + class Model(models.Model): + field = models.DecimalField(max_digits="bad", decimal_places="bad") + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + '"decimal_places" attribute must be a non-negative integer.', + hint=None, + obj=field, + id='E042', + ), + Error( + '"max_digits" attribute must be a positive integer.', + hint=None, + obj=field, + id='E044', + ), + ] + self.assertEqual(errors, expected) + + def test_decimal_places_greater_than_max_digits(self): + class Model(models.Model): + field = models.DecimalField(max_digits=9, decimal_places=10) + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + '"max_digits" must be greater or equal to "decimal_places".', + hint=None, + obj=field, + id='E040', + ), + ] + self.assertEqual(errors, expected) + + def test_valid_field(self): + class Model(models.Model): + field = models.DecimalField(max_digits=10, decimal_places=10) + + field = Model._meta.get_field('field') + errors = field.check() + expected = [] + self.assertEqual(errors, expected) + + +class FileFieldTests(IsolatedModelsTestCase): + + def test_valid_case(self): + class Model(models.Model): + field = models.FileField(upload_to='somewhere') + + field = Model._meta.get_field('field') + errors = field.check() + expected = [] + self.assertEqual(errors, expected) + + # def test_missing_upload_to(self): + # class Model(models.Model): + # field = models.FileField() + + # field = Model._meta.get_field('field') + # errors = field.check() + # expected = [ + # Error( + # 'The field requires an "upload_to" attribute.', + # hint=None, + # obj=field, + # id='E031', + # ), + # ] + # self.assertEqual(errors, expected) + + def test_unique(self): + class Model(models.Model): + field = models.FileField(unique=False, upload_to='somewhere') + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + '"unique" is not a valid argument for FileField.', + hint=None, + obj=field, + id='E049', + ) + ] + self.assertEqual(errors, expected) + + def test_primary_key(self): + class Model(models.Model): + field = models.FileField(primary_key=False, upload_to='somewhere') + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + '"primary_key" is not a valid argument for FileField.', + hint=None, + obj=field, + id='E050', + ) + ] + self.assertEqual(errors, expected) + + +class FilePathFieldTests(IsolatedModelsTestCase): + + def test_forbidden_files_and_folders(self): + class Model(models.Model): + field = models.FilePathField(allow_files=False, allow_folders=False) + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + 'The field must have either "allow_files" or "allow_folders" set to True.', + hint=None, + obj=field, + id='E045', + ), + ] + self.assertEqual(errors, expected) + + +class GenericIPAddressFieldTests(IsolatedModelsTestCase): + + def test_non_nullable_blank(self): + class Model(models.Model): + field = models.GenericIPAddressField(null=False, blank=True) + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + ('The field cannot accept blank values if null values ' + 'are not allowed, as blank values are stored as null.'), + hint=None, + obj=field, + id='E046', + ), + ] + self.assertEqual(errors, expected) + + +class ImageFieldTests(IsolatedModelsTestCase): + + def test_pillow_installed(self): + try: + import django.utils.image # NOQA + except ImproperlyConfigured: + pillow_installed = False + else: + pillow_installed = True + + class Model(models.Model): + field = models.ImageField(upload_to='somewhere') + + field = Model._meta.get_field('field') + errors = field.check() + expected = [] if pillow_installed else [ + Error( + 'To use ImageFields, Pillow must be installed.', + hint=('Get Pillow at https://pypi.python.org/pypi/Pillow ' + 'or run command "pip install pillow".'), + obj=field, + id='E032', + ), + ] + self.assertEqual(errors, expected) diff --git a/tests/invalid_models_tests/test_relative_fields.py b/tests/invalid_models_tests/test_relative_fields.py new file mode 100644 index 0000000000..4853763031 --- /dev/null +++ b/tests/invalid_models_tests/test_relative_fields.py @@ -0,0 +1,1037 @@ +# -*- encoding: utf-8 -*- +from __future__ import unicode_literals + +from django.core.checks import Error +from django.db import models +from django.test.utils import override_settings +from django.test.testcases import skipIfDBFeature + +from .base import IsolatedModelsTestCase + + +class RelativeFieldTests(IsolatedModelsTestCase): + + def test_valid_foreign_key_without_accessor(self): + class Target(models.Model): + # There would be a clash if Model.field installed an accessor. + model = models.IntegerField() + + class Model(models.Model): + field = models.ForeignKey(Target, related_name='+') + + field = Model._meta.get_field('field') + errors = field.check() + self.assertEqual(errors, []) + + def test_foreign_key_to_missing_model(self): + # Model names are resolved when a model is being created, so we cannot + # test relative fields in isolation and we need to attach them to a + # model. + class Model(models.Model): + foreign_key = models.ForeignKey('Rel1') + + field = Model._meta.get_field('foreign_key') + errors = field.check() + expected = [ + Error( + ('The field has a relation with model Rel1, ' + 'which has either not been installed or is abstract.'), + hint=('Ensure that you did not misspell the model name and ' + 'the model is not abstract. Does your INSTALLED_APPS ' + 'setting contain the app where Rel1 is defined?'), + obj=field, + id='E030', + ), + ] + self.assertEqual(errors, expected) + + def test_many_to_many_to_missing_model(self): + class Model(models.Model): + m2m = models.ManyToManyField("Rel2") + + field = Model._meta.get_field('m2m') + errors = field.check(from_model=Model) + expected = [ + Error( + ('The field has a relation with model Rel2, ' + 'which has either not been installed or is abstract.'), + hint=('Ensure that you did not misspell the model name and ' + 'the model is not abstract. Does your INSTALLED_APPS ' + 'setting contain the app where Rel2 is defined?'), + obj=field, + id='E030', + ), + ] + self.assertEqual(errors, expected) + + def test_ambiguous_relationship_model(self): + + class Person(models.Model): + pass + + class Group(models.Model): + field = models.ManyToManyField('Person', + through="AmbiguousRelationship", related_name='tertiary') + + class AmbiguousRelationship(models.Model): + # Too much foreign keys to Person. + first_person = models.ForeignKey(Person, related_name="first") + second_person = models.ForeignKey(Person, related_name="second") + second_model = models.ForeignKey(Group) + + field = Group._meta.get_field('field') + errors = field.check(from_model=Group) + expected = [ + Error( + ('The model is used as an intermediary model by ' + 'invalid_models_tests.Group.field, but it has more than one ' + 'foreign key to Person, ' + 'which is ambiguous and is not permitted.'), + hint=('If you want to create a recursive relationship, use ' + 'ForeignKey("self", symmetrical=False, ' + 'through="AmbiguousRelationship").'), + obj=field, + id='E027', + ), + ] + self.assertEqual(errors, expected) + + def test_relationship_model_with_foreign_key_to_wrong_model(self): + class WrongModel(models.Model): + pass + + class Person(models.Model): + pass + + class Group(models.Model): + members = models.ManyToManyField('Person', + through="InvalidRelationship") + + class InvalidRelationship(models.Model): + person = models.ForeignKey(Person) + wrong_foreign_key = models.ForeignKey(WrongModel) + # The last foreign key should point to Group model. + + field = Group._meta.get_field('members') + errors = field.check(from_model=Group) + expected = [ + Error( + ('The model is used as an intermediary model by ' + 'invalid_models_tests.Group.members, but it misses ' + 'a foreign key to Group or Person.'), + hint=None, + obj=InvalidRelationship, + id='E028', + ), + ] + self.assertEqual(errors, expected) + + def test_relationship_model_missing_foreign_key(self): + class Person(models.Model): + pass + + class Group(models.Model): + members = models.ManyToManyField('Person', + through="InvalidRelationship") + + class InvalidRelationship(models.Model): + group = models.ForeignKey(Group) + # No foreign key to Person + + field = Group._meta.get_field('members') + errors = field.check(from_model=Group) + expected = [ + Error( + ('The model is used as an intermediary model by ' + 'invalid_models_tests.Group.members, but it misses ' + 'a foreign key to Group or Person.'), + hint=None, + obj=InvalidRelationship, + id='E028', + ), + ] + self.assertEqual(errors, expected) + + def test_missing_relationship_model(self): + class Person(models.Model): + pass + + class Group(models.Model): + members = models.ManyToManyField('Person', + through="MissingM2MModel") + + field = Group._meta.get_field('members') + errors = field.check(from_model=Group) + expected = [ + Error( + ('The field specifies a many-to-many relation through model ' + 'MissingM2MModel, which has not been installed.'), + hint=('Ensure that you did not misspell the model name and ' + 'the model is not abstract. Does your INSTALLED_APPS ' + 'setting contain the app where MissingM2MModel is defined?'), + obj=field, + id='E023', + ), + ] + self.assertEqual(errors, expected) + + def test_symmetrical_self_referential_field(self): + class Person(models.Model): + # Implicit symmetrical=False. + friends = models.ManyToManyField('self', through="Relationship") + + class Relationship(models.Model): + first = models.ForeignKey(Person, related_name="rel_from_set") + second = models.ForeignKey(Person, related_name="rel_to_set") + + field = Person._meta.get_field('friends') + errors = field.check(from_model=Person) + expected = [ + Error( + 'Many-to-many fields with intermediate tables must not be symmetrical.', + hint=None, + obj=field, + id='E024', + ), + ] + self.assertEqual(errors, expected) + + def test_too_many_foreign_keys_in_self_referential_model(self): + class Person(models.Model): + friends = models.ManyToManyField('self', + through="InvalidRelationship", symmetrical=False) + + class InvalidRelationship(models.Model): + first = models.ForeignKey(Person, related_name="rel_from_set_2") + second = models.ForeignKey(Person, related_name="rel_to_set_2") + third = models.ForeignKey(Person, related_name="too_many_by_far") + + field = Person._meta.get_field('friends') + errors = field.check(from_model=Person) + expected = [ + Error( + ('The model is used as an intermediary model by ' + 'invalid_models_tests.Person.friends, but it has more than two ' + 'foreign keys to Person, which is ambiguous and ' + 'is not permitted.'), + hint=None, + obj=InvalidRelationship, + id='E025', + ), + ] + self.assertEqual(errors, expected) + + def test_symmetric_self_reference_with_intermediate_table(self): + class Person(models.Model): + # Explicit symmetrical=True. + friends = models.ManyToManyField('self', + through="Relationship", symmetrical=True) + + class Relationship(models.Model): + first = models.ForeignKey(Person, related_name="rel_from_set") + second = models.ForeignKey(Person, related_name="rel_to_set") + + field = Person._meta.get_field('friends') + errors = field.check(from_model=Person) + expected = [ + Error( + 'Many-to-many fields with intermediate tables must not be symmetrical.', + hint=None, + obj=field, + id='E024', + ), + ] + self.assertEqual(errors, expected) + + def test_foreign_key_to_abstract_model(self): + class Model(models.Model): + foreign_key = models.ForeignKey('AbstractModel') + + class AbstractModel(models.Model): + class Meta: + abstract = True + + field = Model._meta.get_field('foreign_key') + errors = field.check() + expected = [ + Error( + ('The field has a relation with model AbstractModel, ' + 'which has either not been installed or is abstract.'), + hint=('Ensure that you did not misspell the model name and ' + 'the model is not abstract. Does your INSTALLED_APPS ' + 'setting contain the app where AbstractModel is defined?'), + obj=field, + id='E030', + ), + ] + self.assertEqual(errors, expected) + + def test_m2m_to_abstract_model(self): + class AbstractModel(models.Model): + class Meta: + abstract = True + + class Model(models.Model): + m2m = models.ManyToManyField('AbstractModel') + + field = Model._meta.get_field('m2m') + errors = field.check(from_model=Model) + expected = [ + Error( + ('The field has a relation with model AbstractModel, ' + 'which has either not been installed or is abstract.'), + hint=('Ensure that you did not misspell the model name and ' + 'the model is not abstract. Does your INSTALLED_APPS ' + 'setting contain the app where AbstractModel is defined?'), + obj=field, + id='E030', + ), + ] + self.assertEqual(errors, expected) + + def test_unique_m2m(self): + class Person(models.Model): + name = models.CharField(max_length=5) + + class Group(models.Model): + members = models.ManyToManyField('Person', unique=True) + + field = Group._meta.get_field('members') + errors = field.check(from_model=Group) + expected = [ + Error( + 'ManyToManyFields must not be unique.', + hint=None, + obj=field, + id='E022', + ), + ] + self.assertEqual(errors, expected) + + def test_foreign_key_to_non_unique_field(self): + class Target(models.Model): + bad = models.IntegerField() # No unique=True + + class Model(models.Model): + foreign_key = models.ForeignKey('Target', to_field='bad') + + field = Model._meta.get_field('foreign_key') + errors = field.check() + expected = [ + Error( + 'Target.bad must have unique=True because it is referenced by a foreign key.', + hint=None, + obj=field, + id='E019', + ), + ] + self.assertEqual(errors, expected) + + def test_foreign_key_to_non_unique_field_under_explicit_model(self): + class Target(models.Model): + bad = models.IntegerField() + + class Model(models.Model): + field = models.ForeignKey(Target, to_field='bad') + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + 'Target.bad must have unique=True because it is referenced by a foreign key.', + hint=None, + obj=field, + id='E019', + ), + ] + self.assertEqual(errors, expected) + + def test_foreign_object_to_non_unique_fields(self): + class Person(models.Model): + # Note that both fields are not unique. + country_id = models.IntegerField() + city_id = models.IntegerField() + + class MMembership(models.Model): + person_country_id = models.IntegerField() + person_city_id = models.IntegerField() + + person = models.ForeignObject(Person, + from_fields=['person_country_id', 'person_city_id'], + to_fields=['country_id', 'city_id']) + + field = MMembership._meta.get_field('person') + errors = field.check() + expected = [ + Error( + ('No unique=True constraint on field combination ' + '"country_id,city_id" under model Person.'), + hint=('Set unique=True argument on any of the fields ' + '"country_id,city_id" under model Person.'), + obj=field, + id='E018', + ) + ] + self.assertEqual(errors, expected) + + def test_on_delete_set_null_on_non_nullable_field(self): + class Person(models.Model): + pass + + class Model(models.Model): + foreign_key = models.ForeignKey('Person', + on_delete=models.SET_NULL) + + field = Model._meta.get_field('foreign_key') + errors = field.check() + expected = [ + Error( + 'The field specifies on_delete=SET_NULL, but cannot be null.', + hint='Set null=True argument on the field.', + obj=field, + id='E020', + ), + ] + self.assertEqual(errors, expected) + + def test_on_delete_set_default_without_default_value(self): + class Person(models.Model): + pass + + class Model(models.Model): + foreign_key = models.ForeignKey('Person', + on_delete=models.SET_DEFAULT) + + field = Model._meta.get_field('foreign_key') + errors = field.check() + expected = [ + Error( + 'The field specifies on_delete=SET_DEFAULT, but has no default value.', + hint=None, + obj=field, + id='E021', + ), + ] + self.assertEqual(errors, expected) + + @skipIfDBFeature('interprets_empty_strings_as_nulls') + def test_nullable_primary_key(self): + class Model(models.Model): + field = models.IntegerField(primary_key=True, null=True) + + field = Model._meta.get_field('field') + errors = field.check() + expected = [ + Error( + 'Primary keys must not have null=True.', + hint='Set null=False on the field or remove primary_key=True argument.', + obj=field, + id='E036', + ), + ] + self.assertEqual(errors, expected) + + def test_not_swapped_model(self): + class SwappableModel(models.Model): + # A model that can be, but isn't swapped out. References to this + # model should *not* raise any validation error. + class Meta: + swappable = 'TEST_SWAPPABLE_MODEL' + + class Model(models.Model): + explicit_fk = models.ForeignKey(SwappableModel, + related_name='explicit_fk') + implicit_fk = models.ForeignKey('invalid_models_tests.SwappableModel', + related_name='implicit_fk') + explicit_m2m = models.ManyToManyField(SwappableModel, + related_name='explicit_m2m') + implicit_m2m = models.ManyToManyField( + 'invalid_models_tests.SwappableModel', + related_name='implicit_m2m') + + explicit_fk = Model._meta.get_field('explicit_fk') + self.assertEqual(explicit_fk.check(), []) + + implicit_fk = Model._meta.get_field('implicit_fk') + self.assertEqual(implicit_fk.check(), []) + + explicit_m2m = Model._meta.get_field('explicit_m2m') + self.assertEqual(explicit_m2m.check(from_model=Model), []) + + implicit_m2m = Model._meta.get_field('implicit_m2m') + self.assertEqual(implicit_m2m.check(from_model=Model), []) + + @override_settings(TEST_SWAPPED_MODEL='invalid_models_tests.Replacement') + def test_referencing_to_swapped_model(self): + class Replacement(models.Model): + pass + + class SwappedModel(models.Model): + class Meta: + swappable = 'TEST_SWAPPED_MODEL' + + class Model(models.Model): + explicit_fk = models.ForeignKey(SwappedModel, + related_name='explicit_fk') + implicit_fk = models.ForeignKey('invalid_models_tests.SwappedModel', + related_name='implicit_fk') + explicit_m2m = models.ManyToManyField(SwappedModel, + related_name='explicit_m2m') + implicit_m2m = models.ManyToManyField( + 'invalid_models_tests.SwappedModel', + related_name='implicit_m2m') + + fields = [ + Model._meta.get_field('explicit_fk'), + Model._meta.get_field('implicit_fk'), + Model._meta.get_field('explicit_m2m'), + Model._meta.get_field('implicit_m2m'), + ] + + expected_error = Error( + ('The field defines a relation with the model ' + 'invalid_models_tests.SwappedModel, which has been swapped out.'), + hint='Update the relation to point at settings.TEST_SWAPPED_MODEL', + id='E029', + ) + + for field in fields: + expected_error.obj = field + errors = field.check(from_model=Model) + self.assertEqual(errors, [expected_error]) + + +class AccessorClashTests(IsolatedModelsTestCase): + + def test_fk_to_integer(self): + self._test_accessor_clash( + target=models.IntegerField(), + relative=models.ForeignKey('Target')) + + def test_fk_to_fk(self): + self._test_accessor_clash( + target=models.ForeignKey('Another'), + relative=models.ForeignKey('Target')) + + def test_fk_to_m2m(self): + self._test_accessor_clash( + target=models.ManyToManyField('Another'), + relative=models.ForeignKey('Target')) + + def test_m2m_to_integer(self): + self._test_accessor_clash( + target=models.IntegerField(), + relative=models.ManyToManyField('Target')) + + def test_m2m_to_fk(self): + self._test_accessor_clash( + target=models.ForeignKey('Another'), + relative=models.ManyToManyField('Target')) + + def test_m2m_to_m2m(self): + self._test_accessor_clash( + target=models.ManyToManyField('Another'), + relative=models.ManyToManyField('Target')) + + def _test_accessor_clash(self, target, relative): + class Another(models.Model): + pass + + class Target(models.Model): + model_set = target + + class Model(models.Model): + rel = relative + + errors = Model.check() + expected = [ + Error( + 'Accessor for field Model.rel clashes with field Target.model_set.', + hint=('Rename field Target.model_set or add/change ' + 'a related_name argument to the definition ' + 'for field Model.rel.'), + obj=Model._meta.get_field('rel'), + id='E014', + ), + ] + self.assertEqual(errors, expected) + + def test_clash_between_accessors(self): + class Target(models.Model): + pass + + class Model(models.Model): + foreign = models.ForeignKey(Target) + m2m = models.ManyToManyField(Target) + + errors = Model.check() + expected = [ + Error( + 'Clash between accessors for Model.foreign and Model.m2m.', + hint=('Add or change a related_name argument to the definition ' + 'for Model.foreign or Model.m2m.'), + obj=Model._meta.get_field('foreign'), + id='E016', + ), + Error( + 'Clash between accessors for Model.m2m and Model.foreign.', + hint=('Add or change a related_name argument to the definition ' + 'for Model.m2m or Model.foreign.'), + obj=Model._meta.get_field('m2m'), + id='E016', + ), + ] + self.assertEqual(errors, expected) + + +class ReverseQueryNameClashTests(IsolatedModelsTestCase): + + def test_fk_to_integer(self): + self._test_reverse_query_name_clash( + target=models.IntegerField(), + relative=models.ForeignKey('Target')) + + def test_fk_to_fk(self): + self._test_reverse_query_name_clash( + target=models.ForeignKey('Another'), + relative=models.ForeignKey('Target')) + + def test_fk_to_m2m(self): + self._test_reverse_query_name_clash( + target=models.ManyToManyField('Another'), + relative=models.ForeignKey('Target')) + + def test_m2m_to_integer(self): + self._test_reverse_query_name_clash( + target=models.IntegerField(), + relative=models.ManyToManyField('Target')) + + def test_m2m_to_fk(self): + self._test_reverse_query_name_clash( + target=models.ForeignKey('Another'), + relative=models.ManyToManyField('Target')) + + def test_m2m_to_m2m(self): + self._test_reverse_query_name_clash( + target=models.ManyToManyField('Another'), + relative=models.ManyToManyField('Target')) + + def _test_reverse_query_name_clash(self, target, relative): + class Another(models.Model): + pass + + class Target(models.Model): + model = target + + class Model(models.Model): + rel = relative + + errors = Model.check() + expected = [ + Error( + 'Reverse query name for field Model.rel clashes with field Target.model.', + hint=('Rename field Target.model or add/change ' + 'a related_name argument to the definition ' + 'for field Model.rel.'), + obj=Model._meta.get_field('rel'), + id='E015', + ), + ] + self.assertEqual(errors, expected) + + +class ExplicitRelatedNameClashTests(IsolatedModelsTestCase): + + def test_fk_to_integer(self): + self._test_explicit_related_name_clash( + target=models.IntegerField(), + relative=models.ForeignKey('Target', related_name='clash')) + + def test_fk_to_fk(self): + self._test_explicit_related_name_clash( + target=models.ForeignKey('Another'), + relative=models.ForeignKey('Target', related_name='clash')) + + def test_fk_to_m2m(self): + self._test_explicit_related_name_clash( + target=models.ManyToManyField('Another'), + relative=models.ForeignKey('Target', related_name='clash')) + + def test_m2m_to_integer(self): + self._test_explicit_related_name_clash( + target=models.IntegerField(), + relative=models.ManyToManyField('Target', related_name='clash')) + + def test_m2m_to_fk(self): + self._test_explicit_related_name_clash( + target=models.ForeignKey('Another'), + relative=models.ManyToManyField('Target', related_name='clash')) + + def test_m2m_to_m2m(self): + self._test_explicit_related_name_clash( + target=models.ManyToManyField('Another'), + relative=models.ManyToManyField('Target', related_name='clash')) + + def _test_explicit_related_name_clash(self, target, relative): + class Another(models.Model): + pass + + class Target(models.Model): + clash = target + + class Model(models.Model): + rel = relative + + errors = Model.check() + expected = [ + Error( + 'Accessor for field Model.rel clashes with field Target.clash.', + hint=('Rename field Target.clash or add/change ' + 'a related_name argument to the definition ' + 'for field Model.rel.'), + obj=Model._meta.get_field('rel'), + id='E014', + ), + Error( + 'Reverse query name for field Model.rel clashes with field Target.clash.', + hint=('Rename field Target.clash or add/change ' + 'a related_name argument to the definition ' + 'for field Model.rel.'), + obj=Model._meta.get_field('rel'), + id='E015', + ), + ] + self.assertEqual(errors, expected) + + +class ExplicitRelatedQueryNameClashTests(IsolatedModelsTestCase): + + def test_fk_to_integer(self): + self._test_explicit_related_query_name_clash( + target=models.IntegerField(), + relative=models.ForeignKey('Target', + related_query_name='clash')) + + def test_fk_to_fk(self): + self._test_explicit_related_query_name_clash( + target=models.ForeignKey('Another'), + relative=models.ForeignKey('Target', + related_query_name='clash')) + + def test_fk_to_m2m(self): + self._test_explicit_related_query_name_clash( + target=models.ManyToManyField('Another'), + relative=models.ForeignKey('Target', + related_query_name='clash')) + + def test_m2m_to_integer(self): + self._test_explicit_related_query_name_clash( + target=models.IntegerField(), + relative=models.ManyToManyField('Target', + related_query_name='clash')) + + def test_m2m_to_fk(self): + self._test_explicit_related_query_name_clash( + target=models.ForeignKey('Another'), + relative=models.ManyToManyField('Target', + related_query_name='clash')) + + def test_m2m_to_m2m(self): + self._test_explicit_related_query_name_clash( + target=models.ManyToManyField('Another'), + relative=models.ManyToManyField('Target', + related_query_name='clash')) + + def _test_explicit_related_query_name_clash(self, target, relative): + class Another(models.Model): + pass + + class Target(models.Model): + clash = target + + class Model(models.Model): + rel = relative + + errors = Model.check() + expected = [ + Error( + 'Reverse query name for field Model.rel clashes with field Target.clash.', + hint=('Rename field Target.clash or add/change a related_name ' + 'argument to the definition for field Model.rel.'), + obj=Model._meta.get_field('rel'), + id='E015', + ), + ] + self.assertEqual(errors, expected) + + +class SelfReferentialM2MClashTests(IsolatedModelsTestCase): + + def test_clash_between_accessors(self): + class Model(models.Model): + first_m2m = models.ManyToManyField('self', symmetrical=False) + second_m2m = models.ManyToManyField('self', symmetrical=False) + + errors = Model.check() + expected = [ + Error( + 'Clash between accessors for Model.first_m2m and Model.second_m2m.', + hint=('Add or change a related_name argument to the definition ' + 'for Model.first_m2m or Model.second_m2m.'), + obj=Model._meta.get_field('first_m2m'), + id='E016', + ), + Error( + 'Clash between accessors for Model.second_m2m and Model.first_m2m.', + hint=('Add or change a related_name argument to the definition ' + 'for Model.second_m2m or Model.first_m2m.'), + obj=Model._meta.get_field('second_m2m'), + id='E016', + ), + ] + self.assertEqual(errors, expected) + + def test_accessor_clash(self): + class Model(models.Model): + model_set = models.ManyToManyField("self", symmetrical=False) + + errors = Model.check() + expected = [ + Error( + 'Accessor for field Model.model_set clashes with field Model.model_set.', + hint=('Rename field Model.model_set or add/change ' + 'a related_name argument to the definition ' + 'for field Model.model_set.'), + obj=Model._meta.get_field('model_set'), + id='E014', + ), + ] + self.assertEqual(errors, expected) + + def test_reverse_query_name_clash(self): + class Model(models.Model): + model = models.ManyToManyField("self", symmetrical=False) + + errors = Model.check() + expected = [ + Error( + 'Reverse query name for field Model.model clashes with field Model.model.', + hint=('Rename field Model.model or add/change a related_name ' + 'argument to the definition for field Model.model.'), + obj=Model._meta.get_field('model'), + id='E015', + ), + ] + self.assertEqual(errors, expected) + + def test_clash_under_explicit_related_name(self): + class Model(models.Model): + clash = models.IntegerField() + m2m = models.ManyToManyField("self", + symmetrical=False, related_name='clash') + + errors = Model.check() + expected = [ + Error( + 'Accessor for field Model.m2m clashes with field Model.clash.', + hint=('Rename field Model.clash or add/change a related_name ' + 'argument to the definition for field Model.m2m.'), + obj=Model._meta.get_field('m2m'), + id='E014', + ), + Error( + 'Reverse query name for field Model.m2m clashes with field Model.clash.', + hint=('Rename field Model.clash or add/change a related_name ' + 'argument to the definition for field Model.m2m.'), + obj=Model._meta.get_field('m2m'), + id='E015', + ), + ] + self.assertEqual(errors, expected) + + def test_valid_model(self): + class Model(models.Model): + first = models.ManyToManyField("self", + symmetrical=False, related_name='first_accessor') + second = models.ManyToManyField("self", + symmetrical=False, related_name='second_accessor') + + errors = Model.check() + self.assertEqual(errors, []) + + +class SelfReferentialFKClashTests(IsolatedModelsTestCase): + + def test_accessor_clash(self): + class Model(models.Model): + model_set = models.ForeignKey("Model") + + errors = Model.check() + expected = [ + Error( + 'Accessor for field Model.model_set clashes with field Model.model_set.', + hint=('Rename field Model.model_set or add/change ' + 'a related_name argument to the definition ' + 'for field Model.model_set.'), + obj=Model._meta.get_field('model_set'), + id='E014', + ), + ] + self.assertEqual(errors, expected) + + def test_reverse_query_name_clash(self): + class Model(models.Model): + model = models.ForeignKey("Model") + + errors = Model.check() + expected = [ + Error( + 'Reverse query name for field Model.model clashes with field Model.model.', + hint=('Rename field Model.model or add/change ' + 'a related_name argument to the definition ' + 'for field Model.model.'), + obj=Model._meta.get_field('model'), + id='E015', + ), + ] + self.assertEqual(errors, expected) + + def test_clash_under_explicit_related_name(self): + class Model(models.Model): + clash = models.CharField(max_length=10) + foreign = models.ForeignKey("Model", related_name='clash') + + errors = Model.check() + expected = [ + Error( + 'Accessor for field Model.foreign clashes with field Model.clash.', + hint=('Rename field Model.clash or add/change ' + 'a related_name argument to the definition ' + 'for field Model.foreign.'), + obj=Model._meta.get_field('foreign'), + id='E014', + ), + Error( + 'Reverse query name for field Model.foreign clashes with field Model.clash.', + hint=('Rename field Model.clash or add/change ' + 'a related_name argument to the definition ' + 'for field Model.foreign.'), + obj=Model._meta.get_field('foreign'), + id='E015', + ), + ] + self.assertEqual(errors, expected) + + +class ComplexClashTests(IsolatedModelsTestCase): + + # New tests should not be included here, because this is a single, + # self-contained sanity check, not a test of everything. + def test_complex_clash(self): + class Target(models.Model): + tgt_safe = models.CharField(max_length=10) + clash = models.CharField(max_length=10) + model = models.CharField(max_length=10) + + clash1_set = models.CharField(max_length=10) + + class Model(models.Model): + src_safe = models.CharField(max_length=10) + + foreign_1 = models.ForeignKey(Target, related_name='id') + foreign_2 = models.ForeignKey(Target, related_name='src_safe') + + m2m_1 = models.ManyToManyField(Target, related_name='id') + m2m_2 = models.ManyToManyField(Target, related_name='src_safe') + + errors = Model.check() + expected = [ + Error( + 'Accessor for field Model.foreign_1 clashes with field Target.id.', + hint=('Rename field Target.id or add/change a related_name ' + 'argument to the definition for field Model.foreign_1.'), + obj=Model._meta.get_field('foreign_1'), + id='E014', + ), + Error( + 'Reverse query name for field Model.foreign_1 clashes with field Target.id.', + hint=('Rename field Target.id or add/change a related_name ' + 'argument to the definition for field Model.foreign_1.'), + obj=Model._meta.get_field('foreign_1'), + id='E015', + ), + Error( + 'Clash between accessors for Model.foreign_1 and Model.m2m_1.', + hint=('Add or change a related_name argument to ' + 'the definition for Model.foreign_1 or Model.m2m_1.'), + obj=Model._meta.get_field('foreign_1'), + id='E016', + ), + Error( + 'Clash between reverse query names for Model.foreign_1 and Model.m2m_1.', + hint=('Add or change a related_name argument to ' + 'the definition for Model.foreign_1 or Model.m2m_1.'), + obj=Model._meta.get_field('foreign_1'), + id='E017', + ), + + Error( + 'Clash between accessors for Model.foreign_2 and Model.m2m_2.', + hint=('Add or change a related_name argument ' + 'to the definition for Model.foreign_2 or Model.m2m_2.'), + obj=Model._meta.get_field('foreign_2'), + id='E016', + ), + Error( + 'Clash between reverse query names for Model.foreign_2 and Model.m2m_2.', + hint=('Add or change a related_name argument to ' + 'the definition for Model.foreign_2 or Model.m2m_2.'), + obj=Model._meta.get_field('foreign_2'), + id='E017', + ), + + Error( + 'Accessor for field Model.m2m_1 clashes with field Target.id.', + hint=('Rename field Target.id or add/change a related_name ' + 'argument to the definition for field Model.m2m_1.'), + obj=Model._meta.get_field('m2m_1'), + id='E014', + ), + Error( + 'Reverse query name for field Model.m2m_1 clashes with field Target.id.', + hint=('Rename field Target.id or add/change a related_name ' + 'argument to the definition for field Model.m2m_1.'), + obj=Model._meta.get_field('m2m_1'), + id='E015', + ), + Error( + 'Clash between accessors for Model.m2m_1 and Model.foreign_1.', + hint=('Add or change a related_name argument to the definition ' + 'for Model.m2m_1 or Model.foreign_1.'), + obj=Model._meta.get_field('m2m_1'), + id='E016', + ), + Error( + 'Clash between reverse query names for Model.m2m_1 and Model.foreign_1.', + hint=('Add or change a related_name argument to ' + 'the definition for Model.m2m_1 or Model.foreign_1.'), + obj=Model._meta.get_field('m2m_1'), + id='E017', + ), + + Error( + 'Clash between accessors for Model.m2m_2 and Model.foreign_2.', + hint=('Add or change a related_name argument to the definition ' + 'for Model.m2m_2 or Model.foreign_2.'), + obj=Model._meta.get_field('m2m_2'), + id='E016', + ), + Error( + 'Clash between reverse query names for Model.m2m_2 and Model.foreign_2.', + hint=('Add or change a related_name argument to the definition ' + 'for Model.m2m_2 or Model.foreign_2.'), + obj=Model._meta.get_field('m2m_2'), + id='E017', + ), + ] + self.assertEqual(errors, expected) diff --git a/tests/invalid_models_tests/tests.py b/tests/invalid_models_tests/tests.py deleted file mode 100644 index 08cea2e15f..0000000000 --- a/tests/invalid_models_tests/tests.py +++ /dev/null @@ -1,46 +0,0 @@ -import sys -import unittest - -from django.apps import apps -from django.core.management.validation import get_validation_errors -from django.test import override_settings -from django.utils.six import StringIO - - -class InvalidModelTestCase(unittest.TestCase): - """Import an appliation with invalid models and test the exceptions.""" - - def setUp(self): - # Make sure sys.stdout is not a tty so that we get errors without - # coloring attached (makes matching the results easier). We restore - # sys.stderr afterwards. - self.old_stdout = sys.stdout - self.stdout = StringIO() - sys.stdout = self.stdout - - def tearDown(self): - sys.stdout = self.old_stdout - - # Technically, this isn't an override -- TEST_SWAPPED_MODEL must be - # set to *something* in order for the test to work. However, it's - # easier to set this up as an override than to require every developer - # to specify a value in their test settings. - @override_settings( - INSTALLED_APPS=['invalid_models_tests.invalid_models'], - TEST_SWAPPED_MODEL='invalid_models.ReplacementModel', - TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model', - TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target', - ) - def test_invalid_models(self): - app_config = apps.get_app_config("invalid_models") - get_validation_errors(self.stdout, app_config) - - self.stdout.seek(0) - error_log = self.stdout.read() - actual = error_log.split('\n') - expected = app_config.models_module.model_errors.split('\n') - - unexpected = [err for err in actual if err not in expected] - missing = [err for err in expected if err not in actual] - self.assertFalse(unexpected, "Unexpected Errors: " + '\n'.join(unexpected)) - self.assertFalse(missing, "Missing Errors: " + '\n'.join(missing)) |
