From f25fa9d8590c7759c1ec7ffecf1d67be81237674 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 18 Dec 2013 17:56:11 +0100 Subject: Deprecated load_app(). Adjusted several tests that used it to add apps to the app cache and then attempted to remove them by manipulating attributes directly. Also renamed invalid_models to invalid_models_tests to avoid clashing application labels between the outer and the inner invalid_models applications. --- django/core/apps/cache.py | 25 +- tests/admin_scripts/tests.py | 2 +- tests/app_loading/tests.py | 35 +- tests/invalid_models/__init__.py | 0 tests/invalid_models/invalid_models/__init__.py | 0 tests/invalid_models/invalid_models/models.py | 535 --------------------- tests/invalid_models/tests.py | 55 --- tests/invalid_models_tests/__init__.py | 0 .../invalid_models/__init__.py | 0 .../invalid_models_tests/invalid_models/models.py | 535 +++++++++++++++++++++ tests/invalid_models_tests/tests.py | 46 ++ tests/migrations/test_writer.py | 10 +- tests/proxy_model_inheritance/tests.py | 21 +- tests/runtests.py | 3 +- 14 files changed, 625 insertions(+), 642 deletions(-) delete mode 100644 tests/invalid_models/__init__.py delete mode 100644 tests/invalid_models/invalid_models/__init__.py delete mode 100644 tests/invalid_models/invalid_models/models.py delete mode 100644 tests/invalid_models/tests.py create mode 100644 tests/invalid_models_tests/__init__.py create mode 100644 tests/invalid_models_tests/invalid_models/__init__.py create mode 100644 tests/invalid_models_tests/invalid_models/models.py create mode 100644 tests/invalid_models_tests/tests.py diff --git a/django/core/apps/cache.py b/django/core/apps/cache.py index 74488325a8..65f51801ec 100644 --- a/django/core/apps/cache.py +++ b/django/core/apps/cache.py @@ -133,16 +133,6 @@ class AppCache(object): self._models_loaded = True - def load_app(self, app_name): - """ - Loads the app with the provided fully qualified name, and returns the - model module. - """ - app_config = AppConfig(app_name) - app_config.import_models(self.all_models[app_config.label]) - self.app_configs[app_config.label] = app_config - return app_config.models_module - def app_cache_ready(self): """ Returns true if the model cache is fully populated. @@ -377,6 +367,19 @@ class AppCache(object): ### DEPRECATED METHODS GO BELOW THIS LINE ### + def load_app(self, app_name): + """ + Loads the app with the provided fully qualified name, and returns the + model module. + """ + warnings.warn( + "load_app(app_name) is deprecated.", + PendingDeprecationWarning, stacklevel=2) + app_config = AppConfig(app_name) + app_config.import_models(self.all_models[app_config.label]) + self.app_configs[app_config.label] = app_config + return app_config.models_module + def get_app(self, app_label): """ Returns the module containing the models for the given app_label. @@ -447,7 +450,7 @@ class AppCache(object): Register a set of models as belonging to an app. """ warnings.warn( - "register_models(app_label, models) is deprecated.", + "register_models(app_label, *models) is deprecated.", PendingDeprecationWarning, stacklevel=2) for model in models: self.register_model(app_label, model) diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index db0b8d6030..3f9a336c16 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -1094,7 +1094,7 @@ class ManageValidate(AdminScriptTestCase): self.assertOutput(err, 'ImportError') def test_complex_app(self): - "manage.py validate does not raise an ImportError validating a complex app with nested calls to load_app" + "manage.py validate does not raise an ImportError validating a complex app" self.write_settings('settings.py', apps=['admin_scripts.complex_app', 'admin_scripts.simple_app'], sdict={'DEBUG': True}) diff --git a/tests/app_loading/tests.py b/tests/app_loading/tests.py index 45f6cd6c31..bb71ad84a2 100644 --- a/tests/app_loading/tests.py +++ b/tests/app_loading/tests.py @@ -8,6 +8,7 @@ from django.core.apps import app_cache from django.core.apps.cache import AppCache from django.test.utils import override_settings from django.utils._os import upath +from django.utils import six class EggLoadingTest(TestCase): @@ -31,45 +32,41 @@ class EggLoadingTest(TestCase): """Models module can be loaded from an app in an egg""" egg_name = '%s/modelapp.egg' % self.egg_dir sys.path.append(egg_name) - models = app_cache.load_app('app_with_models') - self.assertFalse(models is None) + with app_cache._with_app('app_with_models'): + models_module = app_cache.get_app_config('app_with_models').models_module + self.assertIsNotNone(models_module) def test_egg2(self): """Loading an app from an egg that has no models returns no models (and no error)""" egg_name = '%s/nomodelapp.egg' % self.egg_dir sys.path.append(egg_name) - models = app_cache.load_app('app_no_models') - self.assertTrue(models is None) + with app_cache._with_app('app_no_models'): + models_module = app_cache.get_app_config('app_no_models').models_module + self.assertIsNone(models_module) def test_egg3(self): """Models module can be loaded from an app located under an egg's top-level package""" egg_name = '%s/omelet.egg' % self.egg_dir sys.path.append(egg_name) - models = app_cache.load_app('omelet.app_with_models') - self.assertFalse(models is None) + with app_cache._with_app('omelet.app_with_models'): + models_module = app_cache.get_app_config('app_with_models').models_module + self.assertIsNotNone(models_module) def test_egg4(self): """Loading an app with no models from under the top-level egg package generates no error""" egg_name = '%s/omelet.egg' % self.egg_dir sys.path.append(egg_name) - models = app_cache.load_app('omelet.app_no_models') - self.assertTrue(models is None) + with app_cache._with_app('omelet.app_no_models'): + models_module = app_cache.get_app_config('app_no_models').models_module + self.assertIsNone(models_module) def test_egg5(self): """Loading an app from an egg that has an import error in its models module raises that error""" egg_name = '%s/brokenapp.egg' % self.egg_dir sys.path.append(egg_name) - self.assertRaises(ImportError, app_cache.load_app, 'broken_app') - raised = None - try: - app_cache.load_app('broken_app') - except ImportError as e: - raised = e - - # Make sure the message is indicating the actual - # problem in the broken app. - self.assertTrue(raised is not None) - self.assertTrue("modelz" in raised.args[0]) + with six.assertRaisesRegex(self, ImportError, 'modelz'): + with app_cache._with_app('broken_app'): + app_cache.get_app_config('omelet.app_no_models').models_module def test_missing_app(self): """ diff --git a/tests/invalid_models/__init__.py b/tests/invalid_models/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/invalid_models/invalid_models/__init__.py b/tests/invalid_models/invalid_models/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/invalid_models/invalid_models/models.py b/tests/invalid_models/invalid_models/models.py deleted file mode 100644 index 0c991dcf13..0000000000 --- a/tests/invalid_models/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.py b/tests/invalid_models/tests.py deleted file mode 100644 index 10d8fa47a7..0000000000 --- a/tests/invalid_models/tests.py +++ /dev/null @@ -1,55 +0,0 @@ -import sys -import unittest - -from django.core.apps import app_cache -from django.core.management.validation import get_validation_errors -from django.test.utils 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 - - # The models need to be removed after the test in order to prevent bad - # interactions with the flush operation in other tests. - self._old_models = app_cache.app_configs['invalid_models'].models.copy() - - def tearDown(self): - app_cache.app_configs['invalid_models'].models = self._old_models - app_cache.all_models['invalid_models'] = self._old_models - app_cache._get_models_cache = {} - 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( - 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): - try: - module = app_cache.load_app("invalid_models.invalid_models") - except Exception: - self.fail('Unable to load invalid model module') - - get_validation_errors(self.stdout, module) - self.stdout.seek(0) - error_log = self.stdout.read() - actual = error_log.split('\n') - expected = 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)) diff --git a/tests/invalid_models_tests/__init__.py b/tests/invalid_models_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/invalid_models_tests/invalid_models/__init__.py b/tests/invalid_models_tests/invalid_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/invalid_models_tests/invalid_models/models.py b/tests/invalid_models_tests/invalid_models/models.py new file mode 100644 index 0000000000..0c991dcf13 --- /dev/null +++ b/tests/invalid_models_tests/invalid_models/models.py @@ -0,0 +1,535 @@ +# 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/tests.py b/tests/invalid_models_tests/tests.py new file mode 100644 index 0000000000..4e0cef546b --- /dev/null +++ b/tests/invalid_models_tests/tests.py @@ -0,0 +1,46 @@ +import sys +import unittest + +from django.core.apps import app_cache +from django.core.management.validation import get_validation_errors +from django.test.utils 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( + 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): + with app_cache._with_app("invalid_models_tests.invalid_models"): + module = app_cache.get_app_config("invalid_models").models_module + get_validation_errors(self.stdout, module) + + self.stdout.seek(0) + error_log = self.stdout.read() + actual = error_log.split('\n') + expected = 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)) diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 76fe5b5a9e..f921f9f03c 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -124,8 +124,8 @@ class WriterTests(TestCase): with override_settings(INSTALLED_APPS=test_apps): for app in test_apps: - app_cache.load_app(app) - migration = migrations.Migration('0001_initial', app.split('.')[-1]) - expected_path = os.path.join(base_dir, *(app.split('.') + ['migrations', '0001_initial.py'])) - writer = MigrationWriter(migration) - self.assertEqual(writer.path, expected_path) + with app_cache._with_app(app): + migration = migrations.Migration('0001_initial', app.split('.')[-1]) + expected_path = os.path.join(base_dir, *(app.split('.') + ['migrations', '0001_initial.py'])) + writer = MigrationWriter(migration) + self.assertEqual(writer.path, expected_path) diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py index 601fdc5b42..01a2ddfef6 100644 --- a/tests/proxy_model_inheritance/tests.py +++ b/tests/proxy_model_inheritance/tests.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import os import sys -from django.conf import settings from django.core.apps import app_cache from django.core.management import call_command from django.test import TestCase, TransactionTestCase @@ -21,28 +20,21 @@ class ProxyModelInheritanceTests(TransactionTestCase): for the proxied model (as described in #12286). This test creates two dummy apps and calls migrate, then verifies that the table has been created. """ - - available_apps = [] + available_apps = ['app1', 'app2'] def setUp(self): self.old_sys_path = sys.path[:] sys.path.append(os.path.dirname(os.path.abspath(upath(__file__)))) - for app in settings.INSTALLED_APPS: - app_cache.load_app(app) + self._with_app1 = app_cache._begin_with_app('app1') + self._with_app2 = app_cache._begin_with_app('app2') def tearDown(self): + app_cache._end_with_app(self._with_app1) + app_cache._end_with_app(self._with_app2) sys.path = self.old_sys_path - del app_cache.app_configs['app1'] - del app_cache.app_configs['app2'] - del app_cache.all_models['app1'] - del app_cache.all_models['app2'] def test_table_exists(self): - try: - app_cache.set_available_apps(settings.INSTALLED_APPS) - call_command('migrate', verbosity=0) - finally: - app_cache.unset_available_apps() + call_command('migrate', verbosity=0) from .app1.models import ProxyModel from .app2.models import NiceModel self.assertEqual(NiceModel.objects.all().count(), 0) @@ -56,7 +48,6 @@ class MultiTableInheritanceProxyTest(TestCase): Deleting an instance of a model proxying a multi-table inherited subclass should cascade delete down the whole inheritance chain (see #18083). - """ instance = ConcreteModelSubclassProxy.objects.create() instance.delete() diff --git a/tests/runtests.py b/tests/runtests.py index 77cf005267..64d363a095 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -164,7 +164,8 @@ def setup(verbosity, test_labels): if module_found_in_labels: if verbosity >= 2: print("Importing application %s" % module_name) - app_cache.load_app(module_label) + # HACK. + app_cache._begin_with_app(module_label) if module_label not in settings.INSTALLED_APPS: settings.INSTALLED_APPS.append(module_label) -- cgit v1.3