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 | |
| 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')
41 files changed, 3703 insertions, 1298 deletions
diff --git a/tests/check/__init__.py b/tests/admin_checks/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/tests/check/__init__.py +++ b/tests/admin_checks/__init__.py diff --git a/tests/admin_checks/models.py b/tests/admin_checks/models.py new file mode 100644 index 0000000000..5db1747a64 --- /dev/null +++ b/tests/admin_checks/models.py @@ -0,0 +1,57 @@ +""" +Tests of ModelAdmin system checks logic. +""" + +from django.db import models +from django.utils.encoding import python_2_unicode_compatible + + +class Album(models.Model): + title = models.CharField(max_length=150) + + +@python_2_unicode_compatible +class Song(models.Model): + title = models.CharField(max_length=150) + album = models.ForeignKey(Album) + original_release = models.DateField(editable=False) + + class Meta: + ordering = ('title',) + + def __str__(self): + return self.title + + def readonly_method_on_model(self): + # does nothing + pass + + +class TwoAlbumFKAndAnE(models.Model): + album1 = models.ForeignKey(Album, related_name="album1_set") + album2 = models.ForeignKey(Album, related_name="album2_set") + e = models.CharField(max_length=1) + + +class Author(models.Model): + name = models.CharField(max_length=100) + + +class Book(models.Model): + name = models.CharField(max_length=100) + subtitle = models.CharField(max_length=100) + price = models.FloatField() + authors = models.ManyToManyField(Author, through='AuthorsBooks') + + +class AuthorsBooks(models.Model): + author = models.ForeignKey(Author) + book = models.ForeignKey(Book) + + +class State(models.Model): + name = models.CharField(max_length=15) + + +class City(models.Model): + state = models.ForeignKey(State) diff --git a/tests/admin_checks/tests.py b/tests/admin_checks/tests.py new file mode 100644 index 0000000000..0f6320430e --- /dev/null +++ b/tests/admin_checks/tests.py @@ -0,0 +1,436 @@ +from __future__ import unicode_literals + +from django import forms +from django.contrib import admin +from django.core import checks +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase + +from .models import Song, Book, Album, TwoAlbumFKAndAnE, City, State + + +class SongForm(forms.ModelForm): + pass + + +class ValidFields(admin.ModelAdmin): + form = SongForm + fields = ['title'] + + +class ValidFormFieldsets(admin.ModelAdmin): + def get_form(self, request, obj=None, **kwargs): + class ExtraFieldForm(SongForm): + name = forms.CharField(max_length=50) + return ExtraFieldForm + + fieldsets = ( + (None, { + 'fields': ('name',), + }), + ) + + +class SystemChecksTestCase(TestCase): + + def test_checks_are_performed(self): + class MyAdmin(admin.ModelAdmin): + @classmethod + def check(self, model, **kwargs): + return ['error!'] + + admin.site.register(Song, MyAdmin) + try: + errors = checks.run_checks() + expected = ['error!'] + self.assertEqual(errors, expected) + finally: + admin.site.unregister(Song) + + def test_readonly_and_editable(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ["original_release"] + fieldsets = [ + (None, { + "fields": ["title", "original_release"], + }), + ] + + errors = SongAdmin.check(model=Song) + self.assertEqual(errors, []) + + def test_custom_modelforms_with_fields_fieldsets(self): + """ + # Regression test for #8027: custom ModelForms with fields/fieldsets + """ + + errors = ValidFields.check(model=Song) + self.assertEqual(errors, []) + + def test_custom_get_form_with_fieldsets(self): + """ + Ensure that the fieldsets checks are skipped when the ModelAdmin.get_form() method + is overridden. + Refs #19445. + """ + + errors = ValidFormFieldsets.check(model=Song) + self.assertEqual(errors, []) + + def test_exclude_values(self): + """ + Tests for basic system checks of 'exclude' option values (#12689) + """ + + class ExcludedFields1(admin.ModelAdmin): + exclude = 'foo' + + errors = ExcludedFields1.check(model=Book) + expected = [ + checks.Error( + '"exclude" must be a list or tuple.', + hint=None, + obj=ExcludedFields1, + id='admin.E014', + ) + ] + self.assertEqual(errors, expected) + + def test_exclude_duplicate_values(self): + class ExcludedFields2(admin.ModelAdmin): + exclude = ('name', 'name') + + errors = ExcludedFields2.check(model=Book) + expected = [ + checks.Error( + '"exclude" contains duplicate field(s).', + hint=None, + obj=ExcludedFields2, + id='admin.E015', + ) + ] + self.assertEqual(errors, expected) + + def test_exclude_in_inline(self): + class ExcludedFieldsInline(admin.TabularInline): + model = Song + exclude = 'foo' + + class ExcludedFieldsAlbumAdmin(admin.ModelAdmin): + model = Album + inlines = [ExcludedFieldsInline] + + errors = ExcludedFieldsAlbumAdmin.check(model=Album) + expected = [ + checks.Error( + '"exclude" must be a list or tuple.', + hint=None, + obj=ExcludedFieldsInline, + id='admin.E014', + ) + ] + self.assertEqual(errors, expected) + + def test_exclude_inline_model_admin(self): + """ + Regression test for #9932 - exclude in InlineModelAdmin should not + contain the ForeignKey field used in ModelAdmin.model + """ + + class SongInline(admin.StackedInline): + model = Song + exclude = ['album'] + + class AlbumAdmin(admin.ModelAdmin): + model = Album + inlines = [SongInline] + + errors = AlbumAdmin.check(model=Album) + expected = [ + checks.Error( + ('Cannot exclude the field "album", because it is the foreign key ' + 'to the parent model admin_checks.Album.'), + hint=None, + obj=SongInline, + id='admin.E201', + ) + ] + self.assertEqual(errors, expected) + + def test_app_label_in_admin_checks(self): + """ + Regression test for #15669 - Include app label in admin system check messages + """ + + class RawIdNonexistingAdmin(admin.ModelAdmin): + raw_id_fields = ('nonexisting',) + + errors = RawIdNonexistingAdmin.check(model=Album) + expected = [ + checks.Error( + ('"raw_id_fields[0]" refers to field "nonexisting", which is ' + 'missing from model admin_checks.Album.'), + hint=None, + obj=RawIdNonexistingAdmin, + id='admin.E002', + ) + ] + self.assertEqual(errors, expected) + + def test_fk_exclusion(self): + """ + Regression test for #11709 - when testing for fk excluding (when exclude is + given) make sure fk_name is honored or things blow up when there is more + than one fk to the parent model. + """ + + class TwoAlbumFKAndAnEInline(admin.TabularInline): + model = TwoAlbumFKAndAnE + exclude = ("e",) + fk_name = "album1" + + class MyAdmin(admin.ModelAdmin): + inlines = [TwoAlbumFKAndAnEInline] + + errors = MyAdmin.check(model=Album) + self.assertEqual(errors, []) + + def test_inline_self_check(self): + class TwoAlbumFKAndAnEInline(admin.TabularInline): + model = TwoAlbumFKAndAnE + + class MyAdmin(admin.ModelAdmin): + inlines = [TwoAlbumFKAndAnEInline] + + errors = MyAdmin.check(model=Album) + expected = [ + checks.Error( + "'admin_checks.TwoAlbumFKAndAnE' has more than one ForeignKey to 'admin_checks.Album'.", + hint=None, + obj=TwoAlbumFKAndAnEInline, + id='admin.E202', + ) + ] + self.assertEqual(errors, expected) + + def test_inline_with_specified(self): + class TwoAlbumFKAndAnEInline(admin.TabularInline): + model = TwoAlbumFKAndAnE + fk_name = "album1" + + class MyAdmin(admin.ModelAdmin): + inlines = [TwoAlbumFKAndAnEInline] + + errors = MyAdmin.check(model=Album) + self.assertEqual(errors, []) + + def test_readonly(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("title",) + + errors = SongAdmin.check(model=Song) + self.assertEqual(errors, []) + + def test_readonly_on_method(self): + def my_function(obj): + pass + + class SongAdmin(admin.ModelAdmin): + readonly_fields = (my_function,) + + errors = SongAdmin.check(model=Song) + self.assertEqual(errors, []) + + def test_readonly_on_modeladmin(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("readonly_method_on_modeladmin",) + + def readonly_method_on_modeladmin(self, obj): + pass + + errors = SongAdmin.check(model=Song) + self.assertEqual(errors, []) + + def test_readonly_method_on_model(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("readonly_method_on_model",) + + errors = SongAdmin.check(model=Song) + self.assertEqual(errors, []) + + def test_nonexistant_field(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("title", "nonexistant") + + errors = SongAdmin.check(model=Song) + expected = [ + checks.Error( + ('"readonly_fields[1]" is neither a callable nor an attribute ' + 'of "SongAdmin" nor found in the model admin_checks.Song.'), + hint=None, + obj=SongAdmin, + id='admin.E035', + ) + ] + self.assertEqual(errors, expected) + + def test_nonexistant_field_on_inline(self): + class CityInline(admin.TabularInline): + model = City + readonly_fields = ['i_dont_exist'] # Missing attribute + + errors = CityInline.check(State) + expected = [ + checks.Error( + ('"readonly_fields[0]" is neither a callable nor an attribute ' + 'of "CityInline" nor found in the model admin_checks.City.'), + hint=None, + obj=CityInline, + id='admin.E035', + ) + ] + self.assertEqual(errors, expected) + + def test_extra(self): + class SongAdmin(admin.ModelAdmin): + def awesome_song(self, instance): + if instance.title == "Born to Run": + return "Best Ever!" + return "Status unknown." + + errors = SongAdmin.check(model=Song) + self.assertEqual(errors, []) + + def test_readonly_lambda(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = (lambda obj: "test",) + + errors = SongAdmin.check(model=Song) + self.assertEqual(errors, []) + + def test_graceful_m2m_fail(self): + """ + Regression test for #12203/#12237 - Fail more gracefully when a M2M field that + specifies the 'through' option is included in the 'fields' or the 'fieldsets' + ModelAdmin options. + """ + + class BookAdmin(admin.ModelAdmin): + fields = ['authors'] + + errors = BookAdmin.check(model=Book) + expected = [ + checks.Error( + ('"fields" cannot include the ManyToManyField "authors", ' + 'because "authors" manually specifies relationship model.'), + hint=None, + obj=BookAdmin, + id='admin.E013', + ) + ] + self.assertEqual(errors, expected) + + def test_cannot_include_through(self): + class FieldsetBookAdmin(admin.ModelAdmin): + fieldsets = ( + ('Header 1', {'fields': ('name',)}), + ('Header 2', {'fields': ('authors',)}), + ) + + errors = FieldsetBookAdmin.check(model=Book) + expected = [ + checks.Error( + ('"fieldsets[1][1][\'fields\']" cannot include the ManyToManyField ' + '"authors", because "authors" manually specifies relationship model.'), + hint=None, + obj=FieldsetBookAdmin, + id='admin.E013', + ) + ] + self.assertEqual(errors, expected) + + def test_nested_fields(self): + class NestedFieldsAdmin(admin.ModelAdmin): + fields = ('price', ('name', 'subtitle')) + + errors = NestedFieldsAdmin.check(model=Book) + self.assertEqual(errors, []) + + def test_nested_fieldsets(self): + class NestedFieldsetAdmin(admin.ModelAdmin): + fieldsets = ( + ('Main', {'fields': ('price', ('name', 'subtitle'))}), + ) + + errors = NestedFieldsetAdmin.check(model=Book) + self.assertEqual(errors, []) + + def test_explicit_through_override(self): + """ + Regression test for #12209 -- If the explicitly provided through model + is specified as a string, the admin should still be able use + Model.m2m_field.through + """ + + class AuthorsInline(admin.TabularInline): + model = Book.authors.through + + class BookAdmin(admin.ModelAdmin): + inlines = [AuthorsInline] + + errors = BookAdmin.check(model=Book) + self.assertEqual(errors, []) + + def test_non_model_fields(self): + """ + Regression for ensuring ModelAdmin.fields can contain non-model fields + that broke with r11737 + """ + + class SongForm(forms.ModelForm): + extra_data = forms.CharField() + + class FieldsOnFormOnlyAdmin(admin.ModelAdmin): + form = SongForm + fields = ['title', 'extra_data'] + + errors = FieldsOnFormOnlyAdmin.check(model=Song) + self.assertEqual(errors, []) + + def test_non_model_first_field(self): + """ + Regression for ensuring ModelAdmin.field can handle first elem being a + non-model field (test fix for UnboundLocalError introduced with r16225). + """ + + class SongForm(forms.ModelForm): + extra_data = forms.CharField() + + class Meta: + model = Song + fields = '__all__' + + class FieldsOnFormOnlyAdmin(admin.ModelAdmin): + form = SongForm + fields = ['extra_data', 'title'] + + errors = FieldsOnFormOnlyAdmin.check(model=Song) + self.assertEqual(errors, []) + + def test_validator_compatibility(self): + class MyValidator(object): + def validate(self, cls, model): + raise ImproperlyConfigured("error!") + + class MyModelAdmin(admin.ModelAdmin): + validator_class = MyValidator + + errors = MyModelAdmin.check(model=Song) + expected = [ + checks.Error( + 'error!', + hint=None, + obj=MyModelAdmin, + ) + ] + self.assertEqual(errors, expected) diff --git a/tests/invalid_models_tests/invalid_models/__init__.py b/tests/admin_scripts/app_raising_messages/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/tests/invalid_models_tests/invalid_models/__init__.py +++ b/tests/admin_scripts/app_raising_messages/__init__.py diff --git a/tests/admin_scripts/app_raising_messages/models.py b/tests/admin_scripts/app_raising_messages/models.py new file mode 100644 index 0000000000..aece8a8176 --- /dev/null +++ b/tests/admin_scripts/app_raising_messages/models.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.core import checks +from django.db import models + + +class ModelRaisingMessages(models.Model): + @classmethod + def check(self, **kwargs): + return [ + checks.Warning( + 'First warning', + hint='Hint', + obj='obj' + ), + checks.Warning( + 'Second warning', + hint=None, + obj='a' + ), + checks.Error( + 'An error', + hint='Error hint', + obj=None, + ) + ] diff --git a/tests/admin_scripts/app_raising_warning/__init__.py b/tests/admin_scripts/app_raising_warning/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/admin_scripts/app_raising_warning/__init__.py diff --git a/tests/admin_scripts/app_raising_warning/models.py b/tests/admin_scripts/app_raising_warning/models.py new file mode 100644 index 0000000000..8f58abe127 --- /dev/null +++ b/tests/admin_scripts/app_raising_warning/models.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.core import checks +from django.db import models + + +class ModelRaisingMessages(models.Model): + @classmethod + def check(self, **kwargs): + return [ + checks.Warning( + 'A warning', + hint=None, + ), + ] diff --git a/tests/admin_scripts/management/commands/app_command.py b/tests/admin_scripts/management/commands/app_command.py index 4706645484..f0981eba2d 100644 --- a/tests/admin_scripts/management/commands/app_command.py +++ b/tests/admin_scripts/management/commands/app_command.py @@ -3,7 +3,7 @@ from django.core.management.base import AppCommand class Command(AppCommand): help = 'Test Application-based commands' - requires_model_validation = False + requires_system_checks = False args = '[app_label ...]' def handle_app_config(self, app_config, **options): diff --git a/tests/admin_scripts/management/commands/base_command.py b/tests/admin_scripts/management/commands/base_command.py index 6e37ca238e..c313235ead 100644 --- a/tests/admin_scripts/management/commands/base_command.py +++ b/tests/admin_scripts/management/commands/base_command.py @@ -10,7 +10,7 @@ class Command(BaseCommand): make_option('--option_c', '-c', action='store', dest='option_c', default='3'), ) help = 'Test basic commands' - requires_model_validation = False + requires_system_checks = False args = '[labels ...]' def handle(self, *labels, **options): diff --git a/tests/admin_scripts/management/commands/label_command.py b/tests/admin_scripts/management/commands/label_command.py index 3bce1305bc..9bba413ff3 100644 --- a/tests/admin_scripts/management/commands/label_command.py +++ b/tests/admin_scripts/management/commands/label_command.py @@ -3,7 +3,7 @@ from django.core.management.base import LabelCommand class Command(LabelCommand): help = "Test Label-based commands" - requires_model_validation = False + requires_system_checks = False args = '<label>' def handle_label(self, label, **options): diff --git a/tests/admin_scripts/management/commands/noargs_command.py b/tests/admin_scripts/management/commands/noargs_command.py index e94807f2e2..3a75098c71 100644 --- a/tests/admin_scripts/management/commands/noargs_command.py +++ b/tests/admin_scripts/management/commands/noargs_command.py @@ -3,7 +3,7 @@ from django.core.management.base import NoArgsCommand class Command(NoArgsCommand): help = "Test No-args commands" - requires_model_validation = False + requires_system_checks = False def handle_noargs(self, **options): print('EXECUTE:NoArgsCommand options=%s' % sorted(options.items())) diff --git a/tests/admin_scripts/management/commands/validation_command.py b/tests/admin_scripts/management/commands/validation_command.py new file mode 100644 index 0000000000..e9ba86dc6c --- /dev/null +++ b/tests/admin_scripts/management/commands/validation_command.py @@ -0,0 +1,11 @@ +from django.core.management.base import NoArgsCommand + + +class InvalidCommand(NoArgsCommand): + help = ("Test raising an error if both requires_system_checks " + "and requires_model_validation are defined.") + requires_system_checks = True + requires_model_validation = True + + def handle_noargs(self, **options): + pass diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 63ca63a6c0..0d7ce41dcb 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals + """ A series of tests to establish that the command-line managment tools work as advertised - especially with regards to the handling of the DJANGO_SETTINGS_MODULE @@ -18,14 +20,15 @@ import unittest import django from django import conf, get_version from django.conf import settings +from django.core.exceptions import ImproperlyConfigured from django.core.management import BaseCommand, CommandError, call_command from django.db import connection -from django.test.runner import DiscoverRunner -from django.test.utils import str_prefix from django.utils.encoding import force_text from django.utils._os import upath from django.utils.six import StringIO from django.test import LiveServerTestCase, TestCase +from django.test.runner import DiscoverRunner +from django.test.utils import str_prefix test_dir = os.path.realpath(os.path.join(os.environ['DJANGO_TEST_TEMP_DIR'], 'test_project')) @@ -52,6 +55,7 @@ class AdminScriptTestCase(unittest.TestCase): 'DATABASES', 'ROOT_URLCONF', 'SECRET_KEY', + 'TEST_RUNNER', # We need to include TEST_RUNNER, otherwise we get a compatibility warning. ] for s in exports: if hasattr(settings, s): @@ -1072,49 +1076,125 @@ class ManageSettingsWithSettingsErrors(AdminScriptTestCase): self.assertNoOutput(err) -class ManageValidate(AdminScriptTestCase): +class ManageCheck(AdminScriptTestCase): def tearDown(self): self.remove_settings('settings.py') def test_nonexistent_app(self): - "manage.py validate reports an error on a non-existent app in INSTALLED_APPS" - self.write_settings('settings.py', apps=['admin_scriptz.broken_app'], sdict={'USE_I18N': False}) - args = ['validate'] + """ manage.py check reports an error on a non-existent app in + INSTALLED_APPS """ + + self.write_settings('settings.py', + apps=['admin_scriptz.broken_app'], + sdict={'USE_I18N': False}) + args = ['check'] out, err = self.run_manage(args) self.assertNoOutput(out) + self.assertOutput(err, 'ImportError') self.assertOutput(err, 'No module named') self.assertOutput(err, 'admin_scriptz') def test_broken_app(self): - "manage.py validate reports an ImportError if an app's models.py raises one on import" + """ manage.py check reports an ImportError if an app's models.py + raises one on import """ + self.write_settings('settings.py', apps=['admin_scripts.broken_app']) - args = ['validate'] + args = ['check'] out, err = self.run_manage(args) self.assertNoOutput(out) self.assertOutput(err, 'ImportError') def test_complex_app(self): - "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}) - args = ['validate'] + """ manage.py check does not raise an ImportError validating a + complex app with nested calls to load_app """ + + self.write_settings( + 'settings.py', + apps=[ + 'admin_scripts.complex_app', + 'admin_scripts.simple_app', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + ], + sdict={ + 'DEBUG': True + } + ) + args = ['check'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, '0 errors found') + self.assertEqual(out, 'System check identified no issues.\n') def test_app_with_import(self): - "manage.py validate does not raise errors when an app imports a base class that itself has an abstract base" + """ manage.py check does not raise errors when an app imports a base + class that itself has an abstract base. """ + self.write_settings('settings.py', apps=['admin_scripts.app_with_import', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sites'], sdict={'DEBUG': True}) - args = ['validate'] + args = ['check'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, '0 errors found') + self.assertEqual(out, 'System check identified no issues.\n') + + def test_output_format(self): + """ All errors/warnings should be sorted by level and by message. """ + + self.write_settings('settings.py', + apps=['admin_scripts.app_raising_messages', + 'django.contrib.auth', + 'django.contrib.contenttypes'], + sdict={'DEBUG': True}) + args = ['check'] + out, err = self.run_manage(args) + expected_err = ( + "CommandError: System check identified some issues:\n" + "\n" + "ERRORS:\n" + "?: An error\n" + "\tHINT: Error hint\n" + "\n" + "WARNINGS:\n" + "a: Second warning\n" + "obj: First warning\n" + "\tHINT: Hint\n" + "\n" + "System check identified 3 issues.\n" + ) + self.assertEqual(err, expected_err) + self.assertNoOutput(out) + + def test_warning_does_not_halt(self): + """ + When there are only warnings or less serious messages, then Django + should not prevent user from launching their project, so `check` + command should not raise `CommandError` exception. + + In this test we also test output format. + + """ + + self.write_settings('settings.py', + apps=['admin_scripts.app_raising_warning', + 'django.contrib.auth', + 'django.contrib.contenttypes'], + sdict={'DEBUG': True}) + args = ['check'] + out, err = self.run_manage(args) + expected_err = ( + "System check identified some issues:\n" # No "CommandError: " part + "\n" + "WARNINGS:\n" + "?: A warning\n" + "\n" + "System check identified 1 issue.\n" + ) + self.assertEqual(err, expected_err) + self.assertNoOutput(out) class CustomTestRunner(DiscoverRunner): @@ -1311,37 +1391,44 @@ class CommandTypes(AdminScriptTestCase): def test_base_command(self): "User BaseCommands can execute when a label is provided" args = ['base_command', 'testlabel'] - out, err = self.run_manage(args) - self.assertNoOutput(err) - self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]")) + expected_labels = "('testlabel',)" + self._test_base_command(args, expected_labels) def test_base_command_no_label(self): "User BaseCommands can execute when no labels are provided" args = ['base_command'] - out, err = self.run_manage(args) - self.assertNoOutput(err) - self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=(), options=[('no_color', False), ('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]")) + expected_labels = "()" + self._test_base_command(args, expected_labels) def test_base_command_multiple_label(self): "User BaseCommands can execute when no labels are provided" args = ['base_command', 'testlabel', 'anotherlabel'] - out, err = self.run_manage(args) - self.assertNoOutput(err) - self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel', 'anotherlabel'), options=[('no_color', False), ('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]")) + expected_labels = "('testlabel', 'anotherlabel')" + self._test_base_command(args, expected_labels) def test_base_command_with_option(self): "User BaseCommands can execute with options when a label is provided" args = ['base_command', 'testlabel', '--option_a=x'] - out, err = self.run_manage(args) - self.assertNoOutput(err) - self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]")) + expected_labels = "('testlabel',)" + self._test_base_command(args, expected_labels, option_a="'x'") def test_base_command_with_options(self): "User BaseCommands can execute with multiple options when a label is provided" args = ['base_command', 'testlabel', '-a', 'x', '--option_b=y'] + expected_labels = "('testlabel',)" + self._test_base_command(args, expected_labels, option_a="'x'", option_b="'y'") + + def _test_base_command(self, args, labels, option_a="'1'", option_b="'2'"): out, err = self.run_manage(args) + + expected_out = str_prefix( + ("EXECUTE:BaseCommand labels=%%s, " + "options=[('no_color', False), ('option_a', %%s), ('option_b', %%s), " + "('option_c', '3'), ('pythonpath', None), ('settings', None), " + "('traceback', None), ('verbosity', %(_)s'1')]") + ) % (labels, option_a, option_b) self.assertNoOutput(err) - self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]")) + self.assertOutput(out, expected_out) def test_base_run_from_argv(self): """ @@ -1468,6 +1555,10 @@ class CommandTypes(AdminScriptTestCase): self.assertOutput(out, str_prefix("EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]")) self.assertOutput(out, str_prefix("EXECUTE:LabelCommand label=anotherlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]")) + def test_requires_model_validation_and_requires_system_checks_both_defined(self): + from .management.commands.validation_command import InvalidCommand + self.assertRaises(ImproperlyConfigured, InvalidCommand) + class Discovery(TestCase): @@ -1476,12 +1567,16 @@ class Discovery(TestCase): Apps listed first in INSTALLED_APPS have precendence. """ with self.settings(INSTALLED_APPS=['admin_scripts.complex_app', - 'admin_scripts.simple_app']): + 'admin_scripts.simple_app', + 'django.contrib.auth', + 'django.contrib.contenttypes']): out = StringIO() call_command('duplicate', stdout=out) self.assertEqual(out.getvalue().strip(), 'complex_app') with self.settings(INSTALLED_APPS=['admin_scripts.simple_app', - 'admin_scripts.complex_app']): + 'admin_scripts.complex_app', + 'django.contrib.auth', + 'django.contrib.contenttypes']): out = StringIO() call_command('duplicate', stdout=out) self.assertEqual(out.getvalue().strip(), 'simple_app') @@ -1505,39 +1600,35 @@ class ArgumentOrder(AdminScriptTestCase): self.remove_settings('alternate_settings.py') def test_setting_then_option(self): - "Options passed after settings are correctly handled" + """ Options passed after settings are correctly handled. """ args = ['base_command', 'testlabel', '--settings=alternate_settings', '--option_a=x'] - out, err = self.run_manage(args) - self.assertNoOutput(err) - self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]")) + self._test(args) def test_setting_then_short_option(self): - "Short options passed after settings are correctly handled" - args = ['base_command', 'testlabel', '--settings=alternate_settings', '--option_a=x'] - out, err = self.run_manage(args) - self.assertNoOutput(err) - self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]")) + """ Short options passed after settings are correctly handled. """ + args = ['base_command', 'testlabel', '--settings=alternate_settings', '-a', 'x'] + self._test(args) def test_option_then_setting(self): - "Options passed before settings are correctly handled" + """ Options passed before settings are correctly handled. """ args = ['base_command', 'testlabel', '--option_a=x', '--settings=alternate_settings'] - out, err = self.run_manage(args) - self.assertNoOutput(err) - self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]")) + self._test(args) def test_short_option_then_setting(self): - "Short options passed before settings are correctly handled" + """ Short options passed before settings are correctly handled. """ args = ['base_command', 'testlabel', '-a', 'x', '--settings=alternate_settings'] - out, err = self.run_manage(args) - self.assertNoOutput(err) - self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]")) + self._test(args) def test_option_then_setting_then_option(self): - "Options are correctly handled when they are passed before and after a setting" + """ Options are correctly handled when they are passed before and after + a setting. """ args = ['base_command', 'testlabel', '--option_a=x', '--settings=alternate_settings', '--option_b=y'] + self._test(args, option_b="'y'") + + def _test(self, args, option_b="'2'"): out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]")) + self.assertOutput(out, str_prefix("EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', %%s), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]") % option_b) class StartProject(LiveServerTestCase, AdminScriptTestCase): diff --git a/tests/admin_validation/tests.py b/tests/admin_validation/tests.py index 5e38af82f0..f724099c53 100644 --- a/tests/admin_validation/tests.py +++ b/tests/admin_validation/tests.py @@ -142,8 +142,8 @@ class ValidationTestCase(TestCase): class MyAdmin(admin.ModelAdmin): inlines = [TwoAlbumFKAndAnEInline] - self.assertRaisesMessage(Exception, - "<class 'admin_validation.models.TwoAlbumFKAndAnE'> has more than 1 ForeignKey to <class 'admin_validation.models.Album'>", + self.assertRaisesMessage(ValueError, + "'admin_validation.TwoAlbumFKAndAnE' has more than one ForeignKey to 'admin_validation.Album'.", MyAdmin.validate, Album) def test_inline_with_specified(self): diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index d01a37725a..60280655b6 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -8,6 +8,7 @@ import unittest from django.conf import settings, global_settings from django.core import mail +from django.core.checks import Error from django.core.files import temp as tempfile from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse, NoReverseMatch @@ -4675,17 +4676,27 @@ class AdminViewOnSiteTests(TestCase): self.assertEqual(['Children must share a family name with their parents in this contrived test case'], error_set.get('__all__')) - def test_validate(self): + def test_check(self): "Ensure that the view_on_site value is either a boolean or a callable" - CityAdmin.view_on_site = True - CityAdmin.validate(City) - CityAdmin.view_on_site = False - CityAdmin.validate(City) - CityAdmin.view_on_site = lambda obj: obj.get_absolute_url() - CityAdmin.validate(City) - CityAdmin.view_on_site = [] - with self.assertRaisesMessage(ImproperlyConfigured, 'CityAdmin.view_on_site is not a callable or a boolean value.'): - CityAdmin.validate(City) + try: + CityAdmin.view_on_site = True + self.assertEqual(CityAdmin.check(City), []) + CityAdmin.view_on_site = False + self.assertEqual(CityAdmin.check(City), []) + CityAdmin.view_on_site = lambda obj: obj.get_absolute_url() + self.assertEqual(CityAdmin.check(City), []) + CityAdmin.view_on_site = [] + self.assertEqual(CityAdmin.check(City), [ + Error( + '"view_on_site" is not a callable or a boolean value.', + hint=None, + obj=CityAdmin, + id='admin.E025', + ), + ]) + finally: + # Restore the original values for the benefit of other tests. + CityAdmin.view_on_site = True def test_false(self): "Ensure that the 'View on site' button is not displayed if view_on_site is False" diff --git a/tests/check/tests.py b/tests/check/tests.py deleted file mode 100644 index 577dcd610b..0000000000 --- a/tests/check/tests.py +++ /dev/null @@ -1,128 +0,0 @@ -from django.core.checks.compatibility import base -from django.core.checks.compatibility import django_1_6_0 -from django.core.management.commands import check -from django.core.management import call_command -from django.db.models.fields import NOT_PROVIDED -from django.test import TestCase - -from .models import Book - - -class StubCheckModule(object): - # Has no ``run_checks`` attribute & will trigger a warning. - __name__ = 'StubCheckModule' - - -class FakeWarnings(object): - def __init__(self): - self._warnings = [] - - def warn(self, message): - self._warnings.append(message) - - -class CompatChecksTestCase(TestCase): - def setUp(self): - super(CompatChecksTestCase, self).setUp() - - # We're going to override the list of checks to perform for test - # consistency in the future. - self.old_compat_checks = base.COMPAT_CHECKS - base.COMPAT_CHECKS = [ - django_1_6_0, - ] - - def tearDown(self): - # Restore what's supposed to be in ``COMPAT_CHECKS``. - base.COMPAT_CHECKS = self.old_compat_checks - super(CompatChecksTestCase, self).tearDown() - - def test_check_test_runner_new_default(self): - with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): - result = django_1_6_0.check_test_runner() - self.assertTrue("Django 1.6 introduced a new default test runner" in result) - - def test_check_test_runner_overridden(self): - with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - self.assertEqual(django_1_6_0.check_test_runner(), None) - - def test_run_checks_new_default(self): - with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): - result = django_1_6_0.run_checks() - self.assertEqual(len(result), 1) - self.assertTrue("Django 1.6 introduced a new default test runner" in result[0]) - - def test_run_checks_overridden(self): - with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - self.assertEqual(len(django_1_6_0.run_checks()), 0) - - def test_boolean_field_default_value(self): - with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - # We patch the field's default value to trigger the warning - boolean_field = Book._meta.get_field('is_published') - old_default = boolean_field.default - try: - boolean_field.default = NOT_PROVIDED - result = django_1_6_0.run_checks() - self.assertEqual(len(result), 1) - self.assertTrue("You have not set a default value for one or more BooleanFields" in result[0]) - self.assertTrue('check.Book: "is_published"' in result[0]) - # We did not patch the BlogPost.is_published field so - # there should not be a warning about it - self.assertFalse('check.BlogPost' in result[0]) - finally: - # Restore the ``default`` - boolean_field.default = old_default - - def test_check_compatibility(self): - with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): - result = base.check_compatibility() - self.assertEqual(len(result), 1) - self.assertTrue("Django 1.6 introduced a new default test runner" in result[0]) - - with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - self.assertEqual(len(base.check_compatibility()), 0) - - def test_check_compatibility_warning(self): - # First, we're patching over the ``COMPAT_CHECKS`` with a stub which - # will trigger the warning. - base.COMPAT_CHECKS = [ - StubCheckModule(), - ] - - # Next, we unfortunately have to patch out ``warnings``. - old_warnings = base.warnings - base.warnings = FakeWarnings() - - self.assertEqual(len(base.warnings._warnings), 0) - - with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - self.assertEqual(len(base.check_compatibility()), 0) - - self.assertEqual(len(base.warnings._warnings), 1) - self.assertTrue("The 'StubCheckModule' module lacks a 'run_checks'" in base.warnings._warnings[0]) - - # Restore the ``warnings``. - base.warnings = old_warnings - - def test_management_command(self): - # Again, we unfortunately have to patch out ``warnings``. Different - old_warnings = check.warnings - check.warnings = FakeWarnings() - - self.assertEqual(len(check.warnings._warnings), 0) - - # Should not produce any warnings. - with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - call_command('check') - - self.assertEqual(len(check.warnings._warnings), 0) - - with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): - call_command('check') - - self.assertEqual(len(check.warnings._warnings), 1) - self.assertTrue("Django 1.6 introduced a new default test runner" in check.warnings._warnings[0]) - - # Restore the ``warnings``. - base.warnings = old_warnings diff --git a/tests/check_framework/__init__.py b/tests/check_framework/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/check_framework/__init__.py diff --git a/tests/check/models.py b/tests/check_framework/models.py index ddeed23982..da2bf0c58d 100644 --- a/tests/check/models.py +++ b/tests/check_framework/models.py @@ -1,6 +1,14 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + from django.db import models +class SimpleModel(models.Model): + field = models.IntegerField() + manager = models.manager.Manager() + + class Book(models.Model): title = models.CharField(max_length=250) is_published = models.BooleanField(default=False) diff --git a/tests/check_framework/tests.py b/tests/check_framework/tests.py new file mode 100644 index 0000000000..5ad57d0659 --- /dev/null +++ b/tests/check_framework/tests.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.utils.six import StringIO +import sys + +from django.apps import apps +from django.conf import settings +from django.core import checks +from django.core.checks import Error +from django.core.checks.registry import CheckRegistry +from django.core.checks.compatibility.django_1_6_0 import check_1_6_compatibility +from django.core.management.base import CommandError +from django.core.management import call_command +from django.db.models.fields import NOT_PROVIDED +from django.test import TestCase +from django.test.utils import override_settings, override_system_checks +from django.utils.encoding import force_text + +from .models import SimpleModel, Book + + +class DummyObj(object): + def __repr__(self): + return "obj" + + +class SystemCheckFrameworkTests(TestCase): + + def test_register_and_run_checks(self): + calls = [0] + + registry = CheckRegistry() + + @registry.register() + def f(**kwargs): + calls[0] += 1 + return [1, 2, 3] + errors = registry.run_checks() + self.assertEqual(errors, [1, 2, 3]) + self.assertEqual(calls[0], 1) + + +class MessageTests(TestCase): + + def test_printing(self): + e = Error("Message", hint="Hint", obj=DummyObj()) + expected = "obj: Message\n\tHINT: Hint" + self.assertEqual(force_text(e), expected) + + def test_printing_no_hint(self): + e = Error("Message", hint=None, obj=DummyObj()) + expected = "obj: Message" + self.assertEqual(force_text(e), expected) + + def test_printing_no_object(self): + e = Error("Message", hint="Hint", obj=None) + expected = "?: Message\n\tHINT: Hint" + self.assertEqual(force_text(e), expected) + + def test_printing_with_given_id(self): + e = Error("Message", hint="Hint", obj=DummyObj(), id="ID") + expected = "obj: (ID) Message\n\tHINT: Hint" + self.assertEqual(force_text(e), expected) + + def test_printing_field_error(self): + field = SimpleModel._meta.get_field('field') + e = Error("Error", hint=None, obj=field) + expected = "check_framework.SimpleModel.field: Error" + self.assertEqual(force_text(e), expected) + + def test_printing_model_error(self): + e = Error("Error", hint=None, obj=SimpleModel) + expected = "check_framework.SimpleModel: Error" + self.assertEqual(force_text(e), expected) + + def test_printing_manager_error(self): + manager = SimpleModel.manager + e = Error("Error", hint=None, obj=manager) + expected = "check_framework.SimpleModel.manager: Error" + self.assertEqual(force_text(e), expected) + + +class Django_1_6_0_CompatibilityChecks(TestCase): + + @override_settings(TEST_RUNNER='django.test.runner.DiscoverRunner') + def test_test_runner_new_default(self): + errors = check_1_6_compatibility() + self.assertEqual(errors, []) + + @override_settings(TEST_RUNNER='myapp.test.CustomRunner') + def test_test_runner_overriden(self): + errors = check_1_6_compatibility() + self.assertEqual(errors, []) + + def test_test_runner_not_set_explicitly(self): + # We remove some settings to make this look like a project generated under Django 1.5. + old_test_runner = settings._wrapped.TEST_RUNNER + del settings._wrapped.TEST_RUNNER + settings._wrapped._explicit_settings.add('MANAGERS') + settings._wrapped._explicit_settings.add('ADMINS') + try: + errors = check_1_6_compatibility() + expected = [ + checks.Warning( + "Some project unittests may not execute as expected.", + hint=("Django 1.6 introduced a new default test runner. It looks like " + "this project was generated using Django 1.5 or earlier. You should " + "ensure your tests are all running & behaving as expected. See " + "https://docs.djangoproject.com/en/dev/releases/1.6/#discovery-of-tests-in-any-test-module " + "for more information."), + obj=None, + id='1_6.W001', + ) + ] + self.assertEqual(errors, expected) + finally: + # Restore settings value + settings._wrapped.TEST_RUNNER = old_test_runner + settings._wrapped._explicit_settings.remove('MANAGERS') + settings._wrapped._explicit_settings.remove('ADMINS') + + def test_boolean_field_default_value(self): + with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): + # We patch the field's default value to trigger the warning + boolean_field = Book._meta.get_field('is_published') + old_default = boolean_field.default + try: + boolean_field.default = NOT_PROVIDED + errors = check_1_6_compatibility() + expected = [ + checks.Warning( + 'BooleanField does not have a default value. ', + hint=('Django 1.6 changed the default value of BooleanField from False to None. ' + 'See https://docs.djangoproject.com/en/1.6/ref/models/fields/#booleanfield ' + 'for more information.'), + obj=boolean_field, + id='1_6.W002', + ) + ] + self.assertEqual(errors, expected) + finally: + # Restore the ``default`` + boolean_field.default = old_default + + +def simple_system_check(**kwargs): + simple_system_check.kwargs = kwargs + return [] + + +def tagged_system_check(**kwargs): + tagged_system_check.kwargs = kwargs + return [] +tagged_system_check.tags = ['simpletag'] + + +class CheckCommandTests(TestCase): + + def setUp(self): + simple_system_check.kwargs = None + tagged_system_check.kwargs = None + self.old_stdout, self.old_stderr = sys.stdout, sys.stderr + sys.stdout, sys.stderr = StringIO(), StringIO() + + def tearDown(self): + sys.stdout, sys.stderr = self.old_stdout, self.old_stderr + + @override_system_checks([simple_system_check, tagged_system_check]) + def test_simple_call(self): + call_command('check') + self.assertEqual(simple_system_check.kwargs, {'app_configs': None}) + self.assertEqual(tagged_system_check.kwargs, {'app_configs': None}) + + @override_system_checks([simple_system_check, tagged_system_check]) + def test_given_app(self): + call_command('check', 'auth', 'admin') + auth_config = apps.get_app_config('auth') + admin_config = apps.get_app_config('admin') + self.assertEqual(simple_system_check.kwargs, {'app_configs': [auth_config, admin_config]}) + self.assertEqual(tagged_system_check.kwargs, {'app_configs': [auth_config, admin_config]}) + + @override_system_checks([simple_system_check, tagged_system_check]) + def test_given_tag(self): + call_command('check', tags=['simpletag']) + self.assertEqual(simple_system_check.kwargs, None) + self.assertEqual(tagged_system_check.kwargs, {'app_configs': None}) + + @override_system_checks([simple_system_check, tagged_system_check]) + def test_invalid_tag(self): + self.assertRaises(CommandError, call_command, 'check', tags=['missingtag']) + + +def custom_system_check(app_configs, **kwargs): + return [ + Error( + 'Error', + hint=None, + id='mycheck.E001', + ) + ] + + +class SilencingCheckTests(TestCase): + + def setUp(self): + self.old_stdout, self.old_stderr = sys.stdout, sys.stderr + sys.stdout, sys.stderr = StringIO(), StringIO() + + def tearDown(self): + sys.stdout, sys.stderr = self.old_stdout, self.old_stderr + + @override_settings(SILENCED_SYSTEM_CHECKS=['mycheck.E001']) + @override_system_checks([custom_system_check]) + def test_simple(self): + try: + call_command('check') + except CommandError: + self.fail("The mycheck.E001 check should be silenced.") diff --git a/tests/contenttypes_tests/tests.py b/tests/contenttypes_tests/tests.py index 1f0af709cb..624e60d7a5 100644 --- a/tests/contenttypes_tests/tests.py +++ b/tests/contenttypes_tests/tests.py @@ -1,9 +1,14 @@ -from __future__ import unicode_literals +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals -from django.apps.registry import Apps +from django.apps.registry import Apps, apps +from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType +from django.core import checks from django.db import models from django.test import TestCase +from django.test.utils import override_settings +from django.utils.encoding import force_str from .models import Author, Article @@ -67,3 +72,297 @@ class ContentTypesViewsTests(TestCase): self.assertEqual(ct.app_label, 'my_great_app') self.assertEqual(ct.model, 'modelcreatedonthefly') self.assertEqual(ct.name, 'a model created on the fly') + + +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['contenttypes_tests'].models.copy() + + def tearDown(self): + apps.app_configs['contenttypes_tests'].models = self._old_models + apps.all_models['contenttypes_tests'] = self._old_models + apps.clear_cache() + + +class GenericForeignKeyTests(IsolatedModelsTestCase): + + def test_str(self): + class Model(models.Model): + field = generic.GenericForeignKey() + expected = "contenttypes_tests.Model.field" + actual = force_str(Model.field) + self.assertEqual(expected, actual) + + def test_missing_content_type_field(self): + class TaggedItem(models.Model): + # no content_type field + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey() + + errors = TaggedItem.content_object.check() + expected = [ + checks.Error( + 'The field refers to TaggedItem.content_type field which is missing.', + hint=None, + obj=TaggedItem.content_object, + id='contenttypes.E005', + ) + ] + self.assertEqual(errors, expected) + + def test_invalid_content_type_field(self): + class Model(models.Model): + content_type = models.IntegerField() # should be ForeignKey + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey( + 'content_type', 'object_id') + + errors = Model.content_object.check() + expected = [ + checks.Error( + ('"content_type" field is used by a GenericForeignKey ' + 'as content type field and therefore it must be ' + 'a ForeignKey.'), + hint=None, + obj=Model.content_object, + id='contenttypes.E006', + ) + ] + self.assertEqual(errors, expected) + + def test_content_type_field_pointing_to_wrong_model(self): + class Model(models.Model): + content_type = models.ForeignKey('self') # should point to ContentType + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey( + 'content_type', 'object_id') + + errors = Model.content_object.check() + expected = [ + checks.Error( + ('"content_type" field is used by a GenericForeignKey ' + 'as content type field and therefore it must be ' + 'a ForeignKey to ContentType.'), + hint=None, + obj=Model.content_object, + id='contenttypes.E007', + ) + ] + self.assertEqual(errors, expected) + + def test_missing_object_id_field(self): + class TaggedItem(models.Model): + content_type = models.ForeignKey(ContentType) + # missing object_id field + content_object = generic.GenericForeignKey() + + errors = TaggedItem.content_object.check() + expected = [ + checks.Error( + 'The field refers to "object_id" field which is missing.', + hint=None, + obj=TaggedItem.content_object, + id='contenttypes.E001', + ) + ] + self.assertEqual(errors, expected) + + def test_field_name_ending_with_underscore(self): + class Model(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object_ = generic.GenericForeignKey( + 'content_type', 'object_id') + + errors = Model.content_object_.check() + expected = [ + checks.Error( + 'Field names must not end with underscores.', + hint=None, + obj=Model.content_object_, + id='contenttypes.E002', + ) + ] + self.assertEqual(errors, expected) + + def test_generic_foreign_key_checks_are_performed(self): + class MyGenericForeignKey(generic.GenericForeignKey): + def check(self, **kwargs): + return ['performed!'] + + class Model(models.Model): + content_object = MyGenericForeignKey() + + errors = checks.run_checks() + self.assertEqual(errors, ['performed!']) + + +class GenericRelationshipTests(IsolatedModelsTestCase): + + def test_valid_generic_relationship(self): + class TaggedItem(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey() + + class Bookmark(models.Model): + tags = generic.GenericRelation('TaggedItem') + + errors = Bookmark.tags.field.check() + self.assertEqual(errors, []) + + def test_valid_generic_relationship_with_explicit_fields(self): + class TaggedItem(models.Model): + custom_content_type = models.ForeignKey(ContentType) + custom_object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey( + 'custom_content_type', 'custom_object_id') + + class Bookmark(models.Model): + tags = generic.GenericRelation('TaggedItem', + content_type_field='custom_content_type', + object_id_field='custom_object_id') + + errors = Bookmark.tags.field.check() + self.assertEqual(errors, []) + + def test_pointing_to_missing_model(self): + class Model(models.Model): + rel = generic.GenericRelation('MissingModel') + + errors = Model.rel.field.check() + expected = [ + checks.Error( + ('The field has a relation with model MissingModel, ' + '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 MissingModel is defined?'), + obj=Model.rel.field, + id='E030', + ) + ] + self.assertEqual(errors, expected) + + def test_valid_self_referential_generic_relationship(self): + class Model(models.Model): + rel = generic.GenericRelation('Model') + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey( + 'content_type', 'object_id') + + errors = Model.rel.field.check() + self.assertEqual(errors, []) + + def test_missing_content_type_field(self): + class TaggedItem(models.Model): + # no content_type field + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey() + + class Bookmark(models.Model): + tags = generic.GenericRelation('TaggedItem') + + errors = Bookmark.tags.field.check() + expected = [ + checks.Error( + 'The field refers to TaggedItem.content_type field which is missing.', + hint=None, + obj=Bookmark.tags.field, + id='contenttypes.E005', + ) + ] + self.assertEqual(errors, expected) + + def test_missing_object_id_field(self): + class TaggedItem(models.Model): + content_type = models.ForeignKey(ContentType) + # missing object_id field + content_object = generic.GenericForeignKey() + + class Bookmark(models.Model): + tags = generic.GenericRelation('TaggedItem') + + errors = Bookmark.tags.field.check() + expected = [ + checks.Error( + 'The field refers to TaggedItem.object_id field which is missing.', + hint=None, + obj=Bookmark.tags.field, + id='contenttypes.E003', + ) + ] + self.assertEqual(errors, expected) + + def test_missing_generic_foreign_key(self): + class TaggedItem(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + + class Bookmark(models.Model): + tags = generic.GenericRelation('TaggedItem') + + errors = Bookmark.tags.field.check() + expected = [ + checks.Warning( + ('The field defines a generic relation with the model ' + 'contenttypes_tests.TaggedItem, but the model lacks ' + 'GenericForeignKey.'), + hint=None, + obj=Bookmark.tags.field, + id='contenttypes.E004', + ) + ] + self.assertEqual(errors, expected) + + @override_settings(TEST_SWAPPED_MODEL='contenttypes_tests.Replacement') + def test_pointing_to_swapped_model(self): + class Replacement(models.Model): + pass + + class SwappedModel(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey() + + class Meta: + swappable = 'TEST_SWAPPED_MODEL' + + class Model(models.Model): + rel = generic.GenericRelation('SwappedModel') + + errors = Model.rel.field.check() + expected = [ + checks.Error( + ('The field defines a relation with the model ' + 'contenttypes_tests.SwappedModel, ' + 'which has been swapped out.'), + hint='Update the relation to point at settings.TEST_SWAPPED_MODEL', + obj=Model.rel.field, + id='E029', + ) + ] + self.assertEqual(errors, expected) + + def test_field_name_ending_with_underscore(self): + class TaggedItem(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey() + + class InvalidBookmark(models.Model): + tags_ = generic.GenericRelation('TaggedItem') + + errors = InvalidBookmark.tags_.field.check() + expected = [ + checks.Error( + 'Field names must not end with underscores.', + hint=None, + obj=InvalidBookmark.tags_.field, + id='E001', + ) + ] + self.assertEqual(errors, expected) diff --git a/tests/fixtures_model_package/tests.py b/tests/fixtures_model_package/tests.py index b869c34329..9e41ee1cdf 100644 --- a/tests/fixtures_model_package/tests.py +++ b/tests/fixtures_model_package/tests.py @@ -5,6 +5,7 @@ import warnings from django.core import management from django.db import transaction from django.test import TestCase, TransactionTestCase +from django.test.utils import override_system_checks from django.utils.six import StringIO from .models import Article, Book @@ -30,6 +31,7 @@ class TestNoInitialDataLoading(TransactionTestCase): available_apps = ['fixtures_model_package'] + @override_system_checks([]) def test_migrate(self): with transaction.atomic(): Book.objects.all().delete() @@ -41,6 +43,7 @@ class TestNoInitialDataLoading(TransactionTestCase): ) self.assertQuerysetEqual(Book.objects.all(), []) + @override_system_checks([]) def test_flush(self): # Test presence of fixture (flush called by TransactionTestCase) self.assertQuerysetEqual( diff --git a/tests/inline_formsets/tests.py b/tests/inline_formsets/tests.py index e3c37bb320..87add7f6e6 100644 --- a/tests/inline_formsets/tests.py +++ b/tests/inline_formsets/tests.py @@ -124,8 +124,9 @@ class InlineFormsetFactoryTest(TestCase): to use for the inline formset, we should get an exception. """ six.assertRaisesRegex( - self, Exception, - "<class 'inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'inline_formsets.models.Parent'>", + self, + ValueError, + "'inline_formsets.Child' has more than one ForeignKey to 'inline_formsets.Parent'.", inlineformset_factory, Parent, Child ) @@ -146,8 +147,8 @@ class InlineFormsetFactoryTest(TestCase): exception. """ six.assertRaisesRegex( - self, Exception, - "<class 'inline_formsets.models.Child'> has no field named 'test'", + self, ValueError, + "'inline_formsets.Child' has no field named 'test'.", inlineformset_factory, Parent, Child, fk_name='test' ) 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/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)) diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py index 5b536f9e82..0797b907cf 100644 --- a/tests/logging_tests/tests.py +++ b/tests/logging_tests/tests.py @@ -333,7 +333,7 @@ class SettingsConfigTest(AdminScriptTestCase): # validate is just an example command to trigger settings configuration out, err = self.run_manage(['validate']) self.assertNoOutput(err) - self.assertOutput(out, "0 errors found") + self.assertOutput(out, "System check identified no issues.") def dictConfig(config): diff --git a/tests/migrate_signals/tests.py b/tests/migrate_signals/tests.py index a18d9fe076..95c73f9901 100644 --- a/tests/migrate_signals/tests.py +++ b/tests/migrate_signals/tests.py @@ -1,7 +1,8 @@ from django.apps import apps +from django.core import management from django.db.models import signals from django.test import TestCase -from django.core import management +from django.test.utils import override_system_checks from django.utils import six @@ -61,6 +62,9 @@ class MigrateSignalTests(TestCase): def test_pre_migrate_call_time(self): self.assertEqual(pre_migrate_receiver.call_counter, 1) + # `auth` app is imported, but not installed in this test, so we need to + # exclude checks registered by this app. + @override_system_checks([]) def test_pre_migrate_args(self): r = PreMigrateReceiver() signals.pre_migrate.connect(r, sender=APP_CONFIG) diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index e53b26c638..74881f8510 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -7,7 +7,7 @@ import shutil from django.apps import apps from django.core.management import call_command, CommandError -from django.test import override_settings +from django.test import override_settings, override_system_checks from django.utils import six from django.utils._os import upath from django.utils.encoding import force_text @@ -21,6 +21,10 @@ class MigrateTests(MigrationTestBase): Tests running the migrate command. """ + # `auth` app is imported, but not installed in these tests (thanks to + # MigrationTestBase), so we need to exclude checks registered by this app. + + @override_system_checks([]) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) def test_migrate(self): """ @@ -49,6 +53,7 @@ class MigrateTests(MigrationTestBase): self.assertTableNotExists("migrations_tribble") self.assertTableNotExists("migrations_book") + @override_system_checks([]) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) def test_migrate_list(self): """ @@ -71,6 +76,7 @@ class MigrateTests(MigrationTestBase): # Cleanup by unmigrating everything call_command("migrate", "migrations", "zero", verbosity=0) + @override_system_checks([]) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"}) def test_migrate_conflict_exit(self): """ @@ -79,6 +85,7 @@ class MigrateTests(MigrationTestBase): with self.assertRaises(CommandError): call_command("migrate", "migrations") + @override_system_checks([]) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"}) def test_makemigrations_conflict_exit(self): """ @@ -87,6 +94,7 @@ class MigrateTests(MigrationTestBase): with self.assertRaises(CommandError): call_command("makemigrations") + @override_system_checks([]) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"}) def test_makemigrations_merge_basic(self): """ @@ -99,6 +107,7 @@ class MigrateTests(MigrationTestBase): except CommandError: self.fail("Makemigrations errored in merge mode with conflicts") + @override_system_checks([]) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) def test_sqlmigrate(self): """ @@ -150,6 +159,9 @@ class MakeMigrationsTests(MigrationTestBase): return shutil.rmtree(dname) + # `auth` app is imported, but not installed in this test (thanks to + # MigrationTestBase), so we need to exclude checks registered by this app. + @override_system_checks([]) def test_files_content(self): self.assertTableNotExists("migrations_unicodemodel") apps.register_model('migrations', UnicodeModel) @@ -186,6 +198,9 @@ class MakeMigrationsTests(MigrationTestBase): self.assertTrue('\\xda\\xd1\\xcd\\xa2\\xd3\\xd0\\xc9' in content) # title.verbose_name self.assertTrue('\\u201c\\xd0j\\xe1\\xf1g\\xf3\\u201d' in content) # title.default + # `auth` app is imported, but not installed in this test (thanks to + # MigrationTestBase), so we need to exclude checks registered by this app. + @override_system_checks([]) def test_failing_migration(self): #21280 - If a migration fails to serialize, it shouldn't generate an empty file. apps.register_model('migrations', UnserializableModel) diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py index d78f3d7c2d..45ffe44e7a 100644 --- a/tests/model_fields/tests.py +++ b/tests/model_fields/tests.py @@ -85,6 +85,11 @@ class BasicFieldTests(test.TestCase): klass = forms.TypedMultipleChoiceField self.assertIsInstance(field.formfield(choices_form_class=klass), klass) + def test_field_str(self): + from django.utils.encoding import force_str + f = Foo._meta.get_field('a') + self.assertEqual(force_str(f), "model_fields.Foo.a") + class DecimalFieldTests(test.TestCase): def test_to_python(self): diff --git a/tests/model_validation/tests.py b/tests/model_validation/tests.py index 225656e4cb..b166f0c072 100644 --- a/tests/model_validation/tests.py +++ b/tests/model_validation/tests.py @@ -1,7 +1,5 @@ from django.core import management -from django.core.management.validation import ( - ModelErrorCollection, validate_model_signals -) +from django.core.checks import run_checks, Error from django.db.models.signals import post_init from django.test import TestCase from django.utils import six @@ -24,26 +22,32 @@ class ModelValidationTest(TestCase): # See: https://code.djangoproject.com/ticket/20430 # * related_name='+' doesn't clash with another '+' # See: https://code.djangoproject.com/ticket/21375 - management.call_command("validate", stdout=six.StringIO()) + management.call_command("check", stdout=six.StringIO()) def test_model_signal(self): unresolved_references = post_init.unresolved_references.copy() post_init.connect(on_post_init, sender='missing-app.Model') post_init.connect(OnPostInit(), sender='missing-app.Model') - e = ModelErrorCollection(six.StringIO()) - validate_model_signals(e) - self.assertSetEqual( - set(e.errors), - {( - 'model_validation.tests', + + errors = run_checks() + expected = [ + Error( "The `on_post_init` function was connected to the `post_init` " "signal with a lazy reference to the 'missing-app.Model' " - "sender, which has not been installed." - ), ( - 'model_validation.tests', + "sender, which has not been installed.", + hint=None, + obj='model_validation.tests', + id='E014', + ), + Error( "An instance of the `OnPostInit` class was connected to " "the `post_init` signal with a lazy reference to the " - "'missing-app.Model' sender, which has not been installed." - )} - ) + "'missing-app.Model' sender, which has not been installed.", + hint=None, + obj='model_validation.tests', + id='E014', + ) + ] + self.assertEqual(errors, expected) + post_init.unresolved_references = unresolved_references diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 0c7a115fef..a7712433db 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -1,21 +1,21 @@ from __future__ import unicode_literals from datetime import date -import unittest from django import forms from django.contrib.admin.options import (ModelAdmin, TabularInline, HORIZONTAL, VERTICAL) from django.contrib.admin.sites import AdminSite from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect +from django.contrib.admin.validation import ModelAdminValidator from django.contrib.admin import (SimpleListFilter, BooleanFieldListFilter) +from django.core.checks import Error from django.core.exceptions import ImproperlyConfigured from django.forms.models import BaseModelFormSet from django.forms.widgets import Select -from django.test import TestCase, override_settings -from django.test.utils import str_prefix from django.utils import six +from django.test import TestCase from .models import Band, Concert, ValidationTestModel, ValidationTestInlineModel @@ -536,179 +536,193 @@ class ModelAdminTests(TestCase): ['extra', 'transport', 'id', 'DELETE', 'main_band']) -class ValidationTests(unittest.TestCase): - def test_validation_only_runs_in_debug(self): - # Ensure validation only runs when DEBUG = True - with override_settings(DEBUG=True): - class ValidationTestModelAdmin(ModelAdmin): - raw_id_fields = 10 +class CheckTestCase(TestCase): - site = AdminSite() - - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.raw_id_fields' must be a list or tuple.", - site.register, - ValidationTestModel, - ValidationTestModelAdmin, + def assertIsInvalid(self, model_admin, model, msg, + id=None, hint=None, invalid_obj=None): + invalid_obj = invalid_obj or model_admin + errors = model_admin.check(model=model) + expected = [ + Error( + msg, + hint=hint, + obj=invalid_obj, + id=id, ) + ] + self.assertEqual(errors, expected) + + def assertIsValid(self, model_admin, model): + errors = model_admin.check(model=model) + expected = [] + self.assertEqual(errors, expected) - with override_settings(DEBUG=False): - site = AdminSite() - site.register(ValidationTestModel, ValidationTestModelAdmin) - def test_raw_id_fields_validation(self): +class RawIdCheckTests(CheckTestCase): + def test_not_iterable(self): class ValidationTestModelAdmin(ModelAdmin): raw_id_fields = 10 - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.raw_id_fields' must be a list or tuple.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"raw_id_fields" must be a list or tuple.', + 'admin.E001') + def test_missing_field(self): class ValidationTestModelAdmin(ModelAdmin): raw_id_fields = ('non_existent_field',) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.raw_id_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + ('"raw_id_fields[0]" refers to field "non_existent_field", ' + 'which is missing from model modeladmin.ValidationTestModel.'), + 'admin.E002') + def test_invalid_field_type(self): class ValidationTestModelAdmin(ModelAdmin): raw_id_fields = ('name',) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.raw_id_fields\[0\]', 'name' must be either a ForeignKey or ManyToManyField.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"raw_id_fields[0]" must be a ForeignKey or ManyToManyField.', + 'admin.E003') + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): raw_id_fields = ('users',) - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + + +class FieldsetsCheckTests(CheckTestCase): + + def test_valid_case(self): + class ValidationTestModelAdmin(ModelAdmin): + fieldsets = (("General", {"fields": ("name",)}),) + + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) - def test_fieldsets_validation(self): + def test_not_iterable(self): class ValidationTestModelAdmin(ModelAdmin): fieldsets = 10 - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.fieldsets' must be a list or tuple.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"fieldsets" must be a list or tuple.', + 'admin.E007') + def test_non_iterable_item(self): class ValidationTestModelAdmin(ModelAdmin): fieldsets = ({},) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.fieldsets\[0\]' must be a list or tuple.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"fieldsets[0]" must be a list or tuple.', + 'admin.E008') + def test_item_not_a_pair(self): class ValidationTestModelAdmin(ModelAdmin): fieldsets = ((),) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.fieldsets\[0\]' does not have exactly two elements.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"fieldsets[0]" must be a pair.', + 'admin.E009') + def test_second_element_of_item_not_a_dict(self): class ValidationTestModelAdmin(ModelAdmin): fieldsets = (("General", ()),) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.fieldsets\[0\]\[1\]' must be a dictionary.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"fieldsets[0][1]" must be a dictionary.', + 'admin.E010') + def test_missing_fields_key(self): class ValidationTestModelAdmin(ModelAdmin): fieldsets = (("General", {}),) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'fields' key is required in ValidationTestModelAdmin.fieldsets\[0\]\[1\] field options dict.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"fieldsets[0][1]" must contain "fields" key.', + 'admin.E011') class ValidationTestModelAdmin(ModelAdmin): fieldsets = (("General", {"fields": ("name",)}),) - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + def test_specified_both_fields_and_fieldsets(self): class ValidationTestModelAdmin(ModelAdmin): fieldsets = (("General", {"fields": ("name",)}),) fields = ["name"] - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "Both fieldsets and fields are specified in ValidationTestModelAdmin.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + 'Both "fieldsets" and "fields" are specified.', + 'admin.E005') + def test_duplicate_fields(self): class ValidationTestModelAdmin(ModelAdmin): fieldsets = [(None, {'fields': ['name', 'name']})] - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "There are duplicate field\(s\) in ValidationTestModelAdmin.fieldsets", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + 'There are duplicate field(s) in "fieldsets[0][1]".', + 'admin.E012') + + def test_fieldsets_with_custom_form_validation(self): + class BandAdmin(ModelAdmin): + fieldsets = ( + ('Band', { + 'fields': ('name',) + }), + ) + + self.assertIsValid(BandAdmin, Band) + +class FieldsCheckTests(CheckTestCase): + + def test_duplicate_fields_in_fields(self): class ValidationTestModelAdmin(ModelAdmin): fields = ["name", "name"] - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "There are duplicate field\(s\) in ValidationTestModelAdmin.fields", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + 'There are duplicate field(s) in "fields".', + 'admin.E006') + + def test_inline(self): + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + fields = 10 + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"fields" must be a list or tuple.', + 'admin.E004', + invalid_obj=ValidationTestInline) - def test_form_validation(self): +class FormCheckTests(CheckTestCase): + + def test_invalid_type(self): class FakeForm(object): pass class ValidationTestModelAdmin(ModelAdmin): form = FakeForm - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "ValidationTestModelAdmin.form does not inherit from BaseModelForm.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"form" must inherit from BaseModelForm.', + 'admin.E016') def test_fieldsets_with_custom_form_validation(self): @@ -719,8 +733,9 @@ class ValidationTests(unittest.TestCase): }), ) - BandAdmin.validate(Band) + self.assertIsValid(BandAdmin, Band) + def test_valid_case(self): class AdminBandForm(forms.ModelForm): delete = forms.BooleanField() @@ -733,50 +748,49 @@ class ValidationTests(unittest.TestCase): }), ) - BandAdmin.validate(Band) + self.assertIsValid(BandAdmin, Band) + - def test_filter_vertical_validation(self): +class FilterVerticalCheckTests(CheckTestCase): + def test_not_iterable(self): class ValidationTestModelAdmin(ModelAdmin): filter_vertical = 10 - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.filter_vertical' must be a list or tuple.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"filter_vertical" must be a list or tuple.', + 'admin.E017') + def test_missing_field(self): class ValidationTestModelAdmin(ModelAdmin): filter_vertical = ("non_existent_field",) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.filter_vertical' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + ('"filter_vertical[0]" refers to field "non_existent_field", ' + 'which is missing from model modeladmin.ValidationTestModel.'), + 'admin.E019') + def test_invalid_field_type(self): class ValidationTestModelAdmin(ModelAdmin): filter_vertical = ("name",) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.filter_vertical\[0\]' must be a ManyToManyField.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"filter_vertical[0]" must be a ManyToManyField.', + 'admin.E020') + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): filter_vertical = ("users",) - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) - def test_filter_horizontal_validation(self): +class FilterHorizontalCheckTests(CheckTestCase): + + def test_not_iterable(self): class ValidationTestModelAdmin(ModelAdmin): filter_horizontal = 10 @@ -788,136 +802,132 @@ class ValidationTests(unittest.TestCase): ValidationTestModel, ) + def test_missing_field(self): class ValidationTestModelAdmin(ModelAdmin): filter_horizontal = ("non_existent_field",) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.filter_horizontal' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + ('"filter_horizontal[0]" refers to field "non_existent_field", ' + 'which is missing from model modeladmin.ValidationTestModel.'), + 'admin.E019') + def test_invalid_field_type(self): class ValidationTestModelAdmin(ModelAdmin): filter_horizontal = ("name",) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.filter_horizontal\[0\]' must be a ManyToManyField.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"filter_horizontal[0]" must be a ManyToManyField.', + 'admin.E020') + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): filter_horizontal = ("users",) - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + + +class RadioFieldsCheckTests(CheckTestCase): - def test_radio_fields_validation(self): + def test_not_dictionary(self): class ValidationTestModelAdmin(ModelAdmin): radio_fields = () - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.radio_fields' must be a dictionary.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"radio_fields" must be a dictionary.', + 'admin.E021') + def test_missing_field(self): class ValidationTestModelAdmin(ModelAdmin): - radio_fields = {"non_existent_field": None} + radio_fields = {"non_existent_field": VERTICAL} - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.radio_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + ('"radio_fields" refers to field "non_existent_field", ' + 'which is missing from model modeladmin.ValidationTestModel.'), + 'admin.E022') + def test_invalid_field_type(self): class ValidationTestModelAdmin(ModelAdmin): - radio_fields = {"name": None} + radio_fields = {"name": VERTICAL} - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.radio_fields\['name'\]' is neither an instance of ForeignKey nor does have choices set.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + ('"radio_fields" refers to "name", which is neither an instance ' + 'of ForeignKey nor does have choices set.'), + 'admin.E023') + def test_invalid_value(self): class ValidationTestModelAdmin(ModelAdmin): radio_fields = {"state": None} - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.radio_fields\['state'\]' is neither admin.HORIZONTAL nor admin.VERTICAL.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"radio_fields[\'state\']" is neither admin.HORIZONTAL nor admin.VERTICAL.', + 'admin.E024') + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): radio_fields = {"state": VERTICAL} - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + + +class PrepopulatedFieldsCheckTests(CheckTestCase): - def test_prepopulated_fields_validation(self): + def test_not_dictionary(self): class ValidationTestModelAdmin(ModelAdmin): prepopulated_fields = () - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.prepopulated_fields' must be a dictionary.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"prepopulated_fields" must be a dictionary.', + 'admin.E026') + def test_missing_field(self): class ValidationTestModelAdmin(ModelAdmin): - prepopulated_fields = {"non_existent_field": None} + prepopulated_fields = {"non_existent_field": ("slug",)} - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.prepopulated_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + ('"prepopulated_fields" refers to field "non_existent_field", ' + 'which is missing from model modeladmin.ValidationTestModel.'), + 'admin.E027') + def test_missing_field_again(self): class ValidationTestModelAdmin(ModelAdmin): prepopulated_fields = {"slug": ("non_existent_field",)} - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.prepopulated_fields\['slug'\]\[0\]' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + ('"prepopulated_fields[\'slug\'][0]" refers to field "non_existent_field", ' + 'which is missing from model modeladmin.ValidationTestModel.'), + 'admin.E030') + def test_invalid_field_type(self): class ValidationTestModelAdmin(ModelAdmin): prepopulated_fields = {"users": ("name",)} - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.prepopulated_fields\['users'\]' is either a DateTimeField, ForeignKey or ManyToManyField. This isn't allowed.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + ('"prepopulated_fields" refers to "users", which must not be ' + 'a DateTimeField, ForeignKey or ManyToManyField.'), + 'admin.E028') + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): prepopulated_fields = {"slug": ("name",)} - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + + +class ListDisplayTests(CheckTestCase): - def test_list_display_validation(self): + def test_not_iterable(self): class ValidationTestModelAdmin(ModelAdmin): list_display = 10 @@ -930,28 +940,26 @@ class ValidationTests(unittest.TestCase): ValidationTestModel, ) + def test_missing_field(self): class ValidationTestModelAdmin(ModelAdmin): list_display = ('non_existent_field',) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - str_prefix("ValidationTestModelAdmin.list_display\[0\], %(_)s'non_existent_field' is not a callable or an attribute of 'ValidationTestModelAdmin' or found in the model 'ValidationTestModel'."), - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + ('"list_display[0]" is neither a callable nor an attribute ' + 'of "ValidationTestModelAdmin" nor found in model modeladmin.ValidationTestModel.'), + 'admin.E110') + def test_invalid_field_type(self): class ValidationTestModelAdmin(ModelAdmin): list_display = ('users',) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_display\[0\]', 'users' is a ManyToManyField which is not supported.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_display[0]" must not be a ManyToManyField.', + 'admin.E109') + def test_valid_case(self): def a_callable(obj): pass @@ -960,43 +968,39 @@ class ValidationTests(unittest.TestCase): pass list_display = ('name', 'decade_published_in', 'a_method', a_callable) - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) - def test_list_display_links_validation(self): +class ListDisplayLinksCheckTests(CheckTestCase): + + def test_not_iterable(self): class ValidationTestModelAdmin(ModelAdmin): list_display_links = 10 - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_display_links' must be a list or tuple.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_display_links" must be a list or tuple or None.', + 'admin.E111') + def test_missing_field(self): class ValidationTestModelAdmin(ModelAdmin): list_display_links = ('non_existent_field',) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_display_links\[0\]' refers to 'non_existent_field' which is not defined in 'list_display'.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_display_links[0]" refers to "non_existent_field", which is not defined in "list_display".', + 'admin.E112') + def test_missing_in_list_display(self): class ValidationTestModelAdmin(ModelAdmin): list_display_links = ('name',) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_display_links\[0\]' refers to 'name' which is not defined in 'list_display'.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_display_links[0]" refers to "name", which is not defined in "list_display".', + 'admin.E112') + def test_valid_case(self): def a_callable(obj): pass @@ -1006,62 +1010,60 @@ class ValidationTests(unittest.TestCase): list_display = ('name', 'decade_published_in', 'a_method', a_callable) list_display_links = ('name', 'decade_published_in', 'a_method', a_callable) - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + def test_None_is_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): list_display_links = None - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) - def test_list_filter_validation(self): +class ListFilterTests(CheckTestCase): + + def test_list_filter_validation(self): class ValidationTestModelAdmin(ModelAdmin): list_filter = 10 - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_filter' must be a list or tuple.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_filter" must be a list or tuple.', + 'admin.E113') + def test_missing_field(self): class ValidationTestModelAdmin(ModelAdmin): list_filter = ('non_existent_field',) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_filter\[0\]' refers to 'non_existent_field' which does not refer to a Field.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_filter[0]" refers to "non_existent_field", which does not refer to a Field.', + 'admin.E117') + def test_not_filter(self): class RandomClass(object): pass class ValidationTestModelAdmin(ModelAdmin): list_filter = (RandomClass,) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_filter\[0\]' is 'RandomClass' which is not a descendant of ListFilter.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_filter[0]" must inherit from ListFilter.', + 'admin.E114') + + def test_not_filter_again(self): + class RandomClass(object): + pass class ValidationTestModelAdmin(ModelAdmin): list_filter = (('is_active', RandomClass),) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'RandomClass' which is not of type FieldListFilter.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_filter[0][1]" must inherit from FieldListFilter.', + 'admin.E116') + def test_not_filter_again_again(self): class AwesomeFilter(SimpleListFilter): def get_title(self): return 'awesomeness' @@ -1075,255 +1077,257 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): list_filter = (('is_active', AwesomeFilter),) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'AwesomeFilter' which is not of type FieldListFilter.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_filter[0][1]" must inherit from FieldListFilter.', + 'admin.E116') + def test_not_associated_with_field_name(self): class ValidationTestModelAdmin(ModelAdmin): list_filter = (BooleanFieldListFilter,) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_filter\[0\]' is 'BooleanFieldListFilter' which is of type FieldListFilter but is not associated with a field name.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_filter[0]" must not inherit from FieldListFilter.', + 'admin.E115') - # Valid declarations below ----------- + def test_valid_case(self): + class AwesomeFilter(SimpleListFilter): + def get_title(self): + return 'awesomeness' + + def get_choices(self, request): + return (('bit', 'A bit awesome'), ('very', 'Very awesome'), ) + + def get_queryset(self, cl, qs): + return qs class ValidationTestModelAdmin(ModelAdmin): list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter), 'no') - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + - def test_list_per_page_validation(self): +class ListPerPageCheckTests(CheckTestCase): + def test_not_integer(self): class ValidationTestModelAdmin(ModelAdmin): list_per_page = 'hello' - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_per_page' should be a int.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_per_page" must be an integer.', + 'admin.E119') + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): list_per_page = 100 - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + - def test_max_show_all_allowed_validation(self): +class ListMaxShowAllCheckTests(CheckTestCase): + def test_not_integer(self): class ValidationTestModelAdmin(ModelAdmin): list_max_show_all = 'hello' - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_max_show_all' should be a int.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"list_max_show_all" must be an integer.', + 'admin.E120') + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): list_max_show_all = 200 - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) - def test_search_fields_validation(self): + +class SearchFieldsCheckTests(CheckTestCase): + + def test_not_iterable(self): class ValidationTestModelAdmin(ModelAdmin): search_fields = 10 - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.search_fields' must be a list or tuple.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"search_fields" must be a list or tuple.', + 'admin.E127') + + +class DateHierarchyCheckTests(CheckTestCase): - def test_date_hierarchy_validation(self): + def test_missing_field(self): class ValidationTestModelAdmin(ModelAdmin): date_hierarchy = 'non_existent_field' - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.date_hierarchy' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + ('"date_hierarchy" refers to field "non_existent_field", which ' + 'is missing from model modeladmin.ValidationTestModel.'), + 'admin.E128') + def test_invalid_field_type(self): class ValidationTestModelAdmin(ModelAdmin): date_hierarchy = 'name' - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.date_hierarchy is neither an instance of DateField nor DateTimeField.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"date_hierarchy" must be a DateField or DateTimeField.', + 'admin.E129') + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): date_hierarchy = 'pub_date' - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + - def test_ordering_validation(self): +class OrderingCheckTests(CheckTestCase): + def test_not_iterable(self): class ValidationTestModelAdmin(ModelAdmin): ordering = 10 - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.ordering' must be a list or tuple.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"ordering" must be a list or tuple.', + 'admin.E031') class ValidationTestModelAdmin(ModelAdmin): ordering = ('non_existent_field',) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.ordering\[0\]' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - ValidationTestModelAdmin.validate, + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"ordering[0]" refers to field "non_existent_field", which is missing from model modeladmin.ValidationTestModel.', + 'admin.E033', ) + def test_random_marker_not_alone(self): class ValidationTestModelAdmin(ModelAdmin): ordering = ('?', 'name') - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.ordering' has the random ordering marker '\?', but contains other fields as well. Please either remove '\?' or the other fields.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + ('"ordering" has the random ordering marker "?", but contains ' + 'other fields as well.'), + 'admin.E032', + hint='Either remove the "?", or remove the other fields.') + def test_valid_random_marker_case(self): class ValidationTestModelAdmin(ModelAdmin): ordering = ('?',) - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + def test_valid_complex_case(self): class ValidationTestModelAdmin(ModelAdmin): ordering = ('band__name',) - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): ordering = ('name',) - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) - def test_list_select_related_validation(self): +class ListSelectRelatedCheckTests(CheckTestCase): + + def test_invalid_type(self): class ValidationTestModelAdmin(ModelAdmin): list_select_related = 1 - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.list_select_related' should be either a " - "bool, a tuple or a list", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid(ValidationTestModelAdmin, ValidationTestModel, + '"list_select_related" must be a boolean, tuple or list.', + 'admin.E118') + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): list_select_related = False - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) - def test_save_as_validation(self): +class SaveAsCheckTests(CheckTestCase): + + def test_not_boolean(self): class ValidationTestModelAdmin(ModelAdmin): save_as = 1 - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.save_as' should be a bool.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"save_as" must be a boolean.', + 'admin.E101') + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): save_as = True - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + - def test_save_on_top_validation(self): +class SaveOnTopCheckTests(CheckTestCase): + def test_not_boolean(self): class ValidationTestModelAdmin(ModelAdmin): save_on_top = 1 - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.save_on_top' should be a bool.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"save_on_top" must be a boolean.', + 'admin.E102') + def test_valid_case(self): class ValidationTestModelAdmin(ModelAdmin): save_on_top = True - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + - def test_inlines_validation(self): +class InlinesCheckTests(CheckTestCase): + def test_not_iterable(self): class ValidationTestModelAdmin(ModelAdmin): inlines = 10 - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.inlines' must be a list or tuple.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"inlines" must be a list or tuple.', + 'admin.E103') + def test_not_model_admin(self): class ValidationTestInline(object): pass class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.inlines\[0\]' does not inherit from BaseModelAdmin.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"inlines[0]" must inherit from BaseModelAdmin.', + 'admin.E104') + def test_missing_model_field(self): class ValidationTestInline(TabularInline): pass class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'model' is a required attribute of 'ValidationTestModelAdmin.inlines\[0\]'.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"model" is a required attribute of "inlines[0]".', + 'admin.E105') + + def test_invalid_model_type(self): + """ Test if `model` attribute on inline model admin is a models.Model. + """ class SomethingBad(object): pass @@ -1334,41 +1338,24 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestModelAdmin.inlines\[0\].model' does not inherit from models.Model.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"inlines[0].model" must be a Model.', + 'admin.E106') + def test_valid_case(self): class ValidationTestInline(TabularInline): model = ValidationTestInlineModel class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - ValidationTestModelAdmin.validate(ValidationTestModel) - - def test_fields_validation(self): - - class ValidationTestInline(TabularInline): - model = ValidationTestInlineModel - fields = 10 - - class ValidationTestModelAdmin(ModelAdmin): - inlines = [ValidationTestInline] + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestInline.fields' must be a list or tuple.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) - def test_fk_name_validation(self): +class FkNameCheckTests(CheckTestCase): + def test_missing_field(self): class ValidationTestInline(TabularInline): model = ValidationTestInlineModel fk_name = "non_existent_field" @@ -1376,14 +1363,13 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestInline.fk_name' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestInlineModel'.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + "'modeladmin.ValidationTestInlineModel' has no field named 'non_existent_field'.", + 'admin.E202', + invalid_obj=ValidationTestInline) + def test_valid_case(self): class ValidationTestInline(TabularInline): model = ValidationTestInlineModel fk_name = "parent" @@ -1391,10 +1377,12 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) - def test_extra_validation(self): +class ExtraCheckTests(CheckTestCase): + + def test_not_integer(self): class ValidationTestInline(TabularInline): model = ValidationTestInlineModel extra = "hello" @@ -1402,14 +1390,13 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestInline.extra' should be a int.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"extra" must be an integer.', + 'admin.E203', + invalid_obj=ValidationTestInline) + def test_valid_case(self): class ValidationTestInline(TabularInline): model = ValidationTestInlineModel extra = 2 @@ -1417,10 +1404,12 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) - def test_max_num_validation(self): +class MaxNumCheckTests(CheckTestCase): + + def test_not_integer(self): class ValidationTestInline(TabularInline): model = ValidationTestInlineModel max_num = "hello" @@ -1428,14 +1417,13 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestInline.max_num' should be a int.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"max_num" must be an integer.', + 'admin.E204', + invalid_obj=ValidationTestInline) + def test_valid_case(self): class ValidationTestInline(TabularInline): model = ValidationTestInlineModel max_num = 2 @@ -1443,10 +1431,12 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + - def test_formset_validation(self): +class FormsetCheckTests(CheckTestCase): + def test_invalid_type(self): class FakeFormSet(object): pass @@ -1457,14 +1447,13 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - six.assertRaisesRegex( - self, - ImproperlyConfigured, - "'ValidationTestInline.formset' does not inherit from BaseModelFormSet.", - ValidationTestModelAdmin.validate, - ValidationTestModel, - ) + self.assertIsInvalid( + ValidationTestModelAdmin, ValidationTestModel, + '"formset" must inherit from BaseModelFormSet.', + 'admin.E205', + invalid_obj=ValidationTestInline) + def test_valid_case(self): class RealModelFormSet(BaseModelFormSet): pass @@ -1475,4 +1464,17 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - ValidationTestModelAdmin.validate(ValidationTestModel) + self.assertIsValid(ValidationTestModelAdmin, ValidationTestModel) + + +class CustomModelAdminTests(CheckTestCase): + def test_deprecation(self): + "Deprecated Custom Validator definitions still work with the check framework." + class CustomValidator(ModelAdminValidator): + def validate_me(self, model_admin, model): + raise ImproperlyConfigured('error!') + + class CustomModelAdmin(ModelAdmin): + validator_class = CustomValidator + + self.assertIsInvalid(CustomModelAdmin, ValidationTestModel, 'error!') diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py index 5ad5e08c25..11acbd216d 100644 --- a/tests/proxy_model_inheritance/tests.py +++ b/tests/proxy_model_inheritance/tests.py @@ -5,6 +5,7 @@ import sys from django.core.management import call_command from django.test import TestCase, TransactionTestCase +from django.test.utils import override_system_checks from django.utils._os import upath from .models import (ConcreteModel, ConcreteModelSubclass, @@ -26,6 +27,9 @@ class ProxyModelInheritanceTests(TransactionTestCase): def tearDown(self): sys.path = self.old_sys_path + # `auth` app is imported, but not installed in this test, so we need to + # exclude checks registered by this app. + @override_system_checks([]) def test_table_exists(self): with self.modify_settings(INSTALLED_APPS={'append': ['app1', 'app2']}): call_command('migrate', verbosity=0) diff --git a/tests/sites_framework/models.py b/tests/sites_framework/models.py index 12b1d08dd8..23d39cec14 100644 --- a/tests/sites_framework/models.py +++ b/tests/sites_framework/models.py @@ -31,14 +31,3 @@ class CustomArticle(AbstractArticle): objects = models.Manager() on_site = CurrentSiteManager("places_this_article_should_appear") - - -class InvalidArticle(AbstractArticle): - site = models.ForeignKey(Site) - - objects = models.Manager() - on_site = CurrentSiteManager("places_this_article_should_appear") - - -class ConfusedArticle(AbstractArticle): - site = models.IntegerField() diff --git a/tests/sites_framework/tests.py b/tests/sites_framework/tests.py index fb62c28c14..680d5d4fb2 100644 --- a/tests/sites_framework/tests.py +++ b/tests/sites_framework/tests.py @@ -1,9 +1,13 @@ +from django.apps import apps from django.conf import settings +from django.contrib.sites.managers import CurrentSiteManager from django.contrib.sites.models import Site +from django.core import checks +from django.db import models from django.test import TestCase from .models import (SyndicatedArticle, ExclusiveArticle, CustomArticle, - InvalidArticle, ConfusedArticle) + AbstractArticle) class SitesFrameworkTestCase(TestCase): @@ -11,6 +15,13 @@ class SitesFrameworkTestCase(TestCase): Site.objects.get_or_create(id=settings.SITE_ID, domain="example.com", name="example.com") Site.objects.create(id=settings.SITE_ID + 1, domain="example2.com", name="example2.com") + self._old_models = apps.app_configs['sites_framework'].models.copy() + + def tearDown(self): + apps.app_configs['sites_framework'].models = self._old_models + apps.all_models['sites_framework'] = self._old_models + apps.clear_cache() + def test_site_fk(self): article = ExclusiveArticle.objects.create(title="Breaking News!", site_id=settings.SITE_ID) self.assertEqual(ExclusiveArticle.on_site.all().get(), article) @@ -28,9 +39,38 @@ class SitesFrameworkTestCase(TestCase): self.assertEqual(CustomArticle.on_site.all().get(), article) def test_invalid_name(self): - InvalidArticle.objects.create(title="Bad News!", site_id=settings.SITE_ID) - self.assertRaises(ValueError, InvalidArticle.on_site.all) + + class InvalidArticle(AbstractArticle): + site = models.ForeignKey(Site) + + objects = models.Manager() + on_site = CurrentSiteManager("places_this_article_should_appear") + + errors = InvalidArticle.check() + expected = [ + checks.Error( + ("CurrentSiteManager could not find a field named " + "'places_this_article_should_appear'."), + hint=('Ensure that you did not misspell the field name. ' + 'Does the field exist?'), + obj=InvalidArticle.on_site, + id='sites.E001', + ) + ] + self.assertEqual(errors, expected) def test_invalid_field_type(self): - ConfusedArticle.objects.create(title="More Bad News!", site=settings.SITE_ID) - self.assertRaises(TypeError, ConfusedArticle.on_site.all) + + class ConfusedArticle(AbstractArticle): + site = models.IntegerField() + + errors = ConfusedArticle.check() + expected = [ + checks.Error( + "CurrentSiteManager requires that 'ConfusedArticle.site' must be a ForeignKey or ManyToManyField.", + hint=None, + obj=ConfusedArticle.on_site, + id='sites.E002', + ) + ] + self.assertEqual(errors, expected) diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index 2066181c67..0d026971fe 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -12,7 +12,7 @@ from django.core.management import call_command from django import db from django.test import runner, TestCase, TransactionTestCase, skipUnlessDBFeature from django.test.testcases import connections_support_transactions -from django.test.utils import IgnoreAllDeprecationWarningsMixin +from django.test.utils import IgnoreAllDeprecationWarningsMixin, override_system_checks from admin_scripts.tests import AdminScriptTestCase from .models import Person @@ -245,6 +245,9 @@ class Sqlite3InMemoryTestDbs(TestCase): available_apps = [] + # `setup_databases` triggers system check framework, but we do not want to + # perform checks. + @override_system_checks([]) @unittest.skipUnless(all(db.connections[conn].vendor == 'sqlite' for conn in db.connections), "This is an sqlite-specific issue") def test_transaction_support(self): diff --git a/tests/user_commands/management/commands/dance.py b/tests/user_commands/management/commands/dance.py index 911530d223..7297568f06 100644 --- a/tests/user_commands/management/commands/dance.py +++ b/tests/user_commands/management/commands/dance.py @@ -6,7 +6,7 @@ from django.core.management.base import BaseCommand, CommandError class Command(BaseCommand): help = "Dance around like a madman." args = '' - requires_model_validation = True + requires_system_checks = True option_list = BaseCommand.option_list + ( make_option("-s", "--style", default="Rock'n'Roll"), diff --git a/tests/validation/test_error_messages.py b/tests/validation/test_error_messages.py index aa01db6007..9b0bf701b1 100644 --- a/tests/validation/test_error_messages.py +++ b/tests/validation/test_error_messages.py @@ -5,7 +5,6 @@ from unittest import TestCase from django.core.exceptions import ValidationError from django.db import models -from django.utils import six class ValidationMessagesTest(TestCase): @@ -19,10 +18,6 @@ class ValidationMessagesTest(TestCase): f = models.AutoField(primary_key=True) self._test_validation_messages(f, 'fõo', ["'fõo' value must be an integer."]) - # primary_key must be True. Refs #12467. - with six.assertRaisesRegex(self, AssertionError, - "AutoFields must have primary_key=True."): - models.AutoField(primary_key=False) def test_integer_field_raises_error_message(self): f = models.IntegerField() |
