diff options
| author | Andrew Godwin <andrew@aeracode.org> | 2013-06-07 11:15:34 +0100 |
|---|---|---|
| committer | Andrew Godwin <andrew@aeracode.org> | 2013-06-07 11:15:34 +0100 |
| commit | 3c296382b8dea5de7f4e1e11b66bd7cecaf2ee51 (patch) | |
| tree | 0ca12593be82971691ffca01a836d00d3fcb3bd4 /tests | |
| parent | 7609e0b42e0014a6ad0adf9dafc7018cb268070e (diff) | |
| parent | 357d62d9f2972bf1bc21e5835c12c849143e06af (diff) | |
Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts:
django/db/models/fields/related.py
Diffstat (limited to 'tests')
136 files changed, 2951 insertions, 819 deletions
diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py index 8387ba77a1..175b1972c9 100644 --- a/tests/admin_changelist/admin.py +++ b/tests/admin_changelist/admin.py @@ -67,6 +67,11 @@ class ChordsBandAdmin(admin.ModelAdmin): list_filter = ['members'] +class InvitationAdmin(admin.ModelAdmin): + list_display = ('band', 'player') + list_select_related = ('player',) + + class DynamicListDisplayChildAdmin(admin.ModelAdmin): list_display = ('parent', 'name', 'age') diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index fd433967d3..7f3f0d162e 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -4,6 +4,7 @@ import datetime from django.contrib import admin from django.contrib.admin.options import IncorrectLookupParameters +from django.contrib.admin.templatetags.admin_list import pagination from django.contrib.admin.views.main import ChangeList, SEARCH_VAR, ALL_VAR from django.contrib.auth.models import User from django.core.urlresolvers import reverse @@ -17,7 +18,7 @@ from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin, GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin, DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin, FilteredChildAdmin, CustomPaginator, site as custom_site, - SwallowAdmin, DynamicListFilterChildAdmin) + SwallowAdmin, DynamicListFilterChildAdmin, InvitationAdmin) from .models import (Event, Child, Parent, Genre, Band, Musician, Group, Quartet, Membership, ChordsMusician, ChordsBand, Invitation, Swallow, UnorderedObject, OrderedObject, CustomIdUser) @@ -45,9 +46,32 @@ class ChangeListTests(TestCase): m = ChildAdmin(Child, admin.site) request = self.factory.get('/child/') cl = ChangeList(request, Child, m.list_display, m.list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, - m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m) - self.assertEqual(cl.queryset.query.select_related, {'parent': {'name': {}}}) + m.list_filter, m.date_hierarchy, m.search_fields, + m.list_select_related, m.list_per_page, + m.list_max_show_all, m.list_editable, m) + self.assertEqual(cl.queryset.query.select_related, { + 'parent': {'name': {}} + }) + + def test_select_related_as_tuple(self): + ia = InvitationAdmin(Invitation, admin.site) + request = self.factory.get('/invitation/') + cl = ChangeList(request, Child, ia.list_display, ia.list_display_links, + ia.list_filter, ia.date_hierarchy, ia.search_fields, + ia.list_select_related, ia.list_per_page, + ia.list_max_show_all, ia.list_editable, ia) + self.assertEqual(cl.queryset.query.select_related, {'player': {}}) + + def test_select_related_as_empty_tuple(self): + ia = InvitationAdmin(Invitation, admin.site) + ia.list_select_related = () + request = self.factory.get('/invitation/') + cl = ChangeList(request, Child, ia.list_display, ia.list_display_links, + ia.list_filter, ia.date_hierarchy, ia.search_fields, + ia.list_select_related, ia.list_per_page, + ia.list_max_show_all, ia.list_editable, ia) + self.assertEqual(cl.queryset.query.select_related, False) + def test_result_list_empty_changelist_value(self): """ @@ -564,6 +588,44 @@ class ChangeListTests(TestCase): response = m.changelist_view(request) self.assertEqual(response.context_data['cl'].list_filter, ('parent', 'name', 'age')) + def test_pagination_page_range(self): + """ + Regression tests for ticket #15653: ensure the number of pages + generated for changelist views are correct. + """ + # instantiating and setting up ChangeList object + m = GroupAdmin(Group, admin.site) + request = self.factory.get('/group/') + cl = ChangeList(request, Group, m.list_display, + m.list_display_links, m.list_filter, m.date_hierarchy, + m.search_fields, m.list_select_related, m.list_per_page, + m.list_max_show_all, m.list_editable, m) + per_page = cl.list_per_page = 10 + + for page_num, objects_count, expected_page_range in [ + (0, per_page, []), + (0, per_page * 2, list(range(2))), + (5, per_page * 11, list(range(11))), + (5, per_page * 12, [0, 1, 2, 3, 4, 5, 6, 7, 8, '.', 10, 11]), + (6, per_page * 12, [0, 1, '.', 3, 4, 5, 6, 7, 8, 9, 10, 11]), + (6, per_page * 13, [0, 1, '.', 3, 4, 5, 6, 7, 8, 9, '.', 11, 12]), + ]: + # assuming we have exactly `objects_count` objects + Group.objects.all().delete() + for i in range(objects_count): + Group.objects.create(name='test band') + + # setting page number and calculating page range + cl.page_num = page_num + cl.get_results(request) + real_page_range = pagination(cl)['page_range'] + + self.assertListEqual( + expected_page_range, + list(real_page_range), + ) + + class AdminLogNodeTestCase(TestCase): def test_get_admin_log_templatetag_custom_user(self): diff --git a/tests/special_headers/__init__.py b/tests/admin_docs/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/tests/special_headers/__init__.py +++ b/tests/admin_docs/__init__.py diff --git a/tests/special_headers/fixtures/data.xml b/tests/admin_docs/fixtures/data.xml index 7e60d45199..aba8f4aace 100644 --- a/tests/special_headers/fixtures/data.xml +++ b/tests/admin_docs/fixtures/data.xml @@ -14,7 +14,4 @@ <field to="auth.group" name="groups" rel="ManyToManyRel"></field> <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> </object> - <object pk="1" model="special_headers.article"> - <field type="TextField" name="text">text</field> - </object> </django-objects> diff --git a/tests/admin_docs/models.py b/tests/admin_docs/models.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/admin_docs/models.py diff --git a/tests/admin_docs/tests.py b/tests/admin_docs/tests.py new file mode 100644 index 0000000000..aeb527c7b9 --- /dev/null +++ b/tests/admin_docs/tests.py @@ -0,0 +1,45 @@ +from django.contrib.auth.models import User +from django.test import TestCase +from django.test.utils import override_settings + + +@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) +class XViewMiddlewareTest(TestCase): + fixtures = ['data.xml'] + urls = 'admin_docs.urls' + + def test_xview_func(self): + user = User.objects.get(username='super') + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + self.client.login(username='super', password='secret') + response = self.client.head('/xview/func/') + self.assertTrue('X-View' in response) + self.assertEqual(response['X-View'], 'admin_docs.views.xview') + user.is_staff = False + user.save() + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + user.is_staff = True + user.is_active = False + user.save() + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + + def test_xview_class(self): + user = User.objects.get(username='super') + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) + self.client.login(username='super', password='secret') + response = self.client.head('/xview/class/') + self.assertTrue('X-View' in response) + self.assertEqual(response['X-View'], 'admin_docs.views.XViewClass') + user.is_staff = False + user.save() + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) + user.is_staff = True + user.is_active = False + user.save() + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) diff --git a/tests/admin_docs/urls.py b/tests/admin_docs/urls.py new file mode 100644 index 0000000000..3c3a8fe5d8 --- /dev/null +++ b/tests/admin_docs/urls.py @@ -0,0 +1,11 @@ +# coding: utf-8 +from __future__ import absolute_import + +from django.conf.urls import patterns + +from . import views + +urlpatterns = patterns('', + (r'^xview/func/$', views.xview_dec(views.xview)), + (r'^xview/class/$', views.xview_dec(views.XViewClass.as_view())), +) diff --git a/tests/special_headers/views.py b/tests/admin_docs/views.py index a8bbd6542e..e47177c37f 100644 --- a/tests/special_headers/views.py +++ b/tests/admin_docs/views.py @@ -1,21 +1,13 @@ -from django.core.xheaders import populate_xheaders from django.http import HttpResponse from django.utils.decorators import decorator_from_middleware from django.views.generic import View -from django.middleware.doc import XViewMiddleware - -from .models import Article +from django.contrib.admindocs.middleware import XViewMiddleware xview_dec = decorator_from_middleware(XViewMiddleware) def xview(request): return HttpResponse() -def xview_xheaders(request, object_id): - response = HttpResponse() - populate_xheaders(request, response, Article, 1) - return response - class XViewClass(View): def get(self, request): return HttpResponse() diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py index 44671d0ac4..2f88248ca4 100644 --- a/tests/admin_inlines/admin.py +++ b/tests/admin_inlines/admin.py @@ -129,6 +129,22 @@ class ChildModel1Inline(admin.TabularInline): class ChildModel2Inline(admin.StackedInline): model = ChildModel2 +# admin for #19425 and #18388 +class BinaryTreeAdmin(admin.TabularInline): + model = BinaryTree + + def get_extra(self, request, obj=None, **kwargs): + extra = 2 + if obj: + return extra - obj.binarytree_set.count() + return extra + + def get_max_num(self, request, obj=None, **kwargs): + max_num = 3 + if obj: + return max_num - obj.binarytree_set.count() + return max_num + # admin for #19524 class SightingInline(admin.TabularInline): model = Sighting @@ -150,4 +166,5 @@ site.register(Author, AuthorAdmin) site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline, ReadOnlyInlineInline]) site.register(ProfileCollection, inlines=[ProfileInline]) site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2Inline]) +site.register(BinaryTree, inlines=[BinaryTreeAdmin]) site.register(ExtraTerrestrial, inlines=[SightingInline]) diff --git a/tests/admin_inlines/models.py b/tests/admin_inlines/models.py index 82c1c3f078..d4ba0ab6bc 100644 --- a/tests/admin_inlines/models.py +++ b/tests/admin_inlines/models.py @@ -183,6 +183,12 @@ class ChildModel2(models.Model): def get_absolute_url(self): return '/child_model2/' + +# Models for #19425 +class BinaryTree(models.Model): + name = models.CharField(max_length=100) + parent = models.ForeignKey('self', null=True, blank=True) + # Models for #19524 class LifeForm(models.Model): diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 714a2f1c61..78ccf074d5 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -12,7 +12,7 @@ from .admin import InnerInline, TitleInline, site from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile, ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, - Sighting, Title, Novel, Chapter, FootNote) + Sighting, Title, Novel, Chapter, FootNote, BinaryTree) @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -193,6 +193,24 @@ class TestInline(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(Sighting.objects.filter(et__name='Martian').count(), 1) + def test_custom_get_extra_form(self): + bt_head = BinaryTree.objects.create(name="Tree Head") + bt_child = BinaryTree.objects.create(name="First Child", parent=bt_head) + + # The maximum number of forms should respect 'get_max_num' on the + # ModelAdmin + max_forms_input = '<input id="id_binarytree_set-MAX_NUM_FORMS" name="binarytree_set-MAX_NUM_FORMS" type="hidden" value="%d" />' + # The total number of forms will remain the same in either case + total_forms_hidden = '<input id="id_binarytree_set-TOTAL_FORMS" name="binarytree_set-TOTAL_FORMS" type="hidden" value="2" />' + + response = self.client.get('/admin/admin_inlines/binarytree/add/') + self.assertContains(response, max_forms_input % 3) + self.assertContains(response, total_forms_hidden) + + response = self.client.get("/admin/admin_inlines/binarytree/%d/" % bt_head.id) + self.assertContains(response, max_forms_input % 2) + self.assertContains(response, total_forms_hidden) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class TestInlineMedia(TestCase): diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index c8986770f3..9e6b1f557b 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -1305,13 +1305,15 @@ class CommandTypes(AdminScriptTestCase): sys.stderr = err = StringIO() try: command.execute = lambda args: args # This will trigger TypeError - with self.assertRaises(SystemExit): + + # If the Exception is not CommandError it should always + # raise the original exception. + with self.assertRaises(TypeError): command.run_from_argv(['', '']) - err_message = err.getvalue() - # Exceptions other than CommandError automatically output the traceback - self.assertIn("Traceback", err_message) - self.assertIn("TypeError", err_message) + # If the Exception is CommandError and --traceback is not present + # this command should raise a SystemExit and don't print any + # traceback to the stderr. command.execute = raise_command_error err.truncate(0) with self.assertRaises(SystemExit): @@ -1320,12 +1322,12 @@ class CommandTypes(AdminScriptTestCase): self.assertNotIn("Traceback", err_message) self.assertIn("CommandError", err_message) + # If the Exception is CommandError and --traceback is present + # this command should raise the original CommandError as if it + # were not a CommandError. err.truncate(0) - with self.assertRaises(SystemExit): + with self.assertRaises(CommandError): command.run_from_argv(['', '', '--traceback']) - err_message = err.getvalue() - self.assertIn("Traceback (most recent call last)", err_message) - self.assertIn("CommandError", err_message) finally: sys.stderr = old_stderr @@ -1680,3 +1682,22 @@ class DiffSettings(AdminScriptTestCase): out, err = self.run_manage(args) self.assertNoOutput(err) self.assertOutput(out, "### STATIC_URL = None") + +class Dumpdata(AdminScriptTestCase): + """Tests for dumpdata management command.""" + + def setUp(self): + self.write_settings('settings.py') + + def tearDown(self): + self.remove_settings('settings.py') + + def test_pks_parsing(self): + """Regression for #20509 + + Test would raise an exception rather than printing an error message. + """ + args = ['dumpdata', '--pks=1'] + out, err = self.run_manage(args) + self.assertOutput(err, "You can only use --pks option with one model") + self.assertNoOutput(out) diff --git a/tests/admin_util/tests.py b/tests/admin_util/tests.py index 7898f200b5..4a9a203f50 100644 --- a/tests/admin_util/tests.py +++ b/tests/admin_util/tests.py @@ -11,8 +11,7 @@ from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE from django.contrib.sites.models import Site from django.db import models, DEFAULT_DB_ALIAS from django import forms -from django.test import TestCase -from django.utils import unittest +from django.test import SimpleTestCase, TestCase from django.utils.formats import localize from django.utils.safestring import mark_safe from django.utils import six @@ -82,7 +81,7 @@ class NestedObjectsTests(TestCase): # One for Location, one for Guest, and no query for EventGuide n.collect(objs) -class UtilTests(unittest.TestCase): +class UtilTests(SimpleTestCase): def test_values_from_lookup_field(self): """ Regression test for #12654: lookup_field @@ -151,7 +150,7 @@ class UtilTests(unittest.TestCase): # handling. display_value = display_for_field(None, models.NullBooleanField()) expected = '<img src="%sadmin/img/icon-unknown.gif" alt="None" />' % settings.STATIC_URL - self.assertEqual(display_value, expected) + self.assertHTMLEqual(display_value, expected) display_value = display_for_field(None, models.DecimalField()) self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE) @@ -236,6 +235,20 @@ class UtilTests(unittest.TestCase): ("not Really the Model", MockModelAdmin.test_from_model) ) + def test_label_for_property(self): + # NOTE: cannot use @property decorator, because of + # AttributeError: 'property' object has no attribute 'short_description' + class MockModelAdmin(object): + def my_property(self): + return "this if from property" + my_property.short_description = 'property short description' + test_from_property = property(my_property) + + self.assertEqual( + label_for_field("test_from_property", Article, model_admin=MockModelAdmin), + 'property short description' + ) + def test_related_name(self): """ Regression test for #13963 @@ -285,10 +298,10 @@ class UtilTests(unittest.TestCase): cb = forms.BooleanField(label=mark_safe('<i>cb</i>')) form = MyForm() - self.assertEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(), - '<label for="id_text" class="required inline"><i>text</i>:</label>') - self.assertEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(), - '<label for="id_cb" class="vCheckboxLabel required inline"><i>cb</i></label>') + self.assertHTMLEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(), + '<label for="id_text" class="required inline"><i>text</i>:</label>') + self.assertHTMLEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(), + '<label for="id_cb" class="vCheckboxLabel required inline"><i>cb</i></label>') # normal strings needs to be escaped class MyForm(forms.Form): @@ -296,10 +309,10 @@ class UtilTests(unittest.TestCase): cb = forms.BooleanField(label='&cb') form = MyForm() - self.assertEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(), - '<label for="id_text" class="required inline">&text:</label>') - self.assertEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(), - '<label for="id_cb" class="vCheckboxLabel required inline">&cb</label>') + self.assertHTMLEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(), + '<label for="id_text" class="required inline">&text:</label>') + self.assertHTMLEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(), + '<label for="id_cb" class="vCheckboxLabel required inline">&cb</label>') def test_flatten_fieldsets(self): """ diff --git a/tests/admin_validation/tests.py b/tests/admin_validation/tests.py index 16f73c6390..5eee3e7105 100644 --- a/tests/admin_validation/tests.py +++ b/tests/admin_validation/tests.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from django import forms from django.contrib import admin -from django.contrib.admin.validation import validate, validate_inline from django.core.exceptions import ImproperlyConfigured from django.test import TestCase @@ -38,13 +37,13 @@ class ValidationTestCase(TestCase): "fields": ["title", "original_release"], }), ] - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_custom_modelforms_with_fields_fieldsets(self): """ # Regression test for #8027: custom ModelForms with fields/fieldsets """ - validate(ValidFields, Song) + ValidFields.validate(Song) def test_custom_get_form_with_fieldsets(self): """ @@ -52,7 +51,7 @@ class ValidationTestCase(TestCase): is overridden. Refs #19445. """ - validate(ValidFormFieldsets, Song) + ValidFormFieldsets.validate(Song) def test_exclude_values(self): """ @@ -62,16 +61,16 @@ class ValidationTestCase(TestCase): exclude = ('foo') self.assertRaisesMessage(ImproperlyConfigured, "'ExcludedFields1.exclude' must be a list or tuple.", - validate, - ExcludedFields1, Book) + ExcludedFields1.validate, + Book) def test_exclude_duplicate_values(self): class ExcludedFields2(admin.ModelAdmin): exclude = ('name', 'name') self.assertRaisesMessage(ImproperlyConfigured, "There are duplicate field(s) in ExcludedFields2.exclude", - validate, - ExcludedFields2, Book) + ExcludedFields2.validate, + Book) def test_exclude_in_inline(self): class ExcludedFieldsInline(admin.TabularInline): @@ -84,8 +83,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "'ExcludedFieldsInline.exclude' must be a list or tuple.", - validate, - ExcludedFieldsAlbumAdmin, Album) + ExcludedFieldsAlbumAdmin.validate, + Album) def test_exclude_inline_model_admin(self): """ @@ -102,8 +101,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "SongInline cannot exclude the field 'album' - this is the foreign key to the parent model admin_validation.Album.", - validate, - AlbumAdmin, Album) + AlbumAdmin.validate, + Album) def test_app_label_in_admin_validation(self): """ @@ -114,8 +113,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "'RawIdNonexistingAdmin.raw_id_fields' refers to field 'nonexisting' that is missing from model 'admin_validation.Album'.", - validate, - RawIdNonexistingAdmin, Album) + RawIdNonexistingAdmin.validate, + Album) def test_fk_exclusion(self): """ @@ -127,28 +126,35 @@ class ValidationTestCase(TestCase): model = TwoAlbumFKAndAnE exclude = ("e",) fk_name = "album1" - validate_inline(TwoAlbumFKAndAnEInline, None, Album) + class MyAdmin(admin.ModelAdmin): + inlines = [TwoAlbumFKAndAnEInline] + MyAdmin.validate(Album) + def test_inline_self_validation(self): class TwoAlbumFKAndAnEInline(admin.TabularInline): model = TwoAlbumFKAndAnE + 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'>", - validate_inline, - TwoAlbumFKAndAnEInline, None, Album) + MyAdmin.validate, Album) def test_inline_with_specified(self): class TwoAlbumFKAndAnEInline(admin.TabularInline): model = TwoAlbumFKAndAnE fk_name = "album1" - validate_inline(TwoAlbumFKAndAnEInline, None, Album) + + class MyAdmin(admin.ModelAdmin): + inlines = [TwoAlbumFKAndAnEInline] + MyAdmin.validate(Album) def test_readonly(self): class SongAdmin(admin.ModelAdmin): readonly_fields = ("title",) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_on_method(self): def my_function(obj): @@ -157,7 +163,7 @@ class ValidationTestCase(TestCase): class SongAdmin(admin.ModelAdmin): readonly_fields = (my_function,) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_on_modeladmin(self): class SongAdmin(admin.ModelAdmin): @@ -166,13 +172,13 @@ class ValidationTestCase(TestCase): def readonly_method_on_modeladmin(self, obj): pass - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_method_on_model(self): class SongAdmin(admin.ModelAdmin): readonly_fields = ("readonly_method_on_model",) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_nonexistant_field(self): class SongAdmin(admin.ModelAdmin): @@ -180,8 +186,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "SongAdmin.readonly_fields[1], 'nonexistant' is not a callable or an attribute of 'SongAdmin' or found in the model 'Song'.", - validate, - SongAdmin, Song) + SongAdmin.validate, + Song) def test_nonexistant_field_on_inline(self): class CityInline(admin.TabularInline): @@ -190,8 +196,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "CityInline.readonly_fields[0], 'i_dont_exist' is not a callable or an attribute of 'CityInline' or found in the model 'City'.", - validate_inline, - CityInline, None, State) + CityInline.validate, + City) def test_extra(self): class SongAdmin(admin.ModelAdmin): @@ -199,13 +205,13 @@ class ValidationTestCase(TestCase): if instance.title == "Born to Run": return "Best Ever!" return "Status unknown." - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_lambda(self): class SongAdmin(admin.ModelAdmin): readonly_fields = (lambda obj: "test",) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_graceful_m2m_fail(self): """ @@ -219,8 +225,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "'BookAdmin.fields' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model.", - validate, - BookAdmin, Book) + BookAdmin.validate, + Book) def test_cannot_include_through(self): class FieldsetBookAdmin(admin.ModelAdmin): @@ -230,20 +236,20 @@ class ValidationTestCase(TestCase): ) self.assertRaisesMessage(ImproperlyConfigured, "'FieldsetBookAdmin.fieldsets[1][1]['fields']' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model.", - validate, - FieldsetBookAdmin, Book) + FieldsetBookAdmin.validate, + Book) def test_nested_fields(self): class NestedFieldsAdmin(admin.ModelAdmin): fields = ('price', ('name', 'subtitle')) - validate(NestedFieldsAdmin, Book) + NestedFieldsAdmin.validate(Book) def test_nested_fieldsets(self): class NestedFieldsetAdmin(admin.ModelAdmin): fieldsets = ( ('Main', {'fields': ('price', ('name', 'subtitle'))}), ) - validate(NestedFieldsetAdmin, Book) + NestedFieldsetAdmin.validate(Book) def test_explicit_through_override(self): """ @@ -260,7 +266,7 @@ class ValidationTestCase(TestCase): # If the through model is still a string (and hasn't been resolved to a model) # the validation will fail. - validate(BookAdmin, Book) + BookAdmin.validate(Book) def test_non_model_fields(self): """ @@ -274,7 +280,7 @@ class ValidationTestCase(TestCase): form = SongForm fields = ['title', 'extra_data'] - validate(FieldsOnFormOnlyAdmin, Song) + FieldsOnFormOnlyAdmin.validate(Song) def test_non_model_first_field(self): """ @@ -292,4 +298,4 @@ class ValidationTestCase(TestCase): form = SongForm fields = ['extra_data', 'title'] - validate(FieldsOnFormOnlyAdmin, Song) + FieldsOnFormOnlyAdmin.validate(Song) diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index cc7585cd2d..a6ad7cc0bc 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -9,11 +9,13 @@ from django.contrib import admin from django.contrib.admin.views.main import ChangeList from django.core.files.storage import FileSystemStorage from django.core.mail import EmailMessage +from django.core.servers.basehttp import FileWrapper from django.conf.urls import patterns, url from django.db import models from django.forms.models import BaseModelFormSet -from django.http import HttpResponse +from django.http import HttpResponse, StreamingHttpResponse from django.contrib.admin import BooleanFieldListFilter +from django.utils.six import StringIO from .models import (Article, Chapter, Account, Media, Child, Parent, Picture, Widget, DooHickey, Grommet, Whatsit, FancyDoodad, Category, Link, @@ -24,7 +26,7 @@ from .models import (Article, Chapter, Account, Media, Child, Parent, Picture, Gadget, Villain, SuperVillain, Plot, PlotDetails, CyclicOne, CyclicTwo, WorkHour, Reservation, FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory, Book, Promo, ChapterXtra1, Pizza, Topping, - Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug, + Album, Question, Answer, ComplexSortedPerson, PluggableSearchPerson, PrePopulatedPostLargeSlug, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated, RelatedPrepopulated, UndeletableObject, UserMessenger, Simple, Choice, @@ -149,7 +151,7 @@ class InquisitionAdmin(admin.ModelAdmin): class SketchAdmin(admin.ModelAdmin): - raw_id_fields = ('inquisition',) + raw_id_fields = ('inquisition', 'defendant0', 'defendant1') class FabricAdmin(admin.ModelAdmin): @@ -238,8 +240,20 @@ def redirect_to(modeladmin, request, selected): redirect_to.short_description = 'Redirect to (Awesome action)' +def download(modeladmin, request, selected): + buf = StringIO('This is the content of the file') + return StreamingHttpResponse(FileWrapper(buf)) +download.short_description = 'Download subscription' + + +def no_perm(modeladmin, request, selected): + return HttpResponse(content='No permission to perform this action', + status=403) +no_perm.short_description = 'No permission to run' + + class ExternalSubscriberAdmin(admin.ModelAdmin): - actions = [redirect_to, external_mail] + actions = [redirect_to, external_mail, download, no_perm] class Podcast(Media): @@ -530,6 +544,20 @@ class ComplexSortedPersonAdmin(admin.ModelAdmin): colored_name.admin_order_field = 'name' +class PluggableSearchPersonAdmin(admin.ModelAdmin): + list_display = ('name', 'age') + search_fields = ('name',) + + def get_search_results(self, request, queryset, search_term): + queryset, use_distinct = super(PluggableSearchPersonAdmin, self).get_search_results(request, queryset, search_term) + try: + search_term_as_int = int(search_term) + queryset |= self.model.objects.filter(age=search_term_as_int) + except: + pass + return queryset, use_distinct + + class AlbumAdmin(admin.ModelAdmin): list_filter = ['title'] @@ -733,6 +761,7 @@ site.register(Question) site.register(Answer) site.register(PrePopulatedPost, PrePopulatedPostAdmin) site.register(ComplexSortedPerson, ComplexSortedPersonAdmin) +site.register(PluggableSearchPerson, PluggableSearchPersonAdmin) site.register(PrePopulatedPostLargeSlug, PrePopulatedPostLargeSlugAdmin) site.register(AdminOrderedField, AdminOrderedFieldAdmin) site.register(AdminOrderedModelMethod, AdminOrderedModelMethodAdmin) diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py index 1916949f63..5cc6f6251a 100644 --- a/tests/admin_views/models.py +++ b/tests/admin_views/models.py @@ -137,6 +137,7 @@ class Thing(models.Model): class Actor(models.Model): name = models.CharField(max_length=50) age = models.IntegerField() + title = models.CharField(max_length=50, null=True) def __str__(self): return self.name @@ -158,6 +159,8 @@ class Sketch(models.Model): 'leader__age': 27, 'expected': False, }) + defendant0 = models.ForeignKey(Actor, limit_choices_to={'title__isnull': False}, related_name='as_defendant0') + defendant1 = models.ForeignKey(Actor, limit_choices_to={'title__isnull': True}, related_name='as_defendant1') def __str__(self): return self.title @@ -591,6 +594,12 @@ class ComplexSortedPerson(models.Model): age = models.PositiveIntegerField() is_employee = models.NullBooleanField() + +class PluggableSearchPerson(models.Model): + name = models.CharField(max_length=100) + age = models.PositiveIntegerField() + + class PrePopulatedPostLargeSlug(models.Model): """ Regression test for #15938: a large max_length for the slugfield must not diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 8e678a72b3..8c8a65318c 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -11,7 +11,6 @@ except ImportError: # Python 2 from django.conf import settings, global_settings from django.core import mail -from django.core.exceptions import SuspiciousOperation from django.core.files import temp as tempfile from django.core.urlresolvers import reverse # Register auth models with the admin. @@ -30,6 +29,7 @@ from django.db import connection from django.forms.util import ErrorList from django.template.response import TemplateResponse from django.test import TestCase +from django.test.utils import patch_logger from django.utils import formats, translation, unittest from django.utils.cache import get_max_age from django.utils.encoding import iri_to_uri, force_bytes @@ -46,7 +46,7 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount, DooHickey, FancyDoodad, Whatsit, Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee, Question, Answer, Inquisition, Actor, FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, Story, - OtherStory, ComplexSortedPerson, Parent, Child, AdminOrderedField, + OtherStory, ComplexSortedPerson, PluggableSearchPerson, Parent, Child, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject, Simple, UndeletableObject, Choice, ShortMessage, Telegram) @@ -468,8 +468,12 @@ class AdminViewBasicTest(TestCase): self.assertContains(response, '4 articles') response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': 'false'}) self.assertContains(response, '3 articles') + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': '0'}) + self.assertContains(response, '3 articles') response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': 'true'}) self.assertContains(response, '1 article') + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': '1'}) + self.assertContains(response, '1 article') def testLogoutAndPasswordChangeURLs(self): response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit) @@ -543,20 +547,21 @@ class AdminViewBasicTest(TestCase): self.assertContains(response, '%Y-%m-%d %H:%M:%S') def test_disallowed_filtering(self): - self.assertRaises(SuspiciousOperation, - self.client.get, "/test_admin/admin/admin_views/album/?owner__email__startswith=fuzzy" - ) + with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as calls: + response = self.client.get("/test_admin/admin/admin_views/album/?owner__email__startswith=fuzzy") + self.assertEqual(response.status_code, 400) + self.assertEqual(len(calls), 1) - try: - self.client.get("/test_admin/admin/admin_views/thing/?color__value__startswith=red") - self.client.get("/test_admin/admin/admin_views/thing/?color__value=red") - except SuspiciousOperation: - self.fail("Filters are allowed if explicitly included in list_filter") + # Filters are allowed if explicitly included in list_filter + response = self.client.get("/test_admin/admin/admin_views/thing/?color__value__startswith=red") + self.assertEqual(response.status_code, 200) + response = self.client.get("/test_admin/admin/admin_views/thing/?color__value=red") + self.assertEqual(response.status_code, 200) - try: - self.client.get("/test_admin/admin/admin_views/person/?age__gt=30") - except SuspiciousOperation: - self.fail("Filters should be allowed if they involve a local field without the need to whitelist them in list_filter or date_hierarchy.") + # Filters should be allowed if they involve a local field without the + # need to whitelist them in list_filter or date_hierarchy. + response = self.client.get("/test_admin/admin/admin_views/person/?age__gt=30") + self.assertEqual(response.status_code, 200) e1 = Employee.objects.create(name='Anonymous', gender=1, age=22, alive=True, code='123') e2 = Employee.objects.create(name='Visitor', gender=2, age=19, alive=True, code='124') @@ -574,10 +579,9 @@ class AdminViewBasicTest(TestCase): ForeignKey 'limit_choices_to' should be allowed, otherwise raw_id_fields can break. """ - try: - self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27") - except SuspiciousOperation: - self.fail("Filters should be allowed if they are defined on a ForeignKey pointing to this model") + # Filters should be allowed if they are defined on a ForeignKey pointing to this model + response = self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27") + self.assertEqual(response.status_code, 200) def test_hide_change_password(self): """ @@ -1995,7 +1999,7 @@ class AdminViewListEditable(TestCase): } response = self.client.post('/test_admin/admin/admin_views/person/', data) non_form_errors = response.context['cl'].formset.non_form_errors() - self.assertTrue(isinstance(non_form_errors, ErrorList)) + self.assertIsInstance(non_form_errors, ErrorList) self.assertEqual(str(non_form_errors), str(ErrorList(["Grace is not a Zombie"]))) def test_list_editable_ordering(self): @@ -2202,6 +2206,20 @@ class AdminSearchTest(TestCase): self.assertContains(response, "\n0 persons\n") self.assertNotContains(response, "Guido") + def test_pluggable_search(self): + p1 = PluggableSearchPerson.objects.create(name="Bob", age=10) + p2 = PluggableSearchPerson.objects.create(name="Amy", age=20) + + response = self.client.get('/test_admin/admin/admin_views/pluggablesearchperson/?q=Bob') + # confirm the search returned one object + self.assertContains(response, "\n1 pluggable search person\n") + self.assertContains(response, "Bob") + + response = self.client.get('/test_admin/admin/admin_views/pluggablesearchperson/?q=20') + # confirm the search returned one object + self.assertContains(response, "\n1 pluggable search person\n") + self.assertContains(response, "Amy") + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminInheritedInlinesTest(TestCase): @@ -2414,6 +2432,29 @@ class AdminActionsTest(TestCase): response = self.client.post(url, action_data) self.assertRedirects(response, url) + def test_custom_function_action_streaming_response(self): + """Tests a custom action that returns a StreamingHttpResponse.""" + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action': 'download', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) + content = b''.join(response.streaming_content) + self.assertEqual(content, b'This is the content of the file') + self.assertEqual(response.status_code, 200) + + def test_custom_function_action_no_perm_response(self): + """Tests a custom action that returns an HttpResponse with 403 code.""" + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action': 'no_perm', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.content, b'No permission to perform this action') + def test_actions_ordering(self): """ Ensure that actions are ordered as expected. @@ -2422,9 +2463,13 @@ class AdminActionsTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/externalsubscriber/') self.assertContains(response, '''<label>Action: <select name="action"> <option value="" selected="selected">---------</option> -<option value="delete_selected">Delete selected external subscribers</option> +<option value="delete_selected">Delete selected external +subscribers</option> <option value="redirect_to">Redirect to (Awesome action)</option> -<option value="external_mail">External mail (Another awesome action)</option> +<option value="external_mail">External mail (Another awesome +action)</option> +<option value="download">Download subscription</option> +<option value="no_perm">No permission to run</option> </select>''', html=True) def test_model_without_action(self): @@ -3494,7 +3539,6 @@ class RawIdFieldsTest(TestCase): def test_limit_choices_to(self): """Regression test for 14880""" - # This includes tests integers, strings and booleans in the lookup query string actor = Actor.objects.create(name="Palin", age=27) inquisition1 = Inquisition.objects.create(expected=True, leader=actor, @@ -3510,11 +3554,57 @@ class RawIdFieldsTest(TestCase): # Handle relative links popup_url = urljoin(response.request['PATH_INFO'], popup_url) - # Get the popup + # Get the popup and verify the correct objects show up in the resulting + # page. This step also tests integers, strings and booleans in the + # lookup query string; in model we define inquisition field to have a + # limit_choices_to option that includes a filter on a string field + # (inquisition__actor__name), a filter on an integer field + # (inquisition__actor__age), and a filter on a boolean field + # (inquisition__expected). response2 = self.client.get(popup_url) self.assertContains(response2, "Spain") self.assertNotContains(response2, "England") + def test_limit_choices_to_isnull_false(self): + """Regression test for 20182""" + Actor.objects.create(name="Palin", age=27) + Actor.objects.create(name="Kilbraken", age=50, title="Judge") + response = self.client.get('/test_admin/admin/admin_views/sketch/add/') + # Find the link + m = re.search(br'<a href="([^"]*)"[^>]* id="lookup_id_defendant0"', response.content) + self.assertTrue(m) # Got a match + popup_url = m.groups()[0].decode().replace("&", "&") + + # Handle relative links + popup_url = urljoin(response.request['PATH_INFO'], popup_url) + # Get the popup and verify the correct objects show up in the resulting + # page. This step tests field__isnull=0 gets parsed correctly from the + # lookup query string; in model we define defendant0 field to have a + # limit_choices_to option that includes "actor__title__isnull=False". + response2 = self.client.get(popup_url) + self.assertContains(response2, "Kilbraken") + self.assertNotContains(response2, "Palin") + + def test_limit_choices_to_isnull_true(self): + """Regression test for 20182""" + Actor.objects.create(name="Palin", age=27) + Actor.objects.create(name="Kilbraken", age=50, title="Judge") + response = self.client.get('/test_admin/admin/admin_views/sketch/add/') + # Find the link + m = re.search(br'<a href="([^"]*)"[^>]* id="lookup_id_defendant1"', response.content) + self.assertTrue(m) # Got a match + popup_url = m.groups()[0].decode().replace("&", "&") + + # Handle relative links + popup_url = urljoin(response.request['PATH_INFO'], popup_url) + # Get the popup and verify the correct objects show up in the resulting + # page. This step tests field__isnull=1 gets parsed correctly from the + # lookup query string; in model we define defendant1 field to have a + # limit_choices_to option that includes "actor__title__isnull=True". + response2 = self.client.get(popup_url) + self.assertNotContains(response2, "Kilbraken") + self.assertContains(response2, "Palin") + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class UserAdminTest(TestCase): diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 4e09922893..98f41c1490 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -13,6 +13,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.db.models import CharField, DateField from django.test import TestCase as DjangoTestCase from django.test.utils import override_settings +from django.utils import six from django.utils import translation from django.utils.html import conditional_escape from django.utils.unittest import TestCase @@ -139,6 +140,17 @@ class AdminFormfieldForDBFieldTests(TestCase): def testInheritance(self): self.assertFormfield(models.Album, 'backside_art', widgets.AdminFileWidget) + def test_m2m_widgets(self): + """m2m fields help text as it applies to admin app (#9321).""" + class AdvisorAdmin(admin.ModelAdmin): + filter_vertical=['companies'] + + self.assertFormfield(models.Advisor, 'companies', widgets.FilteredSelectMultiple, + filter_vertical=['companies']) + ma = AdvisorAdmin(models.Advisor, admin.site) + f = ma.formfield_for_dbfield(models.Advisor._meta.get_field('companies'), request=None) + self.assertEqual(six.text_type(f.help_text), ' Hold down "Control", or "Command" on a Mac, to select more than one.') + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminFormfieldForDBFieldWithRequestTests(DjangoTestCase): diff --git a/tests/aggregation_regress/tests.py b/tests/aggregation_regress/tests.py index 6c5a43664a..80441b9a60 100644 --- a/tests/aggregation_regress/tests.py +++ b/tests/aggregation_regress/tests.py @@ -581,6 +581,7 @@ class AggregationTests(TestCase): 6 ) + # Note: intentionally no order_by(), that case needs tests, too. publishers = Publisher.objects.filter(id__in=[1, 2]) self.assertEqual( sorted(p.name for p in publishers), @@ -591,10 +592,15 @@ class AggregationTests(TestCase): ) publishers = publishers.annotate(n_books=Count("book")) + sorted_publishers = sorted(publishers, key=lambda x: x.name) self.assertEqual( - publishers[0].n_books, + sorted_publishers[0].n_books, 2 ) + self.assertEqual( + sorted_publishers[1].n_books, + 1 + ) self.assertEqual( sorted(p.name for p in publishers), diff --git a/tests/backends/tests.py b/tests/backends/tests.py index 81b2851403..08cae0f7c3 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -8,7 +8,7 @@ import threading from django.conf import settings from django.core.management.color import no_style -from django.db import (backend, connection, connections, DEFAULT_DB_ALIAS, +from django.db import (connection, connections, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError, transaction) from django.db.backends.signals import connection_created from django.db.backends.postgresql_psycopg2 import version as pg_version @@ -50,7 +50,8 @@ class OracleChecks(unittest.TestCase): def test_dbms_session(self): # If the backend is Oracle, test that we can call a standard # stored procedure through our cursor wrapper. - convert_unicode = backend.convert_unicode + from django.db.backends.oracle.base import convert_unicode + cursor = connection.cursor() cursor.callproc(convert_unicode('DBMS_SESSION.SET_IDENTIFIER'), [convert_unicode('_django_testing!')]) @@ -60,8 +61,10 @@ class OracleChecks(unittest.TestCase): def test_cursor_var(self): # If the backend is Oracle, test that we can pass cursor variables # as query parameters. + from django.db.backends.oracle.base import Database + cursor = connection.cursor() - var = cursor.var(backend.Database.STRING) + var = cursor.var(Database.STRING) cursor.execute("BEGIN %s := 'X'; END; ", [var]) self.assertEqual(var.getvalue(), 'X') @@ -172,7 +175,7 @@ class LastExecutedQueryTest(TestCase): sql, params = persons.query.sql_with_params() cursor = persons.query.get_compiler('default').execute_sql(None) last_sql = cursor.db.ops.last_executed_query(cursor, sql, params) - self.assertTrue(isinstance(last_sql, six.text_type)) + self.assertIsInstance(last_sql, six.text_type) @unittest.skipUnless(connection.vendor == 'sqlite', "This test is specific to SQLite.") diff --git a/tests/base/models.py b/tests/base/models.py index bddb406820..d47ddcfd66 100644 --- a/tests/base/models.py +++ b/tests/base/models.py @@ -14,8 +14,10 @@ class CustomBaseModel(models.base.ModelBase): class MyModel(six.with_metaclass(CustomBaseModel, models.Model)): - """Model subclass with a custom base using six.with_metaclass.""" + """Model subclass with a custom base using six.with_metaclass.""" +# This is done to ensure that for Python2 only, defining metaclasses +# still does not fail to create the model. if not six.PY3: class MyModel(models.Model): diff --git a/tests/basic/models.py b/tests/basic/models.py index 660beddf49..1bffcb9cda 100644 --- a/tests/basic/models.py +++ b/tests/basic/models.py @@ -18,3 +18,11 @@ class Article(models.Model): def __str__(self): return self.headline + +@python_2_unicode_compatible +class SelfRef(models.Model): + selfref = models.ForeignKey('self', null=True, blank=True, + related_name='+') + + def __str__(self): + return SelfRef.objects.get(selfref=self).pk diff --git a/tests/basic/tests.py b/tests/basic/tests.py index ccbb9bd423..6bf46cce9b 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -11,7 +11,7 @@ from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnle from django.utils import six from django.utils.translation import ugettext_lazy -from .models import Article +from .models import Article, SelfRef class ModelTest(TestCase): @@ -87,23 +87,14 @@ class ModelTest(TestCase): # parameters don't match any object. six.assertRaisesRegex(self, ObjectDoesNotExist, - "Article matching query does not exist. Lookup parameters were " - "{'id__exact': 2000}", + "Article matching query does not exist.", Article.objects.get, id__exact=2000, ) # To avoid dict-ordering related errors check only one lookup # in single assert. - six.assertRaisesRegex(self, - ObjectDoesNotExist, - ".*'pub_date__year': 2005.*", - Article.objects.get, - pub_date__year=2005, - pub_date__month=8, - ) - six.assertRaisesRegex(self, + self.assertRaises( ObjectDoesNotExist, - ".*'pub_date__month': 8.*", Article.objects.get, pub_date__year=2005, pub_date__month=8, @@ -111,8 +102,7 @@ class ModelTest(TestCase): six.assertRaisesRegex(self, ObjectDoesNotExist, - "Article matching query does not exist. Lookup parameters were " - "{'pub_date__week_day': 6}", + "Article matching query does not exist.", Article.objects.get, pub_date__week_day=6, ) @@ -442,7 +432,7 @@ class ModelTest(TestCase): Article.objects.all()[0:-5] except Exception as e: error = e - self.assertTrue(isinstance(error, AssertionError)) + self.assertIsInstance(error, AssertionError) self.assertEqual(str(error), "Negative indexing is not supported.") # An Article instance doesn't have access to the "objects" attribute. @@ -647,15 +637,15 @@ class ModelTest(TestCase): # Can't be instantiated with self.assertRaises(TypeError): EmptyQuerySet() - self.assertTrue(isinstance(Article.objects.none(), EmptyQuerySet)) + self.assertIsInstance(Article.objects.none(), EmptyQuerySet) def test_emptyqs_values(self): # test for #15959 Article.objects.create(headline='foo', pub_date=datetime.now()) with self.assertNumQueries(0): qs = Article.objects.none().values_list('pk') - self.assertTrue(isinstance(qs, EmptyQuerySet)) - self.assertTrue(isinstance(qs, ValuesListQuerySet)) + self.assertIsInstance(qs, EmptyQuerySet) + self.assertIsInstance(qs, ValuesListQuerySet) self.assertEqual(len(qs), 0) def test_emptyqs_customqs(self): @@ -670,7 +660,7 @@ class ModelTest(TestCase): qs = qs.none() with self.assertNumQueries(0): self.assertEqual(len(qs), 0) - self.assertTrue(isinstance(qs, EmptyQuerySet)) + self.assertIsInstance(qs, EmptyQuerySet) self.assertEqual(qs.do_something(), 'did something') def test_emptyqs_values_order(self): @@ -689,6 +679,12 @@ class ModelTest(TestCase): with self.assertNumQueries(0): self.assertEqual(len(Article.objects.none().distinct('headline', 'pub_date')), 0) + def test_ticket_20278(self): + sr = SelfRef.objects.create() + with self.assertRaises(ObjectDoesNotExist): + SelfRef.objects.get(selfref=sr) + + class ConcurrentSaveTests(TransactionTestCase): @skipUnlessDBFeature('test_db_allows_multiple_connections') def test_concurrent_delete_with_save(self): diff --git a/tests/cache/tests.py b/tests/cache/tests.py index 00c51638b7..da80c48058 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -28,8 +28,8 @@ from django.middleware.cache import (FetchFromCacheMiddleware, from django.template import Template from django.template.response import TemplateResponse from django.test import TestCase, TransactionTestCase, RequestFactory -from django.test.utils import override_settings, six -from django.utils import timezone, translation, unittest +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin +from django.utils import six, timezone, translation, unittest from django.utils.cache import (patch_vary_headers, get_cache_key, learn_cache_key, patch_cache_control, patch_response_headers) from django.utils.encoding import force_text @@ -441,6 +441,34 @@ class BaseCacheTests(object): self.assertEqual(self.cache.get('key3'), 'sausage') self.assertEqual(self.cache.get('key4'), 'lobster bisque') + def test_forever_timeout(self): + ''' + Passing in None into timeout results in a value that is cached forever + ''' + self.cache.set('key1', 'eggs', None) + self.assertEqual(self.cache.get('key1'), 'eggs') + + self.cache.add('key2', 'ham', None) + self.assertEqual(self.cache.get('key2'), 'ham') + + self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, None) + self.assertEqual(self.cache.get('key3'), 'sausage') + self.assertEqual(self.cache.get('key4'), 'lobster bisque') + + def test_zero_timeout(self): + ''' + Passing in None into timeout results in a value that is cached forever + ''' + self.cache.set('key1', 'eggs', 0) + self.assertEqual(self.cache.get('key1'), None) + + self.cache.add('key2', 'ham', 0) + self.assertEqual(self.cache.get('key2'), None) + + self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, 0) + self.assertEqual(self.cache.get('key3'), None) + self.assertEqual(self.cache.get('key4'), None) + def test_float_timeout(self): # Make sure a timeout given as a float doesn't crash anything. self.cache.set("key1", "spam", 100.2) @@ -482,13 +510,13 @@ class BaseCacheTests(object): # memcached does not allow whitespace or control characters in keys self.cache.set('key with spaces', 'value') self.assertEqual(len(w), 2) - self.assertTrue(isinstance(w[0].message, CacheKeyWarning)) + self.assertIsInstance(w[0].message, CacheKeyWarning) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # memcached limits key length to 250 self.cache.set('a' * 251, 'value') self.assertEqual(len(w), 1) - self.assertTrue(isinstance(w[0].message, CacheKeyWarning)) + self.assertIsInstance(w[0].message, CacheKeyWarning) finally: self.cache.key_func = old_func @@ -1069,10 +1097,10 @@ class GetCacheTests(unittest.TestCase): def test_simple(self): cache = get_cache('locmem://') from django.core.cache.backends.locmem import LocMemCache - self.assertTrue(isinstance(cache, LocMemCache)) + self.assertIsInstance(cache, LocMemCache) from django.core.cache import cache - self.assertTrue(isinstance(cache, get_cache('default').__class__)) + self.assertIsInstance(cache, get_cache('default').__class__) cache = get_cache( 'django.core.cache.backends.dummy.DummyCache', **{'TIMEOUT': 120}) @@ -1564,9 +1592,10 @@ def hello_world_view(request, value): }, }, ) -class CacheMiddlewareTest(TestCase): +class CacheMiddlewareTest(IgnorePendingDeprecationWarningsMixin, TestCase): def setUp(self): + super(CacheMiddlewareTest, self).setUp() self.factory = RequestFactory() self.default_cache = get_cache('default') self.other_cache = get_cache('other') @@ -1574,6 +1603,7 @@ class CacheMiddlewareTest(TestCase): def tearDown(self): self.default_cache.clear() self.other_cache.clear() + super(CacheMiddlewareTest, self).tearDown() def test_constructor(self): """ diff --git a/tests/commands_sql/models.py b/tests/commands_sql/models.py index 089aa96f30..d8f372b403 100644 --- a/tests/commands_sql/models.py +++ b/tests/commands_sql/models.py @@ -3,5 +3,11 @@ from django.utils.encoding import python_2_unicode_compatible @python_2_unicode_compatible +class Comment(models.Model): + pass + + +@python_2_unicode_compatible class Book(models.Model): title = models.CharField(max_length=100, db_index=True) + comments = models.ManyToManyField(Comment) diff --git a/tests/commands_sql/tests.py b/tests/commands_sql/tests.py index 831e9a27c4..e083d3977a 100644 --- a/tests/commands_sql/tests.py +++ b/tests/commands_sql/tests.py @@ -12,41 +12,43 @@ from django.utils import six class SQLCommandsTestCase(TestCase): """Tests for several functions in django/core/management/sql.py""" + def count_ddl(self, output, cmd): + return len([o for o in output if o.startswith(cmd)]) + def test_sql_create(self): app = models.get_app('commands_sql') output = sql_create(app, no_style(), connections[DEFAULT_DB_ALIAS]) + create_tables = [o for o in output if o.startswith('CREATE TABLE')] + self.assertEqual(len(create_tables), 3) # Lower so that Oracle's upper case tbl names wont break - sql = output[0].lower() + sql = create_tables[-1].lower() six.assertRegex(self, sql, r'^create table .commands_sql_book.*') def test_sql_delete(self): app = models.get_app('commands_sql') output = sql_delete(app, no_style(), connections[DEFAULT_DB_ALIAS]) - # Oracle produces DROP SEQUENCE and DROP TABLE for this command. - if connections[DEFAULT_DB_ALIAS].vendor == 'oracle': - sql = output[1].lower() - else: - sql = output[0].lower() - six.assertRegex(self, sql, r'^drop table .commands_sql_book.*') + drop_tables = [o for o in output if o.startswith('DROP TABLE')] + self.assertEqual(len(drop_tables), 3) + # Lower so that Oracle's upper case tbl names wont break + sql = drop_tables[-1].lower() + six.assertRegex(self, sql, r'^drop table .commands_sql_comment.*') def test_sql_indexes(self): app = models.get_app('commands_sql') output = sql_indexes(app, no_style(), connections[DEFAULT_DB_ALIAS]) - # PostgreSQL creates two indexes - self.assertIn(len(output), [1, 2]) - self.assertTrue(output[0].startswith("CREATE INDEX")) + # PostgreSQL creates one additional index for CharField + self.assertIn(self.count_ddl(output, 'CREATE INDEX'), [3, 4]) def test_sql_destroy_indexes(self): app = models.get_app('commands_sql') output = sql_destroy_indexes(app, no_style(), connections[DEFAULT_DB_ALIAS]) - # PostgreSQL creates two indexes - self.assertIn(len(output), [1, 2]) - self.assertTrue(output[0].startswith("DROP INDEX")) + # PostgreSQL creates one additional index for CharField + self.assertIn(self.count_ddl(output, 'DROP INDEX'), [3, 4]) def test_sql_all(self): app = models.get_app('commands_sql') output = sql_all(app, no_style(), connections[DEFAULT_DB_ALIAS]) - # PostgreSQL creates two indexes - self.assertIn(len(output), [2, 3]) - self.assertTrue(output[0].startswith('CREATE TABLE')) - self.assertTrue(output[1].startswith('CREATE INDEX')) + + self.assertEqual(self.count_ddl(output, 'CREATE TABLE'), 3) + # PostgreSQL creates one additional index for CharField + self.assertIn(self.count_ddl(output, 'CREATE INDEX'), [3, 4]) diff --git a/tests/comment_tests/tests/test_comment_form.py b/tests/comment_tests/tests/test_comment_form.py index 39ba57928d..a30f13a073 100644 --- a/tests/comment_tests/tests/test_comment_form.py +++ b/tests/comment_tests/tests/test_comment_form.py @@ -54,7 +54,7 @@ class CommentFormTests(CommentTestCase): def testGetCommentObject(self): f = self.testValidPost() c = f.get_comment_object() - self.assertTrue(isinstance(c, Comment)) + self.assertIsInstance(c, Comment) self.assertEqual(c.content_object, Article.objects.get(pk=1)) self.assertEqual(c.comment, "This is my comment") c.save() diff --git a/tests/comment_tests/tests/test_templatetags.py b/tests/comment_tests/tests/test_templatetags.py index 185f6de297..1971c21a58 100644 --- a/tests/comment_tests/tests/test_templatetags.py +++ b/tests/comment_tests/tests/test_templatetags.py @@ -32,7 +32,7 @@ class CommentTemplateTagTests(CommentTestCase): t = "{% load comments %}" + (tag or "{% get_comment_form for comment_tests.article a.id as form %}") ctx, out = self.render(t, a=Article.objects.get(pk=1)) self.assertEqual(out, "") - self.assertTrue(isinstance(ctx["form"], CommentForm)) + self.assertIsInstance(ctx["form"], CommentForm) def testGetCommentFormFromLiteral(self): self.testGetCommentForm("{% get_comment_form for comment_tests.article 1 as form %}") diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index 5300b2172a..841b24bb42 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import logging from django.conf import settings from django.core.context_processors import csrf @@ -78,18 +79,18 @@ class CsrfViewMiddlewareTest(TestCase): def _check_token_present(self, response, csrf_id=None): self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % (csrf_id or self._csrf_id)) - def test_process_view_token_too_long(self): - """ - Check that if the token is longer than expected, it is ignored and - a new token is created. - """ - req = self._get_GET_no_csrf_cookie_request() - req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 10000000 - CsrfViewMiddleware().process_view(req, token_view, (), {}) - resp = token_view(req) - resp2 = CsrfViewMiddleware().process_response(req, resp) - csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) - self.assertEqual(len(csrf_cookie.value), CSRF_KEY_LENGTH) + def test_process_view_token_too_long(self): + """ + Check that if the token is longer than expected, it is ignored and + a new token is created. + """ + req = self._get_GET_no_csrf_cookie_request() + req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 10000000 + CsrfViewMiddleware().process_view(req, token_view, (), {}) + resp = token_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) + self.assertEqual(len(csrf_cookie.value), CSRF_KEY_LENGTH) def test_process_response_get_token_used(self): """ @@ -284,6 +285,19 @@ class CsrfViewMiddlewareTest(TestCase): self.assertEqual(403, req2.status_code) @override_settings(ALLOWED_HOSTS=['www.example.com']) + def test_https_malformed_referer(self): + """ + Test that a POST HTTPS request with a bad referer is rejected + """ + req = self._get_POST_request_with_token() + req._is_secure_override = True + req.META['HTTP_HOST'] = 'www.example.com' + req.META['HTTP_REFERER'] = 'http://http://www.example.com/' + req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) + self.assertNotEqual(None, req2) + self.assertEqual(403, req2.status_code) + + @override_settings(ALLOWED_HOSTS=['www.example.com']) def test_https_good_referer(self): """ Test that a POST HTTPS request with a good referer is accepted @@ -340,3 +354,29 @@ class CsrfViewMiddlewareTest(TestCase): resp2 = CsrfViewMiddleware().process_response(req, resp) self.assertTrue(resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)) self.assertTrue('Cookie' in resp2.get('Vary','')) + + def test_ensures_csrf_cookie_no_logging(self): + """ + Tests that ensure_csrf_cookie doesn't log warnings. See #19436. + """ + @ensure_csrf_cookie + def view(request): + # Doesn't insert a token or anything + return HttpResponse(content="") + + class TestHandler(logging.Handler): + def emit(self, record): + raise Exception("This shouldn't have happened!") + + logger = logging.getLogger('django.request') + test_handler = TestHandler() + old_log_level = logger.level + try: + logger.addHandler(test_handler) + logger.setLevel(logging.WARNING) + + req = self._get_GET_no_csrf_cookie_request() + resp = view(req) + finally: + logger.removeHandler(test_handler) + logger.setLevel(old_log_level) diff --git a/tests/custom_managers/tests.py b/tests/custom_managers/tests.py index 294920de2b..4fe79fe3fb 100644 --- a/tests/custom_managers/tests.py +++ b/tests/custom_managers/tests.py @@ -19,7 +19,7 @@ class CustomManagerTests(TestCase): ) # The RelatedManager used on the 'books' descriptor extends the default # manager - self.assertTrue(isinstance(p2.books, PublishedBookManager)) + self.assertIsInstance(p2.books, PublishedBookManager) b1 = Book.published_objects.create( title="How to program", author="Rodney Dangerfield", is_published=True @@ -34,7 +34,7 @@ class CustomManagerTests(TestCase): # The RelatedManager used on the 'authors' descriptor extends the # default manager - self.assertTrue(isinstance(b2.authors, PersonManager)) + self.assertIsInstance(b2.authors, PersonManager) self.assertQuerysetEqual( Book.published_objects.all(), [ diff --git a/tests/datatypes/tests.py b/tests/datatypes/tests.py index f0ec5f3c0a..b6b52dedf2 100644 --- a/tests/datatypes/tests.py +++ b/tests/datatypes/tests.py @@ -74,7 +74,7 @@ class DataTypesTestCase(TestCase): database should be unicode.""" d = Donut.objects.create(name='Jelly Donut', review='Outstanding') newd = Donut.objects.get(id=d.id) - self.assertTrue(isinstance(newd.review, six.text_type)) + self.assertIsInstance(newd.review, six.text_type) @skipIfDBFeature('supports_timezones') def test_error_on_timezone(self): @@ -90,7 +90,7 @@ class DataTypesTestCase(TestCase): a Python datetime.date, not a datetime.datetime""" b = RumBaba.objects.create() # Verify we didn't break DateTimeField behavior - self.assertTrue(isinstance(b.baked_timestamp, datetime.datetime)) + self.assertIsInstance(b.baked_timestamp, datetime.datetime) # We need to test this this way because datetime.datetime inherits # from datetime.date: - self.assertTrue(isinstance(b.baked_date, datetime.date) and not isinstance(b.baked_date, datetime.datetime)) + self.assertIsInstance(b.baked_date, datetime.date) and not isinstance(b.baked_date, datetime.datetime) diff --git a/tests/decorators/tests.py b/tests/decorators/tests.py index 8a3f340e9f..05016be231 100644 --- a/tests/decorators/tests.py +++ b/tests/decorators/tests.py @@ -126,15 +126,15 @@ class DecoratorsTest(TestCase): my_safe_view = require_safe(my_view) request = HttpRequest() request.method = 'GET' - self.assertTrue(isinstance(my_safe_view(request), HttpResponse)) + self.assertIsInstance(my_safe_view(request), HttpResponse) request.method = 'HEAD' - self.assertTrue(isinstance(my_safe_view(request), HttpResponse)) + self.assertIsInstance(my_safe_view(request), HttpResponse) request.method = 'POST' - self.assertTrue(isinstance(my_safe_view(request), HttpResponseNotAllowed)) + self.assertIsInstance(my_safe_view(request), HttpResponseNotAllowed) request.method = 'PUT' - self.assertTrue(isinstance(my_safe_view(request), HttpResponseNotAllowed)) + self.assertIsInstance(my_safe_view(request), HttpResponseNotAllowed) request.method = 'DELETE' - self.assertTrue(isinstance(my_safe_view(request), HttpResponseNotAllowed)) + self.assertIsInstance(my_safe_view(request), HttpResponseNotAllowed) # For testing method_decorator, a decorator that assumes a single argument. diff --git a/tests/defaultfilters/tests.py b/tests/defaultfilters/tests.py index 21734faf95..d0009c6e66 100644 --- a/tests/defaultfilters/tests.py +++ b/tests/defaultfilters/tests.py @@ -11,6 +11,8 @@ from django.utils import unittest, translation from django.utils.safestring import SafeData from django.utils.encoding import python_2_unicode_compatible +from i18n import TransRealMixin + class DefaultFiltersTests(TestCase): @@ -306,13 +308,13 @@ class DefaultFiltersTests(TestCase): self.assertEqual(urlize('(Go to http://www.example.com/foo.)'), '(Go to <a href="http://www.example.com/foo" rel="nofollow">http://www.example.com/foo</a>.)') - # Check urlize doesn't crash when square bracket is appended to url (#19070) + # Check urlize handles brackets properly (#19070) self.assertEqual(urlize('[see www.example.com]'), '[see <a href="http://www.example.com" rel="nofollow">www.example.com</a>]' ) - - # Check urlize doesn't crash when square bracket is prepended to url (#19070) self.assertEqual(urlize('see test[at[example.com'), 'see <a href="http://test[at[example.com" rel="nofollow">test[at[example.com</a>' ) + self.assertEqual(urlize('[http://168.192.0.1](http://168.192.0.1)'), + '[<a href="http://168.192.0.1](http://168.192.0.1)" rel="nofollow">http://168.192.0.1](http://168.192.0.1)</a>') # Check urlize works with IPv4/IPv6 addresses self.assertEqual(urlize('http://192.168.0.15/api/9'), @@ -360,7 +362,7 @@ class DefaultFiltersTests(TestCase): escaped = force_escape('<some html & special characters > here') self.assertEqual( escaped, '<some html & special characters > here') - self.assertTrue(isinstance(escaped, SafeData)) + self.assertIsInstance(escaped, SafeData) self.assertEqual( force_escape('<some html & special characters > here ĐÅ€£'), '<some html & special characters > here'\ @@ -527,24 +529,26 @@ class DefaultFiltersTests(TestCase): def test_timesince(self): # real testing is done in timesince.py, where we can provide our own 'now' + # NOTE: \xa0 avoids wrapping between value and unit self.assertEqual( timesince_filter(datetime.datetime.now() - datetime.timedelta(1)), - '1 day') + '1\xa0day') self.assertEqual( timesince_filter(datetime.datetime(2005, 12, 29), datetime.datetime(2005, 12, 30)), - '1 day') + '1\xa0day') def test_timeuntil(self): + # NOTE: \xa0 avoids wrapping between value and unit self.assertEqual( timeuntil_filter(datetime.datetime.now() + datetime.timedelta(1, 1)), - '1 day') + '1\xa0day') self.assertEqual( timeuntil_filter(datetime.datetime(2005, 12, 30), datetime.datetime(2005, 12, 29)), - '1 day') + '1\xa0day') def test_default(self): self.assertEqual(default("val", "default"), 'val') @@ -574,43 +578,23 @@ class DefaultFiltersTests(TestCase): 'get out of town') def test_filesizeformat(self): - self.assertEqual(filesizeformat(1023), '1023 bytes') - self.assertEqual(filesizeformat(1024), '1.0 KB') - self.assertEqual(filesizeformat(10*1024), '10.0 KB') - self.assertEqual(filesizeformat(1024*1024-1), '1024.0 KB') - self.assertEqual(filesizeformat(1024*1024), '1.0 MB') - self.assertEqual(filesizeformat(1024*1024*50), '50.0 MB') - self.assertEqual(filesizeformat(1024*1024*1024-1), '1024.0 MB') - self.assertEqual(filesizeformat(1024*1024*1024), '1.0 GB') - self.assertEqual(filesizeformat(1024*1024*1024*1024), '1.0 TB') - self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), '1.0 PB') + # NOTE: \xa0 avoids wrapping between value and unit + self.assertEqual(filesizeformat(1023), '1023\xa0bytes') + self.assertEqual(filesizeformat(1024), '1.0\xa0KB') + self.assertEqual(filesizeformat(10*1024), '10.0\xa0KB') + self.assertEqual(filesizeformat(1024*1024-1), '1024.0\xa0KB') + self.assertEqual(filesizeformat(1024*1024), '1.0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*50), '50.0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024-1), '1024.0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024), '1.0\xa0GB') + self.assertEqual(filesizeformat(1024*1024*1024*1024), '1.0\xa0TB') + self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), '1.0\xa0PB') self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000), - '2000.0 PB') - self.assertEqual(filesizeformat(complex(1,-1)), '0 bytes') - self.assertEqual(filesizeformat(""), '0 bytes') + '2000.0\xa0PB') + self.assertEqual(filesizeformat(complex(1,-1)), '0\xa0bytes') + self.assertEqual(filesizeformat(""), '0\xa0bytes') self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"), - '0 bytes') - - def test_localized_filesizeformat(self): - with self.settings(USE_L10N=True): - with translation.override('de', deactivate=True): - self.assertEqual(filesizeformat(1023), '1023 Bytes') - self.assertEqual(filesizeformat(1024), '1,0 KB') - self.assertEqual(filesizeformat(10*1024), '10,0 KB') - self.assertEqual(filesizeformat(1024*1024-1), '1024,0 KB') - self.assertEqual(filesizeformat(1024*1024), '1,0 MB') - self.assertEqual(filesizeformat(1024*1024*50), '50,0 MB') - self.assertEqual(filesizeformat(1024*1024*1024-1), '1024,0 MB') - self.assertEqual(filesizeformat(1024*1024*1024), '1,0 GB') - self.assertEqual(filesizeformat(1024*1024*1024*1024), '1,0 TB') - self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), - '1,0 PB') - self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000), - '2000,0 PB') - self.assertEqual(filesizeformat(complex(1,-1)), '0 Bytes') - self.assertEqual(filesizeformat(""), '0 Bytes') - self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"), - '0 Bytes') + '0\xa0bytes') def test_pluralize(self): self.assertEqual(pluralize(1), '') @@ -656,3 +640,27 @@ class DefaultFiltersTests(TestCase): self.assertEqual(removetags(123, 'a'), '123') self.assertEqual(striptags(123), '123') + +class DefaultFiltersI18NTests(TransRealMixin, TestCase): + + def test_localized_filesizeformat(self): + # NOTE: \xa0 avoids wrapping between value and unit + with self.settings(USE_L10N=True): + with translation.override('de', deactivate=True): + self.assertEqual(filesizeformat(1023), '1023\xa0Bytes') + self.assertEqual(filesizeformat(1024), '1,0\xa0KB') + self.assertEqual(filesizeformat(10*1024), '10,0\xa0KB') + self.assertEqual(filesizeformat(1024*1024-1), '1024,0\xa0KB') + self.assertEqual(filesizeformat(1024*1024), '1,0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*50), '50,0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024-1), '1024,0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024), '1,0\xa0GB') + self.assertEqual(filesizeformat(1024*1024*1024*1024), '1,0\xa0TB') + self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), + '1,0\xa0PB') + self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000), + '2000,0\xa0PB') + self.assertEqual(filesizeformat(complex(1,-1)), '0\xa0Bytes') + self.assertEqual(filesizeformat(""), '0\xa0Bytes') + self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"), + '0\xa0Bytes') diff --git a/tests/defer_regress/models.py b/tests/defer_regress/models.py index 7528e3b2c9..0170221cb9 100644 --- a/tests/defer_regress/models.py +++ b/tests/defer_regress/models.py @@ -62,3 +62,22 @@ class OneToOneItem(models.Model): class ItemAndSimpleItem(models.Model): item = models.ForeignKey(Item) simple = models.ForeignKey(SimpleItem) + +class Profile(models.Model): + profile1 = models.CharField(max_length=1000, default='profile1') + +class Location(models.Model): + location1 = models.CharField(max_length=1000, default='location1') + +class Item(models.Model): + pass + +class Request(models.Model): + profile = models.ForeignKey(Profile, null=True, blank=True) + location = models.ForeignKey(Location) + items = models.ManyToManyField(Item) + + request1 = models.CharField(default='request1', max_length=1000) + request2 = models.CharField(default='request2', max_length=1000) + request3 = models.CharField(default='request3', max_length=1000) + request4 = models.CharField(default='request4', max_length=1000) diff --git a/tests/defer_regress/tests.py b/tests/defer_regress/tests.py index d4d722035f..ad2546794c 100644 --- a/tests/defer_regress/tests.py +++ b/tests/defer_regress/tests.py @@ -8,8 +8,9 @@ from django.db.models import Count from django.db.models.loading import cache from django.test import TestCase -from .models import (ResolveThis, Item, RelatedItem, Child, Leaf, Proxy, - SimpleItem, Feature, ItemAndSimpleItem, OneToOneItem, SpecialFeature) +from .models import ( + ResolveThis, Item, RelatedItem, Child, Leaf, Proxy, SimpleItem, Feature, + ItemAndSimpleItem, OneToOneItem, SpecialFeature, Location, Request) class DeferRegressionTest(TestCase): @@ -76,7 +77,9 @@ class DeferRegressionTest(TestCase): self.assertEqual(results[0].child.name, "c1") self.assertEqual(results[0].second_child.name, "c2") - results = Leaf.objects.only("name", "child", "second_child", "child__name", "second_child__name").select_related() + results = Leaf.objects.only( + "name", "child", "second_child", "child__name", "second_child__name" + ).select_related() self.assertEqual(results[0].child.name, "c1") self.assertEqual(results[0].second_child.name, "c2") @@ -98,29 +101,36 @@ class DeferRegressionTest(TestCase): i2 = s["item"] self.assertFalse(i2._deferred) + # Regression for #16409 - make sure defer() and only() work with annotate() + self.assertIsInstance( + list(SimpleItem.objects.annotate(Count('feature')).defer('name')), + list) + self.assertIsInstance( + list(SimpleItem.objects.annotate(Count('feature')).only('name')), + list) + + def test_ticket_11936(self): # Regression for #11936 - loading.get_models should not return deferred # models by default. - klasses = sorted( - cache.get_models(cache.get_app("defer_regress")), - key=lambda klass: klass.__name__ - ) - self.assertEqual( - klasses, [ - Child, - Feature, - Item, - ItemAndSimpleItem, - Leaf, - OneToOneItem, - Proxy, - RelatedItem, - ResolveThis, - SimpleItem, - SpecialFeature, - ] + # Run a couple of defer queries so that app-cache must contain some + # deferred classes. It might contain a lot more classes depending on + # the order the tests are ran. + list(Item.objects.defer("name")) + list(Child.objects.defer("value")) + klasses = set( + map( + attrgetter("__name__"), + cache.get_models(cache.get_app("defer_regress")) + ) ) + self.assertIn("Child", klasses) + self.assertIn("Item", klasses) + self.assertNotIn("Child_Deferred_value", klasses) + self.assertNotIn("Item_Deferred_name", klasses) + self.assertFalse(any( + k._deferred for k in cache.get_models(cache.get_app("defer_regress")))) - klasses = sorted( + klasses_with_deferred = set( map( attrgetter("__name__"), cache.get_models( @@ -128,40 +138,22 @@ class DeferRegressionTest(TestCase): ), ) ) - # FIXME: This is dependent on the order in which tests are run -- - # this test case has to be the first, otherwise a LOT more classes - # appear. - self.assertEqual( - klasses, [ - "Child", - "Child_Deferred_value", - "Feature", - "Item", - "ItemAndSimpleItem", - "Item_Deferred_name", - "Item_Deferred_name_other_value_text", - "Item_Deferred_name_other_value_value", - "Item_Deferred_other_value_text_value", - "Item_Deferred_text_value", - "Leaf", - "Leaf_Deferred_child_id_second_child_id_value", - "Leaf_Deferred_name_value", - "Leaf_Deferred_second_child_id_value", - "Leaf_Deferred_value", - "OneToOneItem", - "Proxy", - "RelatedItem", - "RelatedItem_Deferred_", - "RelatedItem_Deferred_item_id", - "ResolveThis", - "SimpleItem", - "SpecialFeature", - ] + self.assertIn("Child", klasses_with_deferred) + self.assertIn("Item", klasses_with_deferred) + self.assertIn("Child_Deferred_value", klasses_with_deferred) + self.assertIn("Item_Deferred_name", klasses_with_deferred) + self.assertTrue(any( + k._deferred for k in cache.get_models( + cache.get_app("defer_regress"), include_deferred=True)) ) # Regression for #16409 - make sure defer() and only() work with annotate() - self.assertIsInstance(list(SimpleItem.objects.annotate(Count('feature')).defer('name')), list) - self.assertIsInstance(list(SimpleItem.objects.annotate(Count('feature')).only('name')), list) + self.assertIsInstance( + list(SimpleItem.objects.annotate(Count('feature')).defer('name')), + list) + self.assertIsInstance( + list(SimpleItem.objects.annotate(Count('feature')).only('name')), + list) def test_only_and_defer_usage_on_proxy_models(self): # Regression for #15790 - only() broken for proxy models @@ -179,7 +171,7 @@ class DeferRegressionTest(TestCase): self.assertEqual(dp.value, proxy.value, msg=msg) def test_resolve_columns(self): - rt = ResolveThis.objects.create(num=5.0, name='Foobar') + ResolveThis.objects.create(num=5.0, name='Foobar') qs = ResolveThis.objects.defer('num') self.assertEqual(1, qs.count()) self.assertEqual('Foobar', qs[0].name) @@ -215,7 +207,7 @@ class DeferRegressionTest(TestCase): item1 = Item.objects.create(name="first", value=47) item2 = Item.objects.create(name="second", value=42) simple = SimpleItem.objects.create(name="simple", value="23") - related = ItemAndSimpleItem.objects.create(item=item1, simple=simple) + ItemAndSimpleItem.objects.create(item=item1, simple=simple) obj = ItemAndSimpleItem.objects.defer('item').select_related('simple').get() self.assertEqual(obj.item, item1) @@ -242,7 +234,23 @@ class DeferRegressionTest(TestCase): def test_deferred_class_factory(self): from django.db.models.query_utils import deferred_class_factory - new_class = deferred_class_factory(Item, + new_class = deferred_class_factory( + Item, ('this_is_some_very_long_attribute_name_so_modelname_truncation_is_triggered',)) - self.assertEqual(new_class.__name__, + self.assertEqual( + new_class.__name__, 'Item_Deferred_this_is_some_very_long_attribute_nac34b1f495507dad6b02e2cb235c875e') + +class DeferAnnotateSelectRelatedTest(TestCase): + def test_defer_annotate_select_related(self): + location = Location.objects.create() + Request.objects.create(location=location) + self.assertIsInstance(list(Request.objects + .annotate(Count('items')).select_related('profile', 'location') + .only('profile', 'location')), list) + self.assertIsInstance(list(Request.objects + .annotate(Count('items')).select_related('profile', 'location') + .only('profile__profile1', 'location__location1')), list) + self.assertIsInstance(list(Request.objects + .annotate(Count('items')).select_related('profile', 'location') + .defer('request1', 'request2', 'request3', 'request4')), list) diff --git a/tests/dispatch/tests/test_dispatcher.py b/tests/dispatch/tests/test_dispatcher.py index 5f8f92acaf..a1d4c7e176 100644 --- a/tests/dispatch/tests/test_dispatcher.py +++ b/tests/dispatch/tests/test_dispatcher.py @@ -108,7 +108,7 @@ class DispatcherTests(unittest.TestCase): a_signal.connect(fails) result = a_signal.send_robust(sender=self, val="test") err = result[0][1] - self.assertTrue(isinstance(err, ValueError)) + self.assertIsInstance(err, ValueError) self.assertEqual(err.args, ('this',)) a_signal.disconnect(fails) self._testIsClean(a_signal) diff --git a/tests/field_defaults/tests.py b/tests/field_defaults/tests.py index 5d9b45610e..69dabb5168 100644 --- a/tests/field_defaults/tests.py +++ b/tests/field_defaults/tests.py @@ -14,6 +14,6 @@ class DefaultTests(TestCase): now = datetime.now() a.save() - self.assertTrue(isinstance(a.id, six.integer_types)) + self.assertIsInstance(a.id, six.integer_types) self.assertEqual(a.headline, "Default headline") self.assertTrue((now - a.pub_date).seconds < 5) diff --git a/tests/field_subclassing/tests.py b/tests/field_subclassing/tests.py index 9331ff2f3f..4945cff1bf 100644 --- a/tests/field_subclassing/tests.py +++ b/tests/field_subclassing/tests.py @@ -11,21 +11,21 @@ class CustomField(TestCase): def test_defer(self): d = DataModel.objects.create(data=[1, 2, 3]) - self.assertTrue(isinstance(d.data, list)) + self.assertIsInstance(d.data, list) d = DataModel.objects.get(pk=d.pk) - self.assertTrue(isinstance(d.data, list)) + self.assertIsInstance(d.data, list) self.assertEqual(d.data, [1, 2, 3]) d = DataModel.objects.defer("data").get(pk=d.pk) - self.assertTrue(isinstance(d.data, list)) + self.assertIsInstance(d.data, list) self.assertEqual(d.data, [1, 2, 3]) # Refetch for save d = DataModel.objects.defer("data").get(pk=d.pk) d.save() d = DataModel.objects.get(pk=d.pk) - self.assertTrue(isinstance(d.data, list)) + self.assertIsInstance(d.data, list) self.assertEqual(d.data, [1, 2, 3]) def test_custom_field(self): @@ -44,7 +44,7 @@ class CustomField(TestCase): # The data loads back from the database correctly and 'data' has the # right type. m1 = MyModel.objects.get(pk=m.pk) - self.assertTrue(isinstance(m1.data, Small)) + self.assertIsInstance(m1.data, Small) self.assertEqual(str(m1.data), "12") # We can do normal filtering on the custom field (and will get an error diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py index e4b71dba82..406a4d79b6 100644 --- a/tests/file_storage/tests.py +++ b/tests/file_storage/tests.py @@ -588,11 +588,11 @@ class ContentFileTestCase(unittest.TestCase): Test that ContentFile can accept both bytes and unicode and that the retrieved content is of the same type. """ - self.assertTrue(isinstance(ContentFile(b"content").read(), bytes)) + self.assertIsInstance(ContentFile(b"content").read(), bytes) if six.PY3: - self.assertTrue(isinstance(ContentFile("español").read(), six.text_type)) + self.assertIsInstance(ContentFile("español").read(), six.text_type) else: - self.assertTrue(isinstance(ContentFile("español").read(), bytes)) + self.assertIsInstance(ContentFile("español").read(), bytes) def test_content_saving(self): """ diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py index 103612198e..1b4d6eeeef 100644 --- a/tests/fixtures/tests.py +++ b/tests/fixtures/tests.py @@ -1,5 +1,7 @@ from __future__ import absolute_import +import warnings + from django.contrib.sites.models import Site from django.core import management from django.db import connection, IntegrityError @@ -25,14 +27,15 @@ class TestCaseFixtureLoadingTests(TestCase): class DumpDataAssertMixin(object): def _dumpdata_assert(self, args, output, format='json', natural_keys=False, - use_base_manager=False, exclude_list=[]): + use_base_manager=False, exclude_list=[], primary_keys=''): new_io = six.StringIO() management.call_command('dumpdata', *args, **{'format': format, 'stdout': new_io, 'stderr': new_io, 'use_natural_keys': natural_keys, 'use_base_manager': use_base_manager, - 'exclude': exclude_list}) + 'exclude': exclude_list, + 'primary_keys': primary_keys}) command_output = new_io.getvalue().strip() if format == "json": self.assertJSONEqual(command_output, output) @@ -137,8 +140,19 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): '<Book: Music for all ages by Artist formerly known as "Prince" and Django Reinhardt>' ]) - # Load a fixture that doesn't exist - management.call_command('loaddata', 'unknown.json', verbosity=0, commit=False) + # Loading a fixture that doesn't exist emits a warning + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + management.call_command('loaddata', 'unknown.json', verbosity=0, + commit=False) + self.assertEqual(len(w), 1) + self.assertTrue(w[0].message, "No fixture named 'unknown' found.") + + # An attempt to load a nonexistent 'initial_data' fixture isn't an error + with warnings.catch_warnings(record=True) as w: + management.call_command('loaddata', 'initial_data.json', verbosity=0, + commit=False) + self.assertEqual(len(w), 0) # object list is unaffected self.assertQuerysetEqual(Article.objects.all(), [ @@ -211,6 +225,45 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): # even those normally filtered by the manager self._dumpdata_assert(['fixtures.Spy'], '[{"pk": %d, "model": "fixtures.spy", "fields": {"cover_blown": true}}, {"pk": %d, "model": "fixtures.spy", "fields": {"cover_blown": false}}]' % (spy2.pk, spy1.pk), use_base_manager=True) + def test_dumpdata_with_pks(self): + management.call_command('loaddata', 'fixture1.json', verbosity=0, commit=False) + management.call_command('loaddata', 'fixture2.json', verbosity=0, commit=False) + self._dumpdata_assert( + ['fixtures.Article'], + '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + + self._dumpdata_assert( + ['fixtures.Article'], + '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}]', + primary_keys='2' + ) + + with six.assertRaisesRegex(self, management.CommandError, + "You can only use --pks option with one model"): + self._dumpdata_assert( + ['fixtures'], + '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + + with six.assertRaisesRegex(self, management.CommandError, + "You can only use --pks option with one model"): + self._dumpdata_assert( + '', + '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + + with six.assertRaisesRegex(self, management.CommandError, + "You can only use --pks option with one model"): + self._dumpdata_assert( + ['fixtures.Article', 'fixtures.category'], + '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + def test_compress_format_loading(self): # Load fixture 4 (compressed), using format specification management.call_command('loaddata', 'fixture4.json', verbosity=0, commit=False) @@ -273,10 +326,11 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): def test_unmatched_identifier_loading(self): # Try to load db fixture 3. This won't load because the database identifier doesn't match - management.call_command('loaddata', 'db_fixture_3', verbosity=0, commit=False) - self.assertQuerysetEqual(Article.objects.all(), []) + with warnings.catch_warnings(record=True): + management.call_command('loaddata', 'db_fixture_3', verbosity=0, commit=False) - management.call_command('loaddata', 'db_fixture_3', verbosity=0, using='default', commit=False) + with warnings.catch_warnings(record=True): + management.call_command('loaddata', 'db_fixture_3', verbosity=0, using='default', commit=False) self.assertQuerysetEqual(Article.objects.all(), []) def test_output_formats(self): diff --git a/tests/fixtures_model_package/tests.py b/tests/fixtures_model_package/tests.py index c250f647ce..e0a35e300d 100644 --- a/tests/fixtures_model_package/tests.py +++ b/tests/fixtures_model_package/tests.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import warnings + from django.core import management from django.db import transaction from django.test import TestCase, TransactionTestCase @@ -100,7 +102,12 @@ class FixtureTestCase(TestCase): ) # Load a fixture that doesn't exist - management.call_command("loaddata", "unknown.json", verbosity=0, commit=False) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + management.call_command("loaddata", "unknown.json", verbosity=0, commit=False) + self.assertEqual(len(w), 1) + self.assertTrue(w[0].message, "No fixture named 'unknown' found.") + self.assertQuerysetEqual( Article.objects.all(), [ "Django conquers world!", diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py index 02e923e386..52526ec338 100644 --- a/tests/fixtures_regress/tests.py +++ b/tests/fixtures_regress/tests.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, unicode_literals import os import re +import warnings from django.core.serializers.base import DeserializationError from django.core import management @@ -441,14 +442,15 @@ class TestFixtures(TestCase): def test_loaddata_not_existant_fixture_file(self): stdout_output = StringIO() - management.call_command( - 'loaddata', - 'this_fixture_doesnt_exist', - verbosity=2, - commit=False, - stdout=stdout_output, - ) - self.assertTrue("No xml fixture 'this_fixture_doesnt_exist' in" in + with warnings.catch_warnings(record=True): + management.call_command( + 'loaddata', + 'this_fixture_doesnt_exist', + verbosity=2, + commit=False, + stdout=stdout_output, + ) + self.assertTrue("No fixture 'this_fixture_doesnt_exist' in" in force_text(stdout_output.getvalue())) diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index 55dd6a0f47..69636ee49b 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -316,6 +316,11 @@ class MultiColumnFKTests(TestCase): list(Article.objects.filter(active_translation__abstract=None)), [a1, a2]) + def test_foreign_key_raises_informative_does_not_exist(self): + referrer = ArticleTranslation() + with self.assertRaisesMessage(Article.DoesNotExist, 'ArticleTranslation has no article'): + referrer.article + class FormsTests(TestCase): # ForeignObjects should not have any form fields, currently the user needs # to manually deal with the foreignobject relation. diff --git a/tests/forms_tests/tests/test_extra.py b/tests/forms_tests/tests/test_extra.py index a83cdfc05f..ea0f063c30 100644 --- a/tests/forms_tests/tests/test_extra.py +++ b/tests/forms_tests/tests/test_extra.py @@ -569,6 +569,14 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): f = GenericIPAddressField(unpack_ipv4=True) self.assertEqual(f.clean(' ::ffff:0a0a:0a0a'), '10.10.10.10') + def test_slugfield_normalization(self): + f = SlugField() + self.assertEqual(f.clean(' aa-bb-cc '), 'aa-bb-cc') + + def test_urlfield_normalization(self): + f = URLField() + self.assertEqual(f.clean('http://example.com/ '), 'http://example.com/') + def test_smart_text(self): class Test: if six.PY3: diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py index 7516de29b4..47c637befa 100644 --- a/tests/forms_tests/tests/test_fields.py +++ b/tests/forms_tests/tests/test_fields.py @@ -125,6 +125,15 @@ class FieldsTests(SimpleTestCase): self.assertEqual(f.max_length, None) self.assertEqual(f.min_length, 10) + def test_charfield_length_not_int(self): + """ + Ensure that setting min_length or max_length to something that is not a + number returns an exception. + """ + self.assertRaises(ValueError, CharField, min_length='a') + self.assertRaises(ValueError, CharField, max_length='a') + self.assertRaises(ValueError, CharField, 'a') + def test_charfield_widget_attrs(self): """ Ensure that CharField.widget_attrs() always returns a dictionary. diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 45e62a492c..1a3fb44a66 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -1398,7 +1398,7 @@ class FormsTestCase(TestCase): birthday = DateField() def add_prefix(self, field_name): - return self.prefix and '%s-prefix-%s' % (self.prefix, field_name) or field_name + return '%s-prefix-%s' % (self.prefix, field_name) if self.prefix else field_name p = Person(prefix='foo') self.assertHTMLEqual(p.as_ul(), """<li><label for="id_foo-prefix-first_name">First name:</label> <input type="text" name="foo-prefix-first_name" id="id_foo-prefix-first_name" /></li> @@ -1846,3 +1846,20 @@ class FormsTestCase(TestCase): self.assertHTMLEqual(boundfield.label_tag(), 'Field') self.assertHTMLEqual(boundfield.label_tag('Custom&'), 'Custom&') + + def test_boundfield_label_tag_custom_widget_id_for_label(self): + class CustomIdForLabelTextInput(TextInput): + def id_for_label(self, id): + return 'custom_' + id + + class EmptyIdForLabelTextInput(TextInput): + def id_for_label(self, id): + return None + + class SomeForm(Form): + custom = CharField(widget=CustomIdForLabelTextInput) + empty = CharField(widget=EmptyIdForLabelTextInput) + + form = SomeForm() + self.assertHTMLEqual(form['custom'].label_tag(), '<label for="custom_id_custom">Custom</label>') + self.assertHTMLEqual(form['empty'].label_tag(), '<label>Empty</label>') diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index 4ac3c5ecf1..1e9e7db30c 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import datetime + from django.forms import (CharField, DateField, FileField, Form, IntegerField, - ValidationError, formsets) + SplitDateTimeField, ValidationError, formsets) from django.forms.formsets import BaseFormSet, formset_factory from django.forms.util import ErrorList from django.test import TestCase @@ -45,6 +47,13 @@ FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm, formset=BaseFavoriteDrinksFormSet, extra=3) +# Used in ``test_formset_splitdatetimefield``. +class SplitDateTimeForm(Form): + when = SplitDateTimeField(initial=datetime.datetime.now) + +SplitDateTimeFormSet = formset_factory(SplitDateTimeForm) + + class FormsFormsetTestCase(TestCase): def test_basic_formset(self): # A FormSet constructor takes the same arguments as Form. Let's create a FormSet @@ -882,6 +891,19 @@ class FormsFormsetTestCase(TestCase): self.assertEqual(len(formset.forms), 0) self.assertTrue(formset) + def test_formset_splitdatetimefield(self): + """ + Formset should also work with SplitDateTimeField(initial=datetime.datetime.now). + Regression test for #18709. + """ + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-0-when_0': '1904-06-16', + 'form-0-when_1': '15:51:33', + } + formset = SplitDateTimeFormSet(data) + self.assertTrue(formset.is_valid()) def test_formset_error_class(self): # Regression tests for #16479 -- formsets form use ErrorList instead of supplied error_class @@ -972,6 +994,38 @@ class FormsFormsetTestCase(TestCase): finally: formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM + def test_non_form_errors_run_full_clean(self): + # Regression test for #11160 + # If non_form_errors() is called without calling is_valid() first, + # it should ensure that full_clean() is called. + class BaseCustomFormSet(BaseFormSet): + def clean(self): + raise ValidationError("This is a non-form error") + + ChoiceFormSet = formset_factory(Choice, formset=BaseCustomFormSet) + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertIsInstance(formset.non_form_errors(), ErrorList) + self.assertEqual(list(formset.non_form_errors()), + ['This is a non-form error']) + + def test_validate_max_ignores_forms_marked_for_deletion(self): + class CheckForm(Form): + field = IntegerField() + + data = { + 'check-TOTAL_FORMS': '2', + 'check-INITIAL_FORMS': '0', + 'check-MAX_NUM_FORMS': '1', + 'check-0-field': '200', + 'check-0-DELETE': '', + 'check-1-field': '50', + 'check-1-DELETE': 'on', + } + CheckFormSet = formset_factory(CheckForm, max_num=1, validate_max=True, + can_delete=True) + formset = CheckFormSet(data, prefix='check') + self.assertTrue(formset.is_valid()) + data = { 'choices-TOTAL_FORMS': '1', # the number of forms rendered diff --git a/tests/forms_tests/tests/test_regressions.py b/tests/forms_tests/tests/test_regressions.py index 74509a0f1a..ea138d32d5 100644 --- a/tests/forms_tests/tests/test_regressions.py +++ b/tests/forms_tests/tests/test_regressions.py @@ -8,9 +8,10 @@ from django.test import TestCase from django.utils.translation import ugettext_lazy, override from forms_tests.models import Cheese +from i18n import TransRealMixin -class FormsRegressionsTestCase(TestCase): +class FormsRegressionsTestCase(TransRealMixin, TestCase): def test_class(self): # Tests to prevent against recurrences of earlier bugs. extra_attrs = {'class': 'special'} diff --git a/tests/forms_tests/tests/tests.py b/tests/forms_tests/tests/tests.py index deda4822b8..2616ddaf7d 100644 --- a/tests/forms_tests/tests/tests.py +++ b/tests/forms_tests/tests/tests.py @@ -5,7 +5,7 @@ import datetime from django.core.files.uploadedfile import SimpleUploadedFile from django.db import models -from django.forms import Form, ModelForm, FileField, ModelChoiceField +from django.forms import Form, ModelForm, FileField, ModelChoiceField, CharField from django.forms.models import ModelFormMetaclass from django.test import TestCase from django.utils import six @@ -26,6 +26,14 @@ class OptionalMultiChoiceModelForm(ModelForm): fields = '__all__' +class ChoiceFieldExclusionForm(ModelForm): + multi_choice = CharField(max_length=50) + + class Meta: + exclude = ['multi_choice'] + model = ChoiceFieldModel + + class FileForm(Form): file1 = FileField() @@ -52,9 +60,9 @@ class TestTicket14567(TestCase): form = OptionalMultiChoiceModelForm({'multi_choice_optional': '', 'multi_choice': [option.pk]}) self.assertTrue(form.is_valid()) # Check that the empty value is a QuerySet - self.assertTrue(isinstance(form.cleaned_data['multi_choice_optional'], models.query.QuerySet)) + self.assertIsInstance(form.cleaned_data['multi_choice_optional'], models.query.QuerySet) # While we're at it, test whether a QuerySet is returned if there *is* a value. - self.assertTrue(isinstance(form.cleaned_data['multi_choice'], models.query.QuerySet)) + self.assertIsInstance(form.cleaned_data['multi_choice'], models.query.QuerySet) class ModelFormCallableModelDefault(TestCase): @@ -221,3 +229,31 @@ class RelatedModelFormTests(TestCase): model=A self.assertTrue(issubclass(ModelFormMetaclass(str('Form'), (ModelForm,), {'Meta': Meta}), ModelForm)) + + +class ManyToManyExclusionTestCase(TestCase): + def test_m2m_field_exclusion(self): + # Issue 12337. save_instance should honor the passed-in exclude keyword. + opt1 = ChoiceOptionModel.objects.create(id=1, name='default') + opt2 = ChoiceOptionModel.objects.create(id=2, name='option 2') + opt3 = ChoiceOptionModel.objects.create(id=3, name='option 3') + initial = { + 'choice': opt1, + 'choice_int': opt1, + } + data = { + 'choice': opt2.pk, + 'choice_int': opt2.pk, + 'multi_choice': 'string data!', + 'multi_choice_int': [opt1.pk], + } + instance = ChoiceFieldModel.objects.create(**initial) + instance.multi_choice = instance.multi_choice_int = [opt2, opt3] + form = ChoiceFieldExclusionForm(data=data, instance=instance) + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['multi_choice'], data['multi_choice']) + form.save() + self.assertEqual(form.instance.choice.pk, data['choice']) + self.assertEqual(form.instance.choice_int.pk, data['choice_int']) + self.assertEqual(list(form.instance.multi_choice.all()), [opt2, opt3]) + self.assertEqual([obj.pk for obj in form.instance.multi_choice_int.all()], data['multi_choice_int']) diff --git a/tests/generic_relations/models.py b/tests/generic_relations/models.py index 2acb982b9a..211df8aa3d 100644 --- a/tests/generic_relations/models.py +++ b/tests/generic_relations/models.py @@ -102,3 +102,22 @@ class Rock(Mineral): class ManualPK(models.Model): id = models.IntegerField(primary_key=True) tags = generic.GenericRelation(TaggedItem) + + +class ForProxyModelModel(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + obj = generic.GenericForeignKey(for_concrete_model=False) + title = models.CharField(max_length=255, null=True) + +class ForConcreteModelModel(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + obj = generic.GenericForeignKey() + +class ConcreteRelatedModel(models.Model): + bases = generic.GenericRelation(ForProxyModelModel, for_concrete_model=False) + +class ProxyRelatedModel(ConcreteRelatedModel): + class Meta: + proxy = True diff --git a/tests/generic_relations/tests.py b/tests/generic_relations/tests.py index 79c7bc6184..734b2e5143 100644 --- a/tests/generic_relations/tests.py +++ b/tests/generic_relations/tests.py @@ -6,7 +6,9 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase from .models import (TaggedItem, ValuableTaggedItem, Comparison, Animal, - Vegetable, Mineral, Gecko, Rock, ManualPK) + Vegetable, Mineral, Gecko, Rock, ManualPK, + ForProxyModelModel, ForConcreteModelModel, + ProxyRelatedModel, ConcreteRelatedModel) class GenericRelationsTests(TestCase): @@ -245,6 +247,22 @@ class GenericRelationsTests(TestCase): TaggedItem.objects.create(content_object=granite, tag="countertop") self.assertEqual(Rock.objects.filter(tags__tag="countertop").count(), 1) + def test_generic_inline_formsets_initial(self): + """ + Test for #17927 Initial values support for BaseGenericInlineFormSet. + """ + quartz = Mineral.objects.create(name="Quartz", hardness=7) + + GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1) + ctype = ContentType.objects.get_for_model(quartz) + initial_data = [{ + 'tag': 'lizard', + 'content_type': ctype.pk, + 'object_id': quartz.pk, + }] + formset = GenericFormSet(initial=initial_data) + self.assertEqual(formset.forms[0].initial, initial_data[0]) + class CustomWidget(forms.TextInput): pass @@ -256,12 +274,120 @@ class TaggedItemForm(forms.ModelForm): widgets = {'tag': CustomWidget} class GenericInlineFormsetTest(TestCase): - """ - Regression for #14572: Using base forms with widgets - defined in Meta should not raise errors. - """ - def test_generic_inlineformset_factory(self): + """ + Regression for #14572: Using base forms with widgets + defined in Meta should not raise errors. + """ Formset = generic_inlineformset_factory(TaggedItem, TaggedItemForm) form = Formset().forms[0] - self.assertTrue(isinstance(form['tag'].field.widget, CustomWidget)) + self.assertIsInstance(form['tag'].field.widget, CustomWidget) + + def test_save_new_for_proxy(self): + Formset = generic_inlineformset_factory(ForProxyModelModel, + fields='__all__', for_concrete_model=False) + + instance = ProxyRelatedModel.objects.create() + + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-title': 'foo', + } + + formset = Formset(data, instance=instance, prefix='form') + self.assertTrue(formset.is_valid()) + + new_obj, = formset.save() + self.assertEqual(new_obj.obj, instance) + + def test_save_new_for_concrete(self): + Formset = generic_inlineformset_factory(ForProxyModelModel, + fields='__all__', for_concrete_model=True) + + instance = ProxyRelatedModel.objects.create() + + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-title': 'foo', + } + + formset = Formset(data, instance=instance, prefix='form') + self.assertTrue(formset.is_valid()) + + new_obj, = formset.save() + self.assertNotIsInstance(new_obj.obj, ProxyRelatedModel) + + +class ProxyRelatedModelTest(TestCase): + def test_default_behavior(self): + """ + The default for for_concrete_model should be True + """ + base = ForConcreteModelModel() + base.obj = rel = ProxyRelatedModel.objects.create() + base.save() + + base = ForConcreteModelModel.objects.get(pk=base.pk) + rel = ConcreteRelatedModel.objects.get(pk=rel.pk) + self.assertEqual(base.obj, rel) + + def test_works_normally(self): + """ + When for_concrete_model is False, we should still be able to get + an instance of the concrete class. + """ + base = ForProxyModelModel() + base.obj = rel = ConcreteRelatedModel.objects.create() + base.save() + + base = ForProxyModelModel.objects.get(pk=base.pk) + self.assertEqual(base.obj, rel) + + def test_proxy_is_returned(self): + """ + Instances of the proxy should be returned when + for_concrete_model is False. + """ + base = ForProxyModelModel() + base.obj = ProxyRelatedModel.objects.create() + base.save() + + base = ForProxyModelModel.objects.get(pk=base.pk) + self.assertIsInstance(base.obj, ProxyRelatedModel) + + def test_query(self): + base = ForProxyModelModel() + base.obj = rel = ConcreteRelatedModel.objects.create() + base.save() + + self.assertEqual(rel, ConcreteRelatedModel.objects.get(bases__id=base.id)) + + def test_query_proxy(self): + base = ForProxyModelModel() + base.obj = rel = ProxyRelatedModel.objects.create() + base.save() + + self.assertEqual(rel, ProxyRelatedModel.objects.get(bases__id=base.id)) + + def test_generic_relation(self): + base = ForProxyModelModel() + base.obj = ProxyRelatedModel.objects.create() + base.save() + + base = ForProxyModelModel.objects.get(pk=base.pk) + rel = ProxyRelatedModel.objects.get(pk=base.obj.pk) + self.assertEqual(base, rel.bases.get()) + + def test_generic_relation_set(self): + base = ForProxyModelModel() + base.obj = ConcreteRelatedModel.objects.create() + base.save() + newrel = ConcreteRelatedModel.objects.create() + + newrel.bases = [base] + newrel = ConcreteRelatedModel.objects.get(pk=newrel.pk) + self.assertEqual(base, newrel.bases.get()) diff --git a/tests/generic_relations_regress/models.py b/tests/generic_relations_regress/models.py index 2795471f7f..d716f09058 100644 --- a/tests/generic_relations_regress/models.py +++ b/tests/generic_relations_regress/models.py @@ -122,3 +122,36 @@ class Tag(models.Model): class Board(models.Model): name = models.CharField(primary_key=True, max_length=15) + +class HasLinks(models.Model): + links = generic.GenericRelation(Link) + + class Meta: + abstract = True + +class HasLinkThing(HasLinks): + pass + +class A(models.Model): + flag = models.NullBooleanField() + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + +class B(models.Model): + a = generic.GenericRelation(A) + + class Meta: + ordering = ('id',) + +class C(models.Model): + b = models.ForeignKey(B) + + class Meta: + ordering = ('id',) + +class D(models.Model): + b = models.ForeignKey(B, null=True) + + class Meta: + ordering = ('id',) diff --git a/tests/generic_relations_regress/tests.py b/tests/generic_relations_regress/tests.py index 474113584a..75edff34f2 100644 --- a/tests/generic_relations_regress/tests.py +++ b/tests/generic_relations_regress/tests.py @@ -2,9 +2,10 @@ from django.db.models import Q from django.db.utils import IntegrityError from django.test import TestCase, skipIfDBFeature -from .models import (Address, Place, Restaurant, Link, CharLink, TextLink, +from .models import ( + Address, Place, Restaurant, Link, CharLink, TextLink, Person, Contact, Note, Organization, OddRelation1, OddRelation2, Company, - Developer, Team, Guild, Tag, Board) + Developer, Team, Guild, Tag, Board, HasLinkThing, A, B, C, D) class GenericRelationTests(TestCase): @@ -28,9 +29,9 @@ class GenericRelationTests(TestCase): originating model of a query. See #12664. """ p = Person.objects.create(account=23, name='Chef') - a = Address.objects.create(street='123 Anywhere Place', - city='Conifer', state='CO', - zipcode='80433', content_object=p) + Address.objects.create(street='123 Anywhere Place', + city='Conifer', state='CO', + zipcode='80433', content_object=p) qs = Person.objects.filter(addresses__zipcode='80433') self.assertEqual(1, qs.count()) @@ -38,12 +39,12 @@ class GenericRelationTests(TestCase): def test_charlink_delete(self): oddrel = OddRelation1.objects.create(name='clink') - cl = CharLink.objects.create(content_object=oddrel) + CharLink.objects.create(content_object=oddrel) oddrel.delete() def test_textlink_delete(self): oddrel = OddRelation2.objects.create(name='tlink') - tl = TextLink.objects.create(content_object=oddrel) + TextLink.objects.create(content_object=oddrel) oddrel.delete() def test_q_object_or(self): @@ -61,12 +62,12 @@ class GenericRelationTests(TestCase): """ note_contact = Contact.objects.create() org_contact = Contact.objects.create() - note = Note.objects.create(note='note', content_object=note_contact) + Note.objects.create(note='note', content_object=note_contact) org = Organization.objects.create(name='org name') org.contacts.add(org_contact) # search with a non-matching note and a matching org name qs = Contact.objects.filter(Q(notes__note__icontains=r'other note') | - Q(organizations__name__icontains=r'org name')) + Q(organizations__name__icontains=r'org name')) self.assertTrue(org_contact in qs) # search again, with the same query parameters, in reverse order qs = Contact.objects.filter( @@ -90,8 +91,8 @@ class GenericRelationTests(TestCase): p1 = Place.objects.create(name="South Park") p2 = Place.objects.create(name="The City") c = Company.objects.create(name="Chubby's Intl.") - l1 = Link.objects.create(content_object=p1) - l2 = Link.objects.create(content_object=c) + Link.objects.create(content_object=p1) + Link.objects.create(content_object=c) places = list(Place.objects.order_by('links__id')) def count_places(place): @@ -114,8 +115,10 @@ class GenericRelationTests(TestCase): try: note = Note(note='Deserve a bonus', content_object=team1) except Exception as e: - if issubclass(type(e), Exception) and str(e) == 'Impossible arguments to GFK.get_content_type!': - self.fail("Saving model with GenericForeignKey to model instance whose __len__ method returns 0 shouldn't fail.") + if (issubclass(type(e), Exception) and + str(e) == 'Impossible arguments to GFK.get_content_type!'): + self.fail("Saving model with GenericForeignKey to model instance whose " + "__len__ method returns 0 shouldn't fail.") raise e note.save() @@ -135,3 +138,77 @@ class GenericRelationTests(TestCase): b1 = Board.objects.create(name='') tag = Tag(label='VP', content_object=b1) tag.save() + + def test_ticket_20378(self): + hs1 = HasLinkThing.objects.create() + hs2 = HasLinkThing.objects.create() + l1 = Link.objects.create(content_object=hs1) + l2 = Link.objects.create(content_object=hs2) + self.assertQuerysetEqual( + HasLinkThing.objects.filter(links=l1), + [hs1], lambda x: x) + self.assertQuerysetEqual( + HasLinkThing.objects.filter(links=l2), + [hs2], lambda x: x) + self.assertQuerysetEqual( + HasLinkThing.objects.exclude(links=l2), + [hs1], lambda x: x) + self.assertQuerysetEqual( + HasLinkThing.objects.exclude(links=l1), + [hs2], lambda x: x) + + def test_ticket_20564(self): + b1 = B.objects.create() + b2 = B.objects.create() + b3 = B.objects.create() + c1 = C.objects.create(b=b1) + c2 = C.objects.create(b=b2) + c3 = C.objects.create(b=b3) + A.objects.create(flag=None, content_object=b1) + A.objects.create(flag=True, content_object=b2) + self.assertQuerysetEqual( + C.objects.filter(b__a__flag=None), + [c1, c3], lambda x: x + ) + self.assertQuerysetEqual( + C.objects.exclude(b__a__flag=None), + [c2], lambda x: x + ) + + def test_ticket_20564_nullable_fk(self): + b1 = B.objects.create() + b2 = B.objects.create() + b3 = B.objects.create() + d1 = D.objects.create(b=b1) + d2 = D.objects.create(b=b2) + d3 = D.objects.create(b=b3) + d4 = D.objects.create() + A.objects.create(flag=None, content_object=b1) + A.objects.create(flag=True, content_object=b1) + A.objects.create(flag=True, content_object=b2) + self.assertQuerysetEqual( + D.objects.exclude(b__a__flag=None), + [d2], lambda x: x + ) + self.assertQuerysetEqual( + D.objects.filter(b__a__flag=None), + [d1, d3, d4], lambda x: x + ) + self.assertQuerysetEqual( + B.objects.filter(a__flag=None), + [b1, b3], lambda x: x + ) + self.assertQuerysetEqual( + B.objects.exclude(a__flag=None), + [b2], lambda x: x + ) + + def test_extra_join_condition(self): + # A crude check that content_type_id is taken in account in the + # join/subquery condition. + self.assertIn("content_type_id", str(B.objects.exclude(a__flag=None).query).lower()) + # No need for any joins - the join from inner query can be trimmed in + # this case (but not in the above case as no a objects at all for given + # B would then fail). + self.assertNotIn(" join ", str(B.objects.exclude(a__flag=True).query).lower()) + self.assertIn("content_type_id", str(B.objects.exclude(a__flag=True).query).lower()) diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py index 0e84e17132..ffd9b1b480 100644 --- a/tests/generic_views/test_base.py +++ b/tests/generic_views/test_base.py @@ -278,7 +278,7 @@ class TemplateViewTest(TestCase): response = self.client.get('/template/simple/bar/') self.assertEqual(response.status_code, 200) self.assertEqual(response.context['foo'], 'bar') - self.assertTrue(isinstance(response.context['view'], View)) + self.assertIsInstance(response.context['view'], View) def test_extra_template_params(self): """ @@ -288,7 +288,7 @@ class TemplateViewTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.context['foo'], 'bar') self.assertEqual(response.context['key'], 'value') - self.assertTrue(isinstance(response.context['view'], View)) + self.assertIsInstance(response.context['view'], View) def test_cached_views(self): """ @@ -384,6 +384,12 @@ class RedirectViewTest(unittest.TestCase): self.assertEqual(response.status_code, 301) self.assertEqual(response.url, '/bar/') + def test_redirect_PATCH(self): + "Default is a permanent redirect" + response = RedirectView.as_view(url='/bar/')(self.rf.patch('/foo/')) + self.assertEqual(response.status_code, 301) + self.assertEqual(response.url, '/bar/') + def test_redirect_DELETE(self): "Default is a permanent redirect" response = RedirectView.as_view(url='/bar/')(self.rf.delete('/foo/')) @@ -411,3 +417,36 @@ class GetContextDataTest(unittest.TestCase): # test that kwarg overrides values assigned higher up context = test_view.get_context_data(test_name='test_value') self.assertEqual(context['test_name'], 'test_value') + + def test_object_at_custom_name_in_context_data(self): + # Checks 'pony' key presence in dict returned by get_context_date + test_view = views.CustomSingleObjectView() + test_view.context_object_name = 'pony' + context = test_view.get_context_data() + self.assertEqual(context['pony'], test_view.object) + + def test_object_in_get_context_data(self): + # Checks 'object' key presence in dict returned by get_context_date #20234 + test_view = views.CustomSingleObjectView() + context = test_view.get_context_data() + self.assertEqual(context['object'], test_view.object) + + +class UseMultipleObjectMixinTest(unittest.TestCase): + rf = RequestFactory() + + def test_use_queryset_from_view(self): + test_view = views.CustomMultipleObjectMixinView() + test_view.get(self.rf.get('/')) + # Don't pass queryset as argument + context = test_view.get_context_data() + self.assertEqual(context['object_list'], test_view.queryset) + + def test_overwrite_queryset(self): + test_view = views.CustomMultipleObjectMixinView() + test_view.get(self.rf.get('/')) + queryset = [{'name': 'Lennon'}, {'name': 'Ono'}] + self.assertNotEqual(test_view.queryset, queryset) + # Overwrite the view's queryset with queryset from kwarg + context = test_view.get_context_data(object_list=queryset) + self.assertEqual(context['object_list'], queryset) diff --git a/tests/generic_views/test_detail.py b/tests/generic_views/test_detail.py index faee05688e..3a97d27995 100644 --- a/tests/generic_views/test_detail.py +++ b/tests/generic_views/test_detail.py @@ -15,7 +15,7 @@ class DetailViewTest(TestCase): res = self.client.get('/detail/obj/') self.assertEqual(res.status_code, 200) self.assertEqual(res.context['object'], {'foo': 'bar'}) - self.assertTrue(isinstance(res.context['view'], View)) + self.assertIsInstance(res.context['view'], View) self.assertTemplateUsed(res, 'generic_views/detail.html') def test_detail_by_pk(self): diff --git a/tests/generic_views/test_edit.py b/tests/generic_views/test_edit.py index 54eab7ffa4..435e48ba99 100644 --- a/tests/generic_views/test_edit.py +++ b/tests/generic_views/test_edit.py @@ -43,8 +43,8 @@ class CreateViewTests(TestCase): def test_create(self): res = self.client.get('/edit/authors/create/') self.assertEqual(res.status_code, 200) - self.assertTrue(isinstance(res.context['form'], forms.ModelForm)) - self.assertTrue(isinstance(res.context['view'], View)) + self.assertIsInstance(res.context['form'], forms.ModelForm) + self.assertIsInstance(res.context['view'], View) self.assertFalse('object' in res.context) self.assertFalse('author' in res.context) self.assertTemplateUsed(res, 'generic_views/author_form.html') @@ -89,7 +89,7 @@ class CreateViewTests(TestCase): def test_create_with_special_properties(self): res = self.client.get('/edit/authors/create/special/') self.assertEqual(res.status_code, 200) - self.assertTrue(isinstance(res.context['form'], views.AuthorForm)) + self.assertIsInstance(res.context['form'], views.AuthorForm) self.assertFalse('object' in res.context) self.assertFalse('author' in res.context) self.assertTemplateUsed(res, 'generic_views/form.html') @@ -165,7 +165,7 @@ class UpdateViewTests(TestCase): ) res = self.client.get('/edit/author/%d/update/' % a.pk) self.assertEqual(res.status_code, 200) - self.assertTrue(isinstance(res.context['form'], forms.ModelForm)) + self.assertIsInstance(res.context['form'], forms.ModelForm) self.assertEqual(res.context['object'], Author.objects.get(pk=a.pk)) self.assertEqual(res.context['author'], Author.objects.get(pk=a.pk)) self.assertTemplateUsed(res, 'generic_views/author_form.html') @@ -247,7 +247,7 @@ class UpdateViewTests(TestCase): ) res = self.client.get('/edit/author/%d/update/special/' % a.pk) self.assertEqual(res.status_code, 200) - self.assertTrue(isinstance(res.context['form'], views.AuthorForm)) + self.assertIsInstance(res.context['form'], views.AuthorForm) self.assertEqual(res.context['object'], Author.objects.get(pk=a.pk)) self.assertEqual(res.context['thingy'], Author.objects.get(pk=a.pk)) self.assertFalse('author' in res.context) @@ -279,8 +279,8 @@ class UpdateViewTests(TestCase): ) res = self.client.get('/edit/author/update/') self.assertEqual(res.status_code, 200) - self.assertTrue(isinstance(res.context['form'], forms.ModelForm)) - self.assertTrue(isinstance(res.context['view'], View)) + self.assertIsInstance(res.context['form'], forms.ModelForm) + self.assertIsInstance(res.context['view'], View) self.assertEqual(res.context['object'], Author.objects.get(pk=a.pk)) self.assertEqual(res.context['author'], Author.objects.get(pk=a.pk)) self.assertTemplateUsed(res, 'generic_views/author_form.html') diff --git a/tests/generic_views/test_list.py b/tests/generic_views/test_list.py index cc4d2f5966..a77a6418a3 100644 --- a/tests/generic_views/test_list.py +++ b/tests/generic_views/test_list.py @@ -24,7 +24,7 @@ class ListViewTests(TestCase): self.assertEqual(res.status_code, 200) self.assertTemplateUsed(res, 'generic_views/author_list.html') self.assertEqual(list(res.context['object_list']), list(Author.objects.all())) - self.assertTrue(isinstance(res.context['view'], View)) + self.assertIsInstance(res.context['view'], View) self.assertIs(res.context['author_list'], res.context['object_list']) self.assertIsNone(res.context['paginator']) self.assertIsNone(res.context['page_obj']) diff --git a/tests/generic_views/views.py b/tests/generic_views/views.py index aa8777e8c6..fd331f14b7 100644 --- a/tests/generic_views/views.py +++ b/tests/generic_views/views.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from django.contrib.auth.decorators import login_required -from django.contrib.messages.views import SuccessMessageMixin from django.core.paginator import Paginator from django.core.urlresolvers import reverse, reverse_lazy from django.utils.decorators import method_decorator @@ -201,6 +200,17 @@ class BookDetailGetObjectCustomQueryset(BookDetail): return super(BookDetailGetObjectCustomQueryset,self).get_object( queryset=Book.objects.filter(pk=2)) + +class CustomMultipleObjectMixinView(generic.list.MultipleObjectMixin, generic.View): + queryset = [ + {'name': 'John'}, + {'name': 'Yoko'}, + ] + + def get(self, request): + self.object_list = self.get_queryset() + + class CustomContextView(generic.detail.SingleObjectMixin, generic.View): model = Book object = Book(name='dummy') @@ -216,6 +226,10 @@ class CustomContextView(generic.detail.SingleObjectMixin, generic.View): def get_context_object_name(self, obj): return "test_name" +class CustomSingleObjectView(generic.detail.SingleObjectMixin, generic.View): + model = Book + object = Book(name="dummy") + class BookSigningConfig(object): model = BookSigning date_field = 'event_date' diff --git a/tests/get_earliest_or_latest/tests.py b/tests/get_earliest_or_latest/tests.py index 6317a0974c..8d16af9587 100644 --- a/tests/get_earliest_or_latest/tests.py +++ b/tests/get_earliest_or_latest/tests.py @@ -121,3 +121,34 @@ class EarliestOrLatestTests(TestCase): p2 = Person.objects.create(name="Stephanie", birthday=datetime(1960, 2, 3)) self.assertRaises(AssertionError, Person.objects.latest) self.assertEqual(Person.objects.latest("birthday"), p2) + + def test_first(self): + p1 = Person.objects.create(name="Bob", birthday=datetime(1950, 1, 1)) + p2 = Person.objects.create(name="Alice", birthday=datetime(1961, 2, 3)) + self.assertEqual( + Person.objects.first(), p1) + self.assertEqual( + Person.objects.order_by('name').first(), p2) + self.assertEqual( + Person.objects.filter(birthday__lte=datetime(1955, 1, 1)).first(), + p1) + self.assertIs( + Person.objects.filter(birthday__lte=datetime(1940, 1, 1)).first(), + None) + + def test_last(self): + p1 = Person.objects.create( + name="Alice", birthday=datetime(1950, 1, 1)) + p2 = Person.objects.create( + name="Bob", birthday=datetime(1960, 2, 3)) + # Note: by default PK ordering. + self.assertEqual( + Person.objects.last(), p2) + self.assertEqual( + Person.objects.order_by('-name').last(), p1) + self.assertEqual( + Person.objects.filter(birthday__lte=datetime(1955, 1, 1)).last(), + p1) + self.assertIs( + Person.objects.filter(birthday__lte=datetime(1940, 1, 1)).last(), + None) diff --git a/tests/get_or_create/models.py b/tests/get_or_create/models.py index 82905de4f8..1a85de2e74 100644 --- a/tests/get_or_create/models.py +++ b/tests/get_or_create/models.py @@ -21,6 +21,11 @@ class Person(models.Model): def __str__(self): return '%s %s' % (self.first_name, self.last_name) + +class DefaultPerson(models.Model): + first_name = models.CharField(max_length=100, default="Anonymous") + + class ManualPrimaryKeyTest(models.Model): id = models.IntegerField(primary_key=True) data = models.CharField(max_length=100) @@ -28,3 +33,12 @@ class ManualPrimaryKeyTest(models.Model): class Profile(models.Model): person = models.ForeignKey(Person, primary_key=True) + + +class Tag(models.Model): + text = models.CharField(max_length=255, unique=True) + + +class Thing(models.Model): + name = models.CharField(max_length=256) + tags = models.ManyToManyField(Tag) diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py index e9cce9bbde..36c248b169 100644 --- a/tests/get_or_create/tests.py +++ b/tests/get_or_create/tests.py @@ -2,14 +2,17 @@ from __future__ import absolute_import from datetime import date import traceback +import warnings -from django.db import IntegrityError +from django.db import IntegrityError, DatabaseError +from django.utils.encoding import DjangoUnicodeDecodeError from django.test import TestCase, TransactionTestCase -from .models import Person, ManualPrimaryKeyTest, Profile +from .models import DefaultPerson, Person, ManualPrimaryKeyTest, Profile, Tag, Thing class GetOrCreateTests(TestCase): + def test_get_or_create(self): p = Person.objects.create( first_name='John', last_name='Lennon', birthday=date(1940, 10, 9) @@ -64,6 +67,30 @@ class GetOrCreateTests(TestCase): formatted_traceback = traceback.format_exc() self.assertIn('obj.save', formatted_traceback) + def test_savepoint_rollback(self): + # Regression test for #20463: the database connection should still be + # usable after a DataError or ProgrammingError in .get_or_create(). + try: + # Hide warnings when broken data is saved with a warning (MySQL). + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + Person.objects.get_or_create( + birthday=date(1970, 1, 1), + defaults={'first_name': b"\xff", 'last_name': b"\xff"}) + except (DatabaseError, DjangoUnicodeDecodeError): + Person.objects.create( + first_name="Bob", last_name="Ross", birthday=date(1950, 1, 1)) + else: + self.skipTest("This backend accepts broken utf-8.") + + def test_get_or_create_empty(self): + # Regression test for #16137: get_or_create does not require kwargs. + try: + DefaultPerson.objects.get_or_create() + except AssertionError: + self.fail("If all the attributes on a model have defaults, we " + "shouldn't need to pass any arguments.") + class GetOrCreateTransactionTests(TransactionTestCase): @@ -77,3 +104,28 @@ class GetOrCreateTransactionTests(TransactionTestCase): pass else: self.skipTest("This backend does not support integrity checks.") + + +class GetOrCreateThroughManyToMany(TestCase): + + def test_get_get_or_create(self): + tag = Tag.objects.create(text='foo') + a_thing = Thing.objects.create(name='a') + a_thing.tags.add(tag) + obj, created = a_thing.tags.get_or_create(text='foo') + + self.assertFalse(created) + self.assertEqual(obj.pk, tag.pk) + + def test_create_get_or_create(self): + a_thing = Thing.objects.create(name='a') + obj, created = a_thing.tags.get_or_create(text='foo') + + self.assertTrue(created) + self.assertEqual(obj.text, 'foo') + self.assertIn(obj, a_thing.tags.all()) + + def test_something(self): + Tag.objects.create(text='foo') + a_thing = Thing.objects.create(name='a') + self.assertRaises(IntegrityError, a_thing.tags.get_or_create, text='foo') diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py index 3680eecdd2..397b63647a 100644 --- a/tests/handlers/tests.py +++ b/tests/handlers/tests.py @@ -61,6 +61,7 @@ class TransactionsPerRequestTests(TransactionTestCase): connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests self.assertContains(response, 'False') + class SignalsTests(TestCase): urls = 'handlers.urls' @@ -89,3 +90,11 @@ class SignalsTests(TestCase): self.assertEqual(self.signals, ['started']) self.assertEqual(b''.join(response.streaming_content), b"streaming content") self.assertEqual(self.signals, ['started', 'finished']) + + +class HandlerSuspiciousOpsTest(TestCase): + urls = 'handlers.urls' + + def test_suspiciousop_in_view_returns_400(self): + response = self.client.get('/suspicious/') + self.assertEqual(response.status_code, 400) diff --git a/tests/handlers/urls.py b/tests/handlers/urls.py index 29858055ab..eaf764b00b 100644 --- a/tests/handlers/urls.py +++ b/tests/handlers/urls.py @@ -9,4 +9,5 @@ urlpatterns = patterns('', url(r'^streaming/$', views.streaming), url(r'^in_transaction/$', views.in_transaction), url(r'^not_in_transaction/$', views.not_in_transaction), + url(r'^suspicious/$', views.suspicious), ) diff --git a/tests/handlers/views.py b/tests/handlers/views.py index 22d9ea4c7d..1b75b27043 100644 --- a/tests/handlers/views.py +++ b/tests/handlers/views.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals -from django.db import connection +from django.core.exceptions import SuspiciousOperation +from django.db import connection, transaction from django.http import HttpResponse, StreamingHttpResponse def regular(request): @@ -12,6 +13,9 @@ def streaming(request): def in_transaction(request): return HttpResponse(str(connection.in_atomic_block)) +@transaction.non_atomic_requests def not_in_transaction(request): return HttpResponse(str(connection.in_atomic_block)) -not_in_transaction.transactions_per_request = False + +def suspicious(request): + raise SuspiciousOperation('dubious') diff --git a/tests/http_utils/tests.py b/tests/http_utils/tests.py index 7dfd24d721..9f99c94da7 100644 --- a/tests/http_utils/tests.py +++ b/tests/http_utils/tests.py @@ -1,10 +1,24 @@ from __future__ import unicode_literals +import io +import gzip + from django.http import HttpRequest, HttpResponse, StreamingHttpResponse from django.http.utils import conditional_content_removal from django.test import TestCase +# based on Python 3.3's gzip.compress +def gzip_compress(data): + buf = io.BytesIO() + f = gzip.GzipFile(fileobj=buf, mode='wb', compresslevel=0) + try: + f.write(data) + finally: + f.close() + return buf.getvalue() + + class HttpUtilTests(TestCase): def test_conditional_content_removal(self): @@ -33,6 +47,19 @@ class HttpUtilTests(TestCase): conditional_content_removal(req, res) self.assertEqual(b''.join(res), b'') + # Issue #20472 + abc = gzip_compress(b'abc') + res = HttpResponse(abc, status=304) + res['Content-Encoding'] = 'gzip' + conditional_content_removal(req, res) + self.assertEqual(res.content, b'') + + res = StreamingHttpResponse([abc], status=304) + res['Content-Encoding'] = 'gzip' + conditional_content_removal(req, res) + self.assertEqual(b''.join(res), b'') + + # Strip content for HEAD requests. req.method = 'HEAD' diff --git a/tests/i18n/__init__.py b/tests/i18n/__init__.py index e69de29bb2..c5aaa31fe3 100644 --- a/tests/i18n/__init__.py +++ b/tests/i18n/__init__.py @@ -0,0 +1,18 @@ +from threading import local + + +class TransRealMixin(object): + """This is the only way to reset the translation machinery. Otherwise + the test suite occasionally fails because of global state pollution + between tests.""" + def flush_caches(self): + from django.utils.translation import trans_real + trans_real._translations = {} + trans_real._active = local() + trans_real._default = None + trans_real._accepted = {} + trans_real._checked_languages = {} + + def tearDown(self): + self.flush_caches() + super(TransRealMixin, self).tearDown() diff --git a/tests/i18n/commands/extraction.py b/tests/i18n/commands/extraction.py index 7c482e58fb..8696ae453b 100644 --- a/tests/i18n/commands/extraction.py +++ b/tests/i18n/commands/extraction.py @@ -279,17 +279,22 @@ class IgnoredExtractorTests(ExtractorTests): def test_ignore_option(self): os.chdir(self.test_dir) - pattern1 = os.path.join('ignore_dir', '*') + ignore_patterns = [ + os.path.join('ignore_dir', '*'), + 'xxx_*', + ] stdout = StringIO() management.call_command('makemessages', locale=LOCALE, verbosity=2, - ignore_patterns=[pattern1], stdout=stdout) + ignore_patterns=ignore_patterns, stdout=stdout) data = stdout.getvalue() self.assertTrue("ignoring directory ignore_dir" in data) + self.assertTrue("ignoring file xxx_ignored.html" in data) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: po_contents = fp.read() self.assertMsgId('This literal should be included.', po_contents) self.assertNotMsgId('This should be ignored.', po_contents) + self.assertNotMsgId('This should be ignored too.', po_contents) class SymlinkExtractorTests(ExtractorTests): diff --git a/tests/i18n/commands/templates/xxx_ignored.html b/tests/i18n/commands/templates/xxx_ignored.html new file mode 100644 index 0000000000..a41cbe202a --- /dev/null +++ b/tests/i18n/commands/templates/xxx_ignored.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% trans "This should be ignored too." %} diff --git a/tests/i18n/commands/tests.py b/tests/i18n/commands/tests.py deleted file mode 100644 index f9e3c20fff..0000000000 --- a/tests/i18n/commands/tests.py +++ /dev/null @@ -1,24 +0,0 @@ -import os -import re -from subprocess import Popen, PIPE - -from django.core.management.utils import find_command - -can_run_extraction_tests = False -can_run_compilation_tests = False - -# checks if it can find xgettext on the PATH and -# imports the extraction tests if yes -xgettext_cmd = find_command('xgettext') -if xgettext_cmd: - p = Popen('%s --version' % xgettext_cmd, shell=True, stdout=PIPE, stderr=PIPE, close_fds=os.name != 'nt', universal_newlines=True) - output = p.communicate()[0] - match = re.search(r'(?P<major>\d+)\.(?P<minor>\d+)', output) - if match: - xversion = (int(match.group('major')), int(match.group('minor'))) - if xversion >= (0, 15): - can_run_extraction_tests = True - del p - -if find_command('msgfmt'): - can_run_compilation_tests = True diff --git a/tests/i18n/contenttypes/tests.py b/tests/i18n/contenttypes/tests.py index 5e8a9823e1..cbac9ec5da 100644 --- a/tests/i18n/contenttypes/tests.py +++ b/tests/i18n/contenttypes/tests.py @@ -10,6 +10,8 @@ from django.utils._os import upath from django.utils import six from django.utils import translation +from i18n import TransRealMixin + @override_settings( USE_I18N=True, @@ -22,7 +24,7 @@ from django.utils import translation ('fr', 'French'), ), ) -class ContentTypeTests(TestCase): +class ContentTypeTests(TransRealMixin, TestCase): def test_verbose_name(self): company_type = ContentType.objects.get(app_label='i18n', model='company') with translation.override('en'): diff --git a/tests/i18n/patterns/tests.py b/tests/i18n/patterns/tests.py index d334e4fe2d..85eb7db084 100644 --- a/tests/i18n/patterns/tests.py +++ b/tests/i18n/patterns/tests.py @@ -52,8 +52,10 @@ class URLPrefixTests(URLTestCaseBase): def test_not_prefixed(self): with translation.override('en'): self.assertEqual(reverse('not-prefixed'), '/not-prefixed/') + self.assertEqual(reverse('not-prefixed-included-url'), '/not-prefixed-include/foo/') with translation.override('nl'): self.assertEqual(reverse('not-prefixed'), '/not-prefixed/') + self.assertEqual(reverse('not-prefixed-included-url'), '/not-prefixed-include/foo/') def test_prefixed(self): with translation.override('en'): @@ -183,7 +185,7 @@ class URLRedirectTests(URLTestCaseBase): class URLVaryAcceptLanguageTests(URLTestCaseBase): """ Tests that 'Accept-Language' is not added to the Vary header when using - prefixed URLs. + prefixed URLs. """ def test_no_prefix_response(self): response = self.client.get('/not-prefixed/') diff --git a/tests/i18n/patterns/urls/default.py b/tests/i18n/patterns/urls/default.py index 0edaea48ad..ff52e26227 100644 --- a/tests/i18n/patterns/urls/default.py +++ b/tests/i18n/patterns/urls/default.py @@ -8,6 +8,7 @@ view = TemplateView.as_view(template_name='dummy.html') urlpatterns = patterns('', url(r'^not-prefixed/$', view, name='not-prefixed'), + url(r'^not-prefixed-include/', include('i18n.patterns.urls.included')), url(_(r'^translated/$'), view, name='no-prefix-translated'), url(_(r'^translated/(?P<slug>[\w-]+)/$'), view, name='no-prefix-translated-slug'), ) diff --git a/tests/i18n/patterns/urls/included.py b/tests/i18n/patterns/urls/included.py new file mode 100644 index 0000000000..3f3d1325dd --- /dev/null +++ b/tests/i18n/patterns/urls/included.py @@ -0,0 +1,9 @@ +from django.conf.urls import patterns, url +from django.views.generic import TemplateView + + +view = TemplateView.as_view(template_name='dummy.html') + +urlpatterns = patterns('', + url(r'^foo/$', view, name='not-prefixed-included-url'), +) diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 1022c8d2f1..019ed88cdb 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -8,6 +8,7 @@ import pickle from threading import local from django.conf import settings +from django.core.management.utils import find_command from django.template import Template, Context from django.template.base import TemplateSyntaxError from django.test import TestCase, RequestFactory @@ -30,19 +31,20 @@ from django.utils.translation import (activate, deactivate, ngettext, ngettext_lazy, ungettext, ungettext_lazy, pgettext, pgettext_lazy, - npgettext, npgettext_lazy) + npgettext, npgettext_lazy, + check_for_language) -from .commands.tests import can_run_extraction_tests, can_run_compilation_tests -if can_run_extraction_tests: +if find_command('xgettext'): from .commands.extraction import (ExtractorTests, BasicExtractorTests, JavascriptExtractorTests, IgnoredExtractorTests, SymlinkExtractorTests, CopyPluralFormsExtractorTests, NoWrapExtractorTests, NoLocationExtractorTests, KeepPotFileExtractorTests, MultipleLocaleExtractionTests) -if can_run_compilation_tests: +if find_command('msgfmt'): from .commands.compilation import (PoFileTests, PoFileContentsTests, PercentRenderingTests, MultipleLocaleCompilationTests, CompilationErrorHandling) +from . import TransRealMixin from .forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm from .models import Company, TestModel @@ -52,7 +54,8 @@ extended_locale_paths = settings.LOCALE_PATHS + ( os.path.join(here, 'other', 'locale'), ) -class TranslationTests(TestCase): + +class TranslationTests(TransRealMixin, TestCase): def test_override(self): activate('de') @@ -333,10 +336,43 @@ class TranslationTests(TestCase): self.assertEqual(rendered, 'My other name is James.') +class TranslationThreadSafetyTests(TestCase): + """Specifically not using TransRealMixin here to test threading.""" + + def setUp(self): + self._old_language = get_language() + self._translations = trans_real._translations + + # here we rely on .split() being called inside the _fetch() + # in trans_real.translation() + class sideeffect_str(str): + def split(self, *args, **kwargs): + res = str.split(self, *args, **kwargs) + trans_real._translations['en-YY'] = None + return res + + trans_real._translations = {sideeffect_str('en-XX'): None} + + def tearDown(self): + trans_real._translations = self._translations + activate(self._old_language) + + def test_bug14894_translation_activate_thread_safety(self): + translation_count = len(trans_real._translations) + try: + translation.activate('pl') + except RuntimeError: + self.fail('translation.activate() is not thread-safe') + + # make sure sideeffect_str actually added a new translation + self.assertLess(translation_count, len(trans_real._translations)) + + @override_settings(USE_L10N=True) -class FormattingTests(TestCase): +class FormattingTests(TransRealMixin, TestCase): def setUp(self): + super(FormattingTests, self).setUp() self.n = decimal.Decimal('66666.666') self.f = 99999.999 self.d = datetime.date(2009, 12, 31) @@ -738,9 +774,10 @@ class FormattingTests(TestCase): self.assertEqual(template2.render(context), output2) self.assertEqual(template3.render(context), output3) -class MiscTests(TestCase): +class MiscTests(TransRealMixin, TestCase): def setUp(self): + super(MiscTests, self).setUp() self.rf = RequestFactory() def test_parse_spec_http_header(self): @@ -884,17 +921,15 @@ class MiscTests(TestCase): self.assertEqual(t_plur.render(Context({'percent': 42, 'num': 4})), '%(percent)s% represents 4 objects') -class ResolutionOrderI18NTests(TestCase): +class ResolutionOrderI18NTests(TransRealMixin, TestCase): def setUp(self): - # Okay, this is brutal, but we have no other choice to fully reset - # the translation framework - trans_real._active = local() - trans_real._translations = {} + super(ResolutionOrderI18NTests, self).setUp() activate('de') def tearDown(self): deactivate() + super(ResolutionOrderI18NTests, self).tearDown() def assertUgettext(self, msgid, msgstr): result = ugettext(msgid) @@ -967,15 +1002,17 @@ class TestLanguageInfo(TestCase): six.assertRaisesRegex(self, KeyError, r"Unknown language code xx-xx and xx\.", get_language_info, 'xx-xx') -class MultipleLocaleActivationTests(TestCase): +class MultipleLocaleActivationTests(TransRealMixin, TestCase): """ Tests for template rendering behavior when multiple locales are activated during the lifetime of the same process. """ def setUp(self): + super(MultipleLocaleActivationTests, self).setUp() self._old_language = get_language() def tearDown(self): + super(MultipleLocaleActivationTests, self).tearDown() activate(self._old_language) def test_single_locale_activation(self): @@ -1104,7 +1141,7 @@ class MultipleLocaleActivationTests(TestCase): 'django.middleware.common.CommonMiddleware', ), ) -class LocaleMiddlewareTests(TestCase): +class LocaleMiddlewareTests(TransRealMixin, TestCase): urls = 'i18n.urls' @@ -1114,3 +1151,86 @@ class LocaleMiddlewareTests(TestCase): self.assertContains(response, "Oui/Non") response = self.client.get('/en/streaming/') self.assertContains(response, "Yes/No") + + @override_settings( + MIDDLEWARE_CLASSES=( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + ), + ) + def test_session_language(self): + """ + Check that language is stored in session if missing. + """ + # Create an empty session + engine = import_module(settings.SESSION_ENGINE) + session = engine.SessionStore() + session.save() + self.client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key + + # Clear the session data before request + session.save() + response = self.client.get('/en/simple/') + self.assertEqual(self.client.session['django_language'], 'en') + + # Clear the session data before request + session.save() + response = self.client.get('/fr/simple/') + self.assertEqual(self.client.session['django_language'], 'fr') + + # Check that language is not changed in session + response = self.client.get('/en/simple/') + self.assertEqual(self.client.session['django_language'], 'fr') + + +@override_settings( + USE_I18N=True, + LANGUAGES=( + ('bg', 'Bulgarian'), + ('en-us', 'English'), + ('pt-br', 'Portugese (Brazil)'), + ), + MIDDLEWARE_CLASSES=( + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + ), +) +class CountrySpecificLanguageTests(TransRealMixin, TestCase): + + urls = 'i18n.urls' + + def setUp(self): + super(CountrySpecificLanguageTests, self).setUp() + self.rf = RequestFactory() + + def test_check_for_language(self): + self.assertTrue(check_for_language('en')) + self.assertTrue(check_for_language('en-us')) + self.assertTrue(check_for_language('en-US')) + + def test_get_language_from_request(self): + # issue 19919 + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.8,bg;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('en-us', lang) + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'bg-bg,en-US;q=0.8,en;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('bg', lang) + + def test_specific_language_codes(self): + # issue 11915 + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt,en-US;q=0.8,en;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('pt-br', lang) + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-pt,en-US;q=0.8,en;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('pt-br', lang) diff --git a/tests/i18n/urls.py b/tests/i18n/urls.py index c118265dab..c7d01897f5 100644 --- a/tests/i18n/urls.py +++ b/tests/i18n/urls.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals from django.conf.urls.i18n import i18n_patterns -from django.http import StreamingHttpResponse +from django.http import HttpResponse, StreamingHttpResponse from django.utils.translation import ugettext_lazy as _ urlpatterns = i18n_patterns('', + (r'^simple/$', lambda r: HttpResponse()), (r'^streaming/$', lambda r: StreamingHttpResponse([_("Yes"), "/", _("No")])), ) diff --git a/tests/invalid_models/invalid_models/models.py b/tests/invalid_models/invalid_models/models.py index 3c21e1ddb8..e1bac9cfb2 100644 --- a/tests/invalid_models/invalid_models/models.py +++ b/tests/invalid_models/invalid_models/models.py @@ -25,6 +25,7 @@ class FieldErrors(models.Model): 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): @@ -375,11 +376,12 @@ invalid_models.fielderrors: "decimalfield3": DecimalFields require a "max_digits 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: "filefield": FileFields require an "upload_to" 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-tuples. -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. +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 related m2m field 'Target.clash1_set'. 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'. diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py index 27ae651042..0c2d269464 100644 --- a/tests/logging_tests/tests.py +++ b/tests/logging_tests/tests.py @@ -8,9 +8,10 @@ import warnings from django.conf import LazySettings from django.core import mail from django.test import TestCase, RequestFactory -from django.test.utils import override_settings +from django.test.utils import override_settings, patch_logger from django.utils.encoding import force_text -from django.utils.log import CallbackFilter, RequireDebugFalse, RequireDebugTrue +from django.utils.log import (CallbackFilter, RequireDebugFalse, + RequireDebugTrue) from django.utils.six import StringIO from django.utils.unittest import skipUnless @@ -293,7 +294,7 @@ class AdminEmailHandlerTest(TestCase): def my_mail_admins(*args, **kwargs): connection = kwargs['connection'] - self.assertTrue(isinstance(connection, MyEmailBackend)) + self.assertIsInstance(connection, MyEmailBackend) mail_admins_called['called'] = True # Monkeypatches @@ -354,3 +355,22 @@ class SettingsConfigureLogging(TestCase): settings.configure( LOGGING_CONFIG='logging_tests.tests.dictConfig') self.assertTrue(dictConfig.called) + + +class SecurityLoggerTest(TestCase): + + urls = 'logging_tests.urls' + + def test_suspicious_operation_creates_log_message(self): + with self.settings(DEBUG=True): + with patch_logger('django.security.SuspiciousOperation', 'error') as calls: + response = self.client.get('/suspicious/') + self.assertEqual(len(calls), 1) + self.assertEqual(calls[0], 'dubious') + + def test_suspicious_operation_uses_sublogger(self): + with self.settings(DEBUG=True): + with patch_logger('django.security.DisallowedHost', 'error') as calls: + response = self.client.get('/suspicious_spec/') + self.assertEqual(len(calls), 1) + self.assertEqual(calls[0], 'dubious') diff --git a/tests/logging_tests/urls.py b/tests/logging_tests/urls.py new file mode 100644 index 0000000000..c738bd565c --- /dev/null +++ b/tests/logging_tests/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals + +from django.conf.urls import patterns, url + +from . import views + +urlpatterns = patterns('', + url(r'^suspicious/$', views.suspicious), + url(r'^suspicious_spec/$', views.suspicious_spec), +) diff --git a/tests/logging_tests/views.py b/tests/logging_tests/views.py new file mode 100644 index 0000000000..c685bcc005 --- /dev/null +++ b/tests/logging_tests/views.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from django.core.exceptions import SuspiciousOperation, DisallowedHost + + +def suspicious(request): + raise SuspiciousOperation('dubious') + + +def suspicious_spec(request): + raise DisallowedHost('dubious') diff --git a/tests/mail/tests.py b/tests/mail/tests.py index 0a843db9e7..c90dc7e22a 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -251,16 +251,16 @@ class MailTests(TestCase): def test_backend_arg(self): """Test backend argument of mail.get_connection()""" - self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.smtp.EmailBackend'), smtp.EmailBackend)) - self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.locmem.EmailBackend'), locmem.EmailBackend)) - self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.dummy.EmailBackend'), dummy.EmailBackend)) - self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.console.EmailBackend'), console.EmailBackend)) + self.assertIsInstance(mail.get_connection('django.core.mail.backends.smtp.EmailBackend'), smtp.EmailBackend) + self.assertIsInstance(mail.get_connection('django.core.mail.backends.locmem.EmailBackend'), locmem.EmailBackend) + self.assertIsInstance(mail.get_connection('django.core.mail.backends.dummy.EmailBackend'), dummy.EmailBackend) + self.assertIsInstance(mail.get_connection('django.core.mail.backends.console.EmailBackend'), console.EmailBackend) tmp_dir = tempfile.mkdtemp() try: - self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.filebased.EmailBackend', file_path=tmp_dir), filebased.EmailBackend)) + self.assertIsInstance(mail.get_connection('django.core.mail.backends.filebased.EmailBackend', file_path=tmp_dir), filebased.EmailBackend) finally: shutil.rmtree(tmp_dir) - self.assertTrue(isinstance(mail.get_connection(), locmem.EmailBackend)) + self.assertIsInstance(mail.get_connection(), locmem.EmailBackend) @override_settings( EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend', diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index f2f7f4df66..265eb97c36 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -18,13 +18,11 @@ from django.middleware.http import ConditionalGetMiddleware from django.middleware.gzip import GZipMiddleware from django.middleware.transaction import TransactionMiddleware from django.test import TransactionTestCase, TestCase, RequestFactory -from django.test.utils import override_settings +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin from django.utils import six from django.utils.encoding import force_str from django.utils.six.moves import xrange -from django.utils.unittest import expectedFailure - -from transactions.tests import IgnorePendingDeprecationWarningsMixin +from django.utils.unittest import expectedFailure, skipIf from .models import Band @@ -320,6 +318,33 @@ class BrokenLinkEmailsMiddlewareTest(TestCase): BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) self.assertEqual(len(mail.outbox), 0) + @skipIf(six.PY3, "HTTP_REFERER is str type on Python 3") + def test_404_error_nonascii_referrer(self): + # Such referer strings should not happen, but anyway, if it happens, + # let's not crash + self.req.META['HTTP_REFERER'] = b'http://testserver/c/\xd0\xbb\xd0\xb8/' + BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 1) + + def test_custom_request_checker(self): + class SubclassedMiddleware(BrokenLinkEmailsMiddleware): + ignored_user_agent_patterns = (re.compile(r'Spider.*'), + re.compile(r'Robot.*')) + def is_ignorable_request(self, request, uri, domain, referer): + '''Check user-agent in addition to normal checks.''' + if super(SubclassedMiddleware, self).is_ignorable_request(request, uri, domain, referer): + return True + user_agent = request.META['HTTP_USER_AGENT'] + return any(pattern.search(user_agent) for pattern in + self.ignored_user_agent_patterns) + + self.req.META['HTTP_REFERER'] = '/another/url/' + self.req.META['HTTP_USER_AGENT'] = 'Spider machine 3.4' + SubclassedMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 0) + self.req.META['HTTP_USER_AGENT'] = 'My user agent' + SubclassedMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 1) class ConditionalGetMiddlewareTest(TestCase): urls = 'middleware.cond_get_urls' diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py index e1e38d0ec7..ccff8b8cfa 100644 --- a/tests/model_fields/tests.py +++ b/tests/model_fields/tests.py @@ -193,28 +193,28 @@ class BooleanFieldTests(unittest.TestCase): b.bfield = True b.save() b2 = BooleanModel.objects.get(pk=b.pk) - self.assertTrue(isinstance(b2.bfield, bool)) + self.assertIsInstance(b2.bfield, bool) self.assertEqual(b2.bfield, True) b3 = BooleanModel() b3.bfield = False b3.save() b4 = BooleanModel.objects.get(pk=b3.pk) - self.assertTrue(isinstance(b4.bfield, bool)) + self.assertIsInstance(b4.bfield, bool) self.assertEqual(b4.bfield, False) b = NullBooleanModel() b.nbfield = True b.save() b2 = NullBooleanModel.objects.get(pk=b.pk) - self.assertTrue(isinstance(b2.nbfield, bool)) + self.assertIsInstance(b2.nbfield, bool) self.assertEqual(b2.nbfield, True) b3 = NullBooleanModel() b3.nbfield = False b3.save() b4 = NullBooleanModel.objects.get(pk=b3.pk) - self.assertTrue(isinstance(b4.nbfield, bool)) + self.assertIsInstance(b4.nbfield, bool) self.assertEqual(b4.nbfield, False) # http://code.djangoproject.com/ticket/13293 @@ -371,11 +371,11 @@ class BigIntegerFieldTests(test.TestCase): def test_types(self): b = BigInt(value = 0) - self.assertTrue(isinstance(b.value, six.integer_types)) + self.assertIsInstance(b.value, six.integer_types) b.save() - self.assertTrue(isinstance(b.value, six.integer_types)) + self.assertIsInstance(b.value, six.integer_types) b = BigInt.objects.all()[0] - self.assertTrue(isinstance(b.value, six.integer_types)) + self.assertIsInstance(b.value, six.integer_types) def test_coercing(self): BigInt.objects.create(value ='10') diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index a79d9b8c5b..610dc34001 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -207,7 +207,17 @@ class Post(models.Model): posted = models.DateField() def __str__(self): - return self.name + return self.title + +@python_2_unicode_compatible +class DateTimePost(models.Model): + title = models.CharField(max_length=50, unique_for_date='posted', blank=True) + slug = models.CharField(max_length=50, unique_for_year='posted', blank=True) + subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True) + posted = models.DateTimeField(editable=False) + + def __str__(self): + return self.title class DerivedPost(Post): pass @@ -255,3 +265,7 @@ class Colour(models.Model): class ColourfulItem(models.Model): name = models.CharField(max_length=50) colours = models.ManyToManyField(Colour) + +class ArticleStatusNote(models.Model): + name = models.CharField(max_length=20) + status = models.ManyToManyField(ArticleStatus) diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index c5db011404..58dde13a8a 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -24,7 +24,7 @@ from .models import (Article, ArticleStatus, BetterAuthor, BigInt, DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle, ImprovedArticleWithParentLink, Inventory, Post, Price, Product, TextFile, AuthorProfile, Colour, ColourfulItem, - test_images) + ArticleStatusNote, DateTimePost, test_images) if test_images: from .models import ImageFile, OptionalImageFile @@ -76,6 +76,12 @@ class PostForm(forms.ModelForm): fields = '__all__' +class DateTimePostForm(forms.ModelForm): + class Meta: + model = DateTimePost + fields = '__all__' + + class DerivedPostForm(forms.ModelForm): class Meta: model = DerivedPost @@ -234,6 +240,20 @@ class ColourfulItemForm(forms.ModelForm): model = ColourfulItem fields = '__all__' +# model forms for testing work on #9321: + +class StatusNoteForm(forms.ModelForm): + class Meta: + model = ArticleStatusNote + fields = '__all__' + + +class StatusNoteCBM2mForm(forms.ModelForm): + class Meta: + model = ArticleStatusNote + fields = '__all__' + widgets = {'status': forms.CheckboxSelectMultiple} + class ModelFormBaseTest(TestCase): def test_base_form(self): @@ -274,8 +294,8 @@ class ModelFormBaseTest(TestCase): model = Category fields = '__all__' - self.assertTrue(isinstance(ReplaceField.base_fields['url'], - forms.fields.BooleanField)) + self.assertIsInstance(ReplaceField.base_fields['url'], + forms.fields.BooleanField) def test_replace_field_variant_2(self): # Should have the same result as before, @@ -287,8 +307,8 @@ class ModelFormBaseTest(TestCase): model = Category fields = ['url'] - self.assertTrue(isinstance(ReplaceField.base_fields['url'], - forms.fields.BooleanField)) + self.assertIsInstance(ReplaceField.base_fields['url'], + forms.fields.BooleanField) def test_replace_field_variant_3(self): # Should have the same result as before, @@ -300,8 +320,8 @@ class ModelFormBaseTest(TestCase): model = Category fields = [] # url will still appear, since it is explicit above - self.assertTrue(isinstance(ReplaceField.base_fields['url'], - forms.fields.BooleanField)) + self.assertIsInstance(ReplaceField.base_fields['url'], + forms.fields.BooleanField) def test_override_field(self): class AuthorForm(forms.ModelForm): @@ -668,6 +688,23 @@ class UniqueTest(TestCase): self.assertEqual(len(form.errors), 1) self.assertEqual(form.errors['posted'], ['This field is required.']) + def test_unique_for_date_in_exclude(self): + """If the date for unique_for_* constraints is excluded from the + ModelForm (in this case 'posted' has editable=False, then the + constraint should be ignored.""" + p = DateTimePost.objects.create(title="Django 1.0 is released", + slug="Django 1.0", subtitle="Finally", + posted=datetime.datetime(2008, 9, 3, 10, 10, 1)) + # 'title' has unique_for_date='posted' + form = DateTimePostForm({'title': "Django 1.0 is released", 'posted': '2008-09-03'}) + self.assertTrue(form.is_valid()) + # 'slug' has unique_for_year='posted' + form = DateTimePostForm({'slug': "Django 1.0", 'posted': '2008-01-01'}) + self.assertTrue(form.is_valid()) + # 'subtitle' has unique_for_month='posted' + form = DateTimePostForm({'subtitle': "Finally", 'posted': '2008-09-30'}) + self.assertTrue(form.is_valid()) + def test_inherited_unique_for_date(self): p = Post.objects.create(title="Django 1.0 is released", slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) @@ -1677,3 +1714,22 @@ class OldFormForXTests(TestCase): <option value="%(blue_pk)s">Blue</option> </select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>""" % {'blue_pk': colour.pk}) + + +class M2mHelpTextTest(TestCase): + """Tests for ticket #9321.""" + def test_multiple_widgets(self): + """Help text of different widgets for ManyToManyFields model fields""" + dreaded_help_text = '<span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span>' + + # Default widget (SelectMultiple): + std_form = StatusNoteForm() + self.assertInHTML(dreaded_help_text, std_form.as_p()) + + # Overridden widget (CheckboxSelectMultiple, a subclass of + # SelectMultiple but with a UI that doesn't involve Control/Command + # keystrokes to extend selection): + form = StatusNoteCBM2mForm() + html = form.as_p() + self.assertInHTML('<ul id="id_status">', html) + self.assertInHTML(dreaded_help_text, html, count=0) diff --git a/tests/model_forms_regress/tests.py b/tests/model_forms_regress/tests.py index 0e033e033f..80900a59e0 100644 --- a/tests/model_forms_regress/tests.py +++ b/tests/model_forms_regress/tests.py @@ -92,6 +92,41 @@ class OverrideCleanTests(TestCase): self.assertEqual(form.instance.left, 1) + +class PartiallyLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = ('left', 'right',) + + +class FullyLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = "__all__" + +class LocalizedModelFormTest(TestCase): + def test_model_form_applies_localize_to_some_fields(self): + f = PartiallyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) + self.assertTrue(f.is_valid()) + self.assertTrue(f.fields['left'].localize) + self.assertFalse(f.fields['middle'].localize) + self.assertTrue(f.fields['right'].localize) + + def test_model_form_applies_localize_to_all_fields(self): + f = FullyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) + self.assertTrue(f.is_valid()) + self.assertTrue(f.fields['left'].localize) + self.assertTrue(f.fields['middle'].localize) + self.assertTrue(f.fields['right'].localize) + + def test_model_form_refuses_arbitrary_string(self): + with self.assertRaises(TypeError): + class BrokenLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = "foo" + + # Regression test for #12960. # Make sure the cleaned_data returned from ModelForm.clean() is applied to the # model instance. diff --git a/tests/model_formsets_regress/tests.py b/tests/model_formsets_regress/tests.py index 38ebd9d24b..179f79fbcb 100644 --- a/tests/model_formsets_regress/tests.py +++ b/tests/model_formsets_regress/tests.py @@ -236,11 +236,11 @@ class FormsetTests(TestCase): formset = Formset(data) # check if the returned error classes are correct # note: formset.errors returns a list as documented - self.assertTrue(isinstance(formset.errors, list)) - self.assertTrue(isinstance(formset.non_form_errors(), ErrorList)) + self.assertIsInstance(formset.errors, list) + self.assertIsInstance(formset.non_form_errors(), ErrorList) for form in formset.forms: - self.assertTrue(isinstance(form.errors, ErrorDict)) - self.assertTrue(isinstance(form.non_field_errors(), ErrorList)) + self.assertIsInstance(form.errors, ErrorDict) + self.assertIsInstance(form.non_field_errors(), ErrorList) def test_initial_data(self): User.objects.create(username="bibi", serial=1) @@ -273,6 +273,7 @@ class UserSiteForm(forms.ModelForm): 'id': CustomWidget, 'data': CustomWidget, } + localized_fields = ('data',) class Callback(object): @@ -295,21 +296,25 @@ class FormfieldCallbackTests(TestCase): def test_inlineformset_factory_default(self): Formset = inlineformset_factory(User, UserSite, form=UserSiteForm, fields="__all__") form = Formset().forms[0] - self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) - self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) + self.assertIsInstance(form['id'].field.widget, CustomWidget) + self.assertIsInstance(form['data'].field.widget, CustomWidget) + self.assertFalse(form.fields['id'].localize) + self.assertTrue(form.fields['data'].localize) def test_modelformset_factory_default(self): Formset = modelformset_factory(UserSite, form=UserSiteForm) form = Formset().forms[0] - self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) - self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) + self.assertIsInstance(form['id'].field.widget, CustomWidget) + self.assertIsInstance(form['data'].field.widget, CustomWidget) + self.assertFalse(form.fields['id'].localize) + self.assertTrue(form.fields['data'].localize) def assertCallbackCalled(self, callback): id_field, user_field, data_field = UserSite._meta.fields expected_log = [ (id_field, {'widget': CustomWidget}), (user_field, {}), - (data_field, {'widget': CustomWidget}), + (data_field, {'widget': CustomWidget, 'localize': True}), ] self.assertEqual(callback.log, expected_log) diff --git a/tests/model_validation/__init__.py b/tests/model_validation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/model_validation/__init__.py diff --git a/tests/model_validation/models.py b/tests/model_validation/models.py new file mode 100644 index 0000000000..9a2a5f7cd0 --- /dev/null +++ b/tests/model_validation/models.py @@ -0,0 +1,27 @@ +from django.db import models + + +class ThingItem(object): + + def __init__(self, value, display): + self.value = value + self.display = display + + def __iter__(self): + return (x for x in [self.value, self.display]) + + def __len__(self): + return 2 + + +class Things(object): + + def __iter__(self): + return (x for x in [ThingItem(1, 2), ThingItem(3, 4)]) + + +class ThingWithIterableChoices(models.Model): + + # Testing choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + thing = models.CharField(max_length=100, blank=True, choices=Things()) diff --git a/tests/model_validation/tests.py b/tests/model_validation/tests.py new file mode 100644 index 0000000000..ffd0d89412 --- /dev/null +++ b/tests/model_validation/tests.py @@ -0,0 +1,13 @@ +from django.core import management +from django.test import TestCase +from django.utils import six + + +class ModelValidationTest(TestCase): + + def test_models_validate(self): + # All our models should validate properly + # Validation Tests: + # * choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + management.call_command("validate", stdout=six.StringIO()) diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 0d933bc1f9..805b57c070 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -7,7 +7,6 @@ from django.conf import settings from django.contrib.admin.options import (ModelAdmin, TabularInline, HORIZONTAL, VERTICAL) from django.contrib.admin.sites import AdminSite -from django.contrib.admin.validation import validate from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect from django.contrib.admin import (SimpleListFilter, BooleanFieldListFilter) @@ -65,6 +64,30 @@ class ModelAdminTests(TestCase): self.assertEqual(ma.get_fieldsets(request, self.band), [(None, {'fields': ['name', 'bio', 'sign_date']})]) + def test_get_fieldsets(self): + # Test that get_fieldsets is called when figuring out form fields. + # Refs #18681. + + class BandAdmin(ModelAdmin): + def get_fieldsets(self, request, obj=None): + return [(None, {'fields': ['name', 'bio']})] + + ma = BandAdmin(Band, self.site) + form = ma.get_form(None) + self.assertEqual(form._meta.fields, ['name', 'bio']) + + class InlineBandAdmin(TabularInline): + model = Concert + fk_name = 'main_band' + can_delete = False + + def get_fieldsets(self, request, obj=None): + return [(None, {'fields': ['day', 'transport']})] + + ma = InlineBandAdmin(Band, self.site) + form = ma.get_formset(None).form + self.assertEqual(form._meta.fields, ['day', 'transport']) + def test_field_arguments(self): # If we specify the fields argument, fieldsets_add and fielsets_change should # just stick the fields into a formsets structure and return it. @@ -523,8 +546,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.raw_id_fields' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -534,8 +556,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.raw_id_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -545,15 +566,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.raw_id_fields\[0\]', 'name' must be either a ForeignKey or ManyToManyField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): raw_id_fields = ('users',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_fieldsets_validation(self): @@ -563,8 +583,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -574,8 +593,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets\[0\]' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -585,8 +603,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets\[0\]' does not have exactly two elements.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -596,8 +613,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets\[0\]\[1\]' must be a dictionary.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -607,15 +623,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'fields' key is required in ValidationTestModelAdmin.fieldsets\[0\]\[1\] field options dict.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): fieldsets = (("General", {"fields": ("name",)}),) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) class ValidationTestModelAdmin(ModelAdmin): fieldsets = (("General", {"fields": ("name",)}),) @@ -624,8 +639,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "Both fieldsets and fields are specified in ValidationTestModelAdmin.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -635,8 +649,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "There are duplicate field\(s\) in ValidationTestModelAdmin.fieldsets", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -646,8 +659,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "There are duplicate field\(s\) in ValidationTestModelAdmin.fields", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -662,8 +674,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "ValidationTestModelAdmin.form does not inherit from BaseModelForm.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -676,7 +687,7 @@ class ValidationTests(unittest.TestCase): }), ) - validate(BandAdmin, Band) + BandAdmin.validate(Band) class AdminBandForm(forms.ModelForm): delete = forms.BooleanField() @@ -690,7 +701,7 @@ class ValidationTests(unittest.TestCase): }), ) - validate(BandAdmin, Band) + BandAdmin.validate(Band) def test_filter_vertical_validation(self): @@ -700,8 +711,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_vertical' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -711,8 +721,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_vertical' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -722,15 +731,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_vertical\[0\]' must be a ManyToManyField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): filter_vertical = ("users",) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_filter_horizontal_validation(self): @@ -740,8 +748,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_horizontal' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -751,8 +758,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_horizontal' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -762,15 +768,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_horizontal\[0\]' must be a ManyToManyField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): filter_horizontal = ("users",) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_radio_fields_validation(self): @@ -780,8 +785,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields' must be a dictionary.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -791,8 +795,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -802,8 +805,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields\['name'\]' is neither an instance of ForeignKey nor does have choices set.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -813,15 +815,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields\['state'\]' is neither admin.HORIZONTAL nor admin.VERTICAL.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): radio_fields = {"state": VERTICAL} - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_prepopulated_fields_validation(self): @@ -831,8 +832,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields' must be a dictionary.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -842,8 +842,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -853,8 +852,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields\['slug'\]\[0\]' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -864,15 +862,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields\['users'\]' is either a DateTimeField, ForeignKey or ManyToManyField. This isn't allowed.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): prepopulated_fields = {"slug": ("name",)} - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_display_validation(self): @@ -882,8 +879,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -893,8 +889,7 @@ class ValidationTests(unittest.TestCase): 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'."), - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -904,8 +899,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display\[0\]', 'users' is a ManyToManyField which is not supported.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -917,7 +911,7 @@ class ValidationTests(unittest.TestCase): pass list_display = ('name', 'decade_published_in', 'a_method', a_callable) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_display_links_validation(self): @@ -927,8 +921,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display_links' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -938,8 +931,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display_links\[0\]' refers to 'non_existent_field' which is not defined in 'list_display'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -949,8 +941,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display_links\[0\]' refers to 'name' which is not defined in 'list_display'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -963,7 +954,7 @@ 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) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_filter_validation(self): @@ -973,8 +964,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -984,8 +974,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]' refers to 'non_existent_field' which does not refer to a Field.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -998,8 +987,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]' is 'RandomClass' which is not a descendant of ListFilter.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1009,8 +997,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'RandomClass' which is not of type FieldListFilter.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1028,8 +1015,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'AwesomeFilter' which is not of type FieldListFilter.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1039,8 +1025,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]' is 'BooleanFieldListFilter' which is of type FieldListFilter but is not associated with a field name.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1049,7 +1034,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter), 'no') - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_per_page_validation(self): @@ -1058,16 +1043,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.list_per_page' should be a integer.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.list_per_page' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): list_per_page = 100 - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_max_show_all_allowed_validation(self): @@ -1076,16 +1060,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.list_max_show_all' should be an integer.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.list_max_show_all' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): list_max_show_all = 200 - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_search_fields_validation(self): @@ -1095,8 +1078,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.search_fields' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1108,8 +1090,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.date_hierarchy' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1119,15 +1100,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.date_hierarchy is neither an instance of DateField nor DateTimeField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): date_hierarchy = 'pub_date' - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_ordering_validation(self): @@ -1137,8 +1117,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.ordering' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1148,8 +1127,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.ordering\[0\]' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1159,43 +1137,43 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.ordering' has the random ordering marker '\?', but contains other fields as well. Please either remove '\?' or the other fields.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): ordering = ('?',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) class ValidationTestModelAdmin(ModelAdmin): ordering = ('band__name',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) class ValidationTestModelAdmin(ModelAdmin): ordering = ('name',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_select_related_validation(self): class ValidationTestModelAdmin(ModelAdmin): list_select_related = 1 - six.assertRaisesRegex(self, + six.assertRaisesRegex( + self, ImproperlyConfigured, - "'ValidationTestModelAdmin.list_select_related' should be a boolean.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.list_select_related' should be either a " + "bool, a tuple or a list", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): list_select_related = False - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_save_as_validation(self): @@ -1204,16 +1182,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.save_as' should be a boolean.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.save_as' should be a bool.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): save_as = True - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_save_on_top_validation(self): @@ -1222,16 +1199,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.save_on_top' should be a boolean.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.save_on_top' should be a bool.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): save_on_top = True - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_inlines_validation(self): @@ -1241,8 +1217,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.inlines' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1255,8 +1230,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.inlines\[0\]' does not inherit from BaseModelAdmin.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1269,8 +1243,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'model' is a required attribute of 'ValidationTestModelAdmin.inlines\[0\]'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1286,8 +1259,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.inlines\[0\].model' does not inherit from models.Model.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1297,7 +1269,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_fields_validation(self): @@ -1311,8 +1283,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestInline.fields' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1328,8 +1299,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestInline.fk_name' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestInlineModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1340,7 +1310,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_extra_validation(self): @@ -1353,9 +1323,8 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestInline.extra' should be a integer.", - validate, - ValidationTestModelAdmin, + "'ValidationTestInline.extra' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1366,7 +1335,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_max_num_validation(self): @@ -1379,9 +1348,8 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestInline.max_num' should be an integer or None \(default\).", - validate, - ValidationTestModelAdmin, + "'ValidationTestInline.max_num' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1392,7 +1360,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_formset_validation(self): @@ -1409,8 +1377,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestInline.formset' does not inherit from BaseModelFormSet.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1424,4 +1391,4 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) diff --git a/tests/multiple_database/tests.py b/tests/multiple_database/tests.py index 2bff7b3b66..e72fb6a4f9 100644 --- a/tests/multiple_database/tests.py +++ b/tests/multiple_database/tests.py @@ -5,11 +5,13 @@ import pickle from operator import attrgetter import warnings +from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.core import management from django.db import connections, router, DEFAULT_DB_ALIAS from django.db.models import signals +from django.db.utils import ConnectionRouter from django.test import TestCase from django.test.utils import override_settings from django.utils.six import StringIO @@ -918,6 +920,7 @@ class QueryTestCase(TestCase): published=datetime.date(2009, 5, 4), extra_arg=True) + class TestRouter(object): # A test router. The behavior is vaguely master/slave, but the # databases aren't assumed to propagate changes. @@ -972,6 +975,30 @@ class WriteRouter(object): def db_for_write(self, model, **hints): return 'writer' + +class ConnectionRouterTestCase(TestCase): + @override_settings(DATABASE_ROUTERS=[ + 'multiple_database.tests.TestRouter', + 'multiple_database.tests.WriteRouter']) + def test_router_init_default(self): + router = ConnectionRouter() + self.assertListEqual([r.__class__.__name__ for r in router.routers], + ['TestRouter', 'WriteRouter']) + + def test_router_init_arg(self): + router = ConnectionRouter([ + 'multiple_database.tests.TestRouter', + 'multiple_database.tests.WriteRouter' + ]) + self.assertListEqual([r.__class__.__name__ for r in router.routers], + ['TestRouter', 'WriteRouter']) + + # Init with instances instead of strings + router = ConnectionRouter([TestRouter(), WriteRouter()]) + self.assertListEqual([r.__class__.__name__ for r in router.routers], + ['TestRouter', 'WriteRouter']) + + class RouterTestCase(TestCase): multi_db = True diff --git a/tests/one_to_one/tests.py b/tests/one_to_one/tests.py index 6ee78520bd..a36764b788 100644 --- a/tests/one_to_one/tests.py +++ b/tests/one_to_one/tests.py @@ -22,7 +22,8 @@ class OneToOneTests(TestCase): # A Place can access its restaurant, if available. self.assertEqual(repr(self.p1.restaurant), '<Restaurant: Demon Dogs the restaurant>') # p2 doesn't have an associated restaurant. - self.assertRaises(Restaurant.DoesNotExist, getattr, self.p2, 'restaurant') + with self.assertRaisesMessage(Restaurant.DoesNotExist, 'Place has no restaurant'): + self.p2.restaurant def test_setter(self): # Set the place using assignment notation. Because place is the primary diff --git a/tests/pagination/tests.py b/tests/pagination/tests.py index dea5756672..1dea4526e3 100644 --- a/tests/pagination/tests.py +++ b/tests/pagination/tests.py @@ -297,6 +297,7 @@ class ModelPaginationTests(TestCase): self.assertIsNone(p.object_list._result_cache) self.assertRaises(TypeError, lambda: p['has_previous']) self.assertIsNone(p.object_list._result_cache) + self.assertNotIsInstance(p.object_list, list) # Make sure slicing the Page object with numbers and slice objects work. self.assertEqual(p[0], Article.objects.get(headline='Article 1')) @@ -305,3 +306,5 @@ class ModelPaginationTests(TestCase): "<Article: Article 2>", ] ) + # After __getitem__ is called, object_list is a list + self.assertIsInstance(p.object_list, list) diff --git a/tests/prefetch_related/models.py b/tests/prefetch_related/models.py index 81c569844f..82bf85e401 100644 --- a/tests/prefetch_related/models.py +++ b/tests/prefetch_related/models.py @@ -195,3 +195,23 @@ class Employee(models.Model): class Meta: ordering = ['id'] + + +### Ticket 19607 + +@python_2_unicode_compatible +class LessonEntry(models.Model): + name1 = models.CharField(max_length=200) + name2 = models.CharField(max_length=200) + + def __str__(self): + return "%s %s" % (self.name1, self.name2) + + +@python_2_unicode_compatible +class WordEntry(models.Model): + lesson_entry = models.ForeignKey(LessonEntry) + name = models.CharField(max_length=200) + + def __str__(self): + return "%s (%s)" % (self.name, self.id) diff --git a/tests/prefetch_related/tests.py b/tests/prefetch_related/tests.py index e81560f01f..2e3fee6be6 100644 --- a/tests/prefetch_related/tests.py +++ b/tests/prefetch_related/tests.py @@ -8,7 +8,8 @@ from django.utils import six from .models import (Author, Book, Reader, Qualification, Teacher, Department, TaggedItem, Bookmark, AuthorAddress, FavoriteAuthors, AuthorWithAge, - BookWithYear, BookReview, Person, House, Room, Employee, Comment) + BookWithYear, BookReview, Person, House, Room, Employee, Comment, + LessonEntry, WordEntry) class PrefetchRelatedTests(TestCase): @@ -106,6 +107,16 @@ class PrefetchRelatedTests(TestCase): qs = Book.objects.prefetch_related('first_time_authors') [b.first_time_authors.exists() for b in qs] + def test_in_and_prefetch_related(self): + """ + Regression test for #20242 - QuerySet "in" didn't work the first time + when using prefetch_related. This was fixed by the removal of chunked + reads from QuerySet iteration in + 70679243d1786e03557c28929f9762a119e3ac14. + """ + qs = Book.objects.prefetch_related('first_time_authors') + self.assertTrue(qs[0] in qs) + def test_clear(self): """ Test that we can clear the behavior by calling prefetch_related() @@ -618,3 +629,25 @@ class MultiDbTests(TestCase): ages = ", ".join(str(a.authorwithage.age) for a in A.prefetch_related('authorwithage')) self.assertEqual(ages, "50, 49") + + +class Ticket19607Tests(TestCase): + + def setUp(self): + + for id, name1, name2 in [ + (1, 'einfach', 'simple'), + (2, 'schwierig', 'difficult'), + ]: + LessonEntry.objects.create(id=id, name1=name1, name2=name2) + + for id, lesson_entry_id, name in [ + (1, 1, 'einfach'), + (2, 1, 'simple'), + (3, 2, 'schwierig'), + (4, 2, 'difficult'), + ]: + WordEntry.objects.create(id=id, lesson_entry_id=lesson_entry_id, name=name) + + def test_bug(self): + list(WordEntry.objects.prefetch_related('lesson_entry', 'lesson_entry__wordentry_set')) diff --git a/tests/queries/tests.py b/tests/queries/tests.py index cdc26248c9..481b690c20 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -9,7 +9,6 @@ from django.conf import settings from django.core.exceptions import FieldError from django.db import DatabaseError, connection, connections, DEFAULT_DB_ALIAS from django.db.models import Count, F, Q -from django.db.models.query import ITER_CHUNK_SIZE from django.db.models.sql.where import WhereNode, EverythingNode, NothingNode from django.db.models.sql.datastructures import EmptyResultSet from django.test import TestCase, skipUnlessDBFeature @@ -18,7 +17,7 @@ from django.utils import unittest from django.utils.datastructures import SortedDict from .models import (Annotation, Article, Author, Celebrity, Child, Cover, - Detail, DumbCategory, ExtraInfo, Fan, Item, LeafA, LoopX, LoopZ, + Detail, DumbCategory, ExtraInfo, Fan, Item, LeafA, Join, LeafB, LoopX, LoopZ, ManagedModel, Member, NamedCategory, Note, Number, Plaything, PointerA, Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory, @@ -1112,6 +1111,17 @@ class Queries1Tests(BaseQuerysetTest): ['<Report: r1>'] ) + def test_ticket_20250(self): + # A negated Q along with an annotated queryset failed in Django 1.4 + qs = Author.objects.annotate(Count('item')) + qs = qs.filter(~Q(extra__value=0)) + + self.assertTrue('SELECT' in str(qs.query)) + self.assertQuerysetEqual( + qs, + ['<Author: a1>', '<Author: a2>', '<Author: a3>', '<Author: a4>'] + ) + class Queries2Tests(TestCase): def setUp(self): @@ -1211,16 +1221,6 @@ class Queries2Tests(TestCase): ordered=False ) - def test_ticket7411(self): - # Saving to db must work even with partially read result set in another - # cursor. - for num in range(2 * ITER_CHUNK_SIZE + 1): - _ = Number.objects.create(num=num) - - for i, obj in enumerate(Number.objects.all()): - obj.save() - if i > 10: break - def test_ticket7759(self): # Count should work with a partially read result set. count = Number.objects.count() @@ -1700,31 +1700,6 @@ class Queries6Tests(TestCase): ann1.notes.add(n1) ann2 = Annotation.objects.create(name='a2', tag=t4) - # This next test used to cause really weird PostgreSQL behavior, but it was - # only apparent much later when the full test suite ran. - # - Yeah, it leaves global ITER_CHUNK_SIZE to 2 instead of 100... - #@unittest.expectedFailure - def test_slicing_and_cache_interaction(self): - # We can do slicing beyond what is currently in the result cache, - # too. - - # We need to mess with the implementation internals a bit here to decrease the - # cache fill size so that we don't read all the results at once. - from django.db.models import query - query.ITER_CHUNK_SIZE = 2 - qs = Tag.objects.all() - - # Fill the cache with the first chunk. - self.assertTrue(bool(qs)) - self.assertEqual(len(qs._result_cache), 2) - - # Query beyond the end of the cache and check that it is filled out as required. - self.assertEqual(repr(qs[4]), '<Tag: t5>') - self.assertEqual(len(qs._result_cache), 5) - - # But querying beyond the end of the result set will fail. - self.assertRaises(IndexError, lambda: qs[100]) - def test_parallel_iterators(self): # Test that parallel iterators work. qs = Tag.objects.all() @@ -2533,6 +2508,21 @@ class WhereNodeTest(TestCase): w = WhereNode(children=[empty_w, NothingNode()], connector='OR') self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) + +class IteratorExceptionsTest(TestCase): + def test_iter_exceptions(self): + qs = ExtraInfo.objects.only('author') + with self.assertRaises(AttributeError): + list(qs) + + def test_invalid_qs_list(self): + # Test for #19895 - second iteration over invalid queryset + # raises errors. + qs = Article.objects.order_by('invalid_column') + self.assertRaises(FieldError, list, qs) + self.assertRaises(FieldError, list, qs) + + class NullJoinPromotionOrTest(TestCase): def setUp(self): self.d1 = ModelD.objects.create(name='foo') @@ -2831,3 +2821,45 @@ class EmptyStringPromotionTests(TestCase): self.assertIn('LEFT OUTER JOIN', str(qs.query)) else: self.assertNotIn('LEFT OUTER JOIN', str(qs.query)) + +class ValuesSubqueryTests(TestCase): + def test_values_in_subquery(self): + # Check that if a values() queryset is used, then the given values + # will be used instead of forcing use of the relation's field. + o1 = Order.objects.create(id=-2) + o2 = Order.objects.create(id=-1) + oi1 = OrderItem.objects.create(order=o1, status=0) + oi1.status = oi1.pk + oi1.save() + OrderItem.objects.create(order=o2, status=0) + + # The query below should match o1 as it has related order_item + # with id == status. + self.assertQuerysetEqual( + Order.objects.filter(items__in=OrderItem.objects.values_list('status')), + [o1.pk], lambda x: x.pk) + +class DoubleInSubqueryTests(TestCase): + def test_double_subquery_in(self): + lfa1 = LeafA.objects.create(data='foo') + lfa2 = LeafA.objects.create(data='bar') + lfb1 = LeafB.objects.create(data='lfb1') + lfb2 = LeafB.objects.create(data='lfb2') + Join.objects.create(a=lfa1, b=lfb1) + Join.objects.create(a=lfa2, b=lfb2) + leaf_as = LeafA.objects.filter(data='foo').values_list('pk', flat=True) + joins = Join.objects.filter(a__in=leaf_as).values_list('b__id', flat=True) + qs = LeafB.objects.filter(pk__in=joins) + self.assertQuerysetEqual( + qs, [lfb1], lambda x: x) + +class Ticket18785Tests(unittest.TestCase): + def test_ticket_18785(self): + # Test join trimming from ticket18785 + qs = Item.objects.exclude( + note__isnull=False + ).filter( + name='something', creator__extra__isnull=True + ).order_by() + self.assertEqual(1, str(qs.query).count('INNER JOIN')) + self.assertEqual(0, str(qs.query).count('OUTER JOIN')) diff --git a/tests/requests/tests.py b/tests/requests/tests.py index daf426ea47..4d730bb561 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -503,9 +503,9 @@ class RequestsTests(SimpleTestCase): }) self.assertEqual(request.POST, {'key': ['España']}) - def test_body_after_POST_multipart(self): + def test_body_after_POST_multipart_form_data(self): """ - Reading body after parsing multipart is not allowed + Reading body after parsing multipart/form-data is not allowed """ # Because multipart is used for large amounts fo data i.e. file uploads, # we don't want the data held in memory twice, and we don't want to @@ -524,6 +524,29 @@ class RequestsTests(SimpleTestCase): self.assertEqual(request.POST, {'name': ['value']}) self.assertRaises(Exception, lambda: request.body) + def test_body_after_POST_multipart_related(self): + """ + Reading body after parsing multipart that isn't form-data is allowed + """ + # Ticket #9054 + # There are cases in which the multipart data is related instead of + # being a binary upload, in which case it should still be accessible + # via body. + payload_data = b"\r\n".join([ + b'--boundary', + b'Content-ID: id; name="name"', + b'', + b'value', + b'--boundary--' + b'']) + payload = FakePayload(payload_data) + request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'multipart/related; boundary=boundary', + 'CONTENT_LENGTH': len(payload), + 'wsgi.input': payload}) + self.assertEqual(request.POST, {}) + self.assertEqual(request.body, payload_data) + def test_POST_multipart_with_content_length_zero(self): """ Multipart POST requests with Content-Length >= 0 are valid and need to be handled. @@ -636,6 +659,24 @@ class RequestsTests(SimpleTestCase): with self.assertRaises(UnreadablePostError): request.body + def test_FILES_connection_error(self): + """ + If wsgi.input.read() raises an exception while trying to read() the + FILES, the exception should be identifiable (not a generic IOError). + """ + class ExplodingBytesIO(BytesIO): + def read(self, len=0): + raise IOError("kaboom!") + + payload = b'x' + request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'multipart/form-data; boundary=foo_', + 'CONTENT_LENGTH': len(payload), + 'wsgi.input': ExplodingBytesIO(payload)}) + + with self.assertRaises(UnreadablePostError): + request.FILES + @skipIf(connection.vendor == 'sqlite' and connection.settings_dict['NAME'] in ('', ':memory:'), diff --git a/tests/responses/__init__.py b/tests/responses/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/responses/__init__.py diff --git a/tests/responses/models.py b/tests/responses/models.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/responses/models.py diff --git a/tests/responses/tests.py b/tests/responses/tests.py new file mode 100644 index 0000000000..e5320f5af9 --- /dev/null +++ b/tests/responses/tests.py @@ -0,0 +1,15 @@ +from django.http import HttpResponse +import unittest + +class HttpResponseTests(unittest.TestCase): + + def test_status_code(self): + resp = HttpResponse(status=418) + self.assertEqual(resp.status_code, 418) + self.assertEqual(resp.reason_phrase, "I'M A TEAPOT") + + def test_reason_phrase(self): + reason = "I'm an anarchist coffee pot on crack." + resp = HttpResponse(status=814, reason=reason) + self.assertEqual(resp.status_code, 814) + self.assertEqual(resp.reason_phrase, reason) diff --git a/tests/select_related/tests.py b/tests/select_related/tests.py index 27d65fecb1..baa141d123 100644 --- a/tests/select_related/tests.py +++ b/tests/select_related/tests.py @@ -172,3 +172,7 @@ class SelectRelatedTests(TestCase): Species.objects.select_related, 'genus__family__order', depth=4 ) + + def test_none_clears_list(self): + queryset = Species.objects.select_related('genus').select_related(None) + self.assertEqual(queryset.query.select_related, False) diff --git a/tests/special_headers/models.py b/tests/special_headers/models.py deleted file mode 100644 index e05c5a6920..0000000000 --- a/tests/special_headers/models.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.db import models - - -class Article(models.Model): - text = models.TextField() diff --git a/tests/special_headers/templates/special_headers/article_detail.html b/tests/special_headers/templates/special_headers/article_detail.html deleted file mode 100644 index 3cbd38cb7e..0000000000 --- a/tests/special_headers/templates/special_headers/article_detail.html +++ /dev/null @@ -1 +0,0 @@ -{{ object }} diff --git a/tests/special_headers/tests.py b/tests/special_headers/tests.py deleted file mode 100644 index b4b704ae21..0000000000 --- a/tests/special_headers/tests.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.contrib.auth.models import User -from django.test import TestCase -from django.test.utils import override_settings - - -@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class SpecialHeadersTest(TestCase): - fixtures = ['data.xml'] - urls = 'special_headers.urls' - - def test_xheaders(self): - user = User.objects.get(username='super') - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - self.client.login(username='super', password='secret') - response = self.client.get('/special_headers/article/1/') - self.assertTrue('X-Object-Type' in response) - user.is_staff = False - user.save() - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - - def test_xview_func(self): - user = User.objects.get(username='super') - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - self.client.login(username='super', password='secret') - response = self.client.head('/special_headers/xview/func/') - self.assertTrue('X-View' in response) - self.assertEqual(response['X-View'], 'special_headers.views.xview') - user.is_staff = False - user.save() - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - - def test_xview_class(self): - user = User.objects.get(username='super') - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) - self.client.login(username='super', password='secret') - response = self.client.head('/special_headers/xview/class/') - self.assertTrue('X-View' in response) - self.assertEqual(response['X-View'], 'special_headers.views.XViewClass') - user.is_staff = False - user.save() - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) diff --git a/tests/special_headers/urls.py b/tests/special_headers/urls.py deleted file mode 100644 index f7ba141207..0000000000 --- a/tests/special_headers/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -# coding: utf-8 -from __future__ import absolute_import - -from django.conf.urls import patterns - -from . import views -from .models import Article - -urlpatterns = patterns('', - (r'^special_headers/article/(?P<object_id>\d+)/$', views.xview_xheaders), - (r'^special_headers/xview/func/$', views.xview_dec(views.xview)), - (r'^special_headers/xview/class/$', views.xview_dec(views.XViewClass.as_view())), -) diff --git a/tests/string_lookup/tests.py b/tests/string_lookup/tests.py index 02f766adce..b011720ddf 100644 --- a/tests/string_lookup/tests.py +++ b/tests/string_lookup/tests.py @@ -73,9 +73,11 @@ class StringLookupTests(TestCase): """ Regression test for #708 - "like" queries on IP address fields require casting to text (on PostgreSQL). + "like" queries on IP address fields require casting with HOST() (on PostgreSQL). """ a = Article(name='IP test', text='The body', submitted_from='192.0.2.100') a.save() self.assertEqual(repr(Article.objects.filter(submitted_from__contains='192.0.2')), repr([a])) + # Test that the searches do not match the subnet mask (/32 in this case) + self.assertEqual(Article.objects.filter(submitted_from__contains='32').count(), 0)
\ No newline at end of file diff --git a/tests/syncdb_signals/__init__.py b/tests/syncdb_signals/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/syncdb_signals/__init__.py diff --git a/tests/syncdb_signals/models.py b/tests/syncdb_signals/models.py new file mode 100644 index 0000000000..c41d993e94 --- /dev/null +++ b/tests/syncdb_signals/models.py @@ -0,0 +1,11 @@ +# from django.db import models + + +# class Author(models.Model): +# name = models.CharField(max_length=100) + +# class Meta: +# ordering = ['name'] + +# def __unicode__(self): +# return self.name diff --git a/tests/syncdb_signals/tests.py b/tests/syncdb_signals/tests.py new file mode 100644 index 0000000000..d9be3b65d4 --- /dev/null +++ b/tests/syncdb_signals/tests.py @@ -0,0 +1,74 @@ +from django.db.models import signals +from django.test import TestCase +from django.core import management +from django.utils import six + +from shared_models import models + + +PRE_SYNCDB_ARGS = ['app', 'create_models', 'verbosity', 'interactive', 'db'] +SYNCDB_DATABASE = 'default' +SYNCDB_VERBOSITY = 1 +SYNCDB_INTERACTIVE = False + + +class PreSyncdbReceiver(object): + def __init__(self): + self.call_counter = 0 + self.call_args = None + + def __call__(self, signal, sender, **kwargs): + self.call_counter = self.call_counter + 1 + self.call_args = kwargs + + +class OneTimeReceiver(object): + """ + Special receiver for handle the fact that test runner calls syncdb for + several databases and several times for some of them. + """ + + def __init__(self): + self.call_counter = 0 + self.call_args = None + + def __call__(self, signal, sender, **kwargs): + # Although test runner calls syncdb for several databases, + # testing for only one of them is quite sufficient. + if kwargs['db'] == SYNCDB_DATABASE: + self.call_counter = self.call_counter + 1 + self.call_args = kwargs + # we need to test only one call of syncdb + signals.pre_syncdb.disconnect(pre_syncdb_receiver, sender=models) + + +# We connect receiver here and not in unit test code because we need to +# connect receiver before test runner creates database. That is, sequence of +# actions would be: +# +# 1. Test runner imports this module. +# 2. We connect receiver. +# 3. Test runner calls syncdb for create default database. +# 4. Test runner execute our unit test code. +pre_syncdb_receiver = OneTimeReceiver() +signals.pre_syncdb.connect(pre_syncdb_receiver, sender=models) + + +class SyncdbSignalTests(TestCase): + def test_pre_syncdb_call_time(self): + self.assertEqual(pre_syncdb_receiver.call_counter, 1) + + def test_pre_syncdb_args(self): + r = PreSyncdbReceiver() + signals.pre_syncdb.connect(r, sender=models) + management.call_command('syncdb', database=SYNCDB_DATABASE, + verbosity=SYNCDB_VERBOSITY, interactive=SYNCDB_INTERACTIVE, + load_initial_data=False, stdout=six.StringIO()) + + args = r.call_args + self.assertEqual(r.call_counter, 1) + self.assertEqual(set(args), set(PRE_SYNCDB_ARGS)) + self.assertEqual(args['app'], models) + self.assertEqual(args['verbosity'], SYNCDB_VERBOSITY) + self.assertEqual(args['interactive'], SYNCDB_INTERACTIVE) + self.assertEqual(args['db'], 'default') diff --git a/tests/template_tests/filters.py b/tests/template_tests/filters.py index 7ba1681fd5..68ef15d827 100644 --- a/tests/template_tests/filters.py +++ b/tests/template_tests/filters.py @@ -35,59 +35,60 @@ def get_filter_tests(): now_tz_i = datetime.now(FixedOffset((3 * 60) + 15)) # imaginary time zone today = date.today() + # NOTE: \xa0 avoids wrapping between value and unit return { # Default compare with datetime.now() - 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), - 'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1 day'), - 'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1 hour, 25 minutes'), + 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1\xa0minute'), + 'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1\xa0day'), + 'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1\xa0hour, 25\xa0minutes'), # Compare to a given parameter - 'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=1)}, '1 day'), - 'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2, minutes=1), 'b':now - timedelta(days=2)}, '1 minute'), + 'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=1)}, '1\xa0day'), + 'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2, minutes=1), 'b':now - timedelta(days=2)}, '1\xa0minute'), # Check that timezone is respected - 'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz - timedelta(hours=8), 'b':now_tz}, '8 hours'), + 'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz - timedelta(hours=8), 'b':now_tz}, '8\xa0hours'), # Regression for #7443 - 'filter-timesince07': ('{{ earlier|timesince }}', { 'earlier': now - timedelta(days=7) }, '1 week'), - 'filter-timesince08': ('{{ earlier|timesince:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '1 week'), - 'filter-timesince09': ('{{ later|timesince }}', { 'later': now + timedelta(days=7) }, '0 minutes'), - 'filter-timesince10': ('{{ later|timesince:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '0 minutes'), + 'filter-timesince07': ('{{ earlier|timesince }}', { 'earlier': now - timedelta(days=7) }, '1\xa0week'), + 'filter-timesince08': ('{{ earlier|timesince:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '1\xa0week'), + 'filter-timesince09': ('{{ later|timesince }}', { 'later': now + timedelta(days=7) }, '0\xa0minutes'), + 'filter-timesince10': ('{{ later|timesince:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '0\xa0minutes'), # Ensures that differing timezones are calculated correctly - 'filter-timesince11' : ('{{ a|timesince }}', {'a': now}, '0 minutes'), - 'filter-timesince12' : ('{{ a|timesince }}', {'a': now_tz}, '0 minutes'), - 'filter-timesince13' : ('{{ a|timesince }}', {'a': now_tz_i}, '0 minutes'), - 'filter-timesince14' : ('{{ a|timesince:b }}', {'a': now_tz, 'b': now_tz_i}, '0 minutes'), + 'filter-timesince11' : ('{{ a|timesince }}', {'a': now}, '0\xa0minutes'), + 'filter-timesince12' : ('{{ a|timesince }}', {'a': now_tz}, '0\xa0minutes'), + 'filter-timesince13' : ('{{ a|timesince }}', {'a': now_tz_i}, '0\xa0minutes'), + 'filter-timesince14' : ('{{ a|timesince:b }}', {'a': now_tz, 'b': now_tz_i}, '0\xa0minutes'), 'filter-timesince15' : ('{{ a|timesince:b }}', {'a': now, 'b': now_tz_i}, ''), 'filter-timesince16' : ('{{ a|timesince:b }}', {'a': now_tz_i, 'b': now}, ''), # Regression for #9065 (two date objects). - 'filter-timesince17' : ('{{ a|timesince:b }}', {'a': today, 'b': today}, '0 minutes'), - 'filter-timesince18' : ('{{ a|timesince:b }}', {'a': today, 'b': today + timedelta(hours=24)}, '1 day'), + 'filter-timesince17' : ('{{ a|timesince:b }}', {'a': today, 'b': today}, '0\xa0minutes'), + 'filter-timesince18' : ('{{ a|timesince:b }}', {'a': today, 'b': today + timedelta(hours=24)}, '1\xa0day'), # Default compare with datetime.now() - 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), - 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), - 'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'), + 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2\xa0minutes'), + 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1\xa0day'), + 'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8\xa0hours, 10\xa0minutes'), # Compare to a given parameter - 'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1 day'), - 'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1 minute'), + 'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1\xa0day'), + 'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1\xa0minute'), # Regression for #7443 - 'filter-timeuntil06': ('{{ earlier|timeuntil }}', { 'earlier': now - timedelta(days=7) }, '0 minutes'), - 'filter-timeuntil07': ('{{ earlier|timeuntil:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '0 minutes'), - 'filter-timeuntil08': ('{{ later|timeuntil }}', { 'later': now + timedelta(days=7, hours=1) }, '1 week'), - 'filter-timeuntil09': ('{{ later|timeuntil:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '1 week'), + 'filter-timeuntil06': ('{{ earlier|timeuntil }}', { 'earlier': now - timedelta(days=7) }, '0\xa0minutes'), + 'filter-timeuntil07': ('{{ earlier|timeuntil:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '0\xa0minutes'), + 'filter-timeuntil08': ('{{ later|timeuntil }}', { 'later': now + timedelta(days=7, hours=1) }, '1\xa0week'), + 'filter-timeuntil09': ('{{ later|timeuntil:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '1\xa0week'), # Ensures that differing timezones are calculated correctly - 'filter-timeuntil10' : ('{{ a|timeuntil }}', {'a': now_tz_i}, '0 minutes'), - 'filter-timeuntil11' : ('{{ a|timeuntil:b }}', {'a': now_tz_i, 'b': now_tz}, '0 minutes'), + 'filter-timeuntil10' : ('{{ a|timeuntil }}', {'a': now_tz_i}, '0\xa0minutes'), + 'filter-timeuntil11' : ('{{ a|timeuntil:b }}', {'a': now_tz_i, 'b': now_tz}, '0\xa0minutes'), # Regression for #9065 (two date objects). - 'filter-timeuntil12' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today}, '0 minutes'), - 'filter-timeuntil13' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today - timedelta(hours=24)}, '1 day'), + 'filter-timeuntil12' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today}, '0\xa0minutes'), + 'filter-timeuntil13' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today - timedelta(hours=24)}, '1\xa0day'), 'filter-addslash01': ("{% autoescape off %}{{ a|addslashes }} {{ b|addslashes }}{% endautoescape %}", {"a": "<a>'", "b": mark_safe("<a>'")}, r"<a>\' <a>\'"), 'filter-addslash02': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "<a>'", "b": mark_safe("<a>'")}, r"<a>\' <a>\'"), diff --git a/tests/template_tests/templates/included_base.html b/tests/template_tests/templates/included_base.html new file mode 100644 index 0000000000..eae222cf9d --- /dev/null +++ b/tests/template_tests/templates/included_base.html @@ -0,0 +1,3 @@ +{% block content %} + {% block error_here %}{% endblock %} +{% endblock %} diff --git a/tests/template_tests/templates/included_content.html b/tests/template_tests/templates/included_content.html new file mode 100644 index 0000000000..bfc87c0425 --- /dev/null +++ b/tests/template_tests/templates/included_content.html @@ -0,0 +1,11 @@ +{% extends "included_base.html" %} + +{% block content %} + content + {{ block.super }} +{% endblock %} + +{% block error_here %} + error here + {% url "non_existing_url" %} +{% endblock %} diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 2aeaee9464..206c648398 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -36,6 +36,7 @@ from django.utils.safestring import mark_safe from django.utils import six from django.utils.tzinfo import LocalTimezone +from i18n import TransRealMixin try: from .loaders import RenderToStringTest, EggLoaderTest @@ -154,8 +155,8 @@ class UTF8Class: def __str__(self): return 'ŠĐĆŽćžšđ' -@override_settings(MEDIA_URL="/media/", STATIC_URL="/static/") -class Templates(TestCase): + +class TemplateLoaderTests(TestCase): def test_loaders_security(self): ad_loader = app_directories.Loader() @@ -347,6 +348,9 @@ class Templates(TestCase): loader.template_source_loaders = old_loaders settings.TEMPLATE_DEBUG = old_td + +class TemplateRegressionTests(TestCase): + def test_token_smart_split(self): # Regression test for #7027 token = template.Token(template.TOKEN_BLOCK, 'sometag _("Page not found") value|yesno:_("yes,no")') @@ -444,6 +448,18 @@ class Templates(TestCase): output = template.render(Context({})) self.assertEqual(output, '1st time') + def test_super_errors(self): + """ + Test behavior of the raise errors into included blocks. + See #18169 + """ + t = loader.get_template('included_content.html') + with self.assertRaises(urlresolvers.NoReverseMatch): + t.render(Context({})) + + +@override_settings(MEDIA_URL="/media/", STATIC_URL="/static/") +class TemplateTests(TransRealMixin, TestCase): def test_templates(self): template_tests = self.get_template_tests() filter_tests = filters.get_filter_tests() diff --git a/tests/test_client/urls.py b/tests/test_client/urls.py index 67c475eaff..bd395ca552 100644 --- a/tests/test_client/urls.py +++ b/tests/test_client/urls.py @@ -21,6 +21,7 @@ urlpatterns = patterns('', (r'^bad_view/$', views.bad_view), (r'^form_view/$', views.form_view), (r'^form_view_with_template/$', views.form_view_with_template), + (r'^formset_view/$', views.formset_view), (r'^login_protected_view/$', views.login_protected_view), (r'^login_protected_method_view/$', views.login_protected_method_view), (r'^login_protected_view_custom_redirect/$', views.login_protected_view_changed_redirect), diff --git a/tests/test_client/views.py b/tests/test_client/views.py index f760466497..76296cb80d 100644 --- a/tests/test_client/views.py +++ b/tests/test_client/views.py @@ -7,7 +7,8 @@ from xml.dom.minidom import parseString from django.contrib.auth.decorators import login_required, permission_required from django.core import mail from django.forms import fields -from django.forms.forms import Form +from django.forms.forms import Form, ValidationError +from django.forms.formsets import formset_factory, BaseFormSet from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound from django.shortcuts import render_to_response from django.template import Context, Template @@ -95,6 +96,12 @@ class TestForm(Form): single = fields.ChoiceField(choices=TestChoices) multi = fields.MultipleChoiceField(choices=TestChoices) + def clean(self): + cleaned_data = self.cleaned_data + if cleaned_data.get("text") == "Raise non-field error": + raise ValidationError("Non-field error.") + return cleaned_data + def form_view(request): "A view that tests a simple form" if request.method == 'POST': @@ -130,6 +137,43 @@ def form_view_with_template(request): } ) +class BaseTestFormSet(BaseFormSet): + def clean(self): + """Checks that no two email addresses are the same.""" + if any(self.errors): + # Don't bother validating the formset unless each form is valid + return + + emails = [] + for i in range(0, self.total_form_count()): + form = self.forms[i] + email = form.cleaned_data['email'] + if email in emails: + raise ValidationError( + "Forms in a set must have distinct email addresses." + ) + emails.append(email) + +TestFormSet = formset_factory(TestForm, BaseTestFormSet) + +def formset_view(request): + "A view that tests a simple formset" + if request.method == 'POST': + formset = TestFormSet(request.POST) + if formset.is_valid(): + t = Template('Valid POST data.', name='Valid POST Template') + c = Context() + else: + t = Template('Invalid POST data. {{ my_formset.errors }}', + name='Invalid POST Template') + c = Context({'my_formset': formset}) + else: + formset = TestForm(request.GET) + t = Template('Viewing base formset. {{ my_formset }}.', + name='Formset GET Template') + c = Context({'my_formset': formset}) + return HttpResponse(t.render(c)) + def login_protected_view(request): "A simple view that is login protected." t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template') diff --git a/tests/test_client_regress/models.py b/tests/test_client_regress/models.py index e69de29bb2..b72d4480bf 100644 --- a/tests/test_client_regress/models.py +++ b/tests/test_client_regress/models.py @@ -0,0 +1,12 @@ +from django.db import models +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager + + +class CustomUser(AbstractBaseUser): + email = models.EmailField(verbose_name='email address', max_length=255, unique=True) + custom_objects = BaseUserManager() + + USERNAME_FIELD = 'email' + + class Meta: + app_label = 'test_client_regress' diff --git a/tests/test_client_regress/tests.py b/tests/test_client_regress/tests.py index 2582b210c4..67e66fa52d 100644 --- a/tests/test_client_regress/tests.py +++ b/tests/test_client_regress/tests.py @@ -6,10 +6,8 @@ from __future__ import unicode_literals import os -from django.conf import settings -from django.core.exceptions import SuspiciousOperation from django.core.urlresolvers import reverse -from django.template import (TemplateDoesNotExist, TemplateSyntaxError, +from django.template import (TemplateSyntaxError, Context, Template, loader) import django.template.context from django.test import Client, TestCase @@ -19,7 +17,11 @@ from django.template.response import SimpleTemplateResponse from django.utils._os import upath from django.utils.translation import ugettext_lazy from django.http import HttpResponse +from django.contrib.auth.signals import user_logged_out, user_logged_in +from django.contrib.auth.models import User +from .models import CustomUser +from .views import CustomTestException @override_settings( TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'templates'),) @@ -543,6 +545,197 @@ class AssertFormErrorTests(TestCase): except AssertionError as e: self.assertIn("abc: The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )", str(e)) +class AssertFormsetErrorTests(TestCase): + msg_prefixes = [("", {}), ("abc: ", {"msg_prefix": "abc"})] + def setUp(self): + """Makes response object for testing field and non-field errors""" + # For testing field and non-field errors + self.response_form_errors = self.getResponse({ + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '2', + 'form-0-text': 'Raise non-field error', + 'form-0-email': 'not an email address', + 'form-0-value': 37, + 'form-0-single': 'b', + 'form-0-multi': ('b','c','e'), + 'form-1-text': 'Hello World', + 'form-1-email': 'email@domain.com', + 'form-1-value': 37, + 'form-1-single': 'b', + 'form-1-multi': ('b','c','e'), + }) + # For testing non-form errors + self.response_nonform_errors = self.getResponse({ + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '2', + 'form-0-text': 'Hello World', + 'form-0-email': 'email@domain.com', + 'form-0-value': 37, + 'form-0-single': 'b', + 'form-0-multi': ('b','c','e'), + 'form-1-text': 'Hello World', + 'form-1-email': 'email@domain.com', + 'form-1-value': 37, + 'form-1-single': 'b', + 'form-1-multi': ('b','c','e'), + }) + + def getResponse(self, post_data): + response = self.client.post('/test_client/formset_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + return response + + def test_unknown_formset(self): + "An assertion is raised if the formset name is unknown" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'wrong_formset', + 0, + 'Some_field', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'wrong_formset' was not " + "used to render the response", + str(cm.exception)) + + def test_unknown_field(self): + "An assertion is raised if the field name is unknown" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'Some_field', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset', " + "form 0 in context 0 " + "does not contain the field 'Some_field'", + str(cm.exception)) + + def test_no_error_field(self): + "An assertion is raised if the field doesn't have any errors" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 1, + 'value', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The field 'value' " + "on formset 'my_formset', form 1 " + "in context 0 contains no errors", + str(cm.exception)) + + def test_unknown_error(self): + "An assertion is raised if the field doesn't contain the specified error" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'email', + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + "The field 'email' " + "on formset 'my_formset', form 0 in context 0 does not " + "contain the error 'Some error.' (actual errors: " + "[%(_)s'Enter a valid email address.'])"), + str(cm.exception)) + + def test_field_error(self): + "No assertion is raised if the field contains the provided error" + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'email', + ['Enter a valid email address.'], + **kwargs) + + def test_no_nonfield_error(self): + "An assertion is raised if the formsets non-field errors doesn't contain any errors." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 1, + None, + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset', form 1 in " + "context 0 does not contain any " + "non-field errors.", + str(cm.exception)) + + def test_unknown_nonfield_error(self): + "An assertion is raised if the formsets non-field errors doesn't contain the provided error." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + None, + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + + "The formset 'my_formset', form 0 in context 0 does not " + "contain the non-field error 'Some error.' (actual errors: " + "[%(_)s'Non-field error.'])"), str(cm.exception)) + + def test_nonfield_error(self): + "No assertion is raised if the formsets non-field errors contains the provided error." + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + None, + 'Non-field error.', + **kwargs) + + def test_no_nonform_error(self): + "An assertion is raised if the formsets non-form errors doesn't contain any errors." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + None, + None, + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset' in context 0 " + "does not contain any non-form errors.", + str(cm.exception)) + + def test_unknown_nonform_error(self): + "An assertion is raised if the formsets non-form errors doesn't contain the provided error." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_nonform_errors, + 'my_formset', + None, + None, + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + + "The formset 'my_formset' in context 0 does not contain the " + "non-form error 'Some error.' (actual errors: [%(_)s'Forms " + "in a set must have distinct email addresses.'])"), str(cm.exception)) + + def test_nonform_error(self): + "No assertion is raised if the formsets non-form errors contains the provided error." + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_nonform_errors, + 'my_formset', + None, + None, + 'Forms in a set must have distinct email ' + 'addresses.', + **kwargs) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class LoginTests(TestCase): fixtures = ['testdata'] @@ -619,7 +812,7 @@ class ExceptionTests(TestCase): try: response = self.client.get("/test_client_regress/staff_only/") self.fail("General users should not be able to visit this page") - except SuspiciousOperation: + except CustomTestException: pass # At this point, an exception has been raised, and should be cleared. @@ -629,7 +822,7 @@ class ExceptionTests(TestCase): self.assertTrue(login, 'Could not log in') try: self.client.get("/test_client_regress/staff_only/") - except SuspiciousOperation: + except CustomTestException: self.fail("Staff should be able to visit this page") @@ -706,6 +899,21 @@ class ContextTests(TestCase): except KeyError as e: self.assertEqual(e.args[0], 'does-not-exist') + def test_contextlist_keys(self): + c1 = Context() + c1.update({'hello': 'world', 'goodbye': 'john'}) + c1.update({'hello': 'dolly', 'dolly': 'parton'}) + c2 = Context() + c2.update({'goodbye': 'world', 'python': 'rocks'}) + c2.update({'goodbye': 'dolly'}) + + l = ContextList([c1, c2]) + # None, True and False are builtins of BaseContext, and present + # in every Context without needing to be added. + self.assertEqual(set(['None', 'True', 'False', 'hello', 'goodbye', + 'python', 'dolly']), + l.keys()) + def test_15368(self): # Need to insert a context processor that assumes certain things about # the request instance. This triggers a bug caused by some ways of @@ -756,6 +964,76 @@ class SessionTests(TestCase): self.client.logout() self.client.logout() + def test_logout_with_user(self): + """Logout should send user_logged_out signal if user was logged in.""" + def listener(*args, **kwargs): + listener.executed = True + self.assertEqual(kwargs['sender'], User) + listener.executed = False + + user_logged_out.connect(listener) + self.client.login(username='testclient', password='password') + self.client.logout() + user_logged_out.disconnect(listener) + self.assertTrue(listener.executed) + + @override_settings(AUTH_USER_MODEL='test_client_regress.CustomUser') + def test_logout_with_custom_user(self): + """Logout should send user_logged_out signal if custom user was logged in.""" + def listener(*args, **kwargs): + self.assertEqual(kwargs['sender'], CustomUser) + listener.executed = True + listener.executed = False + u = CustomUser.custom_objects.create(email='test@test.com') + u.set_password('password') + u.save() + + user_logged_out.connect(listener) + self.client.login(username='test@test.com', password='password') + self.client.logout() + user_logged_out.disconnect(listener) + self.assertTrue(listener.executed) + + def test_logout_without_user(self): + """Logout should send signal even if user not authenticated.""" + def listener(user, *args, **kwargs): + listener.user = user + listener.executed = True + listener.executed = False + + user_logged_out.connect(listener) + self.client.login(username='incorrect', password='password') + self.client.logout() + user_logged_out.disconnect(listener) + + self.assertTrue(listener.executed) + self.assertIsNone(listener.user) + + def test_login_with_user(self): + """Login should send user_logged_in signal on successful login.""" + def listener(*args, **kwargs): + listener.executed = True + listener.executed = False + + user_logged_in.connect(listener) + self.client.login(username='testclient', password='password') + user_logged_out.disconnect(listener) + + self.assertTrue(listener.executed) + + def test_login_without_signal(self): + """Login shouldn't send signal if user wasn't logged in""" + def listener(*args, **kwargs): + listener.executed = True + listener.executed = False + + user_logged_in.connect(listener) + self.client.login(username='incorrect', password='password') + user_logged_in.disconnect(listener) + + self.assertFalse(listener.executed) + + class RequestMethodTests(TestCase): def test_get(self): "Request a view via request method GET" diff --git a/tests/test_client_regress/views.py b/tests/test_client_regress/views.py index 7e86ffd8ca..71e5b526e5 100644 --- a/tests/test_client_regress/views.py +++ b/tests/test_client_regress/views.py @@ -3,12 +3,15 @@ import json from django.conf import settings from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseRedirect -from django.core.exceptions import SuspiciousOperation from django.shortcuts import render_to_response from django.core.serializers.json import DjangoJSONEncoder from django.test.client import CONTENT_TYPE_RE from django.template import RequestContext + +class CustomTestException(Exception): + pass + def no_template_view(request): "A simple view that expects a GET request, and returns a rendered template" return HttpResponse("No template used. Sample content: twice once twice. Content ends.") @@ -18,7 +21,7 @@ def staff_only_view(request): if request.user.is_staff: return HttpResponse('') else: - raise SuspiciousOperation() + raise CustomTestException() def get_view(request): "A simple login protected view" diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py index 3dc364b351..1a0fb88367 100644 --- a/tests/test_runner/test_discover_runner.py +++ b/tests/test_runner/test_discover_runner.py @@ -1,5 +1,22 @@ +from contextlib import contextmanager +import os +import sys + from django.test import TestCase from django.test.runner import DiscoverRunner +from django.utils.unittest import expectedFailure + +try: + import unittest2 +except ImportError: + unittest2 = None + + +def expectedFailureIf(condition): + """Marks a test as an expected failure if ``condition`` is met.""" + if condition: + return expectedFailure + return lambda func: func class DiscoverRunnerTest(TestCase): @@ -32,6 +49,9 @@ class DiscoverRunnerTest(TestCase): self.assertEqual(count, 1) + # this test fails if unittest2 is installed from PyPI on Python 2.6 + # refs https://code.djangoproject.com/ticket/20437 + @expectedFailureIf(sys.version_info < (2, 7) and unittest2) def test_dotted_test_method_vanilla_unittest(self): count = DiscoverRunner().build_suite( ["test_discovery_sample.tests_sample.TestVanillaUnittest.test_sample"], @@ -61,8 +81,19 @@ class DiscoverRunnerTest(TestCase): self.assertEqual(count, 1) def test_file_path(self): - count = DiscoverRunner().build_suite( - ["test_discovery_sample/"], - ).countTestCases() + @contextmanager + def change_cwd_to_tests(): + """Change CWD to tests directory (one level up from this file)""" + current_dir = os.path.abspath(os.path.dirname(__file__)) + tests_dir = os.path.join(current_dir, '..') + old_cwd = os.getcwd() + os.chdir(tests_dir) + yield + os.chdir(old_cwd) + + with change_cwd_to_tests(): + count = DiscoverRunner().build_suite( + ["test_discovery_sample/"], + ).countTestCases() self.assertEqual(count, 4) diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index aeb9bc3d2c..0f16a9c805 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -5,6 +5,7 @@ import warnings from django.db import connection, transaction, IntegrityError from django.test import TransactionTestCase, skipUnlessDBFeature +from django.test.utils import IgnorePendingDeprecationWarningsMixin from django.utils import six from django.utils.unittest import skipIf, skipUnless @@ -319,19 +320,6 @@ class AtomicMiscTests(TransactionTestCase): transaction.atomic(Callable()) -class IgnorePendingDeprecationWarningsMixin(object): - - def setUp(self): - super(IgnorePendingDeprecationWarningsMixin, self).setUp() - self.catch_warnings = warnings.catch_warnings() - self.catch_warnings.__enter__() - warnings.filterwarnings("ignore", category=PendingDeprecationWarning) - - def tearDown(self): - self.catch_warnings.__exit__(*sys.exc_info()) - super(IgnorePendingDeprecationWarningsMixin, self).tearDown() - - class TransactionTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): def create_a_reporter_then_fail(self, first, last): diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index fb3f257dab..5339b4a8ea 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -4,11 +4,9 @@ from django.db import (connection, connections, transaction, DEFAULT_DB_ALIAS, D IntegrityError) from django.db.transaction import commit_on_success, commit_manually, TransactionManagementError from django.test import TransactionTestCase, skipUnlessDBFeature -from django.test.utils import override_settings +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin from django.utils.unittest import skipIf, skipUnless -from transactions.tests import IgnorePendingDeprecationWarningsMixin - from .models import Mod, M2mA, M2mB, SubMod class ModelInheritanceTests(TransactionTestCase): diff --git a/tests/unmanaged_models/tests.py b/tests/unmanaged_models/tests.py index 64e33bbb47..d7cf961a37 100644 --- a/tests/unmanaged_models/tests.py +++ b/tests/unmanaged_models/tests.py @@ -23,14 +23,14 @@ class SimpleTests(TestCase): # ... and pull it out via the other set. a2 = A02.objects.all()[0] - self.assertTrue(isinstance(a2, A02)) + self.assertIsInstance(a2, A02) self.assertEqual(a2.f_a, "foo") b2 = B02.objects.all()[0] - self.assertTrue(isinstance(b2, B02)) + self.assertIsInstance(b2, B02) self.assertEqual(b2.f_a, "fred") - self.assertTrue(isinstance(b2.fk_a, A02)) + self.assertIsInstance(b2.fk_a, A02) self.assertEqual(b2.fk_a.f_a, "foo") self.assertEqual(list(C02.objects.filter(f_a=None)), []) @@ -38,7 +38,7 @@ class SimpleTests(TestCase): resp = list(C02.objects.filter(mm_a=a.id)) self.assertEqual(len(resp), 1) - self.assertTrue(isinstance(resp[0], C02)) + self.assertIsInstance(resp[0], C02) self.assertEqual(resp[0].f_a, 'barney') diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py index 24f96fac7c..f54c796d30 100644 --- a/tests/urlpatterns_reverse/tests.py +++ b/tests/urlpatterns_reverse/tests.py @@ -246,7 +246,7 @@ class ResolverTests(unittest.TestCase): self.assertEqual(len(e.args[0]['tried']), len(url_types_names), 'Wrong number of tried URLs returned. Expected %s, got %s.' % (len(url_types_names), len(e.args[0]['tried']))) for tried, expected in zip(e.args[0]['tried'], url_types_names): for t, e in zip(tried, expected): - self.assertTrue(isinstance(t, e['type']), str('%s is not an instance of %s') % (t, e['type'])) + self.assertIsInstance(t, e['type']), str('%s is not an instance of %s') % (t, e['type']) if 'name' in e: if not e['name']: self.assertTrue(t.name is None, 'Expected no URL name but found %s.' % t.name) @@ -278,11 +278,11 @@ class ReverseShortcutTests(TestCase): return "/hi-there/" res = redirect(FakeObj()) - self.assertTrue(isinstance(res, HttpResponseRedirect)) + self.assertIsInstance(res, HttpResponseRedirect) self.assertEqual(res.url, '/hi-there/') res = redirect(FakeObj(), permanent=True) - self.assertTrue(isinstance(res, HttpResponsePermanentRedirect)) + self.assertIsInstance(res, HttpResponsePermanentRedirect) self.assertEqual(res.url, '/hi-there/') def test_redirect_to_view_name(self): @@ -516,7 +516,7 @@ class RequestURLconfTests(TestCase): b''.join(self.client.get('/second_test/')) class ErrorHandlerResolutionTests(TestCase): - """Tests for handler404 and handler500""" + """Tests for handler400, handler404 and handler500""" def setUp(self): from django.core.urlresolvers import RegexURLResolver @@ -528,12 +528,14 @@ class ErrorHandlerResolutionTests(TestCase): def test_named_handlers(self): from .views import empty_view handler = (empty_view, {}) + self.assertEqual(self.resolver.resolve400(), handler) self.assertEqual(self.resolver.resolve404(), handler) self.assertEqual(self.resolver.resolve500(), handler) def test_callable_handers(self): from .views import empty_view handler = (empty_view, {}) + self.assertEqual(self.callable_resolver.resolve400(), handler) self.assertEqual(self.callable_resolver.resolve404(), handler) self.assertEqual(self.callable_resolver.resolve500(), handler) diff --git a/tests/urlpatterns_reverse/urls_error_handlers.py b/tests/urlpatterns_reverse/urls_error_handlers.py index be4f42afbc..7146fdf43c 100644 --- a/tests/urlpatterns_reverse/urls_error_handlers.py +++ b/tests/urlpatterns_reverse/urls_error_handlers.py @@ -4,5 +4,6 @@ from django.conf.urls import patterns urlpatterns = patterns('') +handler400 = 'urlpatterns_reverse.views.empty_view' handler404 = 'urlpatterns_reverse.views.empty_view' handler500 = 'urlpatterns_reverse.views.empty_view' diff --git a/tests/urlpatterns_reverse/urls_error_handlers_callables.py b/tests/urlpatterns_reverse/urls_error_handlers_callables.py index fe2d3137e9..befeccaf45 100644 --- a/tests/urlpatterns_reverse/urls_error_handlers_callables.py +++ b/tests/urlpatterns_reverse/urls_error_handlers_callables.py @@ -9,5 +9,6 @@ from .views import empty_view urlpatterns = patterns('') +handler400 = empty_view handler404 = empty_view handler500 = empty_view diff --git a/tests/utils_tests/test_baseconv.py b/tests/utils_tests/test_baseconv.py index cc413b4e8e..d69a3a6412 100644 --- a/tests/utils_tests/test_baseconv.py +++ b/tests/utils_tests/test_baseconv.py @@ -1,4 +1,4 @@ -from unittest import TestCase +from django.utils.unittest import TestCase from django.utils.baseconv import base2, base16, base36, base56, base62, base64, BaseConverter from django.utils.six.moves import xrange @@ -39,4 +39,4 @@ class TestBaseConv(TestCase): def test_exception(self): self.assertRaises(ValueError, BaseConverter, 'abc', sign='a') - self.assertTrue(isinstance(BaseConverter('abc', sign='d'), BaseConverter)) + self.assertIsInstance(BaseConverter('abc', sign='d'), BaseConverter) diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py index 090cc32d1c..b973f1c64f 100644 --- a/tests/utils_tests/test_html.py +++ b/tests/utils_tests/test_html.py @@ -5,6 +5,7 @@ import os from django.utils import html from django.utils._os import upath +from django.utils.encoding import force_text from django.utils.unittest import TestCase @@ -63,10 +64,15 @@ class TestUtilsHtml(TestCase): def test_strip_tags(self): f = html.strip_tags items = ( + ('<p>See: 'é is an apostrophe followed by e acute</p>', + 'See: 'é is an apostrophe followed by e acute'), ('<adf>a', 'a'), ('</adf>a', 'a'), ('<asdf><asdf>e', 'e'), - ('<f', '<f'), + ('hi, <f x', 'hi, <f x'), + ('234<235, right?', '234<235, right?'), + ('a4<a5 right?', 'a4<a5 right?'), + ('b7>b2!', 'b7>b2!'), ('</fe', '</fe'), ('<x>b<y>', 'b'), ('a<p onclick="alert(\'<test>\')">b</p>c', 'abc'), @@ -81,8 +87,9 @@ class TestUtilsHtml(TestCase): for filename in ('strip_tags1.html', 'strip_tags2.txt'): path = os.path.join(os.path.dirname(upath(__file__)), 'files', filename) with open(path, 'r') as fp: + content = force_text(fp.read()) start = datetime.now() - stripped = html.strip_tags(fp.read()) + stripped = html.strip_tags(content) elapsed = datetime.now() - start self.assertEqual(elapsed.seconds, 0) self.assertIn("Please try again.", stripped) diff --git a/tests/utils_tests/test_safestring.py b/tests/utils_tests/test_safestring.py new file mode 100644 index 0000000000..a6f0c0a01c --- /dev/null +++ b/tests/utils_tests/test_safestring.py @@ -0,0 +1,53 @@ +from __future__ import absolute_import, unicode_literals + + +from django.template import Template, Context +from django.test import TestCase +from django.utils.encoding import force_text, force_bytes +from django.utils.functional import lazy, Promise +from django.utils.html import escape, conditional_escape +from django.utils.safestring import mark_safe, mark_for_escaping +from django.utils import six +from django.utils import translation + +lazystr = lazy(force_text, six.text_type) +lazybytes = lazy(force_bytes, bytes) + + +class SafeStringTest(TestCase): + def assertRenderEqual(self, tpl, expected, **context): + context = Context(context) + tpl = Template(tpl) + self.assertEqual(tpl.render(context), expected) + + def test_mark_safe(self): + s = mark_safe('a&b') + + self.assertRenderEqual('{{ s }}', 'a&b', s=s) + self.assertRenderEqual('{{ s|force_escape }}', 'a&b', s=s) + + def test_mark_safe_lazy(self): + s = lazystr('a&b') + b = lazybytes(b'a&b') + + self.assertIsInstance(mark_safe(s), Promise) + self.assertIsInstance(mark_safe(b), Promise) + self.assertRenderEqual('{{ s }}', 'a&b', s=mark_safe(s)) + + def test_mark_for_escaping(self): + s = mark_for_escaping('a&b') + self.assertRenderEqual('{{ s }}', 'a&b', s=s) + self.assertRenderEqual('{{ s }}', 'a&b', s=mark_for_escaping(s)) + + def test_mark_for_escaping_lazy(self): + s = lazystr('a&b') + b = lazybytes(b'a&b') + + self.assertIsInstance(mark_for_escaping(s), Promise) + self.assertIsInstance(mark_for_escaping(b), Promise) + self.assertRenderEqual('{% autoescape off %}{{ s }}{% endautoescape %}', 'a&b', s=mark_for_escaping(s)) + + def test_regression_20296(self): + s = mark_safe(translation.ugettext_lazy("username")) + with translation.override('fr'): + self.assertRenderEqual('{{ s }}', "nom d'utilisateur", s=s) diff --git a/tests/utils_tests/test_simplelazyobject.py b/tests/utils_tests/test_simplelazyobject.py index f925e01eb6..4c01bd3adf 100644 --- a/tests/utils_tests/test_simplelazyobject.py +++ b/tests/utils_tests/test_simplelazyobject.py @@ -68,7 +68,7 @@ class TestUtilsSimpleLazyObject(TestCase): # Second, for an evaluated SimpleLazyObject name = x.name # evaluate - self.assertTrue(isinstance(x._wrapped, _ComplexObject)) + self.assertIsInstance(x._wrapped, _ComplexObject) # __repr__ contains __repr__ of wrapped object self.assertEqual("<SimpleLazyObject: %r>" % x._wrapped, repr(x)) @@ -161,3 +161,25 @@ class TestUtilsSimpleLazyObject(TestCase): self.assertNotEqual(lazy1, lazy3) self.assertTrue(lazy1 != lazy3) self.assertFalse(lazy1 != lazy2) + + def test_pickle_py2_regression(self): + from django.contrib.auth.models import User + + # See ticket #20212 + user = User.objects.create_user('johndoe', 'john@example.com', 'pass') + x = SimpleLazyObject(lambda: user) + + # This would fail with "TypeError: can't pickle instancemethod objects", + # only on Python 2.X. + pickled = pickle.dumps(x) + + # Try the variant protocol levels. + pickled = pickle.dumps(x, 0) + pickled = pickle.dumps(x, 1) + pickled = pickle.dumps(x, 2) + + if not six.PY3: + import cPickle + + # This would fail with "TypeError: expected string or Unicode object, NoneType found". + pickled = cPickle.dumps(x) diff --git a/tests/utils_tests/test_timesince.py b/tests/utils_tests/test_timesince.py index 5e641a42c4..cdb95e6877 100644 --- a/tests/utils_tests/test_timesince.py +++ b/tests/utils_tests/test_timesince.py @@ -21,32 +21,33 @@ class TimesinceTests(unittest.TestCase): def test_equal_datetimes(self): """ equal datetimes. """ - self.assertEqual(timesince(self.t, self.t), '0 minutes') + # NOTE: \xa0 avoids wrapping between value and unit + self.assertEqual(timesince(self.t, self.t), '0\xa0minutes') def test_ignore_microseconds_and_seconds(self): """ Microseconds and seconds are ignored. """ self.assertEqual(timesince(self.t, self.t+self.onemicrosecond), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t+self.onesecond), - '0 minutes') + '0\xa0minutes') def test_other_units(self): """ Test other units. """ self.assertEqual(timesince(self.t, self.t+self.oneminute), - '1 minute') - self.assertEqual(timesince(self.t, self.t+self.onehour), '1 hour') - self.assertEqual(timesince(self.t, self.t+self.oneday), '1 day') - self.assertEqual(timesince(self.t, self.t+self.oneweek), '1 week') + '1\xa0minute') + self.assertEqual(timesince(self.t, self.t+self.onehour), '1\xa0hour') + self.assertEqual(timesince(self.t, self.t+self.oneday), '1\xa0day') + self.assertEqual(timesince(self.t, self.t+self.oneweek), '1\xa0week') self.assertEqual(timesince(self.t, self.t+self.onemonth), - '1 month') - self.assertEqual(timesince(self.t, self.t+self.oneyear), '1 year') + '1\xa0month') + self.assertEqual(timesince(self.t, self.t+self.oneyear), '1\xa0year') def test_multiple_units(self): """ Test multiple units. """ self.assertEqual(timesince(self.t, - self.t+2*self.oneday+6*self.onehour), '2 days, 6 hours') + self.t+2*self.oneday+6*self.onehour), '2\xa0days, 6\xa0hours') self.assertEqual(timesince(self.t, - self.t+2*self.oneweek+2*self.oneday), '2 weeks, 2 days') + self.t+2*self.oneweek+2*self.oneday), '2\xa0weeks, 2\xa0days') def test_display_first_unit(self): """ @@ -55,10 +56,10 @@ class TimesinceTests(unittest.TestCase): """ self.assertEqual(timesince(self.t, self.t+2*self.oneweek+3*self.onehour+4*self.oneminute), - '2 weeks') + '2\xa0weeks') self.assertEqual(timesince(self.t, - self.t+4*self.oneday+5*self.oneminute), '4 days') + self.t+4*self.oneday+5*self.oneminute), '4\xa0days') def test_display_second_before_first(self): """ @@ -66,30 +67,30 @@ class TimesinceTests(unittest.TestCase): get 0 minutes. """ self.assertEqual(timesince(self.t, self.t-self.onemicrosecond), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.onesecond), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneminute), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.onehour), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneday), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneweek), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.onemonth), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneyear), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, - self.t-2*self.oneday-6*self.onehour), '0 minutes') + self.t-2*self.oneday-6*self.onehour), '0\xa0minutes') self.assertEqual(timesince(self.t, - self.t-2*self.oneweek-2*self.oneday), '0 minutes') + self.t-2*self.oneweek-2*self.oneday), '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-2*self.oneweek-3*self.onehour-4*self.oneminute), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, - self.t-4*self.oneday-5*self.oneminute), '0 minutes') + self.t-4*self.oneday-5*self.oneminute), '0\xa0minutes') def test_different_timezones(self): """ When using two different timezones. """ @@ -97,28 +98,28 @@ class TimesinceTests(unittest.TestCase): now_tz = datetime.datetime.now(LocalTimezone(now)) now_tz_i = datetime.datetime.now(FixedOffset((3 * 60) + 15)) - self.assertEqual(timesince(now), '0 minutes') - self.assertEqual(timesince(now_tz), '0 minutes') - self.assertEqual(timeuntil(now_tz, now_tz_i), '0 minutes') + self.assertEqual(timesince(now), '0\xa0minutes') + self.assertEqual(timesince(now_tz), '0\xa0minutes') + self.assertEqual(timeuntil(now_tz, now_tz_i), '0\xa0minutes') def test_date_objects(self): """ Both timesince and timeuntil should work on date objects (#17937). """ today = datetime.date.today() - self.assertEqual(timesince(today + self.oneday), '0 minutes') - self.assertEqual(timeuntil(today - self.oneday), '0 minutes') + self.assertEqual(timesince(today + self.oneday), '0\xa0minutes') + self.assertEqual(timeuntil(today - self.oneday), '0\xa0minutes') def test_both_date_objects(self): """ Timesince should work with both date objects (#9672) """ today = datetime.date.today() - self.assertEqual(timeuntil(today + self.oneday, today), '1 day') - self.assertEqual(timeuntil(today - self.oneday, today), '0 minutes') - self.assertEqual(timeuntil(today + self.oneweek, today), '1 week') + self.assertEqual(timeuntil(today + self.oneday, today), '1\xa0day') + self.assertEqual(timeuntil(today - self.oneday, today), '0\xa0minutes') + self.assertEqual(timeuntil(today + self.oneweek, today), '1\xa0week') def test_naive_datetime_with_tzinfo_attribute(self): class naive(datetime.tzinfo): def utcoffset(self, dt): return None future = datetime.datetime(2080, 1, 1, tzinfo=naive()) - self.assertEqual(timesince(future), '0 minutes') + self.assertEqual(timesince(future), '0\xa0minutes') past = datetime.datetime(1980, 1, 1, tzinfo=naive()) - self.assertEqual(timeuntil(past), '0 minutes') + self.assertEqual(timeuntil(past), '0\xa0minutes') diff --git a/tests/validation/models.py b/tests/validation/models.py index db083290fb..e95a1e0744 100644 --- a/tests/validation/models.py +++ b/tests/validation/models.py @@ -95,7 +95,7 @@ class GenericIPAddressTestModel(models.Model): blank=True, null=True) class GenericIPAddrUnpackUniqueTest(models.Model): - generic_v4unpack_ip = models.GenericIPAddressField(blank=True, unique=True, unpack_ipv4=True) + generic_v4unpack_ip = models.GenericIPAddressField(null=True, blank=True, unique=True, unpack_ipv4=True) # A model can't have multiple AutoFields diff --git a/tests/validation/tests.py b/tests/validation/tests.py index b571e0c298..9ddf796c2b 100644 --- a/tests/validation/tests.py +++ b/tests/validation/tests.py @@ -109,9 +109,9 @@ class GenericIPAddressFieldTests(ValidationTestCase): def test_correct_generic_ip_passes(self): giptm = GenericIPAddressTestModel(generic_ip="1.2.3.4") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) giptm = GenericIPAddressTestModel(generic_ip="2001::2") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) def test_invalid_generic_ip_raises_error(self): giptm = GenericIPAddressTestModel(generic_ip="294.4.2.1") @@ -121,7 +121,7 @@ class GenericIPAddressFieldTests(ValidationTestCase): def test_correct_v4_ip_passes(self): giptm = GenericIPAddressTestModel(v4_ip="1.2.3.4") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) def test_invalid_v4_ip_raises_error(self): giptm = GenericIPAddressTestModel(v4_ip="294.4.2.1") @@ -131,7 +131,7 @@ class GenericIPAddressFieldTests(ValidationTestCase): def test_correct_v6_ip_passes(self): giptm = GenericIPAddressTestModel(v6_ip="2001::2") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) def test_invalid_v6_ip_raises_error(self): giptm = GenericIPAddressTestModel(v6_ip="1.2.3.4") @@ -151,10 +151,16 @@ class GenericIPAddressFieldTests(ValidationTestCase): giptm = GenericIPAddressTestModel(generic_ip="::ffff:10.10.10.10") giptm.save() giptm = GenericIPAddressTestModel(generic_ip="10.10.10.10") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) # These two are the same, because we are doing IPv4 unpacking giptm = GenericIPAddrUnpackUniqueTest(generic_v4unpack_ip="::ffff:18.52.18.52") giptm.save() giptm = GenericIPAddrUnpackUniqueTest(generic_v4unpack_ip="18.52.18.52") self.assertFailsValidation(giptm.full_clean, ['generic_v4unpack_ip',]) + + def test_empty_generic_ip_passes(self): + giptm = GenericIPAddressTestModel(generic_ip="") + self.assertIsNone(giptm.full_clean()) + giptm = GenericIPAddressTestModel(generic_ip=None) + self.assertIsNone(giptm.full_clean()) diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index b44cd88abe..f686eee0e0 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -5,7 +5,9 @@ from __future__ import absolute_import, unicode_literals import inspect import os +import shutil import sys +from tempfile import NamedTemporaryFile, mkdtemp, mkstemp from django.core import mail from django.core.files.uploadedfile import SimpleUploadedFile @@ -13,13 +15,14 @@ from django.core.urlresolvers import reverse from django.test import TestCase, RequestFactory from django.test.utils import (override_settings, setup_test_template_loader, restore_template_loaders) -from django.utils.encoding import force_text +from django.utils.encoding import force_text, force_bytes from django.views.debug import ExceptionReporter from .. import BrokenException, except_args from ..views import (sensitive_view, non_sensitive_view, paranoid_view, custom_exception_reporter_filter_view, sensitive_method_view, sensitive_args_function_caller, sensitive_kwargs_function_caller) +from django.utils.unittest import skipIf @override_settings(DEBUG=True, TEMPLATE_DEBUG=True) @@ -77,9 +80,38 @@ class DebugViewTests(TestCase): raising_loc) def test_template_loader_postmortem(self): - response = self.client.get(reverse('raises_template_does_not_exist')) - template_path = os.path.join('templates', 'i_dont_exist.html') - self.assertContains(response, template_path, status_code=500) + """Tests for not existing file""" + template_name = "notfound.html" + with NamedTemporaryFile(prefix=template_name) as tempfile: + tempdir = os.path.dirname(tempfile.name) + template_path = os.path.join(tempdir, template_name) + with override_settings(TEMPLATE_DIRS=(tempdir,)): + response = self.client.get(reverse('raises_template_does_not_exist', kwargs={"path": template_name})) + self.assertContains(response, "%s (File does not exist)" % template_path, status_code=500, count=1) + + @skipIf(sys.platform == "win32", "Python on Windows doesn't have working os.chmod() and os.access().") + def test_template_loader_postmortem_notreadable(self): + """Tests for not readable file""" + with NamedTemporaryFile() as tempfile: + template_name = tempfile.name + tempdir = os.path.dirname(tempfile.name) + template_path = os.path.join(tempdir, template_name) + os.chmod(template_path, 0o0222) + with override_settings(TEMPLATE_DIRS=(tempdir,)): + response = self.client.get(reverse('raises_template_does_not_exist', kwargs={"path": template_name})) + self.assertContains(response, "%s (File is not readable)" % template_path, status_code=500, count=1) + + def test_template_loader_postmortem_notafile(self): + """Tests for not being a file""" + try: + template_path = mkdtemp() + template_name = os.path.basename(template_path) + tempdir = os.path.dirname(template_path) + with override_settings(TEMPLATE_DIRS=(tempdir,)): + response = self.client.get(reverse('raises_template_does_not_exist', kwargs={"path": template_name})) + self.assertContains(response, "%s (Not a file)" % template_path, status_code=500, count=1) + finally: + shutil.rmtree(template_path) class ExceptionReporterTests(TestCase): @@ -122,13 +154,31 @@ class ExceptionReporterTests(TestCase): self.assertIn('<h2>Request information</h2>', html) self.assertIn('<p>Request data not supplied</p>', html) + def test_eol_support(self): + """Test that the ExceptionReporter supports Unix, Windows and Macintosh EOL markers""" + LINES = list('print %d' % i for i in range(1, 6)) + reporter = ExceptionReporter(None, None, None, None) + + for newline in ['\n', '\r\n', '\r']: + fd, filename = mkstemp(text=False) + os.write(fd, force_bytes(newline.join(LINES)+newline)) + os.close(fd) + + try: + self.assertEqual( + reporter._get_lines_from_file(filename, 3, 2), + (1, LINES[1:3], LINES[3], LINES[4:]) + ) + finally: + os.unlink(filename) + def test_no_exception(self): "An exception report can be generated for just a request" request = self.rf.get('/test_view/') reporter = ExceptionReporter(request, None, None, None) html = reporter.get_traceback_html() self.assertIn('<h1>Report at /test_view/</h1>', html) - self.assertIn('<pre class="exception_value">No exception supplied</pre>', html) + self.assertIn('<pre class="exception_value">No exception message supplied</pre>', html) self.assertIn('<th>Request Method:</th>', html) self.assertIn('<th>Request URL:</th>', html) self.assertNotIn('<th>Exception Type:</th>', html) diff --git a/tests/view_tests/tests/test_static.py b/tests/view_tests/tests/test_static.py index bdd9fbfc0b..6104ad063e 100644 --- a/tests/view_tests/tests/test_static.py +++ b/tests/view_tests/tests/test_static.py @@ -60,7 +60,7 @@ class StaticTests(TestCase): # This is 24h before max Unix time. Remember to fix Django and # update this test well before 2038 :) ) - self.assertTrue(isinstance(response, HttpResponseNotModified)) + self.assertIsInstance(response, HttpResponseNotModified) def test_invalid_if_modified_since(self): """Handle bogus If-Modified-Since values gracefully diff --git a/tests/view_tests/urls.py b/tests/view_tests/urls.py index 52e2eb474e..d792e47ddf 100644 --- a/tests/view_tests/urls.py +++ b/tests/view_tests/urls.py @@ -66,5 +66,5 @@ urlpatterns = patterns('', urlpatterns += patterns('view_tests.views', url(r'view_exception/(?P<n>\d+)/$', 'view_exception', name='view_exception'), url(r'template_exception/(?P<n>\d+)/$', 'template_exception', name='template_exception'), - url(r'^raises_template_does_not_exist/$', 'raises_template_does_not_exist', name='raises_template_does_not_exist'), + url(r'^raises_template_does_not_exist/(?P<path>.+)$', 'raises_template_does_not_exist', name='raises_template_does_not_exist'), ) diff --git a/tests/view_tests/views.py b/tests/view_tests/views.py index 50ad98ac2d..1cfafa4333 100644 --- a/tests/view_tests/views.py +++ b/tests/view_tests/views.py @@ -112,11 +112,11 @@ def render_view_with_current_app_conflict(request): 'bar': 'BAR', }, current_app="foobar_app", context_instance=RequestContext(request)) -def raises_template_does_not_exist(request): +def raises_template_does_not_exist(request, path='i_dont_exist.html'): # We need to inspect the HTML generated by the fancy 500 debug view but # the test client ignores it, so we send it explicitly. try: - return render_to_response('i_dont_exist.html') + return render_to_response(path) except TemplateDoesNotExist: return technical_500_response(request, *sys.exc_info()) |
