summaryrefslogtreecommitdiff
path: root/tests/regressiontests
diff options
context:
space:
mode:
Diffstat (limited to 'tests/regressiontests')
-rw-r--r--tests/regressiontests/admin_changelist/tests.py46
-rw-r--r--tests/regressiontests/admin_scripts/tests.py2
-rw-r--r--tests/regressiontests/admin_views/models.py6
-rw-r--r--tests/regressiontests/admin_views/tests.py113
-rw-r--r--tests/regressiontests/admin_widgets/tests.py12
-rw-r--r--tests/regressiontests/aggregation_regress/models.py323
-rw-r--r--tests/regressiontests/aggregation_regress/tests.py620
-rw-r--r--tests/regressiontests/backends/models.py36
-rw-r--r--tests/regressiontests/backends/tests.py133
-rw-r--r--tests/regressiontests/cache/liberal_backend.py9
-rw-r--r--tests/regressiontests/cache/tests.py91
-rw-r--r--tests/regressiontests/csrf_tests/tests.py55
-rw-r--r--tests/regressiontests/defaultfilters/tests.py9
-rw-r--r--tests/regressiontests/file_storage/tests.py16
-rw-r--r--tests/regressiontests/forms/fields.py16
-rw-r--r--tests/regressiontests/forms/forms.py52
-rw-r--r--tests/regressiontests/forms/input_formats.py894
-rw-r--r--tests/regressiontests/forms/localflavor/au.py2
-rw-r--r--tests/regressiontests/forms/models.py100
-rw-r--r--tests/regressiontests/forms/tests.py2
-rw-r--r--tests/regressiontests/forms/widgets.py51
-rw-r--r--tests/regressiontests/httpwrappers/tests.py711
-rw-r--r--tests/regressiontests/i18n/models.py6
-rw-r--r--tests/regressiontests/i18n/tests.py33
-rw-r--r--tests/regressiontests/localflavor/models.py8
-rw-r--r--tests/regressiontests/localflavor/tests.py84
-rw-r--r--tests/regressiontests/localflavor/us/__init__.py0
-rw-r--r--tests/regressiontests/localflavor/us/forms.py (renamed from tests/regressiontests/localflavor/forms.py)6
-rw-r--r--tests/regressiontests/localflavor/us/models.py13
-rw-r--r--tests/regressiontests/localflavor/us/tests.py82
-rw-r--r--tests/regressiontests/m2m_through_regress/models.py158
-rw-r--r--tests/regressiontests/m2m_through_regress/tests.py128
-rw-r--r--tests/regressiontests/model_forms_regress/models.py3
-rw-r--r--tests/regressiontests/model_forms_regress/tests.py123
-rw-r--r--tests/regressiontests/model_formsets_regress/tests.py62
-rw-r--r--tests/regressiontests/multiple_database/fixtures/pets.json18
-rw-r--r--tests/regressiontests/multiple_database/tests.py171
-rw-r--r--tests/regressiontests/requests/tests.py25
-rw-r--r--tests/regressiontests/serializers_regress/tests.py71
-rw-r--r--tests/regressiontests/templates/filters.py9
-rw-r--r--tests/regressiontests/templates/loaders.py42
-rw-r--r--tests/regressiontests/templates/tests.py34
-rw-r--r--tests/regressiontests/test_client_regress/models.py48
-rw-r--r--tests/regressiontests/test_client_regress/urls.py2
-rw-r--r--tests/regressiontests/test_client_regress/views.py5
-rw-r--r--tests/regressiontests/urlpatterns_reverse/included_named_urls.py10
-rw-r--r--tests/regressiontests/urlpatterns_reverse/included_named_urls2.py9
-rw-r--r--tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py7
-rw-r--r--tests/regressiontests/urlpatterns_reverse/named_urls.py9
-rw-r--r--tests/regressiontests/urlpatterns_reverse/namespace_urls.py9
-rw-r--r--tests/regressiontests/urlpatterns_reverse/tests.py127
-rw-r--r--tests/regressiontests/urlpatterns_reverse/urls_without_full_import.py10
-rw-r--r--tests/regressiontests/urlpatterns_reverse/views.py17
-rw-r--r--tests/regressiontests/utils/timesince.py14
-rw-r--r--tests/regressiontests/views/tests/__init__.py1
-rw-r--r--tests/regressiontests/views/tests/generic/simple.py38
-rw-r--r--tests/regressiontests/views/urls.py10
57 files changed, 3406 insertions, 1285 deletions
diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py
index b70d7c51f4..c8ad1ce8f6 100644
--- a/tests/regressiontests/admin_changelist/tests.py
+++ b/tests/regressiontests/admin_changelist/tests.py
@@ -1,10 +1,10 @@
-import unittest
from django.contrib import admin
from django.contrib.admin.views.main import ChangeList
from django.template import Context, Template
+from django.test import TransactionTestCase
from regressiontests.admin_changelist.models import Child, Parent
-class ChangeListTests(unittest.TestCase):
+class ChangeListTests(TransactionTestCase):
def test_select_related_preserved(self):
"""
Regression test for #10348: ChangeList.get_query_set() shouldn't
@@ -18,9 +18,8 @@ class ChangeListTests(unittest.TestCase):
def test_result_list_html(self):
"""
- Regression test for #11791: Inclusion tag result_list generates a
- table and this checks that the items are nested within the table
- element tags.
+ Verifies that inclusion tag result_list generates a table when with
+ default ModelAdmin settings.
"""
new_parent = Parent.objects.create(name='parent')
new_child = Child.objects.create(name='name', parent=new_parent)
@@ -29,16 +28,27 @@ class ChangeListTests(unittest.TestCase):
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_editable, m)
- FormSet = m.get_changelist_formset(request)
- cl.formset = FormSet(queryset=cl.result_list)
+ cl.formset = None
template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
context = Context({'cl': cl})
table_output = template.render(context)
- hidden_input_elem = '<input type="hidden" name="form-0-id" value="1" id="id_form-0-id" />'
- self.failIf(table_output.find(hidden_input_elem) == -1,
- 'Failed to find expected hidden input element in: %s' % table_output)
- self.failIf(table_output.find('<td>%s</td>' % hidden_input_elem) == -1,
- 'Hidden input element is not enclosed in <td> element.')
+ row_html = '<tbody><tr class="row1"><td><input type="checkbox" class="action-select" value="1" name="_selected_action" /></td><th><a href="1/">name</a></th><td>Parent object</td></tr></tbody>'
+ self.failIf(table_output.find(row_html) == -1,
+ 'Failed to find expected row element: %s' % table_output)
+
+ def test_result_list_editable_html(self):
+ """
+ Regression tests for #11791: Inclusion tag result_list generates a
+ table and this checks that the items are nested within the table
+ element tags.
+ Also a regression test for #13599, verifies that hidden fields
+ when list_editable is enabled are rendered in a div outside the
+ table.
+ """
+ new_parent = Parent.objects.create(name='parent')
+ new_child = Child.objects.create(name='name', parent=new_parent)
+ request = MockRequest()
+ m = ChildAdmin(Child, admin.site)
# Test with list_editable fields
m.list_display = ['id', 'name', 'parent']
@@ -52,10 +62,14 @@ class ChangeListTests(unittest.TestCase):
template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
context = Context({'cl': cl})
table_output = template.render(context)
- self.failIf(table_output.find(hidden_input_elem) == -1,
- 'Failed to find expected hidden input element in: %s' % table_output)
- self.failIf(table_output.find('<td>%s</td>' % hidden_input_elem) == -1,
- 'Hidden input element is not enclosed in <td> element.')
+ # make sure that hidden fields are in the correct place
+ hiddenfields_div = '<div class="hiddenfields"><input type="hidden" name="form-0-id" value="1" id="id_form-0-id" /></div>'
+ self.failIf(table_output.find(hiddenfields_div) == -1,
+ 'Failed to find hidden fields in: %s' % table_output)
+ # make sure that list editable fields are rendered in divs correctly
+ editable_name_field = '<input name="form-0-name" value="name" class="vTextField" maxlength="30" type="text" id="id_form-0-name" />'
+ self.failIf('<td>%s</td>' % editable_name_field == -1,
+ 'Failed to find "name" list_editable field in: %s' % table_output)
class ChildAdmin(admin.ModelAdmin):
list_display = ['name', 'parent']
diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py
index 7ec2454561..3dd8ad5d13 100644
--- a/tests/regressiontests/admin_scripts/tests.py
+++ b/tests/regressiontests/admin_scripts/tests.py
@@ -133,7 +133,7 @@ class AdminScriptTestCase(unittest.TestCase):
return out, err
def run_django_admin(self, args, settings_file=None):
- bin_dir = os.path.dirname(bin.__file__)
+ bin_dir = os.path.abspath(os.path.dirname(bin.__file__))
return self.run_test(os.path.join(bin_dir,'django-admin.py'), args, settings_file)
def run_manage(self, args, settings_file=None):
diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
index a2700ba747..b25a9b9a96 100644
--- a/tests/regressiontests/admin_views/models.py
+++ b/tests/regressiontests/admin_views/models.py
@@ -10,6 +10,7 @@ from django.core.mail import EmailMessage
from django.db import models
from django import forms
from django.forms.models import BaseModelFormSet
+from django.contrib.auth.models import User
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
@@ -579,6 +580,10 @@ class Pizza(models.Model):
class PizzaAdmin(admin.ModelAdmin):
readonly_fields = ('toppings',)
+class Album(models.Model):
+ owner = models.ForeignKey(User)
+ title = models.CharField(max_length=30)
+
admin.site.register(Article, ArticleAdmin)
admin.site.register(CustomArticle, CustomArticleAdmin)
admin.site.register(Section, save_as=True, inlines=[ArticleInline])
@@ -625,3 +630,4 @@ admin.site.register(Promo)
admin.site.register(ChapterXtra1)
admin.site.register(Pizza, PizzaAdmin)
admin.site.register(Topping)
+admin.site.register(Album)
diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
index 1385e5e0aa..725369a5b1 100644
--- a/tests/regressiontests/admin_views/tests.py
+++ b/tests/regressiontests/admin_views/tests.py
@@ -604,6 +604,28 @@ class AdminViewPermissionsTest(TestCase):
'Plural error message not found in response to post with multiple errors.')
self.client.get('/test_admin/admin/logout/')
+ def testConditionallyShowAddSectionLink(self):
+ """
+ The foreign key widget should only show the "add related" button if the
+ user has permission to add that related item.
+ """
+ # Set up and log in user.
+ url = '/test_admin/admin/admin_views/article/add/'
+ add_link_text = ' class="add-another"'
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.adduser_login)
+ # The add user can't add sections yet, so they shouldn't see the "add
+ # section" link.
+ response = self.client.get(url)
+ self.assertNotContains(response, add_link_text)
+ # Allow the add user to add sections too. Now they can see the "add
+ # section" link.
+ add_user = User.objects.get(username='adduser')
+ perm = get_perm(Section, Section._meta.get_add_permission())
+ add_user.user_permissions.add(perm)
+ response = self.client.get(url)
+ self.assertContains(response, add_link_text)
+
def testCustomModelAdminTemplates(self):
self.client.get('/test_admin/admin/')
self.client.post('/test_admin/admin/', self.super_login)
@@ -1477,6 +1499,21 @@ class AdminActionsTest(TestCase):
response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
self.failUnlessEqual(response.status_code, 302)
+ def test_default_redirect(self):
+ """
+ Test that actions which don't return an HttpResponse are redirected to
+ the same page, retaining the querystring (which may contain changelist
+ information).
+ """
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1],
+ 'action' : 'external_mail',
+ 'index': 0,
+ }
+ url = '/test_admin/admin/admin_views/externalsubscriber/?ot=asc&o=1'
+ response = self.client.post(url, action_data)
+ self.assertRedirects(response, url)
+
def test_model_without_action(self):
"Tests a ModelAdmin without any action"
response = self.client.get('/test_admin/admin/admin_views/oldsubscriber/')
@@ -2113,11 +2150,9 @@ class ReadonlyTest(TestCase):
response = self.client.get('/test_admin/admin/admin_views/pizza/add/')
self.assertEqual(response.status_code, 200)
-class IncompleteFormTest(TestCase):
+class UserAdminTest(TestCase):
"""
- Tests validation of a ModelForm that doesn't explicitly have all data
- corresponding to model fields. Model validation shouldn't fail
- such a forms.
+ Tests user CRUD functionality.
"""
fixtures = ['admin-views-users.xml']
@@ -2128,6 +2163,7 @@ class IncompleteFormTest(TestCase):
self.client.logout()
def test_user_creation(self):
+ user_count = User.objects.count()
response = self.client.post('/test_admin/admin/auth/user/add/', {
'username': 'newuser',
'password1': 'newpassword',
@@ -2136,6 +2172,7 @@ class IncompleteFormTest(TestCase):
})
new_user = User.objects.order_by('-id')[0]
self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk)
+ self.assertEquals(User.objects.count(), user_count + 1)
self.assertNotEquals(new_user.password, UNUSABLE_PASSWORD)
def test_password_mismatch(self):
@@ -2149,3 +2186,71 @@ class IncompleteFormTest(TestCase):
self.assert_('password' not in adminform.form.errors)
self.assertEquals(adminform.form.errors['password2'],
[u"The two password fields didn't match."])
+
+ def test_user_fk_popup(self):
+ response = self.client.get('/test_admin/admin/admin_views/album/add/')
+ self.failUnlessEqual(response.status_code, 200)
+ self.assertContains(response, '/test_admin/admin/auth/user/add')
+ self.assertContains(response, 'class="add-another" id="add_id_owner" onclick="return showAddAnotherPopup(this);"')
+ response = self.client.get('/test_admin/admin/auth/user/add/?_popup=1')
+ self.assertNotContains(response, 'name="_continue"')
+
+ def test_user_add_another(self):
+ user_count = User.objects.count()
+ response = self.client.post('/test_admin/admin/auth/user/add/', {
+ 'username': 'newuser',
+ 'password1': 'newpassword',
+ 'password2': 'newpassword',
+ '_addanother': '1',
+ })
+ new_user = User.objects.order_by('-id')[0]
+ self.assertRedirects(response, '/test_admin/admin/auth/user/add/')
+ self.assertEquals(User.objects.count(), user_count + 1)
+ self.assertNotEquals(new_user.password, UNUSABLE_PASSWORD)
+
+try:
+ # If docutils isn't installed, skip the AdminDocs tests.
+ import docutils
+
+ class AdminDocsTest(TestCase):
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_tags(self):
+ response = self.client.get('/test_admin/admin/doc/tags/')
+
+ # The builtin tag group exists
+ self.assertContains(response, "<h2>Built-in tags</h2>", count=2)
+
+ # A builtin tag exists in both the index and detail
+ self.assertContains(response, '<h3 id="built_in-autoescape">autoescape</h3>')
+ self.assertContains(response, '<li><a href="#built_in-autoescape">autoescape</a></li>')
+
+ # An app tag exists in both the index and detail
+ self.assertContains(response, '<h3 id="flatpages-get_flatpages">get_flatpages</h3>')
+ self.assertContains(response, '<li><a href="#flatpages-get_flatpages">get_flatpages</a></li>')
+
+ # The admin list tag group exists
+ self.assertContains(response, "<h2>admin_list</h2>", count=2)
+
+ # An admin list tag exists in both the index and detail
+ self.assertContains(response, '<h3 id="admin_list-admin_actions">admin_actions</h3>')
+ self.assertContains(response, '<li><a href="#admin_list-admin_actions">admin_actions</a></li>')
+
+ def test_filters(self):
+ response = self.client.get('/test_admin/admin/doc/filters/')
+
+ # The builtin filter group exists
+ self.assertContains(response, "<h2>Built-in filters</h2>", count=2)
+
+ # A builtin filter exists in both the index and detail
+ self.assertContains(response, '<h3 id="built_in-add">add</h3>')
+ self.assertContains(response, '<li><a href="#built_in-add">add</a></li>')
+
+except ImportError:
+ pass
diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
index fd0c25ca1a..c445644335 100644
--- a/tests/regressiontests/admin_widgets/tests.py
+++ b/tests/regressiontests/admin_widgets/tests.py
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
from django import forms
from django.contrib import admin
from django.contrib.admin import widgets
@@ -151,3 +153,13 @@ class AdminForeignKeyRawIdWidget(DjangoTestCase):
post_data)
self.assertContains(response,
'Select a valid choice. That choice is not one of the available choices.')
+
+ def test_invalid_target_id(self):
+
+ for test_str in ('Iñtërnâtiônàlizætiøn', "1234'", -1234):
+ # This should result in an error message, not a server exception.
+ response = self.client.post('%s/admin_widgets/event/add/' % self.admin_root,
+ {"band": test_str})
+
+ self.assertContains(response,
+ 'Select a valid choice. That choice is not one of the available choices.')
diff --git a/tests/regressiontests/aggregation_regress/models.py b/tests/regressiontests/aggregation_regress/models.py
index ba74357534..783c21956a 100644
--- a/tests/regressiontests/aggregation_regress/models.py
+++ b/tests/regressiontests/aggregation_regress/models.py
@@ -4,6 +4,7 @@ import pickle
from django.db import connection, models, DEFAULT_DB_ALIAS
from django.conf import settings
+
class Author(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()
@@ -12,6 +13,7 @@ class Author(models.Model):
def __unicode__(self):
return self.name
+
class Publisher(models.Model):
name = models.CharField(max_length=255)
num_awards = models.IntegerField()
@@ -19,6 +21,7 @@ class Publisher(models.Model):
def __unicode__(self):
return self.name
+
class Book(models.Model):
isbn = models.CharField(max_length=9)
name = models.CharField(max_length=255)
@@ -36,6 +39,7 @@ class Book(models.Model):
def __unicode__(self):
return self.name
+
class Store(models.Model):
name = models.CharField(max_length=255)
books = models.ManyToManyField(Book)
@@ -45,334 +49,21 @@ class Store(models.Model):
def __unicode__(self):
return self.name
+
class Entries(models.Model):
EntryID = models.AutoField(primary_key=True, db_column='Entry ID')
Entry = models.CharField(unique=True, max_length=50)
Exclude = models.BooleanField()
+
class Clues(models.Model):
ID = models.AutoField(primary_key=True)
EntryID = models.ForeignKey(Entries, verbose_name='Entry', db_column = 'Entry ID')
Clue = models.CharField(max_length=150)
+
class HardbackBook(Book):
weight = models.FloatField()
def __unicode__(self):
return "%s (hardback): %s" % (self.name, self.weight)
-
-__test__ = {'API_TESTS': """
->>> from django.core import management
->>> from django.db.models import get_app, F
-
-# Reset the database representation of this app.
-# This will return the database to a clean initial state.
->>> management.call_command('flush', verbosity=0, interactive=False)
-
->>> from django.db.models import Avg, Sum, Count, Max, Min, StdDev, Variance
-
-# Ordering requests are ignored
->>> Author.objects.all().order_by('name').aggregate(Avg('age'))
-{'age__avg': 37.4...}
-
-# Implicit ordering is also ignored
->>> Book.objects.all().aggregate(Sum('pages'))
-{'pages__sum': 3703}
-
-# Baseline results
->>> Book.objects.all().aggregate(Sum('pages'), Avg('pages'))
-{'pages__sum': 3703, 'pages__avg': 617.1...}
-
-# Empty values query doesn't affect grouping or results
->>> Book.objects.all().values().aggregate(Sum('pages'), Avg('pages'))
-{'pages__sum': 3703, 'pages__avg': 617.1...}
-
-# Aggregate overrides extra selected column
->>> Book.objects.all().extra(select={'price_per_page' : 'price / pages'}).aggregate(Sum('pages'))
-{'pages__sum': 3703}
-
-# Annotations get combined with extra select clauses
->>> sorted((k,v) for k,v in Book.objects.all().annotate(mean_auth_age=Avg('authors__age')).extra(select={'manufacture_cost' : 'price * .5'}).get(pk=2).__dict__.items() if k != '_state')
-[('contact_id', 3), ('id', 2), ('isbn', u'067232959'), ('manufacture_cost', ...11.545...), ('mean_auth_age', 45.0), ('name', u'Sams Teach Yourself Django in 24 Hours'), ('pages', 528), ('price', Decimal("23.09")), ('pubdate', datetime.date(2008, 3, 3)), ('publisher_id', 2), ('rating', 3.0)]
-
-# Order of the annotate/extra in the query doesn't matter
->>> sorted((k,v) for k,v in Book.objects.all().extra(select={'manufacture_cost' : 'price * .5'}).annotate(mean_auth_age=Avg('authors__age')).get(pk=2).__dict__.items()if k != '_state')
-[('contact_id', 3), ('id', 2), ('isbn', u'067232959'), ('manufacture_cost', ...11.545...), ('mean_auth_age', 45.0), ('name', u'Sams Teach Yourself Django in 24 Hours'), ('pages', 528), ('price', Decimal("23.09")), ('pubdate', datetime.date(2008, 3, 3)), ('publisher_id', 2), ('rating', 3.0)]
-
-# Values queries can be combined with annotate and extra
->>> sorted((k,v) for k,v in Book.objects.all().annotate(mean_auth_age=Avg('authors__age')).extra(select={'manufacture_cost' : 'price * .5'}).values().get(pk=2).items()if k != '_state')
-[('contact_id', 3), ('id', 2), ('isbn', u'067232959'), ('manufacture_cost', ...11.545...), ('mean_auth_age', 45.0), ('name', u'Sams Teach Yourself Django in 24 Hours'), ('pages', 528), ('price', Decimal("23.09")), ('pubdate', datetime.date(2008, 3, 3)), ('publisher_id', 2), ('rating', 3.0)]
-
-# The order of the (empty) values, annotate and extra clauses doesn't matter
->>> sorted((k,v) for k,v in Book.objects.all().values().annotate(mean_auth_age=Avg('authors__age')).extra(select={'manufacture_cost' : 'price * .5'}).get(pk=2).items()if k != '_state')
-[('contact_id', 3), ('id', 2), ('isbn', u'067232959'), ('manufacture_cost', ...11.545...), ('mean_auth_age', 45.0), ('name', u'Sams Teach Yourself Django in 24 Hours'), ('pages', 528), ('price', Decimal("23.09")), ('pubdate', datetime.date(2008, 3, 3)), ('publisher_id', 2), ('rating', 3.0)]
-
-# If the annotation precedes the values clause, it won't be included
-# unless it is explicitly named
->>> sorted(Book.objects.all().annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).values('name').get(pk=1).items())
-[('name', u'The Definitive Guide to Django: Web Development Done Right')]
-
->>> sorted(Book.objects.all().annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).values('name','mean_auth_age').get(pk=1).items())
-[('mean_auth_age', 34.5), ('name', u'The Definitive Guide to Django: Web Development Done Right')]
-
-# If an annotation isn't included in the values, it can still be used in a filter
->>> Book.objects.annotate(n_authors=Count('authors')).values('name').filter(n_authors__gt=2)
-[{'name': u'Python Web Development with Django'}]
-
-# The annotations are added to values output if values() precedes annotate()
->>> sorted(Book.objects.all().values('name').annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).get(pk=1).items())
-[('mean_auth_age', 34.5), ('name', u'The Definitive Guide to Django: Web Development Done Right')]
-
-# Check that all of the objects are getting counted (allow_nulls) and that values respects the amount of objects
->>> len(Author.objects.all().annotate(Avg('friends__age')).values())
-9
-
-# Check that consecutive calls to annotate accumulate in the query
->>> Book.objects.values('price').annotate(oldest=Max('authors__age')).order_by('oldest', 'price').annotate(Max('publisher__num_awards'))
-[{'price': Decimal("30..."), 'oldest': 35, 'publisher__num_awards__max': 3}, {'price': Decimal("29.69"), 'oldest': 37, 'publisher__num_awards__max': 7}, {'price': Decimal("23.09"), 'oldest': 45, 'publisher__num_awards__max': 1}, {'price': Decimal("75..."), 'oldest': 57, 'publisher__num_awards__max': 9}, {'price': Decimal("82.8..."), 'oldest': 57, 'publisher__num_awards__max': 7}]
-
-# Aggregates can be composed over annotations.
-# The return type is derived from the composed aggregate
->>> Book.objects.all().annotate(num_authors=Count('authors__id')).aggregate(Max('pages'), Max('price'), Sum('num_authors'), Avg('num_authors'))
-{'num_authors__sum': 10, 'num_authors__avg': 1.66..., 'pages__max': 1132, 'price__max': Decimal("82.80")}
-
-# Bad field requests in aggregates are caught and reported
->>> Book.objects.all().aggregate(num_authors=Count('foo'))
-Traceback (most recent call last):
-...
-FieldError: Cannot resolve keyword 'foo' into field. Choices are: authors, contact, hardbackbook, id, isbn, name, pages, price, pubdate, publisher, rating, store
-
->>> Book.objects.all().annotate(num_authors=Count('foo'))
-Traceback (most recent call last):
-...
-FieldError: Cannot resolve keyword 'foo' into field. Choices are: authors, contact, hardbackbook, id, isbn, name, pages, price, pubdate, publisher, rating, store
-
->>> Book.objects.all().annotate(num_authors=Count('authors__id')).aggregate(Max('foo'))
-Traceback (most recent call last):
-...
-FieldError: Cannot resolve keyword 'foo' into field. Choices are: authors, contact, hardbackbook, id, isbn, name, pages, price, pubdate, publisher, rating, store, num_authors
-
-# Old-style count aggregations can be mixed with new-style
->>> Book.objects.annotate(num_authors=Count('authors')).count()
-6
-
-# Non-ordinal, non-computed Aggregates over annotations correctly inherit
-# the annotation's internal type if the annotation is ordinal or computed
->>> Book.objects.annotate(num_authors=Count('authors')).aggregate(Max('num_authors'))
-{'num_authors__max': 3}
-
->>> Publisher.objects.annotate(avg_price=Avg('book__price')).aggregate(Max('avg_price'))
-{'avg_price__max': 75.0...}
-
-# Aliases are quoted to protected aliases that might be reserved names
->>> Book.objects.aggregate(number=Max('pages'), select=Max('pages'))
-{'number': 1132, 'select': 1132}
-
-# Regression for #10064: select_related() plays nice with aggregates
->>> sorted(Book.objects.select_related('publisher').annotate(num_authors=Count('authors')).values()[0].iteritems())
-[('contact_id', 8), ('id', 5), ('isbn', u'013790395'), ('name', u'Artificial Intelligence: A Modern Approach'), ('num_authors', 2), ('pages', 1132), ('price', Decimal("82.8...")), ('pubdate', datetime.date(1995, 1, 15)), ('publisher_id', 3), ('rating', 4.0)]
-
-# Regression for #10010: exclude on an aggregate field is correctly negated
->>> len(Book.objects.annotate(num_authors=Count('authors')))
-6
->>> len(Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__gt=2))
-1
->>> len(Book.objects.annotate(num_authors=Count('authors')).exclude(num_authors__gt=2))
-5
-
->>> len(Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__lt=3).exclude(num_authors__lt=2))
-2
->>> len(Book.objects.annotate(num_authors=Count('authors')).exclude(num_authors__lt=2).filter(num_authors__lt=3))
-2
-
-# Aggregates can be used with F() expressions
-# ... where the F() is pushed into the HAVING clause
->>> Publisher.objects.annotate(num_books=Count('book')).filter(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards')
-[{'num_books': 1, 'name': u'Morgan Kaufmann', 'num_awards': 9}, {'num_books': 2, 'name': u'Prentice Hall', 'num_awards': 7}]
-
->>> Publisher.objects.annotate(num_books=Count('book')).exclude(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards')
-[{'num_books': 2, 'name': u'Apress', 'num_awards': 3}, {'num_books': 0, 'name': u"Jonno's House of Books", 'num_awards': 0}, {'num_books': 1, 'name': u'Sams', 'num_awards': 1}]
-
-# ... and where the F() references an aggregate
->>> Publisher.objects.annotate(num_books=Count('book')).filter(num_awards__gt=2*F('num_books')).order_by('name').values('name','num_books','num_awards')
-[{'num_books': 1, 'name': u'Morgan Kaufmann', 'num_awards': 9}, {'num_books': 2, 'name': u'Prentice Hall', 'num_awards': 7}]
-
->>> Publisher.objects.annotate(num_books=Count('book')).exclude(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards')
-[{'num_books': 2, 'name': u'Apress', 'num_awards': 3}, {'num_books': 0, 'name': u"Jonno's House of Books", 'num_awards': 0}, {'num_books': 1, 'name': u'Sams', 'num_awards': 1}]
-
-# Tests on fields with non-default table and column names.
->>> Clues.objects.values('EntryID__Entry').annotate(Appearances=Count('EntryID'), Distinct_Clues=Count('Clue', distinct=True))
-[]
-
->>> Entries.objects.annotate(clue_count=Count('clues__ID'))
-[]
-
-# Regression for #10089: Check handling of empty result sets with aggregates
->>> Book.objects.filter(id__in=[]).count()
-0
-
->>> Book.objects.filter(id__in=[]).aggregate(num_authors=Count('authors'), avg_authors=Avg('authors'), max_authors=Max('authors'), max_price=Max('price'), max_rating=Max('rating'))
-{'max_authors': None, 'max_rating': None, 'num_authors': 0, 'avg_authors': None, 'max_price': None}
-
->>> list(Publisher.objects.filter(pk=5).annotate(num_authors=Count('book__authors'), avg_authors=Avg('book__authors'), max_authors=Max('book__authors'), max_price=Max('book__price'), max_rating=Max('book__rating')).values()) == [{'max_authors': None, 'name': u"Jonno's House of Books", 'num_awards': 0, 'max_price': None, 'num_authors': 0, 'max_rating': None, 'id': 5, 'avg_authors': None}]
-True
-
-# Regression for #10113 - Fields mentioned in order_by() must be included in the GROUP BY.
-# This only becomes a problem when the order_by introduces a new join.
->>> Book.objects.annotate(num_authors=Count('authors')).order_by('publisher__name', 'name')
-[<Book: Practical Django Projects>, <Book: The Definitive Guide to Django: Web Development Done Right>, <Book: Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp>, <Book: Artificial Intelligence: A Modern Approach>, <Book: Python Web Development with Django>, <Book: Sams Teach Yourself Django in 24 Hours>]
-
-# Regression for #10127 - Empty select_related() works with annotate
->>> books = Book.objects.all().filter(rating__lt=4.5).select_related().annotate(Avg('authors__age'))
->>> sorted([(b.name, b.authors__age__avg, b.publisher.name, b.contact.name) for b in books])
-[(u'Artificial Intelligence: A Modern Approach', 51.5, u'Prentice Hall', u'Peter Norvig'), (u'Practical Django Projects', 29.0, u'Apress', u'James Bennett'), (u'Python Web Development with Django', 30.3..., u'Prentice Hall', u'Jeffrey Forcier'), (u'Sams Teach Yourself Django in 24 Hours', 45.0, u'Sams', u'Brad Dayley')]
-
-# Regression for #10132 - If the values() clause only mentioned extra(select=) columns, those columns are used for grouping
->>> Book.objects.extra(select={'pub':'publisher_id'}).values('pub').annotate(Count('id')).order_by('pub')
-[{'pub': 1, 'id__count': 2}, {'pub': 2, 'id__count': 1}, {'pub': 3, 'id__count': 2}, {'pub': 4, 'id__count': 1}]
-
->>> Book.objects.extra(select={'pub':'publisher_id','foo':'pages'}).values('pub').annotate(Count('id')).order_by('pub')
-[{'pub': 1, 'id__count': 2}, {'pub': 2, 'id__count': 1}, {'pub': 3, 'id__count': 2}, {'pub': 4, 'id__count': 1}]
-
-# Regression for #10182 - Queries with aggregate calls are correctly realiased when used in a subquery
->>> ids = Book.objects.filter(pages__gt=100).annotate(n_authors=Count('authors')).filter(n_authors__gt=2).order_by('n_authors')
->>> Book.objects.filter(id__in=ids)
-[<Book: Python Web Development with Django>]
-
-# Regression for #10197 -- Queries with aggregates can be pickled.
-# First check that pickling is possible at all. No crash = success
->>> qs = Book.objects.annotate(num_authors=Count('authors'))
->>> out = pickle.dumps(qs)
-
-# Then check that the round trip works.
->>> query = qs.query.get_compiler(qs.db).as_sql()[0]
->>> select_fields = qs.query.select_fields
->>> query2 = pickle.loads(pickle.dumps(qs))
->>> query2.query.get_compiler(query2.db).as_sql()[0] == query
-True
->>> query2.query.select_fields = select_fields
-
-# Regression for #10199 - Aggregate calls clone the original query so the original query can still be used
->>> books = Book.objects.all()
->>> _ = books.aggregate(Avg('authors__age'))
->>> books.all()
-[<Book: Artificial Intelligence: A Modern Approach>, <Book: Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp>, <Book: Practical Django Projects>, <Book: Python Web Development with Django>, <Book: Sams Teach Yourself Django in 24 Hours>, <Book: The Definitive Guide to Django: Web Development Done Right>]
-
-# Regression for #10248 - Annotations work with DateQuerySets
->>> Book.objects.annotate(num_authors=Count('authors')).filter(num_authors=2).dates('pubdate', 'day')
-[datetime.datetime(1995, 1, 15, 0, 0), datetime.datetime(2007, 12, 6, 0, 0)]
-
-# Regression for #10290 - extra selects with parameters can be used for
-# grouping.
->>> qs = Book.objects.all().annotate(mean_auth_age=Avg('authors__age')).extra(select={'sheets' : '(pages + %s) / %s'}, select_params=[1, 2]).order_by('sheets').values('sheets')
->>> [int(x['sheets']) for x in qs]
-[150, 175, 224, 264, 473, 566]
-
-# Regression for 10425 - annotations don't get in the way of a count() clause
->>> Book.objects.values('publisher').annotate(Count('publisher')).count()
-4
-
->>> Book.objects.annotate(Count('publisher')).values('publisher').count()
-6
-
->>> publishers = Publisher.objects.filter(id__in=(1,2))
->>> publishers
-[<Publisher: Apress>, <Publisher: Sams>]
-
->>> publishers = publishers.annotate(n_books=models.Count('book'))
->>> publishers[0].n_books
-2
-
->>> publishers
-[<Publisher: Apress>, <Publisher: Sams>]
-
->>> books = Book.objects.filter(publisher__in=publishers)
->>> books
-[<Book: Practical Django Projects>, <Book: Sams Teach Yourself Django in 24 Hours>, <Book: The Definitive Guide to Django: Web Development Done Right>]
-
->>> publishers
-[<Publisher: Apress>, <Publisher: Sams>]
-
-
-# Regression for 10666 - inherited fields work with annotations and aggregations
->>> HardbackBook.objects.aggregate(n_pages=Sum('book_ptr__pages'))
-{'n_pages': 2078}
-
->>> HardbackBook.objects.aggregate(n_pages=Sum('pages'))
-{'n_pages': 2078}
-
->>> HardbackBook.objects.annotate(n_authors=Count('book_ptr__authors')).values('name','n_authors')
-[{'n_authors': 2, 'name': u'Artificial Intelligence: A Modern Approach'}, {'n_authors': 1, 'name': u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp'}]
-
->>> HardbackBook.objects.annotate(n_authors=Count('authors')).values('name','n_authors')
-[{'n_authors': 2, 'name': u'Artificial Intelligence: A Modern Approach'}, {'n_authors': 1, 'name': u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp'}]
-
-# Regression for #10766 - Shouldn't be able to reference an aggregate fields in an an aggregate() call.
->>> Book.objects.all().annotate(mean_age=Avg('authors__age')).annotate(Avg('mean_age'))
-Traceback (most recent call last):
-...
-FieldError: Cannot compute Avg('mean_age'): 'mean_age' is an aggregate
-
-"""
-}
-
-def run_stddev_tests():
- """Check to see if StdDev/Variance tests should be run.
-
- Stddev and Variance are not guaranteed to be available for SQLite, and
- are not available for PostgreSQL before 8.2.
- """
- if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.sqlite3':
- return False
-
- class StdDevPop(object):
- sql_function = 'STDDEV_POP'
-
- try:
- connection.ops.check_aggregate_support(StdDevPop())
- except:
- return False
- return True
-
-if run_stddev_tests():
- __test__['API_TESTS'] += """
->>> Book.objects.aggregate(StdDev('pages'))
-{'pages__stddev': 311.46...}
-
->>> Book.objects.aggregate(StdDev('rating'))
-{'rating__stddev': 0.60...}
-
->>> Book.objects.aggregate(StdDev('price'))
-{'price__stddev': 24.16...}
-
-
->>> Book.objects.aggregate(StdDev('pages', sample=True))
-{'pages__stddev': 341.19...}
-
->>> Book.objects.aggregate(StdDev('rating', sample=True))
-{'rating__stddev': 0.66...}
-
->>> Book.objects.aggregate(StdDev('price', sample=True))
-{'price__stddev': 26.46...}
-
-
->>> Book.objects.aggregate(Variance('pages'))
-{'pages__variance': 97010.80...}
-
->>> Book.objects.aggregate(Variance('rating'))
-{'rating__variance': 0.36...}
-
->>> Book.objects.aggregate(Variance('price'))
-{'price__variance': 583.77...}
-
-
->>> Book.objects.aggregate(Variance('pages', sample=True))
-{'pages__variance': 116412.96...}
-
->>> Book.objects.aggregate(Variance('rating', sample=True))
-{'rating__variance': 0.44...}
-
->>> Book.objects.aggregate(Variance('price', sample=True))
-{'price__variance': 700.53...}
-
-"""
diff --git a/tests/regressiontests/aggregation_regress/tests.py b/tests/regressiontests/aggregation_regress/tests.py
index 3c4bdfa47d..51f439c5a1 100644
--- a/tests/regressiontests/aggregation_regress/tests.py
+++ b/tests/regressiontests/aggregation_regress/tests.py
@@ -1,12 +1,38 @@
+import datetime
+from decimal import Decimal
+
+from django.core.exceptions import FieldError
from django.conf import settings
-from django.test import TestCase
+from django.test import TestCase, Approximate
from django.db import DEFAULT_DB_ALIAS
-from django.db.models import Count, Max
+from django.db.models import Count, Max, Avg, Sum, StdDev, Variance, F
from regressiontests.aggregation_regress.models import *
+def run_stddev_tests():
+ """Check to see if StdDev/Variance tests should be run.
+
+ Stddev and Variance are not guaranteed to be available for SQLite, and
+ are not available for PostgreSQL before 8.2.
+ """
+ if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.sqlite3':
+ return False
+
+ class StdDevPop(object):
+ sql_function = 'STDDEV_POP'
+
+ try:
+ connection.ops.check_aggregate_support(StdDevPop())
+ except:
+ return False
+ return True
+
+
class AggregationTests(TestCase):
+ def assertObjectAttrs(self, obj, **kwargs):
+ for attr, value in kwargs.iteritems():
+ self.assertEqual(getattr(obj, attr), value)
def test_aggregates_in_where_clause(self):
"""
@@ -70,3 +96,593 @@ class AggregationTests(TestCase):
}).annotate(total_books=Count('book'))
# force execution of the query
list(qs)
+
+ def test_aggregate(self):
+ # Ordering requests are ignored
+ self.assertEqual(
+ Author.objects.order_by("name").aggregate(Avg("age")),
+ {"age__avg": Approximate(37.444, places=1)}
+ )
+
+ # Implicit ordering is also ignored
+ self.assertEqual(
+ Book.objects.aggregate(Sum("pages")),
+ {"pages__sum": 3703},
+ )
+
+ # Baseline results
+ self.assertEqual(
+ Book.objects.aggregate(Sum('pages'), Avg('pages')),
+ {'pages__sum': 3703, 'pages__avg': Approximate(617.166, places=2)}
+ )
+
+ # Empty values query doesn't affect grouping or results
+ self.assertEqual(
+ Book.objects.values().aggregate(Sum('pages'), Avg('pages')),
+ {'pages__sum': 3703, 'pages__avg': Approximate(617.166, places=2)}
+ )
+
+ # Aggregate overrides extra selected column
+ self.assertEqual(
+ Book.objects.extra(select={'price_per_page' : 'price / pages'}).aggregate(Sum('pages')),
+ {'pages__sum': 3703}
+ )
+
+ def test_annotation(self):
+ # Annotations get combined with extra select clauses
+ obj = Book.objects.annotate(mean_auth_age=Avg("authors__age")).extra(select={"manufacture_cost": "price * .5"}).get(pk=2)
+ self.assertObjectAttrs(obj,
+ contact_id=3,
+ id=2,
+ isbn=u'067232959',
+ mean_auth_age=45.0,
+ name='Sams Teach Yourself Django in 24 Hours',
+ pages=528,
+ price=Decimal("23.09"),
+ pubdate=datetime.date(2008, 3, 3),
+ publisher_id=2,
+ rating=3.0
+ )
+ # Different DB backends return different types for the extra select computation
+ self.assertTrue(obj.manufacture_cost == 11.545 or obj.manufacture_cost == Decimal('11.545'))
+
+ # Order of the annotate/extra in the query doesn't matter
+ obj = Book.objects.extra(select={'manufacture_cost' : 'price * .5'}).annotate(mean_auth_age=Avg('authors__age')).get(pk=2)
+ self.assertObjectAttrs(obj,
+ contact_id=3,
+ id=2,
+ isbn=u'067232959',
+ mean_auth_age=45.0,
+ name=u'Sams Teach Yourself Django in 24 Hours',
+ pages=528,
+ price=Decimal("23.09"),
+ pubdate=datetime.date(2008, 3, 3),
+ publisher_id=2,
+ rating=3.0
+ )
+ # Different DB backends return different types for the extra select computation
+ self.assertTrue(obj.manufacture_cost == 11.545 or obj.manufacture_cost == Decimal('11.545'))
+
+ # Values queries can be combined with annotate and extra
+ obj = Book.objects.annotate(mean_auth_age=Avg('authors__age')).extra(select={'manufacture_cost' : 'price * .5'}).values().get(pk=2)
+ manufacture_cost = obj['manufacture_cost']
+ self.assertTrue(manufacture_cost == 11.545 or manufacture_cost == Decimal('11.545'))
+ del obj['manufacture_cost']
+ self.assertEqual(obj, {
+ "contact_id": 3,
+ "id": 2,
+ "isbn": u"067232959",
+ "mean_auth_age": 45.0,
+ "name": u"Sams Teach Yourself Django in 24 Hours",
+ "pages": 528,
+ "price": Decimal("23.09"),
+ "pubdate": datetime.date(2008, 3, 3),
+ "publisher_id": 2,
+ "rating": 3.0,
+ })
+
+ # The order of the (empty) values, annotate and extra clauses doesn't
+ # matter
+ obj = Book.objects.values().annotate(mean_auth_age=Avg('authors__age')).extra(select={'manufacture_cost' : 'price * .5'}).get(pk=2)
+ manufacture_cost = obj['manufacture_cost']
+ self.assertTrue(manufacture_cost == 11.545 or manufacture_cost == Decimal('11.545'))
+ del obj['manufacture_cost']
+ self.assertEqual(obj, {
+ 'contact_id': 3,
+ 'id': 2,
+ 'isbn': u'067232959',
+ 'mean_auth_age': 45.0,
+ 'name': u'Sams Teach Yourself Django in 24 Hours',
+ 'pages': 528,
+ 'price': Decimal("23.09"),
+ 'pubdate': datetime.date(2008, 3, 3),
+ 'publisher_id': 2,
+ 'rating': 3.0
+ })
+
+ # If the annotation precedes the values clause, it won't be included
+ # unless it is explicitly named
+ obj = Book.objects.annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).values('name').get(pk=1)
+ self.assertEqual(obj, {
+ "name": u'The Definitive Guide to Django: Web Development Done Right',
+ })
+
+ obj = Book.objects.annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).values('name','mean_auth_age').get(pk=1)
+ self.assertEqual(obj, {
+ 'mean_auth_age': 34.5,
+ 'name': u'The Definitive Guide to Django: Web Development Done Right',
+ })
+
+ # If an annotation isn't included in the values, it can still be used
+ # in a filter
+ qs = Book.objects.annotate(n_authors=Count('authors')).values('name').filter(n_authors__gt=2)
+ self.assertQuerysetEqual(
+ qs, [
+ {"name": u'Python Web Development with Django'}
+ ],
+ lambda b: b,
+ )
+
+ # The annotations are added to values output if values() precedes
+ # annotate()
+ obj = Book.objects.values('name').annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).get(pk=1)
+ self.assertEqual(obj, {
+ 'mean_auth_age': 34.5,
+ 'name': u'The Definitive Guide to Django: Web Development Done Right',
+ })
+
+ # Check that all of the objects are getting counted (allow_nulls) and
+ # that values respects the amount of objects
+ self.assertEqual(
+ len(Author.objects.annotate(Avg('friends__age')).values()),
+ 9
+ )
+
+ # Check that consecutive calls to annotate accumulate in the query
+ qs = Book.objects.values('price').annotate(oldest=Max('authors__age')).order_by('oldest', 'price').annotate(Max('publisher__num_awards'))
+ self.assertQuerysetEqual(
+ qs, [
+ {'price': Decimal("30"), 'oldest': 35, 'publisher__num_awards__max': 3},
+ {'price': Decimal("29.69"), 'oldest': 37, 'publisher__num_awards__max': 7},
+ {'price': Decimal("23.09"), 'oldest': 45, 'publisher__num_awards__max': 1},
+ {'price': Decimal("75"), 'oldest': 57, 'publisher__num_awards__max': 9},
+ {'price': Decimal("82.8"), 'oldest': 57, 'publisher__num_awards__max': 7}
+ ],
+ lambda b: b,
+ )
+
+ def test_aggrate_annotation(self):
+ # Aggregates can be composed over annotations.
+ # The return type is derived from the composed aggregate
+ vals = Book.objects.all().annotate(num_authors=Count('authors__id')).aggregate(Max('pages'), Max('price'), Sum('num_authors'), Avg('num_authors'))
+ self.assertEqual(vals, {
+ 'num_authors__sum': 10,
+ 'num_authors__avg': Approximate(1.666, places=2),
+ 'pages__max': 1132,
+ 'price__max': Decimal("82.80")
+ })
+
+ def test_field_error(self):
+ # Bad field requests in aggregates are caught and reported
+ self.assertRaises(
+ FieldError,
+ lambda: Book.objects.all().aggregate(num_authors=Count('foo'))
+ )
+
+ self.assertRaises(
+ FieldError,
+ lambda: Book.objects.all().annotate(num_authors=Count('foo'))
+ )
+
+ self.assertRaises(
+ FieldError,
+ lambda: Book.objects.all().annotate(num_authors=Count('authors__id')).aggregate(Max('foo'))
+ )
+
+ def test_more(self):
+ # Old-style count aggregations can be mixed with new-style
+ self.assertEqual(
+ Book.objects.annotate(num_authors=Count('authors')).count(),
+ 6
+ )
+
+ # Non-ordinal, non-computed Aggregates over annotations correctly
+ # inherit the annotation's internal type if the annotation is ordinal
+ # or computed
+ vals = Book.objects.annotate(num_authors=Count('authors')).aggregate(Max('num_authors'))
+ self.assertEqual(
+ vals,
+ {'num_authors__max': 3}
+ )
+
+ vals = Publisher.objects.annotate(avg_price=Avg('book__price')).aggregate(Max('avg_price'))
+ self.assertEqual(
+ vals,
+ {'avg_price__max': 75.0}
+ )
+
+ # Aliases are quoted to protected aliases that might be reserved names
+ vals = Book.objects.aggregate(number=Max('pages'), select=Max('pages'))
+ self.assertEqual(
+ vals,
+ {'number': 1132, 'select': 1132}
+ )
+
+ # Regression for #10064: select_related() plays nice with aggregates
+ obj = Book.objects.select_related('publisher').annotate(num_authors=Count('authors')).values()[0]
+ self.assertEqual(obj, {
+ 'contact_id': 8,
+ 'id': 5,
+ 'isbn': u'013790395',
+ 'name': u'Artificial Intelligence: A Modern Approach',
+ 'num_authors': 2,
+ 'pages': 1132,
+ 'price': Decimal("82.8"),
+ 'pubdate': datetime.date(1995, 1, 15),
+ 'publisher_id': 3,
+ 'rating': 4.0,
+ })
+
+ # Regression for #10010: exclude on an aggregate field is correctly
+ # negated
+ self.assertEqual(
+ len(Book.objects.annotate(num_authors=Count('authors'))),
+ 6
+ )
+ self.assertEqual(
+ len(Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__gt=2)),
+ 1
+ )
+ self.assertEqual(
+ len(Book.objects.annotate(num_authors=Count('authors')).exclude(num_authors__gt=2)),
+ 5
+ )
+
+ self.assertEqual(
+ len(Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__lt=3).exclude(num_authors__lt=2)),
+ 2
+ )
+ self.assertEqual(
+ len(Book.objects.annotate(num_authors=Count('authors')).exclude(num_authors__lt=2).filter(num_authors__lt=3)),
+ 2
+ )
+
+ def test_aggregate_fexpr(self):
+ # Aggregates can be used with F() expressions
+ # ... where the F() is pushed into the HAVING clause
+ qs = Publisher.objects.annotate(num_books=Count('book')).filter(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards')
+ self.assertQuerysetEqual(
+ qs, [
+ {'num_books': 1, 'name': u'Morgan Kaufmann', 'num_awards': 9},
+ {'num_books': 2, 'name': u'Prentice Hall', 'num_awards': 7}
+ ],
+ lambda p: p,
+ )
+
+ qs = Publisher.objects.annotate(num_books=Count('book')).exclude(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards')
+ self.assertQuerysetEqual(
+ qs, [
+ {'num_books': 2, 'name': u'Apress', 'num_awards': 3},
+ {'num_books': 0, 'name': u"Jonno's House of Books", 'num_awards': 0},
+ {'num_books': 1, 'name': u'Sams', 'num_awards': 1}
+ ],
+ lambda p: p,
+ )
+
+ # ... and where the F() references an aggregate
+ qs = Publisher.objects.annotate(num_books=Count('book')).filter(num_awards__gt=2*F('num_books')).order_by('name').values('name','num_books','num_awards')
+ self.assertQuerysetEqual(
+ qs, [
+ {'num_books': 1, 'name': u'Morgan Kaufmann', 'num_awards': 9},
+ {'num_books': 2, 'name': u'Prentice Hall', 'num_awards': 7}
+ ],
+ lambda p: p,
+ )
+
+ qs = Publisher.objects.annotate(num_books=Count('book')).exclude(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards')
+ self.assertQuerysetEqual(
+ qs, [
+ {'num_books': 2, 'name': u'Apress', 'num_awards': 3},
+ {'num_books': 0, 'name': u"Jonno's House of Books", 'num_awards': 0},
+ {'num_books': 1, 'name': u'Sams', 'num_awards': 1}
+ ],
+ lambda p: p,
+ )
+
+ def test_db_col_table(self):
+ # Tests on fields with non-default table and column names.
+ qs = Clues.objects.values('EntryID__Entry').annotate(Appearances=Count('EntryID'), Distinct_Clues=Count('Clue', distinct=True))
+ self.assertQuerysetEqual(qs, [])
+
+ qs = Entries.objects.annotate(clue_count=Count('clues__ID'))
+ self.assertQuerysetEqual(qs, [])
+
+ def test_empty(self):
+ # Regression for #10089: Check handling of empty result sets with
+ # aggregates
+ self.assertEqual(
+ Book.objects.filter(id__in=[]).count(),
+ 0
+ )
+
+ vals = Book.objects.filter(id__in=[]).aggregate(num_authors=Count('authors'), avg_authors=Avg('authors'), max_authors=Max('authors'), max_price=Max('price'), max_rating=Max('rating'))
+ self.assertEqual(
+ vals,
+ {'max_authors': None, 'max_rating': None, 'num_authors': 0, 'avg_authors': None, 'max_price': None}
+ )
+
+ qs = Publisher.objects.filter(pk=5).annotate(num_authors=Count('book__authors'), avg_authors=Avg('book__authors'), max_authors=Max('book__authors'), max_price=Max('book__price'), max_rating=Max('book__rating')).values()
+ self.assertQuerysetEqual(
+ qs, [
+ {'max_authors': None, 'name': u"Jonno's House of Books", 'num_awards': 0, 'max_price': None, 'num_authors': 0, 'max_rating': None, 'id': 5, 'avg_authors': None}
+ ],
+ lambda p: p
+ )
+
+ def test_more_more(self):
+ # Regression for #10113 - Fields mentioned in order_by() must be
+ # included in the GROUP BY. This only becomes a problem when the
+ # order_by introduces a new join.
+ self.assertQuerysetEqual(
+ Book.objects.annotate(num_authors=Count('authors')).order_by('publisher__name', 'name'), [
+ "Practical Django Projects",
+ "The Definitive Guide to Django: Web Development Done Right",
+ "Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp",
+ "Artificial Intelligence: A Modern Approach",
+ "Python Web Development with Django",
+ "Sams Teach Yourself Django in 24 Hours",
+ ],
+ lambda b: b.name
+ )
+
+ # Regression for #10127 - Empty select_related() works with annotate
+ qs = Book.objects.filter(rating__lt=4.5).select_related().annotate(Avg('authors__age'))
+ self.assertQuerysetEqual(
+ qs, [
+ (u'Artificial Intelligence: A Modern Approach', 51.5, u'Prentice Hall', u'Peter Norvig'),
+ (u'Practical Django Projects', 29.0, u'Apress', u'James Bennett'),
+ (u'Python Web Development with Django', Approximate(30.333, places=2), u'Prentice Hall', u'Jeffrey Forcier'),
+ (u'Sams Teach Yourself Django in 24 Hours', 45.0, u'Sams', u'Brad Dayley')
+ ],
+ lambda b: (b.name, b.authors__age__avg, b.publisher.name, b.contact.name)
+ )
+
+ # Regression for #10132 - If the values() clause only mentioned extra
+ # (select=) columns, those columns are used for grouping
+ qs = Book.objects.extra(select={'pub':'publisher_id'}).values('pub').annotate(Count('id')).order_by('pub')
+ self.assertQuerysetEqual(
+ qs, [
+ {'pub': 1, 'id__count': 2},
+ {'pub': 2, 'id__count': 1},
+ {'pub': 3, 'id__count': 2},
+ {'pub': 4, 'id__count': 1}
+ ],
+ lambda b: b
+ )
+
+ qs = Book.objects.extra(select={'pub':'publisher_id', 'foo':'pages'}).values('pub').annotate(Count('id')).order_by('pub')
+ self.assertQuerysetEqual(
+ qs, [
+ {'pub': 1, 'id__count': 2},
+ {'pub': 2, 'id__count': 1},
+ {'pub': 3, 'id__count': 2},
+ {'pub': 4, 'id__count': 1}
+ ],
+ lambda b: b
+ )
+
+ # Regression for #10182 - Queries with aggregate calls are correctly
+ # realiased when used in a subquery
+ ids = Book.objects.filter(pages__gt=100).annotate(n_authors=Count('authors')).filter(n_authors__gt=2).order_by('n_authors')
+ self.assertQuerysetEqual(
+ Book.objects.filter(id__in=ids), [
+ "Python Web Development with Django",
+ ],
+ lambda b: b.name
+ )
+
+ def test_pickle(self):
+ # Regression for #10197 -- Queries with aggregates can be pickled.
+ # First check that pickling is possible at all. No crash = success
+ qs = Book.objects.annotate(num_authors=Count('authors'))
+ out = pickle.dumps(qs)
+
+ # Then check that the round trip works.
+ query = qs.query.get_compiler(qs.db).as_sql()[0]
+ qs2 = pickle.loads(pickle.dumps(qs))
+ self.assertEqual(
+ qs2.query.get_compiler(qs2.db).as_sql()[0],
+ query,
+ )
+
+ def test_more_more_more(self):
+ # Regression for #10199 - Aggregate calls clone the original query so
+ # the original query can still be used
+ books = Book.objects.all()
+ books.aggregate(Avg("authors__age"))
+ self.assertQuerysetEqual(
+ books.all(), [
+ u'Artificial Intelligence: A Modern Approach',
+ u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp',
+ u'Practical Django Projects',
+ u'Python Web Development with Django',
+ u'Sams Teach Yourself Django in 24 Hours',
+ u'The Definitive Guide to Django: Web Development Done Right'
+ ],
+ lambda b: b.name
+ )
+
+ # Regression for #10248 - Annotations work with DateQuerySets
+ qs = Book.objects.annotate(num_authors=Count('authors')).filter(num_authors=2).dates('pubdate', 'day')
+ self.assertQuerysetEqual(
+ qs, [
+ datetime.datetime(1995, 1, 15, 0, 0),
+ datetime.datetime(2007, 12, 6, 0, 0)
+ ],
+ lambda b: b
+ )
+
+ # Regression for #10290 - extra selects with parameters can be used for
+ # grouping.
+ qs = Book.objects.annotate(mean_auth_age=Avg('authors__age')).extra(select={'sheets' : '(pages + %s) / %s'}, select_params=[1, 2]).order_by('sheets').values('sheets')
+ self.assertQuerysetEqual(
+ qs, [
+ 150,
+ 175,
+ 224,
+ 264,
+ 473,
+ 566
+ ],
+ lambda b: int(b["sheets"])
+ )
+
+ # Regression for 10425 - annotations don't get in the way of a count()
+ # clause
+ self.assertEqual(
+ Book.objects.values('publisher').annotate(Count('publisher')).count(),
+ 4
+ )
+ self.assertEqual(
+ Book.objects.annotate(Count('publisher')).values('publisher').count(),
+ 6
+ )
+
+ publishers = Publisher.objects.filter(id__in=[1, 2])
+ self.assertQuerysetEqual(
+ publishers, [
+ "Apress",
+ "Sams"
+ ],
+ lambda p: p.name
+ )
+
+ publishers = publishers.annotate(n_books=Count("book"))
+ self.assertEqual(
+ publishers[0].n_books,
+ 2
+ )
+
+ self.assertQuerysetEqual(
+ publishers, [
+ "Apress",
+ "Sams",
+ ],
+ lambda p: p.name
+ )
+
+ books = Book.objects.filter(publisher__in=publishers)
+ self.assertQuerysetEqual(
+ books, [
+ "Practical Django Projects",
+ "Sams Teach Yourself Django in 24 Hours",
+ "The Definitive Guide to Django: Web Development Done Right",
+ ],
+ lambda b: b.name
+ )
+ self.assertQuerysetEqual(
+ publishers, [
+ "Apress",
+ "Sams",
+ ],
+ lambda p: p.name
+ )
+
+ # Regression for 10666 - inherited fields work with annotations and
+ # aggregations
+ self.assertEqual(
+ HardbackBook.objects.aggregate(n_pages=Sum('book_ptr__pages')),
+ {'n_pages': 2078}
+ )
+
+ self.assertEqual(
+ HardbackBook.objects.aggregate(n_pages=Sum('pages')),
+ {'n_pages': 2078},
+ )
+
+ qs = HardbackBook.objects.annotate(n_authors=Count('book_ptr__authors')).values('name', 'n_authors')
+ self.assertQuerysetEqual(
+ qs, [
+ {'n_authors': 2, 'name': u'Artificial Intelligence: A Modern Approach'},
+ {'n_authors': 1, 'name': u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp'}
+ ],
+ lambda h: h
+ )
+
+ qs = HardbackBook.objects.annotate(n_authors=Count('authors')).values('name', 'n_authors')
+ self.assertQuerysetEqual(
+ qs, [
+ {'n_authors': 2, 'name': u'Artificial Intelligence: A Modern Approach'},
+ {'n_authors': 1, 'name': u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp'}
+ ],
+ lambda h: h,
+ )
+
+ # Regression for #10766 - Shouldn't be able to reference an aggregate
+ # fields in an an aggregate() call.
+ self.assertRaises(
+ FieldError,
+ lambda: Book.objects.annotate(mean_age=Avg('authors__age')).annotate(Avg('mean_age'))
+ )
+
+ if run_stddev_tests():
+ def test_stddev(self):
+ self.assertEqual(
+ Book.objects.aggregate(StdDev('pages')),
+ {'pages__stddev': Approximate(311.46, 1)}
+ )
+
+ self.assertEqual(
+ Book.objects.aggregate(StdDev('rating')),
+ {'rating__stddev': Approximate(0.60, 1)}
+ )
+
+ self.assertEqual(
+ Book.objects.aggregate(StdDev('price')),
+ {'price__stddev': Approximate(24.16, 2)}
+ )
+
+ self.assertEqual(
+ Book.objects.aggregate(StdDev('pages', sample=True)),
+ {'pages__stddev': Approximate(341.19, 2)}
+ )
+
+ self.assertEqual(
+ Book.objects.aggregate(StdDev('rating', sample=True)),
+ {'rating__stddev': Approximate(0.66, 2)}
+ )
+
+ self.assertEqual(
+ Book.objects.aggregate(StdDev('price', sample=True)),
+ {'price__stddev': Approximate(26.46, 1)}
+ )
+
+ self.assertEqual(
+ Book.objects.aggregate(Variance('pages')),
+ {'pages__variance': Approximate(97010.80, 1)}
+ )
+
+ self.assertEqual(
+ Book.objects.aggregate(Variance('rating')),
+ {'rating__variance': Approximate(0.36, 1)}
+ )
+
+ self.assertEqual(
+ Book.objects.aggregate(Variance('price')),
+ {'price__variance': Approximate(583.77, 1)}
+ )
+
+ self.assertEqual(
+ Book.objects.aggregate(Variance('pages', sample=True)),
+ {'pages__variance': Approximate(116412.96, 1)}
+ )
+
+ self.assertEqual(
+ Book.objects.aggregate(Variance('rating', sample=True)),
+ {'rating__variance': Approximate(0.44, 2)}
+ )
+
+ self.assertEqual(
+ Book.objects.aggregate(Variance('price', sample=True)),
+ {'price__variance': Approximate(700.53, 2)}
+ )
diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py
index 423bead1ad..7315408d49 100644
--- a/tests/regressiontests/backends/models.py
+++ b/tests/regressiontests/backends/models.py
@@ -1,5 +1,9 @@
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.conf import settings
from django.db import models
-from django.db import connection
+from django.db import connection, DEFAULT_DB_ALIAS
+
class Square(models.Model):
root = models.IntegerField()
@@ -8,6 +12,7 @@ class Square(models.Model):
def __unicode__(self):
return "%s ** 2 == %s" % (self.root, self.square)
+
class Person(models.Model):
first_name = models.CharField(max_length=20)
last_name = models.CharField(max_length=20)
@@ -15,11 +20,40 @@ class Person(models.Model):
def __unicode__(self):
return u'%s %s' % (self.first_name, self.last_name)
+
class SchoolClass(models.Model):
year = models.PositiveIntegerField()
day = models.CharField(max_length=9, blank=True)
last_updated = models.DateTimeField()
+# Unfortunately, the following model breaks MySQL hard.
+# Until #13711 is fixed, this test can't be run under MySQL.
+if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.mysql':
+ class VeryLongModelNameZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ(models.Model):
+ class Meta:
+ # We need to use a short actual table name or
+ # we hit issue #8548 which we're not testing!
+ verbose_name = 'model_with_long_table_name'
+ primary_key_is_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.AutoField(primary_key=True)
+ charfield_is_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.CharField(max_length=100)
+ m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = models.ManyToManyField(Person,blank=True)
+
+
+class Tag(models.Model):
+ name = models.CharField(max_length=30)
+ content_type = models.ForeignKey(ContentType, related_name='backend_tags')
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey('content_type', 'object_id')
+
+
+class Post(models.Model):
+ name = models.CharField(max_length=30)
+ text = models.TextField()
+ tags = generic.GenericRelation('Tag')
+
+ class Meta:
+ db_table = 'CaseSensitive_Post'
+
qn = connection.ops.quote_name
__test__ = {'API_TESTS': """
diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py
index 6a26a608eb..01764135fa 100644
--- a/tests/regressiontests/backends/tests.py
+++ b/tests/regressiontests/backends/tests.py
@@ -1,13 +1,18 @@
# -*- coding: utf-8 -*-
# Unit and doctests for specific database backends.
import datetime
-import models
import unittest
-from django.db import backend, connection, DEFAULT_DB_ALIAS
-from django.db.backends.signals import connection_created
+
from django.conf import settings
+from django.core import management
+from django.core.management.color import no_style
+from django.db import backend, connection, connections, DEFAULT_DB_ALIAS
+from django.db.backends.signals import connection_created
+from django.db.backends.postgresql import version as pg_version
from django.test import TestCase
+from regressiontests.backends import models
+
class Callproc(unittest.TestCase):
def test_dbms_session(self):
@@ -76,6 +81,7 @@ class DateQuotingTest(TestCase):
classes = models.SchoolClass.objects.filter(last_updated__day=20)
self.assertEqual(len(classes), 1)
+
class ParameterHandlingTest(TestCase):
def test_bad_parameter_count(self):
"An executemany call with too many/not enough parameters will raise an exception (Refs #12612)"
@@ -88,46 +94,95 @@ class ParameterHandlingTest(TestCase):
self.assertRaises(Exception, cursor.executemany, query, [(1,2,3),])
self.assertRaises(Exception, cursor.executemany, query, [(1,),])
+# Unfortunately, the following tests would be a good test to run on all
+# backends, but it breaks MySQL hard. Until #13711 is fixed, it can't be run
+# everywhere (although it would be an effective test of #13711).
+if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.mysql':
+ class LongNameTest(TestCase):
+ """Long primary keys and model names can result in a sequence name
+ that exceeds the database limits, which will result in truncation
+ on certain databases (e.g., Postgres). The backend needs to use
+ the correct sequence name in last_insert_id and other places, so
+ check it is. Refs #8901.
+ """
-def connection_created_test(sender, **kwargs):
- print 'connection_created signal'
-
-__test__ = {'API_TESTS': """
-# Check Postgres version parsing
->>> from django.db.backends.postgresql import version as pg_version
-
->>> pg_version._parse_version("PostgreSQL 8.3.1 on i386-apple-darwin9.2.2, compiled by GCC i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc. build 5478)")
-(8, 3, 1)
-
->>> pg_version._parse_version("PostgreSQL 8.3.6")
-(8, 3, 6)
-
->>> pg_version._parse_version("PostgreSQL 8.3")
-(8, 3, None)
-
->>> pg_version._parse_version("EnterpriseDB 8.3")
-(8, 3, None)
-
->>> pg_version._parse_version("PostgreSQL 8.3 beta4")
-(8, 3, None)
+ def test_sequence_name_length_limits_create(self):
+ """Test creation of model with long name and long pk name doesn't error. Ref #8901"""
+ models.VeryLongModelNameZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ.objects.create()
+
+ def test_sequence_name_length_limits_m2m(self):
+ """Test an m2m save of a model with a long name and a long m2m field name doesn't error as on Django >=1.2 this now uses object saves. Ref #8901"""
+ obj = models.VeryLongModelNameZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ.objects.create()
+ rel_obj = models.Person.objects.create(first_name='Django', last_name='Reinhardt')
+ obj.m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.add(rel_obj)
+
+ def test_sequence_name_length_limits_flush(self):
+ """Test that sequence resetting as part of a flush with model with long name and long pk name doesn't error. Ref #8901"""
+ # A full flush is expensive to the full test, so we dig into the
+ # internals to generate the likely offending SQL and run it manually
+
+ # Some convenience aliases
+ VLM = models.VeryLongModelNameZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
+ VLM_m2m = VLM.m2m_also_quite_long_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.through
+ tables = [
+ VLM._meta.db_table,
+ VLM_m2m._meta.db_table,
+ ]
+ sequences = [
+ {
+ 'column': VLM._meta.pk.column,
+ 'table': VLM._meta.db_table
+ },
+ ]
+ cursor = connection.cursor()
+ for statement in connection.ops.sql_flush(no_style(), tables, sequences):
+ cursor.execute(statement)
->>> pg_version._parse_version("PostgreSQL 8.4beta1")
-(8, 4, None)
+class SequenceResetTest(TestCase):
+ def test_generic_relation(self):
+ "Sequence names are correct when resetting generic relations (Ref #13941)"
+ # Create an object with a manually specified PK
+ models.Post.objects.create(id=10, name='1st post', text='hello world')
-"""}
+ # Reset the sequences for the database
+ cursor = connection.cursor()
+ commands = connections[DEFAULT_DB_ALIAS].ops.sequence_reset_sql(no_style(), [models.Post])
+ for sql in commands:
+ cursor.execute(sql)
+
+ # If we create a new object now, it should have a PK greater
+ # than the PK we specified manually.
+ obj = models.Post.objects.create(name='New post', text='goodbye world')
+ self.assertTrue(obj.pk > 10)
+
+class PostgresVersionTest(TestCase):
+ def assert_parses(self, version_string, version):
+ self.assertEqual(pg_version._parse_version(version_string), version)
+
+ def test_parsing(self):
+ self.assert_parses("PostgreSQL 8.3 beta4", (8, 3, None))
+ self.assert_parses("PostgreSQL 8.3", (8, 3, None))
+ self.assert_parses("EnterpriseDB 8.3", (8, 3, None))
+ self.assert_parses("PostgreSQL 8.3.6", (8, 3, 6))
+ self.assert_parses("PostgreSQL 8.4beta1", (8, 4, None))
+ self.assert_parses("PostgreSQL 8.3.1 on i386-apple-darwin9.2.2, compiled by GCC i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc. build 5478)", (8, 3, 1))
# Unfortunately with sqlite3 the in-memory test database cannot be
# closed, and so it cannot be re-opened during testing, and so we
# sadly disable this test for now.
-if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.sqlite3':
- __test__['API_TESTS'] += """
->>> connection_created.connect(connection_created_test)
->>> connection.close() # Ensure the connection is closed
->>> cursor = connection.cursor()
-connection_created signal
->>> connection_created.disconnect(connection_created_test)
->>> cursor = connection.cursor()
-"""
-
-if __name__ == '__main__':
- unittest.main()
+if settings.DATABASES[DEFAULT_DB_ALIAS]["ENGINE"] != "django.db.backends.sqlite3":
+ class ConnectionCreatedSignalTest(TestCase):
+ def test_signal(self):
+ data = {}
+ def receiver(sender, connection, **kwargs):
+ data["connection"] = connection
+
+ connection_created.connect(receiver)
+ connection.close()
+ cursor = connection.cursor()
+ self.assertTrue(data["connection"] is connection)
+
+ connection_created.disconnect(receiver)
+ data.clear()
+ cursor = connection.cursor()
+ self.assertTrue(data == {})
diff --git a/tests/regressiontests/cache/liberal_backend.py b/tests/regressiontests/cache/liberal_backend.py
new file mode 100644
index 0000000000..5c7e312690
--- /dev/null
+++ b/tests/regressiontests/cache/liberal_backend.py
@@ -0,0 +1,9 @@
+from django.core.cache.backends.locmem import CacheClass as LocMemCacheClass
+
+class LiberalKeyValidationMixin(object):
+ def validate_key(self, key):
+ pass
+
+class CacheClass(LiberalKeyValidationMixin, LocMemCacheClass):
+ pass
+
diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py
index 109374c46c..1e0a4046bb 100644
--- a/tests/regressiontests/cache/tests.py
+++ b/tests/regressiontests/cache/tests.py
@@ -8,11 +8,12 @@ import shutil
import tempfile
import time
import unittest
+import warnings
from django.conf import settings
from django.core import management
from django.core.cache import get_cache
-from django.core.cache.backends.base import InvalidCacheBackendError
+from django.core.cache.backends.base import InvalidCacheBackendError, CacheKeyWarning
from django.http import HttpResponse, HttpRequest
from django.middleware.cache import FetchFromCacheMiddleware, UpdateCacheMiddleware
from django.utils import translation
@@ -352,21 +353,68 @@ class BaseCacheTests(object):
self.assertEqual(self.cache.get('key3'), 'sausage')
self.assertEqual(self.cache.get('key4'), 'lobster bisque')
+ def perform_cull_test(self, initial_count, final_count):
+ """This is implemented as a utility method, because only some of the backends
+ implement culling. The culling algorithm also varies slightly, so the final
+ number of entries will vary between backends"""
+ # Create initial cache key entries. This will overflow the cache, causing a cull
+ for i in range(1, initial_count):
+ self.cache.set('cull%d' % i, 'value', 1000)
+ count = 0
+ # Count how many keys are left in the cache.
+ for i in range(1, initial_count):
+ if self.cache.has_key('cull%d' % i):
+ count = count + 1
+ self.assertEqual(count, final_count)
+
+ def test_invalid_keys(self):
+ """
+ All the builtin backends (except memcached, see below) should warn on
+ keys that would be refused by memcached. This encourages portable
+ caching code without making it too difficult to use production backends
+ with more liberal key rules. Refs #6447.
+
+ """
+ # On Python 2.6+ we could use the catch_warnings context
+ # manager to test this warning nicely. Since we can't do that
+ # yet, the cleanest option is to temporarily ask for
+ # CacheKeyWarning to be raised as an exception.
+ warnings.simplefilter("error", CacheKeyWarning)
+
+ # memcached does not allow whitespace or control characters in keys
+ self.assertRaises(CacheKeyWarning, self.cache.set, 'key with spaces', 'value')
+ # memcached limits key length to 250
+ self.assertRaises(CacheKeyWarning, self.cache.set, 'a' * 251, 'value')
+
+ # The warnings module has no public API for getting the
+ # current list of warning filters, so we can't save that off
+ # and reset to the previous value, we have to globally reset
+ # it. The effect will be the same, as long as the Django test
+ # runner doesn't add any global warning filters (it currently
+ # does not).
+ warnings.resetwarnings()
+
class DBCacheTests(unittest.TestCase, BaseCacheTests):
def setUp(self):
# Spaces are used in the table name to ensure quoting/escaping is working
self._table_name = 'test cache table'
management.call_command('createcachetable', self._table_name, verbosity=0, interactive=False)
- self.cache = get_cache('db://%s' % self._table_name)
+ self.cache = get_cache('db://%s?max_entries=30' % self._table_name)
def tearDown(self):
from django.db import connection
cursor = connection.cursor()
cursor.execute('DROP TABLE %s' % connection.ops.quote_name(self._table_name))
+ def test_cull(self):
+ self.perform_cull_test(50, 29)
+
class LocMemCacheTests(unittest.TestCase, BaseCacheTests):
def setUp(self):
- self.cache = get_cache('locmem://')
+ self.cache = get_cache('locmem://?max_entries=30')
+
+ def test_cull(self):
+ self.perform_cull_test(50, 29)
# memcached backend isn't guaranteed to be available.
# To check the memcached backend, the test settings file will
@@ -377,13 +425,29 @@ if settings.CACHE_BACKEND.startswith('memcached://'):
def setUp(self):
self.cache = get_cache(settings.CACHE_BACKEND)
+ def test_invalid_keys(self):
+ """
+ On memcached, we don't introduce a duplicate key validation
+ step (for speed reasons), we just let the memcached API
+ library raise its own exception on bad keys. Refs #6447.
+
+ In order to be memcached-API-library agnostic, we only assert
+ that a generic exception of some kind is raised.
+
+ """
+ # memcached does not allow whitespace or control characters in keys
+ self.assertRaises(Exception, self.cache.set, 'key with spaces', 'value')
+ # memcached limits key length to 250
+ self.assertRaises(Exception, self.cache.set, 'a' * 251, 'value')
+
+
class FileBasedCacheTests(unittest.TestCase, BaseCacheTests):
"""
Specific test cases for the file-based cache.
"""
def setUp(self):
self.dirname = tempfile.mkdtemp()
- self.cache = get_cache('file://%s' % self.dirname)
+ self.cache = get_cache('file://%s?max_entries=30' % self.dirname)
def test_hashing(self):
"""Test that keys are hashed into subdirectories correctly"""
@@ -406,6 +470,25 @@ class FileBasedCacheTests(unittest.TestCase, BaseCacheTests):
self.assert_(not os.path.exists(os.path.dirname(keypath)))
self.assert_(not os.path.exists(os.path.dirname(os.path.dirname(keypath))))
+ def test_cull(self):
+ self.perform_cull_test(50, 28)
+
+class CustomCacheKeyValidationTests(unittest.TestCase):
+ """
+ Tests for the ability to mixin a custom ``validate_key`` method to
+ a custom cache backend that otherwise inherits from a builtin
+ backend, and override the default key validation. Refs #6447.
+
+ """
+ def test_custom_key_validation(self):
+ cache = get_cache('regressiontests.cache.liberal_backend://')
+
+ # this key is both longer than 250 characters, and has spaces
+ key = 'some key with spaces' * 15
+ val = 'a value'
+ cache.set(key, val)
+ self.assertEqual(cache.get(key), val)
+
class CacheUtils(unittest.TestCase):
"""TestCase for django.utils.cache functions."""
diff --git a/tests/regressiontests/csrf_tests/tests.py b/tests/regressiontests/csrf_tests/tests.py
index 0a24522d9c..9030d397ab 100644
--- a/tests/regressiontests/csrf_tests/tests.py
+++ b/tests/regressiontests/csrf_tests/tests.py
@@ -3,7 +3,7 @@
from django.test import TestCase
from django.http import HttpRequest, HttpResponse
from django.middleware.csrf import CsrfMiddleware, CsrfViewMiddleware
-from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.csrf import csrf_exempt, csrf_view_exempt
from django.core.context_processors import csrf
from django.contrib.sessions.middleware import SessionMiddleware
from django.utils.importlib import import_module
@@ -12,8 +12,8 @@ from django.template import RequestContext, Template
# Response/views used for CsrfResponseMiddleware and CsrfViewMiddleware tests
def post_form_response():
- resp = HttpResponse(content="""
-<html><body><form method="post"><input type="text" /></form></body></html>
+ resp = HttpResponse(content=u"""
+<html><body><h1>\u00a1Unicode!<form method="post"><input type="text" /></form></body></html>
""", mimetype="text/html")
return resp
@@ -56,6 +56,9 @@ class TestingHttpRequest(HttpRequest):
return getattr(self, '_is_secure', False)
class CsrfMiddlewareTest(TestCase):
+ # The csrf token is potentially from an untrusted source, so could have
+ # characters that need dealing with.
+ _csrf_id_cookie = "<1>\xc2\xa1"
_csrf_id = "1"
# This is a valid session token for this ID and secret key. This was generated using
@@ -71,7 +74,7 @@ class CsrfMiddlewareTest(TestCase):
def _get_GET_csrf_cookie_request(self):
req = TestingHttpRequest()
- req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id
+ req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id_cookie
return req
def _get_POST_csrf_cookie_request(self):
@@ -123,6 +126,23 @@ class CsrfMiddlewareTest(TestCase):
# Check the Vary header got patched correctly
self.assert_('Cookie' in resp2.get('Vary',''))
+ def test_process_response_for_exempt_view(self):
+ """
+ Check that a view decorated with 'csrf_view_exempt' is still
+ post-processed to add the CSRF token.
+ """
+ req = self._get_GET_no_csrf_cookie_request()
+ CsrfMiddleware().process_view(req, csrf_view_exempt(post_form_view), (), {})
+
+ resp = post_form_response()
+ resp_content = resp.content # needed because process_response modifies resp
+ resp2 = CsrfMiddleware().process_response(req, resp)
+
+ csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
+ self.assertNotEqual(csrf_cookie, False)
+ self.assertNotEqual(resp_content, resp2.content)
+ self._check_token_present(resp2, csrf_cookie.value)
+
def test_process_response_no_csrf_cookie_view_only_get_token_used(self):
"""
When no prior CSRF cookie exists, check that the cookie is created, even
@@ -187,8 +207,11 @@ class CsrfMiddlewareTest(TestCase):
"""
Check that no post processing is done for an exempt view
"""
- req = self._get_POST_csrf_cookie_request()
- resp = csrf_exempt(post_form_view)(req)
+ req = self._get_GET_csrf_cookie_request()
+ view = csrf_exempt(post_form_view)
+ CsrfMiddleware().process_view(req, view, (), {})
+
+ resp = view(req)
resp_content = resp.content
resp2 = CsrfMiddleware().process_response(req, resp)
self.assertEquals(resp_content, resp2.content)
@@ -270,6 +293,17 @@ class CsrfMiddlewareTest(TestCase):
resp = token_view(req)
self.assertEquals(u"", resp.content)
+ def test_token_node_empty_csrf_cookie(self):
+ """
+ Check that we get a new token if the csrf_cookie is the empty string
+ """
+ req = self._get_GET_no_csrf_cookie_request()
+ req.COOKIES[settings.CSRF_COOKIE_NAME] = ""
+ CsrfViewMiddleware().process_view(req, token_view, (), {})
+ resp = token_view(req)
+
+ self.assertNotEqual(u"", resp.content)
+
def test_token_node_with_csrf_cookie(self):
"""
Check that CsrfTokenNode works when a CSRF cookie is set
@@ -279,6 +313,15 @@ class CsrfMiddlewareTest(TestCase):
resp = token_view(req)
self._check_token_present(resp)
+ def test_get_token_for_exempt_view(self):
+ """
+ Check that get_token still works for a view decorated with 'csrf_view_exempt'.
+ """
+ req = self._get_GET_csrf_cookie_request()
+ CsrfViewMiddleware().process_view(req, csrf_view_exempt(token_view), (), {})
+ resp = token_view(req)
+ self._check_token_present(resp)
+
def test_token_node_with_new_csrf_cookie(self):
"""
Check that CsrfTokenNode works when a CSRF cookie is created by
diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py
index 25555081e4..8341f83e36 100644
--- a/tests/regressiontests/defaultfilters/tests.py
+++ b/tests/regressiontests/defaultfilters/tests.py
@@ -476,6 +476,15 @@ u'1024.0 MB'
>>> filesizeformat(1024*1024*1024)
u'1.0 GB'
+>>> filesizeformat(1024*1024*1024*1024)
+u'1.0 TB'
+
+>>> filesizeformat(1024*1024*1024*1024*1024)
+u'1.0 PB'
+
+>>> filesizeformat(1024*1024*1024*1024*1024*2000)
+u'2000.0 PB'
+
>>> filesizeformat(complex(1,-1))
u'0 bytes'
diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py
index 2c5f0f4551..a1470f9f05 100644
--- a/tests/regressiontests/file_storage/tests.py
+++ b/tests/regressiontests/file_storage/tests.py
@@ -230,3 +230,19 @@ if Image is not None:
finally:
del images.open
self.assert_(FileWrapper._closed)
+
+ class InconsistentGetImageDimensionsBug(TestCase):
+ """
+ Test that get_image_dimensions() works properly after various calls using a file handler (#11158)
+ """
+ def test_multiple_calls(self):
+ """
+ Multiple calls of get_image_dimensions() should return the same size.
+ """
+ from django.core.files.images import ImageFile
+ img_path = os.path.join(os.path.dirname(__file__), "test.png")
+ image = ImageFile(open(img_path))
+ image_pil = Image.open(img_path)
+ size_1, size_2 = get_image_dimensions(image), get_image_dimensions(image)
+ self.assertEqual(image_pil.size, size_1)
+ self.assertEqual(size_1, size_2)
diff --git a/tests/regressiontests/forms/fields.py b/tests/regressiontests/forms/fields.py
index 990a9f74cf..e4f2c261c9 100644
--- a/tests/regressiontests/forms/fields.py
+++ b/tests/regressiontests/forms/fields.py
@@ -766,13 +766,13 @@ class FieldsTests(TestCase):
# FilePathField ###############################################################
def test_filepathfield_65(self):
- path = forms.__file__
+ path = os.path.abspath(forms.__file__)
path = os.path.dirname(path) + '/'
- assert fix_os_paths(path).endswith('/django/forms/')
+ self.assertTrue(fix_os_paths(path).endswith('/django/forms/'))
def test_filepathfield_66(self):
path = forms.__file__
- path = os.path.dirname(path) + '/'
+ path = os.path.dirname(os.path.abspath(path)) + '/'
f = FilePathField(path=path)
f.choices = [p for p in f.choices if p[0].endswith('.py')]
f.choices.sort()
@@ -787,13 +787,13 @@ class FieldsTests(TestCase):
]
for exp, got in zip(expected, fix_os_paths(f.choices)):
self.assertEqual(exp[1], got[1])
- assert got[0].endswith(exp[0])
+ self.assertTrue(got[0].endswith(exp[0]))
self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. fields.py is not one of the available choices.']", f.clean, 'fields.py')
assert fix_os_paths(f.clean(path + 'fields.py')).endswith('/django/forms/fields.py')
def test_filepathfield_67(self):
path = forms.__file__
- path = os.path.dirname(path) + '/'
+ path = os.path.dirname(os.path.abspath(path)) + '/'
f = FilePathField(path=path, match='^.*?\.py$')
f.choices.sort()
expected = [
@@ -807,10 +807,10 @@ class FieldsTests(TestCase):
]
for exp, got in zip(expected, fix_os_paths(f.choices)):
self.assertEqual(exp[1], got[1])
- assert got[0].endswith(exp[0])
+ self.assertTrue(got[0].endswith(exp[0]))
def test_filepathfield_68(self):
- path = forms.__file__
+ path = os.path.abspath(forms.__file__)
path = os.path.dirname(path) + '/'
f = FilePathField(path=path, recursive=True, match='^.*?\.py$')
f.choices.sort()
@@ -827,7 +827,7 @@ class FieldsTests(TestCase):
]
for exp, got in zip(expected, fix_os_paths(f.choices)):
self.assertEqual(exp[1], got[1])
- assert got[0].endswith(exp[0])
+ self.assertTrue(got[0].endswith(exp[0]))
# SplitDateTimeField ##########################################################
diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py
index 58051fd133..91594139f2 100644
--- a/tests/regressiontests/forms/forms.py
+++ b/tests/regressiontests/forms/forms.py
@@ -522,6 +522,18 @@ tags.
<input type="hidden" name="composers" value="P" />
<input type="hidden" name="composers" value="J" />
+DateTimeField rendered as_hidden() is special too
+
+>>> class MessageForm(Form):
+... when = SplitDateTimeField()
+>>> f = MessageForm({'when_0': '1992-01-01', 'when_1': '01:01'})
+>>> print f.is_valid()
+True
+>>> print f['when']
+<input type="text" name="when_0" value="1992-01-01" id="id_when_0" /><input type="text" name="when_1" value="01:01" id="id_when_1" />
+>>> print f['when'].as_hidden()
+<input type="hidden" name="when_0" value="1992-01-01" id="id_when_0" /><input type="hidden" name="when_1" value="01:01" id="id_when_1" />
+
MultipleChoiceField can also be used with the CheckboxSelectMultiple widget.
>>> class SongForm(Form):
... name = CharField()
@@ -705,13 +717,13 @@ Form.clean() is required to return a dictionary of all clean data.
>>> print f.as_table()
<tr><td colspan="2"><ul class="errorlist"><li>Please make sure your passwords match.</li></ul></td></tr>
<tr><th>Username:</th><td><input type="text" name="username" value="adrian" maxlength="10" /></td></tr>
-<tr><th>Password1:</th><td><input type="password" name="password1" value="foo" /></td></tr>
-<tr><th>Password2:</th><td><input type="password" name="password2" value="bar" /></td></tr>
+<tr><th>Password1:</th><td><input type="password" name="password1" /></td></tr>
+<tr><th>Password2:</th><td><input type="password" name="password2" /></td></tr>
>>> print f.as_ul()
<li><ul class="errorlist"><li>Please make sure your passwords match.</li></ul></li>
<li>Username: <input type="text" name="username" value="adrian" maxlength="10" /></li>
-<li>Password1: <input type="password" name="password1" value="foo" /></li>
-<li>Password2: <input type="password" name="password2" value="bar" /></li>
+<li>Password1: <input type="password" name="password1" /></li>
+<li>Password2: <input type="password" name="password2" /></li>
>>> f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'foo'}, auto_id=False)
>>> f.errors
{}
@@ -1258,20 +1270,20 @@ to a Field class. This help text is displayed when a Form is rendered.
... password = CharField(widget=PasswordInput, help_text='Choose wisely.')
>>> p = UserRegistration(auto_id=False)
>>> print p.as_ul()
-<li>Username: <input type="text" name="username" maxlength="10" /> e.g., user@example.com</li>
-<li>Password: <input type="password" name="password" /> Choose wisely.</li>
+<li>Username: <input type="text" name="username" maxlength="10" /> <span class="helptext">e.g., user@example.com</span></li>
+<li>Password: <input type="password" name="password" /> <span class="helptext">Choose wisely.</span></li>
>>> print p.as_p()
-<p>Username: <input type="text" name="username" maxlength="10" /> e.g., user@example.com</p>
-<p>Password: <input type="password" name="password" /> Choose wisely.</p>
+<p>Username: <input type="text" name="username" maxlength="10" /> <span class="helptext">e.g., user@example.com</span></p>
+<p>Password: <input type="password" name="password" /> <span class="helptext">Choose wisely.</span></p>
>>> print p.as_table()
-<tr><th>Username:</th><td><input type="text" name="username" maxlength="10" /><br />e.g., user@example.com</td></tr>
-<tr><th>Password:</th><td><input type="password" name="password" /><br />Choose wisely.</td></tr>
+<tr><th>Username:</th><td><input type="text" name="username" maxlength="10" /><br /><span class="helptext">e.g., user@example.com</span></td></tr>
+<tr><th>Password:</th><td><input type="password" name="password" /><br /><span class="helptext">Choose wisely.</span></td></tr>
The help text is displayed whether or not data is provided for the form.
>>> p = UserRegistration({'username': u'foo'}, auto_id=False)
>>> print p.as_ul()
-<li>Username: <input type="text" name="username" value="foo" maxlength="10" /> e.g., user@example.com</li>
-<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /> Choose wisely.</li>
+<li>Username: <input type="text" name="username" value="foo" maxlength="10" /> <span class="helptext">e.g., user@example.com</span></li>
+<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /> <span class="helptext">Choose wisely.</span></li>
help_text is not displayed for hidden fields. It can be used for documentation
purposes, though.
@@ -1281,7 +1293,7 @@ purposes, though.
... next = CharField(widget=HiddenInput, initial='/', help_text='Redirect destination')
>>> p = UserRegistration(auto_id=False)
>>> print p.as_ul()
-<li>Username: <input type="text" name="username" maxlength="10" /> e.g., user@example.com</li>
+<li>Username: <input type="text" name="username" maxlength="10" /> <span class="helptext">e.g., user@example.com</span></li>
<li>Password: <input type="password" name="password" /><input type="hidden" name="next" value="/" /></li>
Help text can include arbitrary Unicode characters.
@@ -1289,7 +1301,7 @@ Help text can include arbitrary Unicode characters.
... username = CharField(max_length=10, help_text='ŠĐĆŽćžšđ')
>>> p = UserRegistration(auto_id=False)
>>> p.as_ul()
-u'<li>Username: <input type="text" name="username" maxlength="10" /> \u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111</li>'
+u'<li>Username: <input type="text" name="username" maxlength="10" /> <span class="helptext">\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111</span></li>'
# Subclassing forms ###########################################################
@@ -1589,8 +1601,8 @@ Case 2: POST with erroneous data (a redisplayed form, with errors).
<table>
<tr><td colspan="2"><ul class="errorlist"><li>Please make sure your passwords match.</li></ul></td></tr>
<tr><th>Username:</th><td><ul class="errorlist"><li>Ensure this value has at most 10 characters (it has 23).</li></ul><input type="text" name="username" value="this-is-a-long-username" maxlength="10" /></td></tr>
-<tr><th>Password1:</th><td><input type="password" name="password1" value="foo" /></td></tr>
-<tr><th>Password2:</th><td><input type="password" name="password2" value="bar" /></td></tr>
+<tr><th>Password1:</th><td><input type="password" name="password1" /></td></tr>
+<tr><th>Password2:</th><td><input type="password" name="password2" /></td></tr>
</table>
<input type="submit" />
</form>
@@ -1719,8 +1731,8 @@ the list of errors is empty). You can also use it in {% if %} statements.
>>> print t.render(Context({'form': UserRegistration({'username': 'django', 'password1': 'foo', 'password2': 'bar'}, auto_id=False)}))
<form action="">
<p><label>Your username: <input type="text" name="username" value="django" maxlength="10" /></label></p>
-<p><label>Password: <input type="password" name="password1" value="foo" /></label></p>
-<p><label>Password (again): <input type="password" name="password2" value="bar" /></label></p>
+<p><label>Password: <input type="password" name="password1" /></label></p>
+<p><label>Password (again): <input type="password" name="password2" /></label></p>
<input type="submit" />
</form>
>>> t = Template('''<form action="">
@@ -1734,8 +1746,8 @@ the list of errors is empty). You can also use it in {% if %} statements.
<form action="">
<ul class="errorlist"><li>Please make sure your passwords match.</li></ul>
<p><label>Your username: <input type="text" name="username" value="django" maxlength="10" /></label></p>
-<p><label>Password: <input type="password" name="password1" value="foo" /></label></p>
-<p><label>Password (again): <input type="password" name="password2" value="bar" /></label></p>
+<p><label>Password: <input type="password" name="password1" /></label></p>
+<p><label>Password (again): <input type="password" name="password2" /></label></p>
<input type="submit" />
</form>
diff --git a/tests/regressiontests/forms/input_formats.py b/tests/regressiontests/forms/input_formats.py
new file mode 100644
index 0000000000..498c6de9fb
--- /dev/null
+++ b/tests/regressiontests/forms/input_formats.py
@@ -0,0 +1,894 @@
+from datetime import time, date, datetime
+from unittest import TestCase
+
+from django import forms
+from django.conf import settings
+from django.utils.translation import activate, deactivate
+
+
+class LocalizedTimeTests(TestCase):
+ def setUp(self):
+ self.old_TIME_INPUT_FORMATS = settings.TIME_INPUT_FORMATS
+ self.old_USE_L10N = settings.USE_L10N
+
+ settings.TIME_INPUT_FORMATS = ["%I:%M:%S %p", "%I:%M %p"]
+ settings.USE_L10N = True
+
+ activate('de')
+
+ def tearDown(self):
+ settings.TIME_INPUT_FORMATS = self.old_TIME_INPUT_FORMATS
+ settings.USE_L10N = self.old_USE_L10N
+
+ deactivate()
+
+ def test_timeField(self):
+ "TimeFields can parse dates in the default format"
+ f = forms.TimeField()
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13:30:05')
+ self.assertEqual(result, time(13,30,5))
+
+ # Check that the parsed result does a round trip
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '13:30:05')
+
+ # Parse a time in a valid, but non-default format, get a parsed result
+ result = f.clean('13:30')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:00")
+
+ def test_localized_timeField(self):
+ "Localized TimeFields act as unlocalized widgets"
+ f = forms.TimeField(localize=True)
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13:30:05')
+ self.assertEqual(result, time(13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '13:30:05')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13:30')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:00")
+
+ def test_timeField_with_inputformat(self):
+ "TimeFields with manually specified input formats can accept those formats"
+ f = forms.TimeField(input_formats=["%H.%M.%S", "%H.%M"])
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM')
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13.30.05')
+ self.assertEqual(result, time(13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:05")
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13.30')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:00")
+
+ def test_localized_timeField_with_inputformat(self):
+ "Localized TimeFields with manually specified input formats can accept those formats"
+ f = forms.TimeField(input_formats=["%H.%M.%S", "%H.%M"], localize=True)
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM')
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13.30.05')
+ self.assertEqual(result, time(13,30,5))
+
+ # # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:05")
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13.30')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:00")
+
+
+class CustomTimeInputFormatsTests(TestCase):
+ def setUp(self):
+ self.old_TIME_INPUT_FORMATS = settings.TIME_INPUT_FORMATS
+ settings.TIME_INPUT_FORMATS = ["%I:%M:%S %p", "%I:%M %p"]
+
+ def tearDown(self):
+ settings.TIME_INPUT_FORMATS = self.old_TIME_INPUT_FORMATS
+
+ def test_timeField(self):
+ "TimeFields can parse dates in the default format"
+ f = forms.TimeField()
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('1:30:05 PM')
+ self.assertEqual(result, time(13,30,5))
+
+ # Check that the parsed result does a round trip
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '01:30:05 PM')
+
+ # Parse a time in a valid, but non-default format, get a parsed result
+ result = f.clean('1:30 PM')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:00 PM")
+
+ def test_localized_timeField(self):
+ "Localized TimeFields act as unlocalized widgets"
+ f = forms.TimeField(localize=True)
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('1:30:05 PM')
+ self.assertEqual(result, time(13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '01:30:05 PM')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('01:30 PM')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:00 PM")
+
+ def test_timeField_with_inputformat(self):
+ "TimeFields with manually specified input formats can accept those formats"
+ f = forms.TimeField(input_formats=["%H.%M.%S", "%H.%M"])
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM')
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13.30.05')
+ self.assertEqual(result, time(13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:05 PM")
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13.30')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:00 PM")
+
+ def test_localized_timeField_with_inputformat(self):
+ "Localized TimeFields with manually specified input formats can accept those formats"
+ f = forms.TimeField(input_formats=["%H.%M.%S", "%H.%M"], localize=True)
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM')
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13.30.05')
+ self.assertEqual(result, time(13,30,5))
+
+ # # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:05 PM")
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13.30')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:00 PM")
+
+
+class SimpleTimeFormatTests(TestCase):
+ def test_timeField(self):
+ "TimeFields can parse dates in the default format"
+ f = forms.TimeField()
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13:30:05')
+ self.assertEqual(result, time(13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:05")
+
+ # Parse a time in a valid, but non-default format, get a parsed result
+ result = f.clean('13:30')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:00")
+
+ def test_localized_timeField(self):
+ "Localized TimeFields in a non-localized environment act as unlocalized widgets"
+ f = forms.TimeField()
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13:30:05')
+ self.assertEqual(result, time(13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:05")
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('13:30')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:00")
+
+ def test_timeField_with_inputformat(self):
+ "TimeFields with manually specified input formats can accept those formats"
+ f = forms.TimeField(input_formats=["%I:%M:%S %p", "%I:%M %p"])
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('1:30:05 PM')
+ self.assertEqual(result, time(13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:05")
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('1:30 PM')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:00")
+
+ def test_localized_timeField_with_inputformat(self):
+ "Localized TimeFields with manually specified input formats can accept those formats"
+ f = forms.TimeField(input_formats=["%I:%M:%S %p", "%I:%M %p"], localize=True)
+ # Parse a time in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05')
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('1:30:05 PM')
+ self.assertEqual(result, time(13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:05")
+
+ # Parse a time in a valid format, get a parsed result
+ result = f.clean('1:30 PM')
+ self.assertEqual(result, time(13,30,0))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "13:30:00")
+
+
+class LocalizedDateTests(TestCase):
+ def setUp(self):
+ self.old_DATE_INPUT_FORMATS = settings.DATE_INPUT_FORMATS
+ self.old_USE_L10N = settings.USE_L10N
+
+ settings.DATE_INPUT_FORMATS = ["%d/%m/%Y", "%d-%m-%Y"]
+ settings.USE_L10N = True
+
+ activate('de')
+
+ def tearDown(self):
+ settings.DATE_INPUT_FORMATS = self.old_DATE_INPUT_FORMATS
+ settings.USE_L10N = self.old_USE_L10N
+
+ deactivate()
+
+ def test_dateField(self):
+ "DateFields can parse dates in the default format"
+ f = forms.DateField()
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '21/12/2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21.12.2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '21.12.2010')
+
+ # Parse a date in a valid, but non-default format, get a parsed result
+ result = f.clean('21.12.10')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+ def test_localized_dateField(self):
+ "Localized DateFields act as unlocalized widgets"
+ f = forms.DateField(localize=True)
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '21/12/2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21.12.2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '21.12.2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21.12.10')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+ def test_dateField_with_inputformat(self):
+ "DateFields with manually specified input formats can accept those formats"
+ f = forms.DateField(input_formats=["%m.%d.%Y", "%m-%d-%Y"])
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21')
+ self.assertRaises(forms.ValidationError, f.clean, '21/12/2010')
+ self.assertRaises(forms.ValidationError, f.clean, '21.12.2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12.21.2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12-21-2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+ def test_localized_dateField_with_inputformat(self):
+ "Localized DateFields with manually specified input formats can accept those formats"
+ f = forms.DateField(input_formats=["%m.%d.%Y", "%m-%d-%Y"], localize=True)
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21')
+ self.assertRaises(forms.ValidationError, f.clean, '21/12/2010')
+ self.assertRaises(forms.ValidationError, f.clean, '21.12.2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12.21.2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12-21-2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+class CustomDateInputFormatsTests(TestCase):
+ def setUp(self):
+ self.old_DATE_INPUT_FORMATS = settings.DATE_INPUT_FORMATS
+ settings.DATE_INPUT_FORMATS = ["%d.%m.%Y", "%d-%m-%Y"]
+
+ def tearDown(self):
+ settings.DATE_INPUT_FORMATS = self.old_DATE_INPUT_FORMATS
+
+ def test_dateField(self):
+ "DateFields can parse dates in the default format"
+ f = forms.DateField()
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21.12.2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '21.12.2010')
+
+ # Parse a date in a valid, but non-default format, get a parsed result
+ result = f.clean('21-12-2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+ def test_localized_dateField(self):
+ "Localized DateFields act as unlocalized widgets"
+ f = forms.DateField(localize=True)
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21.12.2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '21.12.2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21-12-2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+ def test_dateField_with_inputformat(self):
+ "DateFields with manually specified input formats can accept those formats"
+ f = forms.DateField(input_formats=["%m.%d.%Y", "%m-%d-%Y"])
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '21.12.2010')
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12.21.2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12-21-2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+ def test_localized_dateField_with_inputformat(self):
+ "Localized DateFields with manually specified input formats can accept those formats"
+ f = forms.DateField(input_formats=["%m.%d.%Y", "%m-%d-%Y"], localize=True)
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '21.12.2010')
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12.21.2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12-21-2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010")
+
+class SimpleDateFormatTests(TestCase):
+ def test_dateField(self):
+ "DateFields can parse dates in the default format"
+ f = forms.DateField()
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '21.12.2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('2010-12-21')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21")
+
+ # Parse a date in a valid, but non-default format, get a parsed result
+ result = f.clean('12/21/2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21")
+
+ def test_localized_dateField(self):
+ "Localized DateFields in a non-localized environment act as unlocalized widgets"
+ f = forms.DateField()
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '21.12.2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('2010-12-21')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12/21/2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21")
+
+ def test_dateField_with_inputformat(self):
+ "DateFields with manually specified input formats can accept those formats"
+ f = forms.DateField(input_formats=["%d.%m.%Y", "%d-%m-%Y"])
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21.12.2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21-12-2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21")
+
+ def test_localized_dateField_with_inputformat(self):
+ "Localized DateFields with manually specified input formats can accept those formats"
+ f = forms.DateField(input_formats=["%d.%m.%Y", "%d-%m-%Y"], localize=True)
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21.12.2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21-12-2010')
+ self.assertEqual(result, date(2010,12,21))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21")
+
+class LocalizedDateTimeTests(TestCase):
+ def setUp(self):
+ self.old_DATETIME_INPUT_FORMATS = settings.DATETIME_INPUT_FORMATS
+ self.old_USE_L10N = settings.USE_L10N
+
+ settings.DATETIME_INPUT_FORMATS = ["%I:%M:%S %p %d/%m/%Y", "%I:%M %p %d-%m-%Y"]
+ settings.USE_L10N = True
+
+ activate('de')
+
+ def tearDown(self):
+ settings.DATETIME_INPUT_FORMATS = self.old_DATETIME_INPUT_FORMATS
+ settings.USE_L10N = self.old_USE_L10N
+
+ deactivate()
+
+ def test_dateTimeField(self):
+ "DateTimeFields can parse dates in the default format"
+ f = forms.DateTimeField()
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM 21/12/2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21.12.2010 13:30:05')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '21.12.2010 13:30:05')
+
+ # Parse a date in a valid, but non-default format, get a parsed result
+ result = f.clean('21.12.2010 13:30')
+ self.assertEqual(result, datetime(2010,12,21,13,30))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010 13:30:00")
+
+ def test_localized_dateTimeField(self):
+ "Localized DateTimeFields act as unlocalized widgets"
+ f = forms.DateTimeField(localize=True)
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM 21/12/2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21.12.2010 13:30:05')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '21.12.2010 13:30:05')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('21.12.2010 13:30')
+ self.assertEqual(result, datetime(2010,12,21,13,30))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010 13:30:00")
+
+ def test_dateTimeField_with_inputformat(self):
+ "DateTimeFields with manually specified input formats can accept those formats"
+ f = forms.DateTimeField(input_formats=["%H.%M.%S %m.%d.%Y", "%H.%M %m-%d-%Y"])
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21 13:30:05 13:30:05')
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM 21/12/2010')
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05 21.12.2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('13.30.05 12.21.2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010 13:30:05")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('13.30 12-21-2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010 13:30:00")
+
+ def test_localized_dateTimeField_with_inputformat(self):
+ "Localized DateTimeFields with manually specified input formats can accept those formats"
+ f = forms.DateTimeField(input_formats=["%H.%M.%S %m.%d.%Y", "%H.%M %m-%d-%Y"], localize=True)
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21 13:30:05')
+ self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM 21/12/2010')
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05 21.12.2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('13.30.05 12.21.2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010 13:30:05")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('13.30 12-21-2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "21.12.2010 13:30:00")
+
+
+class CustomDateTimeInputFormatsTests(TestCase):
+ def setUp(self):
+ self.old_DATETIME_INPUT_FORMATS = settings.DATETIME_INPUT_FORMATS
+ settings.DATETIME_INPUT_FORMATS = ["%I:%M:%S %p %d/%m/%Y", "%I:%M %p %d-%m-%Y"]
+
+ def tearDown(self):
+ settings.DATETIME_INPUT_FORMATS = self.old_DATETIME_INPUT_FORMATS
+
+ def test_dateTimeField(self):
+ "DateTimeFields can parse dates in the default format"
+ f = forms.DateTimeField()
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21 13:30:05')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('1:30:05 PM 21/12/2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '01:30:05 PM 21/12/2010')
+
+ # Parse a date in a valid, but non-default format, get a parsed result
+ result = f.clean('1:30 PM 21-12-2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:00 PM 21/12/2010")
+
+ def test_localized_dateTimeField(self):
+ "Localized DateTimeFields act as unlocalized widgets"
+ f = forms.DateTimeField(localize=True)
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21 13:30:05')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('1:30:05 PM 21/12/2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, '01:30:05 PM 21/12/2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('1:30 PM 21-12-2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:00 PM 21/12/2010")
+
+ def test_dateTimeField_with_inputformat(self):
+ "DateTimeFields with manually specified input formats can accept those formats"
+ f = forms.DateTimeField(input_formats=["%m.%d.%Y %H:%M:%S", "%m-%d-%Y %H:%M"])
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05 21.12.2010')
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21 13:30:05')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12.21.2010 13:30:05')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:05 PM 21/12/2010")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12-21-2010 13:30')
+ self.assertEqual(result, datetime(2010,12,21,13,30))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:00 PM 21/12/2010")
+
+ def test_localized_dateTimeField_with_inputformat(self):
+ "Localized DateTimeFields with manually specified input formats can accept those formats"
+ f = forms.DateTimeField(input_formats=["%m.%d.%Y %H:%M:%S", "%m-%d-%Y %H:%M"], localize=True)
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05 21.12.2010')
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21 13:30:05')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12.21.2010 13:30:05')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:05 PM 21/12/2010")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12-21-2010 13:30')
+ self.assertEqual(result, datetime(2010,12,21,13,30))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "01:30:00 PM 21/12/2010")
+
+class SimpleDateTimeFormatTests(TestCase):
+ def test_dateTimeField(self):
+ "DateTimeFields can parse dates in the default format"
+ f = forms.DateTimeField()
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05 21.12.2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('2010-12-21 13:30:05')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21 13:30:05")
+
+ # Parse a date in a valid, but non-default format, get a parsed result
+ result = f.clean('12/21/2010 13:30:05')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21 13:30:05")
+
+ def test_localized_dateTimeField(self):
+ "Localized DateTimeFields in a non-localized environment act as unlocalized widgets"
+ f = forms.DateTimeField()
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '13:30:05 21.12.2010')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('2010-12-21 13:30:05')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21 13:30:05")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('12/21/2010 13:30:05')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21 13:30:05")
+
+ def test_dateTimeField_with_inputformat(self):
+ "DateTimeFields with manually specified input formats can accept those formats"
+ f = forms.DateTimeField(input_formats=["%I:%M:%S %p %d.%m.%Y", "%I:%M %p %d-%m-%Y"])
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21 13:30:05')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('1:30:05 PM 21.12.2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21 13:30:05")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('1:30 PM 21-12-2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21 13:30:00")
+
+ def test_localized_dateTimeField_with_inputformat(self):
+ "Localized DateTimeFields with manually specified input formats can accept those formats"
+ f = forms.DateTimeField(input_formats=["%I:%M:%S %p %d.%m.%Y", "%I:%M %p %d-%m-%Y"], localize=True)
+ # Parse a date in an unaccepted format; get an error
+ self.assertRaises(forms.ValidationError, f.clean, '2010-12-21 13:30:05')
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('1:30:05 PM 21.12.2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30,5))
+
+ # Check that the parsed result does a round trip to the same format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21 13:30:05")
+
+ # Parse a date in a valid format, get a parsed result
+ result = f.clean('1:30 PM 21-12-2010')
+ self.assertEqual(result, datetime(2010,12,21,13,30))
+
+ # Check that the parsed result does a round trip to default format
+ text = f.widget._format_value(result)
+ self.assertEqual(text, "2010-12-21 13:30:00")
diff --git a/tests/regressiontests/forms/localflavor/au.py b/tests/regressiontests/forms/localflavor/au.py
index fd4c0d6980..cda782094a 100644
--- a/tests/regressiontests/forms/localflavor/au.py
+++ b/tests/regressiontests/forms/localflavor/au.py
@@ -50,7 +50,7 @@ u''
## AUPhoneNumberField ########################################################
A field that accepts a 10 digit Australian phone number.
-llows spaces and parentheses around area code.
+Allows spaces and parentheses around area code.
>>> from django.contrib.localflavor.au.forms import AUPhoneNumberField
>>> f = AUPhoneNumberField()
diff --git a/tests/regressiontests/forms/models.py b/tests/regressiontests/forms/models.py
index 229c50556e..028ff9bad2 100644
--- a/tests/regressiontests/forms/models.py
+++ b/tests/regressiontests/forms/models.py
@@ -38,11 +38,28 @@ class ChoiceOptionModel(models.Model):
Can't reuse ChoiceModel because error_message tests require that it have no instances."""
name = models.CharField(max_length=10)
+ class Meta:
+ ordering = ('name',)
+
+ def __unicode__(self):
+ return u'ChoiceOption %d' % self.pk
+
class ChoiceFieldModel(models.Model):
"""Model with ForeignKey to another model, for testing ModelForm
generation with ModelChoiceField."""
choice = models.ForeignKey(ChoiceOptionModel, blank=False,
- default=lambda: ChoiceOptionModel.objects.all()[0])
+ default=lambda: ChoiceOptionModel.objects.get(name='default'))
+ choice_int = models.ForeignKey(ChoiceOptionModel, blank=False, related_name='choice_int',
+ default=lambda: 1)
+
+ multi_choice = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='multi_choice',
+ default=lambda: ChoiceOptionModel.objects.filter(name='default'))
+ multi_choice_int = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='multi_choice_int',
+ default=lambda: [1])
+
+class ChoiceFieldForm(django_forms.ModelForm):
+ class Meta:
+ model = ChoiceFieldModel
class FileModel(models.Model):
file = models.FileField(storage=temp_storage, upload_to='tests')
@@ -74,6 +91,74 @@ class TestTicket12510(TestCase):
# only one query is required to pull the model from DB
self.assertEqual(initial_queries+1, len(connection.queries))
+class ModelFormCallableModelDefault(TestCase):
+ def test_no_empty_option(self):
+ "If a model's ForeignKey has blank=False and a default, no empty option is created (Refs #10792)."
+ option = ChoiceOptionModel.objects.create(name='default')
+
+ choices = list(ChoiceFieldForm().fields['choice'].choices)
+ self.assertEquals(len(choices), 1)
+ self.assertEquals(choices[0], (option.pk, unicode(option)))
+
+ def test_callable_initial_value(self):
+ "The initial value for a callable default returning a queryset is the pk (refs #13769)"
+ obj1 = ChoiceOptionModel.objects.create(id=1, name='default')
+ obj2 = ChoiceOptionModel.objects.create(id=2, name='option 2')
+ obj3 = ChoiceOptionModel.objects.create(id=3, name='option 3')
+ self.assertEquals(ChoiceFieldForm().as_p(), """<p><label for="id_choice">Choice:</label> <select name="choice" id="id_choice">
+<option value="1" selected="selected">ChoiceOption 1</option>
+<option value="2">ChoiceOption 2</option>
+<option value="3">ChoiceOption 3</option>
+</select><input type="hidden" name="initial-choice" value="1" id="initial-id_choice" /></p>
+<p><label for="id_choice_int">Choice int:</label> <select name="choice_int" id="id_choice_int">
+<option value="1" selected="selected">ChoiceOption 1</option>
+<option value="2">ChoiceOption 2</option>
+<option value="3">ChoiceOption 3</option>
+</select><input type="hidden" name="initial-choice_int" value="1" id="initial-id_choice_int" /></p>
+<p><label for="id_multi_choice">Multi choice:</label> <select multiple="multiple" name="multi_choice" id="id_multi_choice">
+<option value="1" selected="selected">ChoiceOption 1</option>
+<option value="2">ChoiceOption 2</option>
+<option value="3">ChoiceOption 3</option>
+</select><input type="hidden" name="initial-multi_choice" value="1" id="initial-id_multi_choice_0" /> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>
+<p><label for="id_multi_choice_int">Multi choice int:</label> <select multiple="multiple" name="multi_choice_int" id="id_multi_choice_int">
+<option value="1" selected="selected">ChoiceOption 1</option>
+<option value="2">ChoiceOption 2</option>
+<option value="3">ChoiceOption 3</option>
+</select><input type="hidden" name="initial-multi_choice_int" value="1" id="initial-id_multi_choice_int_0" /> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>""")
+
+ def test_initial_instance_value(self):
+ "Initial instances for model fields may also be instances (refs #7287)"
+ obj1 = ChoiceOptionModel.objects.create(id=1, name='default')
+ obj2 = ChoiceOptionModel.objects.create(id=2, name='option 2')
+ obj3 = ChoiceOptionModel.objects.create(id=3, name='option 3')
+ self.assertEquals(ChoiceFieldForm(initial={
+ 'choice': obj2,
+ 'choice_int': obj2,
+ 'multi_choice': [obj2,obj3],
+ 'multi_choice_int': ChoiceOptionModel.objects.exclude(name="default"),
+ }).as_p(), """<p><label for="id_choice">Choice:</label> <select name="choice" id="id_choice">
+<option value="1">ChoiceOption 1</option>
+<option value="2" selected="selected">ChoiceOption 2</option>
+<option value="3">ChoiceOption 3</option>
+</select><input type="hidden" name="initial-choice" value="2" id="initial-id_choice" /></p>
+<p><label for="id_choice_int">Choice int:</label> <select name="choice_int" id="id_choice_int">
+<option value="1">ChoiceOption 1</option>
+<option value="2" selected="selected">ChoiceOption 2</option>
+<option value="3">ChoiceOption 3</option>
+</select><input type="hidden" name="initial-choice_int" value="2" id="initial-id_choice_int" /></p>
+<p><label for="id_multi_choice">Multi choice:</label> <select multiple="multiple" name="multi_choice" id="id_multi_choice">
+<option value="1">ChoiceOption 1</option>
+<option value="2" selected="selected">ChoiceOption 2</option>
+<option value="3" selected="selected">ChoiceOption 3</option>
+</select><input type="hidden" name="initial-multi_choice" value="2" id="initial-id_multi_choice_0" />
+<input type="hidden" name="initial-multi_choice" value="3" id="initial-id_multi_choice_1" /> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>
+<p><label for="id_multi_choice_int">Multi choice int:</label> <select multiple="multiple" name="multi_choice_int" id="id_multi_choice_int">
+<option value="1">ChoiceOption 1</option>
+<option value="2" selected="selected">ChoiceOption 2</option>
+<option value="3" selected="selected">ChoiceOption 3</option>
+</select><input type="hidden" name="initial-multi_choice_int" value="2" id="initial-id_multi_choice_int_0" />
+<input type="hidden" name="initial-multi_choice_int" value="3" id="initial-id_multi_choice_int_1" /> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>""")
+
__test__ = {'API_TESTS': """
>>> from django.forms.models import ModelForm
@@ -155,18 +240,5 @@ u'class default value'
datetime.date(1999, 3, 2)
>>> shutil.rmtree(temp_storage_location)
-In a ModelForm with a ModelChoiceField, if the model's ForeignKey has blank=False and a default,
-no empty option is created (regression test for #10792).
-
-First we need at least one instance of ChoiceOptionModel:
-
->>> ChoiceOptionModel.objects.create(name='default')
-<ChoiceOptionModel: ChoiceOptionModel object>
-
->>> class ChoiceFieldForm(ModelForm):
-... class Meta:
-... model = ChoiceFieldModel
->>> list(ChoiceFieldForm().fields['choice'].choices)
-[(1, u'ChoiceOptionModel object')]
"""}
diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
index 8757e799a9..7a91cb701e 100644
--- a/tests/regressiontests/forms/tests.py
+++ b/tests/regressiontests/forms/tests.py
@@ -41,6 +41,8 @@ from fields import FieldsTests
from validators import TestFieldWithValidators
from widgets import WidgetTests
+from input_formats import *
+
__test__ = {
'extra_tests': extra_tests,
'form_tests': form_tests,
diff --git a/tests/regressiontests/forms/widgets.py b/tests/regressiontests/forms/widgets.py
index 39d7d569a3..59b33d5210 100644
--- a/tests/regressiontests/forms/widgets.py
+++ b/tests/regressiontests/forms/widgets.py
@@ -62,6 +62,17 @@ u'<input type="text" class="special" name="email" />'
u'<input type="password" name="email" />'
>>> w.render('email', None)
u'<input type="password" name="email" />'
+>>> w.render('email', 'secret')
+u'<input type="password" name="email" />'
+
+The render_value argument lets you specify whether the widget should render
+its value. For security reasons, this is off by default.
+
+>>> w = PasswordInput(render_value=True)
+>>> w.render('email', '')
+u'<input type="password" name="email" />'
+>>> w.render('email', None)
+u'<input type="password" name="email" />'
>>> w.render('email', 'test@example.com')
u'<input type="password" name="email" value="test@example.com" />'
>>> w.render('email', 'some "quoted" & ampersanded value')
@@ -70,36 +81,20 @@ u'<input type="password" name="email" value="some &quot;quoted&quot; &amp; amper
u'<input type="password" name="email" value="test@example.com" class="fun" />'
You can also pass 'attrs' to the constructor:
->>> w = PasswordInput(attrs={'class': 'fun'})
+>>> w = PasswordInput(attrs={'class': 'fun'}, render_value=True)
>>> w.render('email', '')
u'<input type="password" class="fun" name="email" />'
>>> w.render('email', 'foo@example.com')
u'<input type="password" class="fun" value="foo@example.com" name="email" />'
'attrs' passed to render() get precedence over those passed to the constructor:
->>> w = PasswordInput(attrs={'class': 'pretty'})
+>>> w = PasswordInput(attrs={'class': 'pretty'}, render_value=True)
>>> w.render('email', '', attrs={'class': 'special'})
u'<input type="password" class="special" name="email" />'
>>> w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'})
u'<input type="password" class="fun" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" name="email" />'
-The render_value argument lets you specify whether the widget should render
-its value. You may want to do this for security reasons.
->>> w = PasswordInput(render_value=True)
->>> w.render('email', 'secret')
-u'<input type="password" name="email" value="secret" />'
->>> w = PasswordInput(render_value=False)
->>> w.render('email', '')
-u'<input type="password" name="email" />'
->>> w.render('email', None)
-u'<input type="password" name="email" />'
->>> w.render('email', 'secret')
-u'<input type="password" name="email" />'
->>> w = PasswordInput(attrs={'class': 'fun'}, render_value=False)
->>> w.render('email', 'secret')
-u'<input type="password" class="fun" name="email" />'
-
# HiddenInput Widget ############################################################
>>> w = HiddenInput()
@@ -1286,7 +1281,7 @@ class SelectAndTextWidget(forms.MultiWidget):
forms.TextInput
]
super(SelectAndTextWidget, self).__init__(widgets)
-
+
def _set_choices(self, choices):
"""
When choices are set for this widget, we want to pass those along to the Select widget
@@ -1310,3 +1305,21 @@ class WidgetTests(TestCase):
# w2 ought to be independent of w1, since MultiWidget ought
# to make a copy of its sub-widgets when it is copied.
self.assertEqual(w1.choices, [1,2,3])
+
+ def test_13390(self):
+ # See ticket #13390
+ class SplitDateForm(forms.Form):
+ field = forms.DateTimeField(widget=forms.SplitDateTimeWidget, required=False)
+
+ form = SplitDateForm({'field': ''})
+ self.assertTrue(form.is_valid())
+ form = SplitDateForm({'field': ['', '']})
+ self.assertTrue(form.is_valid())
+
+ class SplitDateRequiredForm(forms.Form):
+ field = forms.DateTimeField(widget=forms.SplitDateTimeWidget, required=True)
+
+ form = SplitDateRequiredForm({'field': ''})
+ self.assertFalse(form.is_valid())
+ form = SplitDateRequiredForm({'field': ['', '']})
+ self.assertFalse(form.is_valid())
diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py
index 132472ca37..d3991aad86 100644
--- a/tests/regressiontests/httpwrappers/tests.py
+++ b/tests/regressiontests/httpwrappers/tests.py
@@ -1,476 +1,237 @@
-"""
-###################
-# Empty QueryDict #
-###################
-
->>> q = QueryDict('')
-
->>> q['foo']
-Traceback (most recent call last):
-...
-MultiValueDictKeyError: "Key 'foo' not found in <QueryDict: {}>"
-
->>> q['something'] = 'bar'
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.get('foo', 'default')
-'default'
-
->>> q.getlist('foo')
-[]
-
->>> q.setlist('foo', ['bar', 'baz'])
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.appendlist('foo', ['bar'])
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.has_key('foo')
-False
-
->>> 'foo' in q
-False
-
->>> q.items()
-[]
-
->>> q.lists()
-[]
-
->>> q.keys()
-[]
-
->>> q.values()
-[]
-
->>> len(q)
-0
-
->>> q.update({'foo': 'bar'})
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.pop('foo')
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.popitem()
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.clear()
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.setdefault('foo', 'bar')
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.urlencode()
-''
-
-###################################
-# Mutable copy of empty QueryDict #
-###################################
-
->>> q = q.copy()
-
->>> q['foo']
-Traceback (most recent call last):
-...
-MultiValueDictKeyError: "Key 'foo' not found in <QueryDict: {}>"
-
->>> q['name'] = 'john'
-
->>> q['name']
-u'john'
-
->>> del q['name']
->>> 'name' in q
-False
-
->>> q['name'] = 'john'
-
->>> q.get('foo', 'default')
-'default'
-
->>> q.get('name', 'default')
-u'john'
-
->>> q.getlist('name')
-[u'john']
-
->>> q.getlist('foo')
-[]
-
->>> q.setlist('foo', ['bar', 'baz'])
-
->>> q.get('foo', 'default')
-u'baz'
-
->>> q.getlist('foo')
-[u'bar', u'baz']
-
->>> q.appendlist('foo', 'another')
-
->>> q.getlist('foo')
-[u'bar', u'baz', u'another']
-
->>> q['foo']
-u'another'
-
->>> q.has_key('foo')
-True
-
->>> 'foo' in q
-True
-
->>> q.items()
-[(u'foo', u'another'), (u'name', u'john')]
-
->>> q.lists()
-[(u'foo', [u'bar', u'baz', u'another']), (u'name', [u'john'])]
-
->>> q.keys()
-[u'foo', u'name']
-
->>> q.values()
-[u'another', u'john']
-
->>> len(q)
-2
-
->>> q.update({'foo': 'hello'})
-
-# Displays last value
->>> q['foo']
-u'hello'
-
->>> q.get('foo', 'not available')
-u'hello'
-
->>> q.getlist('foo')
-[u'bar', u'baz', u'another', u'hello']
-
->>> q.pop('foo')
-[u'bar', u'baz', u'another', u'hello']
-
->>> q.pop('foo', 'not there')
-'not there'
-
->>> q.get('foo', 'not there')
-'not there'
-
->>> q.setdefault('foo', 'bar')
-u'bar'
-
->>> q['foo']
-u'bar'
-
->>> q.getlist('foo')
-[u'bar']
-
->>> q.urlencode()
-'foo=bar&name=john'
-
->>> q.clear()
-
->>> len(q)
-0
-
-#####################################
-# QueryDict with one key/value pair #
-#####################################
-
->>> q = QueryDict('foo=bar')
-
->>> q['foo']
-u'bar'
-
->>> q['bar']
-Traceback (most recent call last):
-...
-MultiValueDictKeyError: "Key 'bar' not found in <QueryDict: {u'foo': [u'bar']}>"
-
->>> q['something'] = 'bar'
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.get('foo', 'default')
-u'bar'
-
->>> q.get('bar', 'default')
-'default'
-
->>> q.getlist('foo')
-[u'bar']
-
->>> q.getlist('bar')
-[]
-
->>> q.setlist('foo', ['bar', 'baz'])
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.appendlist('foo', ['bar'])
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.has_key('foo')
-True
-
->>> 'foo' in q
-True
-
->>> q.has_key('bar')
-False
-
->>> 'bar' in q
-False
-
->>> q.items()
-[(u'foo', u'bar')]
-
->>> q.lists()
-[(u'foo', [u'bar'])]
-
->>> q.keys()
-[u'foo']
-
->>> q.values()
-[u'bar']
-
->>> len(q)
-1
-
->>> q.update({'foo': 'bar'})
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.pop('foo')
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.popitem()
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.clear()
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.setdefault('foo', 'bar')
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.urlencode()
-'foo=bar'
-
-#####################################################
-# QueryDict with two key/value pairs with same keys #
-#####################################################
-
->>> q = QueryDict('vote=yes&vote=no')
-
->>> q['vote']
-u'no'
-
->>> q['something'] = 'bar'
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.get('vote', 'default')
-u'no'
-
->>> q.get('foo', 'default')
-'default'
-
->>> q.getlist('vote')
-[u'yes', u'no']
-
->>> q.getlist('foo')
-[]
-
->>> q.setlist('foo', ['bar', 'baz'])
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.appendlist('foo', ['bar'])
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.has_key('vote')
-True
-
->>> 'vote' in q
-True
-
->>> q.has_key('foo')
-False
-
->>> 'foo' in q
-False
-
->>> q.items()
-[(u'vote', u'no')]
-
->>> q.lists()
-[(u'vote', [u'yes', u'no'])]
-
->>> q.keys()
-[u'vote']
-
->>> q.values()
-[u'no']
-
->>> len(q)
-1
-
->>> q.update({'foo': 'bar'})
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.pop('foo')
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.popitem()
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.clear()
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.setdefault('foo', 'bar')
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
->>> q.urlencode()
-'vote=yes&vote=no'
-
->>> del q['vote']
-Traceback (most recent call last):
-...
-AttributeError: This QueryDict instance is immutable
-
-# QueryDicts must be able to handle invalid input encoding (in this case, bad
-# UTF-8 encoding).
->>> q = QueryDict('foo=bar&foo=\xff')
-
->>> q['foo']
-u'\ufffd'
-
->>> q.getlist('foo')
-[u'bar', u'\ufffd']
-
-
-########################
-# Pickling a QueryDict #
-########################
->>> import pickle
->>> q = QueryDict('')
->>> q1 = pickle.loads(pickle.dumps(q, 2))
->>> q == q1
-True
->>> q = QueryDict('a=b&c=d')
->>> q1 = pickle.loads(pickle.dumps(q, 2))
->>> q == q1
-True
->>> q = QueryDict('a=b&c=d&a=1')
->>> q1 = pickle.loads(pickle.dumps(q, 2))
->>> q == q1
-True
-
-######################################
-# HttpResponse with Unicode headers #
-######################################
-
->>> r = HttpResponse()
-
-If we insert a unicode value it will be converted to an ascii
-string. This makes sure we comply with the HTTP specifications.
-
->>> r['value'] = u'test value'
->>> isinstance(r['value'], str)
-True
-
-An error is raised When a unicode object with non-ascii is assigned.
-
->>> r['value'] = u't\xebst value' # doctest:+ELLIPSIS
-Traceback (most recent call last):
-...
-UnicodeEncodeError: ..., HTTP response headers must be in US-ASCII format
-
-The response also converts unicode keys to strings.
-
->>> r[u'test'] = 'testing key'
->>> l = list(r.items())
->>> l.sort()
->>> l[1]
-('test', 'testing key')
-
-It will also raise errors for keys with non-ascii data.
-
->>> r[u't\xebst'] = 'testing key' # doctest:+ELLIPSIS
-Traceback (most recent call last):
-...
-UnicodeEncodeError: ..., HTTP response headers must be in US-ASCII format
-
-# Bug #10188: Do not allow newlines in headers (CR or LF)
->>> r['test\\rstr'] = 'test'
-Traceback (most recent call last):
-...
-BadHeaderError: Header values can't contain newlines (got 'test\\rstr')
-
->>> r['test\\nstr'] = 'test'
-Traceback (most recent call last):
-...
-BadHeaderError: Header values can't contain newlines (got 'test\\nstr')
-
-#
-# Regression test for #8278: QueryDict.update(QueryDict)
-#
->>> x = QueryDict("a=1&a=2", mutable=True)
->>> y = QueryDict("a=3&a=4")
->>> x.update(y)
->>> x.getlist('a')
-[u'1', u'2', u'3', u'4']
-"""
-
-from django.http import QueryDict, HttpResponse, CompatCookie
-from django.test import TestCase
-
-
-class Cookies(TestCase):
-
+import copy
+import pickle
+import unittest
+from django.http import QueryDict, HttpResponse, CompatCookie, BadHeaderError
+
+class QueryDictTests(unittest.TestCase):
+ def test_missing_key(self):
+ q = QueryDict('')
+ self.assertRaises(KeyError, q.__getitem__, 'foo')
+
+ def test_immutability(self):
+ q = QueryDict('')
+ self.assertRaises(AttributeError, q.__setitem__, 'something', 'bar')
+ self.assertRaises(AttributeError, q.setlist, 'foo', ['bar'])
+ self.assertRaises(AttributeError, q.appendlist, 'foo', ['bar'])
+ self.assertRaises(AttributeError, q.update, {'foo': 'bar'})
+ self.assertRaises(AttributeError, q.pop, 'foo')
+ self.assertRaises(AttributeError, q.popitem)
+ self.assertRaises(AttributeError, q.clear)
+
+ def test_immutable_get_with_default(self):
+ q = QueryDict('')
+ self.assertEqual(q.get('foo', 'default'), 'default')
+
+ def test_immutable_basic_operations(self):
+ q = QueryDict('')
+ self.assertEqual(q.getlist('foo'), [])
+ self.assertEqual(q.has_key('foo'), False)
+ self.assertEqual('foo' in q, False)
+ self.assertEqual(q.items(), [])
+ self.assertEqual(q.lists(), [])
+ self.assertEqual(q.items(), [])
+ self.assertEqual(q.keys(), [])
+ self.assertEqual(q.values(), [])
+ self.assertEqual(len(q), 0)
+ self.assertEqual(q.urlencode(), '')
+
+ def test_single_key_value(self):
+ """Test QueryDict with one key/value pair"""
+
+ q = QueryDict('foo=bar')
+ self.assertEqual(q['foo'], 'bar')
+ self.assertRaises(KeyError, q.__getitem__, 'bar')
+ self.assertRaises(AttributeError, q.__setitem__, 'something', 'bar')
+
+ self.assertEqual(q.get('foo', 'default'), 'bar')
+ self.assertEqual(q.get('bar', 'default'), 'default')
+ self.assertEqual(q.getlist('foo'), ['bar'])
+ self.assertEqual(q.getlist('bar'), [])
+
+ self.assertRaises(AttributeError, q.setlist, 'foo', ['bar'])
+ self.assertRaises(AttributeError, q.appendlist, 'foo', ['bar'])
+
+ self.failUnless(q.has_key('foo'))
+ self.failUnless('foo' in q)
+ self.failIf(q.has_key('bar'))
+ self.failIf('bar' in q)
+
+ self.assertEqual(q.items(), [(u'foo', u'bar')])
+ self.assertEqual(q.lists(), [(u'foo', [u'bar'])])
+ self.assertEqual(q.keys(), ['foo'])
+ self.assertEqual(q.values(), ['bar'])
+ self.assertEqual(len(q), 1)
+
+ self.assertRaises(AttributeError, q.update, {'foo': 'bar'})
+ self.assertRaises(AttributeError, q.pop, 'foo')
+ self.assertRaises(AttributeError, q.popitem)
+ self.assertRaises(AttributeError, q.clear)
+ self.assertRaises(AttributeError, q.setdefault, 'foo', 'bar')
+
+ self.assertEqual(q.urlencode(), 'foo=bar')
+
+ def test_mutable_copy(self):
+ """A copy of a QueryDict is mutable."""
+ q = QueryDict('').copy()
+ self.assertRaises(KeyError, q.__getitem__, "foo")
+ q['name'] = 'john'
+ self.assertEqual(q['name'], 'john')
+
+ def test_mutable_delete(self):
+ q = QueryDict('').copy()
+ q['name'] = 'john'
+ del q['name']
+ self.failIf('name' in q)
+
+ def test_basic_mutable_operations(self):
+ q = QueryDict('').copy()
+ q['name'] = 'john'
+ self.assertEqual(q.get('foo', 'default'), 'default')
+ self.assertEqual(q.get('name', 'default'), 'john')
+ self.assertEqual(q.getlist('name'), ['john'])
+ self.assertEqual(q.getlist('foo'), [])
+
+ q.setlist('foo', ['bar', 'baz'])
+ self.assertEqual(q.get('foo', 'default'), 'baz')
+ self.assertEqual(q.getlist('foo'), ['bar', 'baz'])
+
+ q.appendlist('foo', 'another')
+ self.assertEqual(q.getlist('foo'), ['bar', 'baz', 'another'])
+ self.assertEqual(q['foo'], 'another')
+ self.failUnless(q.has_key('foo'))
+ self.failUnless('foo' in q)
+
+ self.assertEqual(q.items(), [(u'foo', u'another'), (u'name', u'john')])
+ self.assertEqual(q.lists(), [(u'foo', [u'bar', u'baz', u'another']), (u'name', [u'john'])])
+ self.assertEqual(q.keys(), [u'foo', u'name'])
+ self.assertEqual(q.values(), [u'another', u'john'])
+ self.assertEqual(len(q), 2)
+
+ q.update({'foo': 'hello'})
+ self.assertEqual(q['foo'], 'hello')
+ self.assertEqual(q.get('foo', 'not available'), 'hello')
+ self.assertEqual(q.getlist('foo'), [u'bar', u'baz', u'another', u'hello'])
+ self.assertEqual(q.pop('foo'), [u'bar', u'baz', u'another', u'hello'])
+ self.assertEqual(q.pop('foo', 'not there'), 'not there')
+ self.assertEqual(q.get('foo', 'not there'), 'not there')
+ self.assertEqual(q.setdefault('foo', 'bar'), 'bar')
+ self.assertEqual(q['foo'], 'bar')
+ self.assertEqual(q.getlist('foo'), ['bar'])
+ self.assertEqual(q.urlencode(), 'foo=bar&name=john')
+
+ q.clear()
+ self.assertEqual(len(q), 0)
+
+ def test_multiple_keys(self):
+ """Test QueryDict with two key/value pairs with same keys."""
+
+ q = QueryDict('vote=yes&vote=no')
+
+ self.assertEqual(q['vote'], u'no')
+ self.assertRaises(AttributeError, q.__setitem__, 'something', 'bar')
+
+ self.assertEqual(q.get('vote', 'default'), u'no')
+ self.assertEqual(q.get('foo', 'default'), 'default')
+ self.assertEqual(q.getlist('vote'), [u'yes', u'no'])
+ self.assertEqual(q.getlist('foo'), [])
+
+ self.assertRaises(AttributeError, q.setlist, 'foo', ['bar', 'baz'])
+ self.assertRaises(AttributeError, q.setlist, 'foo', ['bar', 'baz'])
+ self.assertRaises(AttributeError, q.appendlist, 'foo', ['bar'])
+
+ self.assertEqual(q.has_key('vote'), True)
+ self.assertEqual('vote' in q, True)
+ self.assertEqual(q.has_key('foo'), False)
+ self.assertEqual('foo' in q, False)
+ self.assertEqual(q.items(), [(u'vote', u'no')])
+ self.assertEqual(q.lists(), [(u'vote', [u'yes', u'no'])])
+ self.assertEqual(q.keys(), [u'vote'])
+ self.assertEqual(q.values(), [u'no'])
+ self.assertEqual(len(q), 1)
+
+ self.assertRaises(AttributeError, q.update, {'foo': 'bar'})
+ self.assertRaises(AttributeError, q.pop, 'foo')
+ self.assertRaises(AttributeError, q.popitem)
+ self.assertRaises(AttributeError, q.clear)
+ self.assertRaises(AttributeError, q.setdefault, 'foo', 'bar')
+ self.assertRaises(AttributeError, q.__delitem__, 'vote')
+
+ def test_invalid_input_encoding(self):
+ """
+ QueryDicts must be able to handle invalid input encoding (in this
+ case, bad UTF-8 encoding).
+ """
+ q = QueryDict('foo=bar&foo=\xff')
+ self.assertEqual(q['foo'], u'\ufffd')
+ self.assertEqual(q.getlist('foo'), [u'bar', u'\ufffd'])
+
+ def test_pickle(self):
+ q = QueryDict('')
+ q1 = pickle.loads(pickle.dumps(q, 2))
+ self.assertEqual(q == q1, True)
+ q = QueryDict('a=b&c=d')
+ q1 = pickle.loads(pickle.dumps(q, 2))
+ self.assertEqual(q == q1, True)
+ q = QueryDict('a=b&c=d&a=1')
+ q1 = pickle.loads(pickle.dumps(q, 2))
+ self.assertEqual(q == q1 , True)
+
+ def test_update_from_querydict(self):
+ """Regression test for #8278: QueryDict.update(QueryDict)"""
+ x = QueryDict("a=1&a=2", mutable=True)
+ y = QueryDict("a=3&a=4")
+ x.update(y)
+ self.assertEqual(x.getlist('a'), [u'1', u'2', u'3', u'4'])
+
+ def test_non_default_encoding(self):
+ """#13572 - QueryDict with a non-default encoding"""
+ q = QueryDict('sbb=one', encoding='rot_13')
+ self.assertEqual(q.encoding , 'rot_13' )
+ self.assertEqual(q.items() , [(u'foo', u'bar')] )
+ self.assertEqual(q.urlencode() , 'sbb=one' )
+ q = q.copy()
+ self.assertEqual(q.encoding , 'rot_13' )
+ self.assertEqual(q.items() , [(u'foo', u'bar')] )
+ self.assertEqual(q.urlencode() , 'sbb=one' )
+ self.assertEqual(copy.copy(q).encoding , 'rot_13' )
+ self.assertEqual(copy.deepcopy(q).encoding , 'rot_13')
+
+class HttpResponseTests(unittest.TestCase):
+ def test_unicode_headers(self):
+ r = HttpResponse()
+
+ # If we insert a unicode value it will be converted to an ascii
+ r['value'] = u'test value'
+ self.failUnless(isinstance(r['value'], str))
+
+ # An error is raised ~hen a unicode object with non-ascii is assigned.
+ self.assertRaises(UnicodeEncodeError, r.__setitem__, 'value', u't\xebst value')
+
+ # An error is raised when a unicode object with non-ASCII format is
+ # passed as initial mimetype or content_type.
+ self.assertRaises(UnicodeEncodeError, HttpResponse,
+ mimetype=u't\xebst value')
+
+ # HttpResponse headers must be convertible to ASCII.
+ self.assertRaises(UnicodeEncodeError, HttpResponse,
+ content_type=u't\xebst value')
+
+ # The response also converts unicode keys to strings.)
+ r[u'test'] = 'testing key'
+ l = list(r.items())
+ l.sort()
+ self.assertEqual(l[1], ('test', 'testing key'))
+
+ # It will also raise errors for keys with non-ascii data.
+ self.assertRaises(UnicodeEncodeError, r.__setitem__, u't\xebst key', 'value')
+
+ def test_newlines_in_headers(self):
+ # Bug #10188: Do not allow newlines in headers (CR or LF)
+ r = HttpResponse()
+ self.assertRaises(BadHeaderError, r.__setitem__, 'test\rstr', 'test')
+ self.assertRaises(BadHeaderError, r.__setitem__, 'test\nstr', 'test')
+
+class CookieTests(unittest.TestCase):
def test_encode(self):
"""
Test that we don't output tricky characters in encoded value
@@ -502,7 +263,3 @@ class Cookies(TestCase):
c2 = CompatCookie()
c2.load(c.output())
self.assertEqual(c['test'].value, c2['test'].value)
-
-if __name__ == "__main__":
- import doctest
- doctest.testmod()
diff --git a/tests/regressiontests/i18n/models.py b/tests/regressiontests/i18n/models.py
index 8b142b36bc..75cd996f83 100644
--- a/tests/regressiontests/i18n/models.py
+++ b/tests/regressiontests/i18n/models.py
@@ -10,9 +10,3 @@ class Company(models.Model):
date_added = models.DateTimeField(default=datetime(1799,1,31,23,59,59,0))
cents_payed = models.DecimalField(max_digits=4, decimal_places=2)
products_delivered = models.IntegerField()
-
-__test__ = {'API_TESTS': '''
->>> tm = TestModel()
->>> tm.save()
-'''
-}
diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py
index 32c991a436..7e2e9f8228 100644
--- a/tests/regressiontests/i18n/tests.py
+++ b/tests/regressiontests/i18n/tests.py
@@ -5,14 +5,17 @@ import os
import sys
import pickle
-from django.template import Template, Context
from django.conf import settings
+from django.template import Template, Context
+from django.test import TestCase
from django.utils.formats import get_format, date_format, time_format, localize, localize_input
from django.utils.numberformat import format as nformat
-from django.test import TestCase
+from django.utils.safestring import mark_safe, SafeString, SafeUnicode
from django.utils.translation import ugettext, ugettext_lazy, activate, deactivate, gettext_lazy, to_locale
+
from forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm
+from models import Company, TestModel
class TranslationTests(TestCase):
@@ -59,7 +62,6 @@ class TranslationTests(TestCase):
"""
Translating a string requiring no auto-escaping shouldn't change the "safe" status.
"""
- from django.utils.safestring import mark_safe, SafeString, SafeUnicode
s = mark_safe('Password')
self.assertEqual(SafeString, type(s))
activate('de')
@@ -170,14 +172,14 @@ class FormattingTests(TestCase):
self.assertEqual(u'desembre 2009', date_format(self.d, 'YEAR_MONTH_FORMAT'))
self.assertEqual(u'12/31/2009 8:50 p.m.', date_format(self.dt, 'SHORT_DATETIME_FORMAT'))
self.assertEqual('No localizable', localize('No localizable'))
- self.assertEqual(decimal.Decimal('66666.666'), localize(self.n))
- self.assertEqual(99999.999, localize(self.f))
- self.assertEqual(datetime.date(2009, 12, 31), localize(self.d))
- self.assertEqual(datetime.datetime(2009, 12, 31, 20, 50), localize(self.dt))
+ self.assertEqual('66666.666', localize(self.n))
+ self.assertEqual('99999.999', localize(self.f))
+ self.assertEqual(u'des. 31, 2009', localize(self.d))
+ self.assertEqual(u'des. 31, 2009, 8:50 p.m.', localize(self.dt))
self.assertEqual(u'66666.666', Template('{{ n }}').render(self.ctxt))
self.assertEqual(u'99999.999', Template('{{ f }}').render(self.ctxt))
- self.assertEqual(u'2009-12-31', Template('{{ d }}').render(self.ctxt))
- self.assertEqual(u'2009-12-31 20:50:00', Template('{{ dt }}').render(self.ctxt))
+ self.assertEqual(u'des. 31, 2009', Template('{{ d }}').render(self.ctxt))
+ self.assertEqual(u'des. 31, 2009, 8:50 p.m.', Template('{{ dt }}').render(self.ctxt))
self.assertEqual(u'66666.67', Template('{{ n|floatformat:2 }}').render(self.ctxt))
self.assertEqual(u'100000.0', Template('{{ f|floatformat }}').render(self.ctxt))
self.assertEqual(u'10:15 a.m.', Template('{{ t|time:"TIME_FORMAT" }}').render(self.ctxt))
@@ -620,3 +622,16 @@ class DjangoFallbackResolutionOrderI18NTests(ResolutionOrderI18NTests):
def test_django_fallback(self):
self.assertUgettext('Date/time', 'Datum/Zeit')
+
+
+class TestModels(TestCase):
+ def test_lazy(self):
+ tm = TestModel()
+ tm.save()
+
+ def test_safestr(self):
+ c = Company(cents_payed=12, products_delivered=1)
+ c.name = SafeUnicode(u'Iñtërnâtiônàlizætiøn1')
+ c.save()
+ c.name = SafeString(u'Iñtërnâtiônàlizætiøn1'.encode('utf-8'))
+ c.save()
diff --git a/tests/regressiontests/localflavor/models.py b/tests/regressiontests/localflavor/models.py
index f74a5051d4..e69de29bb2 100644
--- a/tests/regressiontests/localflavor/models.py
+++ b/tests/regressiontests/localflavor/models.py
@@ -1,8 +0,0 @@
-from django.db import models
-from django.contrib.localflavor.us.models import USStateField
-
-class Place(models.Model):
- state = USStateField(blank=True)
- state_req = USStateField()
- state_default = USStateField(default="CA", blank=True)
- name = models.CharField(max_length=20)
diff --git a/tests/regressiontests/localflavor/tests.py b/tests/regressiontests/localflavor/tests.py
index 0ea3c52568..69682360d3 100644
--- a/tests/regressiontests/localflavor/tests.py
+++ b/tests/regressiontests/localflavor/tests.py
@@ -1,83 +1,5 @@
+import unittest
from django.test import TestCase
-from models import Place
-from forms import PlaceForm
-class USLocalflavorTests(TestCase):
- def setUp(self):
- self.form = PlaceForm({'state':'GA', 'state_req':'NC', 'name':'impossible'})
-
- def test_get_display_methods(self):
- """Test that the get_*_display() methods are added to the model instances."""
- place = self.form.save()
- self.assertEqual(place.get_state_display(), 'Georgia')
- self.assertEqual(place.get_state_req_display(), 'North Carolina')
-
- def test_required(self):
- """Test that required USStateFields throw appropriate errors."""
- form = PlaceForm({'state':'GA', 'name':'Place in GA'})
- self.assertFalse(form.is_valid())
- self.assertEqual(form.errors['state_req'], [u'This field is required.'])
-
- def test_field_blank_option(self):
- """Test that the empty option is there."""
- state_select_html = """\
-<select name="state" id="id_state">
-<option value="">---------</option>
-<option value="AL">Alabama</option>
-<option value="AK">Alaska</option>
-<option value="AS">American Samoa</option>
-<option value="AZ">Arizona</option>
-<option value="AR">Arkansas</option>
-<option value="CA">California</option>
-<option value="CO">Colorado</option>
-<option value="CT">Connecticut</option>
-<option value="DE">Delaware</option>
-<option value="DC">District of Columbia</option>
-<option value="FL">Florida</option>
-<option value="GA" selected="selected">Georgia</option>
-<option value="GU">Guam</option>
-<option value="HI">Hawaii</option>
-<option value="ID">Idaho</option>
-<option value="IL">Illinois</option>
-<option value="IN">Indiana</option>
-<option value="IA">Iowa</option>
-<option value="KS">Kansas</option>
-<option value="KY">Kentucky</option>
-<option value="LA">Louisiana</option>
-<option value="ME">Maine</option>
-<option value="MD">Maryland</option>
-<option value="MA">Massachusetts</option>
-<option value="MI">Michigan</option>
-<option value="MN">Minnesota</option>
-<option value="MS">Mississippi</option>
-<option value="MO">Missouri</option>
-<option value="MT">Montana</option>
-<option value="NE">Nebraska</option>
-<option value="NV">Nevada</option>
-<option value="NH">New Hampshire</option>
-<option value="NJ">New Jersey</option>
-<option value="NM">New Mexico</option>
-<option value="NY">New York</option>
-<option value="NC">North Carolina</option>
-<option value="ND">North Dakota</option>
-<option value="MP">Northern Mariana Islands</option>
-<option value="OH">Ohio</option>
-<option value="OK">Oklahoma</option>
-<option value="OR">Oregon</option>
-<option value="PA">Pennsylvania</option>
-<option value="PR">Puerto Rico</option>
-<option value="RI">Rhode Island</option>
-<option value="SC">South Carolina</option>
-<option value="SD">South Dakota</option>
-<option value="TN">Tennessee</option>
-<option value="TX">Texas</option>
-<option value="UT">Utah</option>
-<option value="VT">Vermont</option>
-<option value="VI">Virgin Islands</option>
-<option value="VA">Virginia</option>
-<option value="WA">Washington</option>
-<option value="WV">West Virginia</option>
-<option value="WI">Wisconsin</option>
-<option value="WY">Wyoming</option>
-</select>"""
- self.assertEqual(str(self.form['state']), state_select_html)
+# just import your tests here
+from us.tests import *
diff --git a/tests/regressiontests/localflavor/us/__init__.py b/tests/regressiontests/localflavor/us/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/localflavor/us/__init__.py
diff --git a/tests/regressiontests/localflavor/forms.py b/tests/regressiontests/localflavor/us/forms.py
index 2dd1da6dd0..9b77e1078b 100644
--- a/tests/regressiontests/localflavor/forms.py
+++ b/tests/regressiontests/localflavor/us/forms.py
@@ -1,7 +1,7 @@
from django.forms import ModelForm
-from models import Place
+from models import USPlace
-class PlaceForm(ModelForm):
+class USPlaceForm(ModelForm):
"""docstring for PlaceForm"""
class Meta:
- model = Place
+ model = USPlace
diff --git a/tests/regressiontests/localflavor/us/models.py b/tests/regressiontests/localflavor/us/models.py
new file mode 100644
index 0000000000..a8a4cf0f50
--- /dev/null
+++ b/tests/regressiontests/localflavor/us/models.py
@@ -0,0 +1,13 @@
+from django.db import models
+from django.contrib.localflavor.us.models import USStateField
+
+# When creating models you need to remember to add a app_label as
+# 'localflavor', so your model can be found
+
+class USPlace(models.Model):
+ state = USStateField(blank=True)
+ state_req = USStateField()
+ state_default = USStateField(default="CA", blank=True)
+ name = models.CharField(max_length=20)
+ class Meta:
+ app_label = 'localflavor'
diff --git a/tests/regressiontests/localflavor/us/tests.py b/tests/regressiontests/localflavor/us/tests.py
new file mode 100644
index 0000000000..07fe057833
--- /dev/null
+++ b/tests/regressiontests/localflavor/us/tests.py
@@ -0,0 +1,82 @@
+from django.test import TestCase
+from forms import USPlaceForm
+
+class USLocalflavorTests(TestCase):
+ def setUp(self):
+ self.form = USPlaceForm({'state':'GA', 'state_req':'NC', 'name':'impossible'})
+
+ def test_get_display_methods(self):
+ """Test that the get_*_display() methods are added to the model instances."""
+ place = self.form.save()
+ self.assertEqual(place.get_state_display(), 'Georgia')
+ self.assertEqual(place.get_state_req_display(), 'North Carolina')
+
+ def test_required(self):
+ """Test that required USStateFields throw appropriate errors."""
+ form = USPlaceForm({'state':'GA', 'name':'Place in GA'})
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.errors['state_req'], [u'This field is required.'])
+
+ def test_field_blank_option(self):
+ """Test that the empty option is there."""
+ state_select_html = """\
+<select name="state" id="id_state">
+<option value="">---------</option>
+<option value="AL">Alabama</option>
+<option value="AK">Alaska</option>
+<option value="AS">American Samoa</option>
+<option value="AZ">Arizona</option>
+<option value="AR">Arkansas</option>
+<option value="CA">California</option>
+<option value="CO">Colorado</option>
+<option value="CT">Connecticut</option>
+<option value="DE">Delaware</option>
+<option value="DC">District of Columbia</option>
+<option value="FL">Florida</option>
+<option value="GA" selected="selected">Georgia</option>
+<option value="GU">Guam</option>
+<option value="HI">Hawaii</option>
+<option value="ID">Idaho</option>
+<option value="IL">Illinois</option>
+<option value="IN">Indiana</option>
+<option value="IA">Iowa</option>
+<option value="KS">Kansas</option>
+<option value="KY">Kentucky</option>
+<option value="LA">Louisiana</option>
+<option value="ME">Maine</option>
+<option value="MD">Maryland</option>
+<option value="MA">Massachusetts</option>
+<option value="MI">Michigan</option>
+<option value="MN">Minnesota</option>
+<option value="MS">Mississippi</option>
+<option value="MO">Missouri</option>
+<option value="MT">Montana</option>
+<option value="NE">Nebraska</option>
+<option value="NV">Nevada</option>
+<option value="NH">New Hampshire</option>
+<option value="NJ">New Jersey</option>
+<option value="NM">New Mexico</option>
+<option value="NY">New York</option>
+<option value="NC">North Carolina</option>
+<option value="ND">North Dakota</option>
+<option value="MP">Northern Mariana Islands</option>
+<option value="OH">Ohio</option>
+<option value="OK">Oklahoma</option>
+<option value="OR">Oregon</option>
+<option value="PA">Pennsylvania</option>
+<option value="PR">Puerto Rico</option>
+<option value="RI">Rhode Island</option>
+<option value="SC">South Carolina</option>
+<option value="SD">South Dakota</option>
+<option value="TN">Tennessee</option>
+<option value="TX">Texas</option>
+<option value="UT">Utah</option>
+<option value="VT">Vermont</option>
+<option value="VI">Virgin Islands</option>
+<option value="VA">Virginia</option>
+<option value="WA">Washington</option>
+<option value="WV">West Virginia</option>
+<option value="WI">Wisconsin</option>
+<option value="WY">Wyoming</option>
+</select>"""
+ self.assertEqual(str(self.form['state']), state_select_html)
diff --git a/tests/regressiontests/m2m_through_regress/models.py b/tests/regressiontests/m2m_through_regress/models.py
index 56aecd6975..ec87985765 100644
--- a/tests/regressiontests/m2m_through_regress/models.py
+++ b/tests/regressiontests/m2m_through_regress/models.py
@@ -1,8 +1,10 @@
from datetime import datetime
+
from django.contrib.auth.models import User
from django.core import management
from django.db import models
+
# Forward declared intermediate model
class Membership(models.Model):
person = models.ForeignKey('Person')
@@ -51,159 +53,3 @@ class Through(ThroughBase):
class B(models.Model):
b_text = models.CharField(max_length=20)
a_list = models.ManyToManyField(A, through=Through)
-
-
-__test__ = {'API_TESTS':"""
-# Create some dummy data
->>> bob = Person.objects.create(name='Bob')
->>> jim = Person.objects.create(name='Jim')
-
->>> rock = Group.objects.create(name='Rock')
->>> roll = Group.objects.create(name='Roll')
-
->>> frank = User.objects.create_user('frank','frank@example.com','password')
->>> jane = User.objects.create_user('jane','jane@example.com','password')
-
-# Now test that the forward declared Membership works
->>> Membership.objects.create(person=bob, group=rock)
-<Membership: Bob is a member of Rock>
-
->>> Membership.objects.create(person=bob, group=roll)
-<Membership: Bob is a member of Roll>
-
->>> Membership.objects.create(person=jim, group=rock)
-<Membership: Jim is a member of Rock>
-
->>> bob.group_set.all()
-[<Group: Rock>, <Group: Roll>]
-
->>> roll.members.all()
-[<Person: Bob>]
-
-# Error messages use the model name, not repr of the class name
->>> bob.group_set = []
-Traceback (most recent call last):
-...
-AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
-
->>> roll.members = []
-Traceback (most recent call last):
-...
-AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
-
->>> rock.members.create(name='Anne')
-Traceback (most recent call last):
-...
-AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
-
->>> bob.group_set.create(name='Funk')
-Traceback (most recent call last):
-...
-AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
-
-# Now test that the intermediate with a relationship outside
-# the current app (i.e., UserMembership) workds
->>> UserMembership.objects.create(user=frank, group=rock)
-<UserMembership: frank is a user and member of Rock>
-
->>> UserMembership.objects.create(user=frank, group=roll)
-<UserMembership: frank is a user and member of Roll>
-
->>> UserMembership.objects.create(user=jane, group=rock)
-<UserMembership: jane is a user and member of Rock>
-
->>> frank.group_set.all()
-[<Group: Rock>, <Group: Roll>]
-
->>> roll.user_members.all()
-[<User: frank>]
-
-# Regression test for #8134 --
-# m2m-through models shouldn't be serialized as m2m fields on the model.
-
-# First, clean up a lot of objects we don't need.
-# The serialization test only requires three objects to work -
-# one for each end of the m2m, plus the through model.
-
->>> User.objects.all().delete()
->>> UserMembership.objects.all().delete()
->>> frank.delete()
->>> rock.delete()
->>> jim.delete()
-
-# Dump the current contents of the database as a JSON fixture
->>> management.call_command('dumpdata', 'm2m_through_regress', format='json', indent=2)
-[
- {
- "pk": 2,
- "model": "m2m_through_regress.membership",
- "fields": {
- "person": 1,
- "price": 100,
- "group": 2
- }
- },
- {
- "pk": 1,
- "model": "m2m_through_regress.person",
- "fields": {
- "name": "Bob"
- }
- },
- {
- "pk": 2,
- "model": "m2m_through_regress.group",
- "fields": {
- "name": "Roll"
- }
- }
-]
-
-# Check the XML serializer too, since it doesn't use the common implementation
->>> management.call_command('dumpdata', 'm2m_through_regress', format='xml', indent=2)
-<?xml version="1.0" encoding="utf-8"?>
-<django-objects version="1.0">
- <object pk="2" model="m2m_through_regress.membership">
- <field to="m2m_through_regress.person" name="person" rel="ManyToOneRel">1</field>
- <field to="m2m_through_regress.group" name="group" rel="ManyToOneRel">2</field>
- <field type="IntegerField" name="price">100</field>
- </object>
- <object pk="1" model="m2m_through_regress.person">
- <field type="CharField" name="name">Bob</field>
- </object>
- <object pk="2" model="m2m_through_regress.group">
- <field type="CharField" name="name">Roll</field>
- </object>
-</django-objects>
-
-## Regression test for #8046:
-Check that we don't involve too many copies of the intermediate table when
-doing a join.
-
->>> bob = Person.objects.create(name='Bob')
->>> jim = Person.objects.create(name='Jim')
->>> rock = Group.objects.create(name='Rock')
->>> roll = Group.objects.create(name='Roll')
->>> _ = Membership.objects.create(person=bob, group=rock)
->>> _ = Membership.objects.create(person=jim, group=rock, price=50)
->>> _ = Membership.objects.create(person=bob, group=roll, price=50)
->>> rock.members.filter(membership__price=50)
-[<Person: Jim>]
-
-## Regression test for #8254
->>> bob.group_set.filter(membership__price=50)
-[<Group: Roll>]
-
-## Regression test for #9804
-# Flush the database, just to make sure we can.
->>> management.call_command('flush', verbosity=0, interactive=False)
-
-## Regression test for #11107
-Ensure that sequences on m2m_through tables are being created for the through
-model, not for a phantom auto-generated m2m table.
-
->>> management.call_command('loaddata', 'm2m_through', verbosity=0)
->>> management.call_command('dumpdata', 'm2m_through_regress', format='json')
-[{"pk": 1, "model": "m2m_through_regress.usermembership", "fields": {"price": 100, "group": 1, "user": 1}}, {"pk": 1, "model": "m2m_through_regress.person", "fields": {"name": "Guido"}}, {"pk": 1, "model": "m2m_through_regress.group", "fields": {"name": "Python Core Group"}}]
-
-"""}
diff --git a/tests/regressiontests/m2m_through_regress/tests.py b/tests/regressiontests/m2m_through_regress/tests.py
new file mode 100644
index 0000000000..406851acfd
--- /dev/null
+++ b/tests/regressiontests/m2m_through_regress/tests.py
@@ -0,0 +1,128 @@
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+from django.core import management
+from django.contrib.auth.models import User
+from django.test import TestCase
+
+from models import Person, Group, Membership, UserMembership
+
+
+class M2MThroughTestCase(TestCase):
+ def test_everything(self):
+ bob = Person.objects.create(name="Bob")
+ jim = Person.objects.create(name="Jim")
+
+ rock = Group.objects.create(name="Rock")
+ roll = Group.objects.create(name="Roll")
+
+ frank = User.objects.create_user("frank", "frank@example.com", "password")
+ jane = User.objects.create_user("jane", "jane@example.com", "password")
+
+ Membership.objects.create(person=bob, group=rock)
+ Membership.objects.create(person=bob, group=roll)
+ Membership.objects.create(person=jim, group=rock)
+
+ self.assertQuerysetEqual(
+ bob.group_set.all(), [
+ "<Group: Rock>",
+ "<Group: Roll>",
+ ]
+ )
+
+ self.assertQuerysetEqual(
+ roll.members.all(), [
+ "<Person: Bob>",
+ ]
+ )
+
+ self.assertRaises(AttributeError, setattr, bob, "group_set", [])
+ self.assertRaises(AttributeError, setattr, roll, "members", [])
+
+ self.assertRaises(AttributeError, rock.members.create, name="Anne")
+ self.assertRaises(AttributeError, bob.group_set.create, name="Funk")
+
+ UserMembership.objects.create(user=frank, group=rock)
+ UserMembership.objects.create(user=frank, group=roll)
+ UserMembership.objects.create(user=jane, group=rock)
+
+ self.assertQuerysetEqual(
+ frank.group_set.all(), [
+ "<Group: Rock>",
+ "<Group: Roll>",
+ ]
+ )
+
+ self.assertQuerysetEqual(
+ roll.user_members.all(), [
+ "<User: frank>",
+ ]
+ )
+
+ def test_serialization(self):
+ "m2m-through models aren't serialized as m2m fields. Refs #8134"
+
+ p = Person.objects.create(name="Bob")
+ g = Group.objects.create(name="Roll")
+ m =Membership.objects.create(person=p, group=g)
+
+ pks = {"p_pk": p.pk, "g_pk": g.pk, "m_pk": m.pk}
+
+ out = StringIO()
+ management.call_command("dumpdata", "m2m_through_regress", format="json", stdout=out)
+ self.assertEqual(out.getvalue().strip(), """[{"pk": %(m_pk)s, "model": "m2m_through_regress.membership", "fields": {"person": %(p_pk)s, "price": 100, "group": %(g_pk)s}}, {"pk": %(p_pk)s, "model": "m2m_through_regress.person", "fields": {"name": "Bob"}}, {"pk": %(g_pk)s, "model": "m2m_through_regress.group", "fields": {"name": "Roll"}}]""" % pks)
+
+ out = StringIO()
+ management.call_command("dumpdata", "m2m_through_regress", format="xml",
+ indent=2, stdout=out)
+ self.assertEqual(out.getvalue().strip(), """
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="%(m_pk)s" model="m2m_through_regress.membership">
+ <field to="m2m_through_regress.person" name="person" rel="ManyToOneRel">%(p_pk)s</field>
+ <field to="m2m_through_regress.group" name="group" rel="ManyToOneRel">%(g_pk)s</field>
+ <field type="IntegerField" name="price">100</field>
+ </object>
+ <object pk="%(p_pk)s" model="m2m_through_regress.person">
+ <field type="CharField" name="name">Bob</field>
+ </object>
+ <object pk="%(g_pk)s" model="m2m_through_regress.group">
+ <field type="CharField" name="name">Roll</field>
+ </object>
+</django-objects>
+ """.strip() % pks)
+
+ def test_join_trimming(self):
+ "Check that we don't involve too many copies of the intermediate table when doing a join. Refs #8046, #8254"
+ bob = Person.objects.create(name="Bob")
+ jim = Person.objects.create(name="Jim")
+
+ rock = Group.objects.create(name="Rock")
+ roll = Group.objects.create(name="Roll")
+
+ Membership.objects.create(person=bob, group=rock)
+ Membership.objects.create(person=jim, group=rock, price=50)
+ Membership.objects.create(person=bob, group=roll, price=50)
+
+ self.assertQuerysetEqual(
+ rock.members.filter(membership__price=50), [
+ "<Person: Jim>",
+ ]
+ )
+
+ self.assertQuerysetEqual(
+ bob.group_set.filter(membership__price=50), [
+ "<Group: Roll>",
+ ]
+ )
+
+class ThroughLoadDataTestCase(TestCase):
+ fixtures = ["m2m_through"]
+
+ def test_sequence_creation(self):
+ "Check that sequences on an m2m_through are created for the through model, not a phantom auto-generated m2m table. Refs #11107"
+ out = StringIO()
+ management.call_command("dumpdata", "m2m_through_regress", format="json", stdout=out)
+ self.assertEqual(out.getvalue().strip(), """[{"pk": 1, "model": "m2m_through_regress.usermembership", "fields": {"price": 100, "group": 1, "user": 1}}, {"pk": 1, "model": "m2m_through_regress.person", "fields": {"name": "Guido"}}, {"pk": 1, "model": "m2m_through_regress.group", "fields": {"name": "Python Core Group"}}]""")
diff --git a/tests/regressiontests/model_forms_regress/models.py b/tests/regressiontests/model_forms_regress/models.py
index 871bb6f815..4f9811a963 100644
--- a/tests/regressiontests/model_forms_regress/models.py
+++ b/tests/regressiontests/model_forms_regress/models.py
@@ -54,3 +54,6 @@ class Author(models.Model):
class Author1(models.Model):
publication = models.OneToOneField(Publication, null=False)
full_name = models.CharField(max_length=255)
+
+class Homepage(models.Model):
+ url = models.URLField(verify_exists=False)
diff --git a/tests/regressiontests/model_forms_regress/tests.py b/tests/regressiontests/model_forms_regress/tests.py
index 5a7a83bc0e..baf769c02a 100644
--- a/tests/regressiontests/model_forms_regress/tests.py
+++ b/tests/regressiontests/model_forms_regress/tests.py
@@ -5,8 +5,10 @@ from django import forms
from django.forms.models import modelform_factory, ModelChoiceField
from django.conf import settings
from django.test import TestCase
+from django.core.exceptions import FieldError
-from models import Person, RealPerson, Triple, FilePathModel, Article, Publication, CustomFF, Author, Author1
+from models import Person, RealPerson, Triple, FilePathModel, Article, \
+ Publication, CustomFF, Author, Author1, Homepage
class ModelMultipleChoiceFieldTests(TestCase):
@@ -128,7 +130,7 @@ class ManyToManyCallableInitialTests(TestCase):
<option value="1" selected="selected">First Book</option>
<option value="2" selected="selected">Second Book</option>
<option value="3">Third Book</option>
-</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>""")
+</select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></li>""")
class CFFForm(forms.ModelForm):
class Meta:
@@ -212,7 +214,122 @@ class TestTicket11183(TestCase):
def test_11183(self):
form1 = ModelChoiceForm()
field1 = form1.fields['person']
- # To allow the widget to change the queryset of field1.widget.choices correctly,
+ # To allow the widget to change the queryset of field1.widget.choices correctly,
# without affecting other forms, the following must hold:
self.assert_(field1 is not ModelChoiceForm.base_fields['person'])
self.assert_(field1.widget.choices.field is field1)
+
+class HomepageForm(forms.ModelForm):
+ class Meta:
+ model = Homepage
+
+class URLFieldTests(TestCase):
+ def test_url_on_modelform(self):
+ "Check basic URL field validation on model forms"
+ self.assertFalse(HomepageForm({'url': 'foo'}).is_valid())
+ self.assertFalse(HomepageForm({'url': 'http://'}).is_valid())
+ self.assertFalse(HomepageForm({'url': 'http://example'}).is_valid())
+ self.assertFalse(HomepageForm({'url': 'http://example.'}).is_valid())
+ self.assertFalse(HomepageForm({'url': 'http://com.'}).is_valid())
+
+ self.assertTrue(HomepageForm({'url': 'http://localhost'}).is_valid())
+ self.assertTrue(HomepageForm({'url': 'http://example.com'}).is_valid())
+ self.assertTrue(HomepageForm({'url': 'http://www.example.com'}).is_valid())
+ self.assertTrue(HomepageForm({'url': 'http://www.example.com:8000'}).is_valid())
+ self.assertTrue(HomepageForm({'url': 'http://www.example.com/test'}).is_valid())
+ self.assertTrue(HomepageForm({'url': 'http://www.example.com:8000/test'}).is_valid())
+ self.assertTrue(HomepageForm({'url': 'http://example.com/foo/bar'}).is_valid())
+
+ def test_http_prefixing(self):
+ "If the http:// prefix is omitted on form input, the field adds it again. (Refs #13613)"
+ form = HomepageForm({'url': 'example.com'})
+ form.is_valid()
+ # self.assertTrue(form.is_valid())
+ # self.assertEquals(form.cleaned_data['url'], 'http://example.com/')
+
+ form = HomepageForm({'url': 'example.com/test'})
+ form.is_valid()
+ # self.assertTrue(form.is_valid())
+ # self.assertEquals(form.cleaned_data['url'], 'http://example.com/test')
+
+
+class FormFieldCallbackTests(TestCase):
+
+ def test_baseform_with_widgets_in_meta(self):
+ """Regression for #13095: Using base forms with widgets defined in Meta should not raise errors."""
+ widget = forms.Textarea()
+
+ class BaseForm(forms.ModelForm):
+ class Meta:
+ model = Person
+ widgets = {'name': widget}
+
+ Form = modelform_factory(Person, form=BaseForm)
+ self.assertTrue(Form.base_fields['name'].widget is widget)
+
+ def test_custom_callback(self):
+ """Test that a custom formfield_callback is used if provided"""
+
+ callback_args = []
+
+ def callback(db_field, **kwargs):
+ callback_args.append((db_field, kwargs))
+ return db_field.formfield(**kwargs)
+
+ widget = forms.Textarea()
+
+ class BaseForm(forms.ModelForm):
+ class Meta:
+ model = Person
+ widgets = {'name': widget}
+
+ _ = modelform_factory(Person, form=BaseForm,
+ formfield_callback=callback)
+ id_field, name_field = Person._meta.fields
+
+ self.assertEqual(callback_args,
+ [(id_field, {}), (name_field, {'widget': widget})])
+
+ def test_bad_callback(self):
+ # A bad callback provided by user still gives an error
+ self.assertRaises(TypeError, modelform_factory, Person,
+ formfield_callback='not a function or callable')
+
+
+class InvalidFieldAndFactory(TestCase):
+ """ Tests for #11905 """
+
+ def test_extra_field_model_form(self):
+ try:
+ class ExtraPersonForm(forms.ModelForm):
+ """ ModelForm with an extra field """
+
+ age = forms.IntegerField()
+
+ class Meta:
+ model = Person
+ fields = ('name', 'no-field')
+ except FieldError, e:
+ # Make sure the exception contains some reference to the
+ # field responsible for the problem.
+ self.assertTrue('no-field' in e.args[0])
+ else:
+ self.fail('Invalid "no-field" field not caught')
+
+ def test_extra_declared_field_model_form(self):
+ try:
+ class ExtraPersonForm(forms.ModelForm):
+ """ ModelForm with an extra field """
+
+ age = forms.IntegerField()
+
+ class Meta:
+ model = Person
+ fields = ('name', 'age')
+ except FieldError:
+ self.fail('Declarative field raised FieldError incorrectly')
+
+ def test_extra_field_modelform_factory(self):
+ self.assertRaises(FieldError, modelform_factory,
+ Person, fields=['no-field', 'name'])
+
diff --git a/tests/regressiontests/model_formsets_regress/tests.py b/tests/regressiontests/model_formsets_regress/tests.py
index 61bc514324..ee2a26f6c2 100644
--- a/tests/regressiontests/model_formsets_regress/tests.py
+++ b/tests/regressiontests/model_formsets_regress/tests.py
@@ -1,8 +1,10 @@
-from django.forms.models import modelform_factory, inlineformset_factory
+from django import forms
+from django.forms.models import modelform_factory, inlineformset_factory, modelformset_factory
from django.test import TestCase
from models import User, UserSite, Restaurant, Manager
+
class InlineFormsetTests(TestCase):
def test_formset_over_to_field(self):
"A formset over a ForeignKey with a to_field can be saved. Regression for #10243"
@@ -156,3 +158,61 @@ class InlineFormsetTests(TestCase):
# you can create a formset with an instance of None
form = Form(instance=None)
formset = FormSet(instance=None)
+
+
+class CustomWidget(forms.CharField):
+ pass
+
+
+class UserSiteForm(forms.ModelForm):
+ class Meta:
+ model = UserSite
+ widgets = {'data': CustomWidget}
+
+
+class Callback(object):
+
+ def __init__(self):
+ self.log = []
+
+ def __call__(self, db_field, **kwargs):
+ self.log.append((db_field, kwargs))
+ return db_field.formfield(**kwargs)
+
+
+class FormfieldCallbackTests(TestCase):
+ """
+ Regression for #13095: Using base forms with widgets
+ defined in Meta should not raise errors.
+ """
+
+ def test_inlineformset_factory_default(self):
+ Formset = inlineformset_factory(User, UserSite, form=UserSiteForm)
+ form = Formset({}).forms[0]
+ self.assertTrue(isinstance(form['data'].field.widget, CustomWidget))
+
+ def test_modelformset_factory_default(self):
+ Formset = modelformset_factory(UserSite, form=UserSiteForm)
+ form = Formset({}).forms[0]
+ self.assertTrue(isinstance(form['data'].field.widget, CustomWidget))
+
+ def assertCallbackCalled(self, callback):
+ id_field, user_field, data_field = UserSite._meta.fields
+ expected_log = [
+ (id_field, {}),
+ (user_field, {}),
+ (data_field, {'widget': CustomWidget}),
+ ]
+ self.assertEqual(callback.log, expected_log)
+
+ def test_inlineformset_custom_callback(self):
+ callback = Callback()
+ inlineformset_factory(User, UserSite, form=UserSiteForm,
+ formfield_callback=callback)
+ self.assertCallbackCalled(callback)
+
+ def test_modelformset_custom_callback(self):
+ callback = Callback()
+ modelformset_factory(UserSite, form=UserSiteForm,
+ formfield_callback=callback)
+ self.assertCallbackCalled(callback)
diff --git a/tests/regressiontests/multiple_database/fixtures/pets.json b/tests/regressiontests/multiple_database/fixtures/pets.json
new file mode 100644
index 0000000000..89756a3e5b
--- /dev/null
+++ b/tests/regressiontests/multiple_database/fixtures/pets.json
@@ -0,0 +1,18 @@
+[
+ {
+ "pk": 1,
+ "model": "multiple_database.pet",
+ "fields": {
+ "name": "Mr Bigglesworth",
+ "owner": 1
+ }
+ },
+ {
+ "pk": 2,
+ "model": "multiple_database.pet",
+ "fields": {
+ "name": "Spot",
+ "owner": 2
+ }
+ }
+] \ No newline at end of file
diff --git a/tests/regressiontests/multiple_database/tests.py b/tests/regressiontests/multiple_database/tests.py
index 7bde8bf037..7f66ea3bcf 100644
--- a/tests/regressiontests/multiple_database/tests.py
+++ b/tests/regressiontests/multiple_database/tests.py
@@ -7,6 +7,7 @@ from django.conf import settings
from django.contrib.auth.models import User
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
@@ -883,7 +884,13 @@ class QueryTestCase(TestCase):
self.assertRaises(ValueError, str, qs.query)
# Evaluating the query shouldn't work, either
- self.assertRaises(ValueError, list, qs)
+ try:
+ for obj in qs:
+ pass
+ self.fail('Iterating over query should raise ValueError')
+ except ValueError:
+ pass
+
class TestRouter(object):
# A test router. The behaviour is vaguely master/slave, but the
@@ -1491,19 +1498,10 @@ class AuthTestCase(TestCase):
self.old_routers = router.routers
router.routers = [AuthRouter()]
- # Redirect stdout to a buffer so we can test
- # the output of a management command
- self.old_stdout = sys.stdout
- self.stdout = StringIO()
- sys.stdout = self.stdout
-
def tearDown(self):
# Restore the 'other' database as an independent database
router.routers = self.old_routers
- # Restore stdout
- sys.stdout = self.old_stdout
-
def test_auth_manager(self):
"The methods on the auth manager obey database hints"
# Create one user using default allocation policy
@@ -1539,14 +1537,16 @@ class AuthTestCase(TestCase):
# Check that dumping the default database doesn't try to include auth
# because allow_syncdb prohibits auth on default
- self.stdout.flush()
- management.call_command('dumpdata', 'auth', format='json', database='default')
- self.assertEquals(self.stdout.getvalue(), '[]\n')
+ new_io = StringIO()
+ management.call_command('dumpdata', 'auth', format='json', database='default', stdout=new_io)
+ command_output = new_io.getvalue().strip()
+ self.assertEqual(command_output, '[]')
# Check that dumping the other database does include auth
- self.stdout.flush()
- management.call_command('dumpdata', 'auth', format='json', database='other')
- self.assertTrue('alice@example.com' in self.stdout.getvalue())
+ new_io = StringIO()
+ management.call_command('dumpdata', 'auth', format='json', database='other', stdout=new_io)
+ command_output = new_io.getvalue().strip()
+ self.assertTrue('"email": "alice@example.com",' in command_output)
class UserProfileTestCase(TestCase):
def setUp(self):
@@ -1570,11 +1570,31 @@ class UserProfileTestCase(TestCase):
self.assertEquals(alice.get_profile().flavor, 'chocolate')
self.assertEquals(bob.get_profile().flavor, 'crunchy frog')
+class AntiPetRouter(object):
+ # A router that only expresses an opinion on syncdb,
+ # passing pets to the 'other' database
+
+ def allow_syncdb(self, db, model):
+ "Make sure the auth app only appears on the 'other' db"
+ if db == 'other':
+ return model._meta.object_name == 'Pet'
+ else:
+ return model._meta.object_name != 'Pet'
+ return None
class FixtureTestCase(TestCase):
multi_db = True
fixtures = ['multidb-common', 'multidb']
+ def setUp(self):
+ # Install the anti-pet router
+ self.old_routers = router.routers
+ router.routers = [AntiPetRouter()]
+
+ def tearDown(self):
+ # Restore the 'other' database as an independent database
+ router.routers = self.old_routers
+
def test_fixture_loading(self):
"Multi-db fixtures are loaded correctly"
# Check that "Pro Django" exists on the default database, but not on other database
@@ -1612,6 +1632,14 @@ class FixtureTestCase(TestCase):
except Book.DoesNotExist:
self.fail('"The Definitive Guide to Django" should exist on both databases')
+ def test_pseudo_empty_fixtures(self):
+ "A fixture can contain entries, but lead to nothing in the database; this shouldn't raise an error (ref #14068)"
+ new_io = StringIO()
+ management.call_command('loaddata', 'pets', stdout=new_io, stderr=new_io)
+ command_output = new_io.getvalue().strip()
+ # No objects will actually be loaded
+ self.assertEqual(command_output, "Installed 0 object(s) (of 2) from 1 fixture(s)")
+
class PickleQuerySetTestCase(TestCase):
multi_db = True
@@ -1620,3 +1648,114 @@ class PickleQuerySetTestCase(TestCase):
Book.objects.using(db).create(title='Dive into Python', published=datetime.date(2009, 5, 4))
qs = Book.objects.all()
self.assertEqual(qs.db, pickle.loads(pickle.dumps(qs)).db)
+
+
+class DatabaseReceiver(object):
+ """
+ Used in the tests for the database argument in signals (#13552)
+ """
+ def __call__(self, signal, sender, **kwargs):
+ self._database = kwargs['using']
+
+class WriteToOtherRouter(object):
+ """
+ A router that sends all writes to the other database.
+ """
+ def db_for_write(self, model, **hints):
+ return "other"
+
+class SignalTests(TestCase):
+ multi_db = True
+
+ def setUp(self):
+ self.old_routers = router.routers
+
+ def tearDown(self):
+ router.routser = self.old_routers
+
+ def _write_to_other(self):
+ "Sends all writes to 'other'."
+ router.routers = [WriteToOtherRouter()]
+
+ def _write_to_default(self):
+ "Sends all writes to the default DB"
+ router.routers = self.old_routers
+
+ def test_database_arg_save_and_delete(self):
+ """
+ Tests that the pre/post_save signal contains the correct database.
+ (#13552)
+ """
+ # Make some signal receivers
+ pre_save_receiver = DatabaseReceiver()
+ post_save_receiver = DatabaseReceiver()
+ pre_delete_receiver = DatabaseReceiver()
+ post_delete_receiver = DatabaseReceiver()
+ # Make model and connect receivers
+ signals.pre_save.connect(sender=Person, receiver=pre_save_receiver)
+ signals.post_save.connect(sender=Person, receiver=post_save_receiver)
+ signals.pre_delete.connect(sender=Person, receiver=pre_delete_receiver)
+ signals.post_delete.connect(sender=Person, receiver=post_delete_receiver)
+ p = Person.objects.create(name='Darth Vader')
+ # Save and test receivers got calls
+ p.save()
+ self.assertEqual(pre_save_receiver._database, DEFAULT_DB_ALIAS)
+ self.assertEqual(post_save_receiver._database, DEFAULT_DB_ALIAS)
+ # Delete, and test
+ p.delete()
+ self.assertEqual(pre_delete_receiver._database, DEFAULT_DB_ALIAS)
+ self.assertEqual(post_delete_receiver._database, DEFAULT_DB_ALIAS)
+ # Save again to a different database
+ p.save(using="other")
+ self.assertEqual(pre_save_receiver._database, "other")
+ self.assertEqual(post_save_receiver._database, "other")
+ # Delete, and test
+ p.delete(using="other")
+ self.assertEqual(pre_delete_receiver._database, "other")
+ self.assertEqual(post_delete_receiver._database, "other")
+
+ def test_database_arg_m2m(self):
+ """
+ Test that the m2m_changed signal has a correct database arg (#13552)
+ """
+ # Make a receiver
+ receiver = DatabaseReceiver()
+ # Connect it, and make the models
+ signals.m2m_changed.connect(receiver=receiver)
+
+ b = Book.objects.create(title="Pro Django",
+ published=datetime.date(2008, 12, 16))
+
+ p = Person.objects.create(name="Marty Alchin")
+
+ # Test addition
+ b.authors.add(p)
+ self.assertEqual(receiver._database, DEFAULT_DB_ALIAS)
+ self._write_to_other()
+ b.authors.add(p)
+ self._write_to_default()
+ self.assertEqual(receiver._database, "other")
+
+ # Test removal
+ b.authors.remove(p)
+ self.assertEqual(receiver._database, DEFAULT_DB_ALIAS)
+ self._write_to_other()
+ b.authors.remove(p)
+ self._write_to_default()
+ self.assertEqual(receiver._database, "other")
+
+ # Test addition in reverse
+ p.book_set.add(b)
+ self.assertEqual(receiver._database, DEFAULT_DB_ALIAS)
+ self._write_to_other()
+ p.book_set.add(b)
+ self._write_to_default()
+ self.assertEqual(receiver._database, "other")
+
+ # Test clearing
+ b.authors.clear()
+ self.assertEqual(receiver._database, DEFAULT_DB_ALIAS)
+ self._write_to_other()
+ b.authors.clear()
+ self._write_to_default()
+ self.assertEqual(receiver._database, "other")
diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py
index 1615a73406..22bc88c172 100644
--- a/tests/regressiontests/requests/tests.py
+++ b/tests/regressiontests/requests/tests.py
@@ -1,5 +1,5 @@
"""
->>> from django.http import HttpRequest
+>>> from django.http import HttpRequest, HttpResponse
>>> print repr(HttpRequest())
<HttpRequest
GET:{},
@@ -44,4 +44,27 @@ https://www.example.com/asdf
>>> request.path = ''
>>> print request.build_absolute_uri(location="/path/with:colons")
http://www.example.com/path/with:colons
+
+
+# Test cookie datetime expiration logic
+>>> from datetime import datetime, timedelta
+>>> delta = timedelta(seconds=10)
+>>> response = HttpResponse()
+>>> response.set_cookie('datetime', expires=datetime.utcnow()+delta)
+>>> datetime_cookie = response.cookies['datetime']
+>>> datetime_cookie['max-age']
+10
+>>> response.set_cookie('datetime', expires=datetime(2028, 1, 1, 4, 5, 6))
+>>> response.cookies['datetime']['expires']
+'Sat, 01-Jan-2028 04:05:06 GMT'
+
+# Test automatically setting cookie expires if only max_age is provided
+>>> response.set_cookie('max_age', max_age=10)
+>>> max_age_cookie = response.cookies['max_age']
+>>> max_age_cookie['max-age']
+10
+>>> from django.utils.http import cookie_date
+>>> import time
+>>> max_age_cookie['expires'] == cookie_date(time.time()+10)
+True
"""
diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py
index 84e90ff7e1..be920c6920 100644
--- a/tests/regressiontests/serializers_regress/tests.py
+++ b/tests/regressiontests/serializers_regress/tests.py
@@ -10,14 +10,16 @@ forward, backwards and self references.
import datetime
import decimal
-import unittest
-from cStringIO import StringIO
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
-from django.utils.functional import curry
-from django.core import serializers
-from django.db import transaction, DEFAULT_DB_ALIAS
-from django.core import management
from django.conf import settings
+from django.core import serializers, management
+from django.db import transaction, DEFAULT_DB_ALIAS
+from django.test import TestCase
+from django.utils.functional import curry
from models import *
@@ -59,10 +61,10 @@ def im2m_create(pk, klass, data):
def im_create(pk, klass, data):
instance = klass(id=pk)
- setattr(instance, 'right_id', data['right'])
- setattr(instance, 'left_id', data['left'])
+ instance.right_id = data['right']
+ instance.left_id = data['left']
if 'extra' in data:
- setattr(instance, 'extra', data['extra'])
+ instance.extra = data['extra']
models.Model.save_base(instance, raw=True)
return [instance]
@@ -96,7 +98,9 @@ def inherited_create(pk, klass, data):
def data_compare(testcase, pk, klass, data):
instance = klass.objects.get(id=pk)
testcase.assertEqual(data, instance.data,
- "Objects with PK=%d not equal; expected '%s' (%s), got '%s' (%s)" % (pk,data, type(data), instance.data, type(instance.data)))
+ "Objects with PK=%d not equal; expected '%s' (%s), got '%s' (%s)" % (
+ pk, data, type(data), instance.data, type(instance.data))
+ )
def generic_compare(testcase, pk, klass, data):
instance = klass.objects.get(id=pk)
@@ -348,28 +352,16 @@ if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.mysql':
# Dynamically create serializer tests to ensure that all
# registered serializers are automatically tested.
-class SerializerTests(unittest.TestCase):
+class SerializerTests(TestCase):
pass
def serializerTest(format, self):
- # Clear the database first
- management.call_command('flush', verbosity=0, interactive=False)
# Create all the objects defined in the test data
objects = []
instance_count = {}
- transaction.enter_transaction_management()
- try:
- transaction.managed(True)
- for (func, pk, klass, datum) in test_data:
- objects.extend(func[0](pk, klass, datum))
- instance_count[klass] = 0
- transaction.commit()
- except:
- transaction.rollback()
- transaction.leave_transaction_management()
- raise
- transaction.leave_transaction_management()
+ for (func, pk, klass, datum) in test_data:
+ objects.extend(func[0](pk, klass, datum))
# Get a count of the number of objects created for each class
for klass in instance_count:
@@ -381,19 +373,8 @@ def serializerTest(format, self):
# Serialize the test database
serialized_data = serializers.serialize(format, objects, indent=2)
- # Flush the database and recreate from the serialized data
- management.call_command('flush', verbosity=0, interactive=False)
- transaction.enter_transaction_management()
- try:
- transaction.managed(True)
- for obj in serializers.deserialize(format, serialized_data):
- obj.save()
- transaction.commit()
- except:
- transaction.rollback()
- transaction.leave_transaction_management()
- raise
- transaction.leave_transaction_management()
+ for obj in serializers.deserialize(format, serialized_data):
+ obj.save()
# Assert that the deserialized data is the same
# as the original source
@@ -406,10 +387,7 @@ def serializerTest(format, self):
self.assertEquals(count, klass.objects.count())
def fieldsTest(format, self):
- # Clear the database first
- management.call_command('flush', verbosity=0, interactive=False)
-
- obj = ComplexModel(field1='first',field2='second',field3='third')
+ obj = ComplexModel(field1='first', field2='second', field3='third')
obj.save_base(raw=True)
# Serialize then deserialize the test database
@@ -422,9 +400,6 @@ def fieldsTest(format, self):
self.assertEqual(result.object.field3, 'third')
def streamTest(format, self):
- # Clear the database first
- management.call_command('flush', verbosity=0, interactive=False)
-
obj = ComplexModel(field1='first',field2='second',field3='third')
obj.save_base(raw=True)
@@ -440,7 +415,7 @@ def streamTest(format, self):
stream.close()
for format in serializers.get_serializer_formats():
- setattr(SerializerTests, 'test_'+format+'_serializer', curry(serializerTest, format))
- setattr(SerializerTests, 'test_'+format+'_serializer_fields', curry(fieldsTest, format))
+ setattr(SerializerTests, 'test_' + format + '_serializer', curry(serializerTest, format))
+ setattr(SerializerTests, 'test_' + format + '_serializer_fields', curry(fieldsTest, format))
if format != 'python':
- setattr(SerializerTests, 'test_'+format+'_serializer_stream', curry(streamTest, format))
+ setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))
diff --git a/tests/regressiontests/templates/filters.py b/tests/regressiontests/templates/filters.py
index 3d6284e881..af34c58a9f 100644
--- a/tests/regressiontests/templates/filters.py
+++ b/tests/regressiontests/templates/filters.py
@@ -328,7 +328,12 @@ def get_filter_tests():
'join03': (r'{{ a|join:" &amp; " }}', {'a': ['alpha', 'beta & me']}, 'alpha &amp; beta &amp; me'),
'join04': (r'{% autoescape off %}{{ a|join:" &amp; " }}{% endautoescape %}', {'a': ['alpha', 'beta & me']}, 'alpha &amp; beta & me'),
-
+ # Test that joining with unsafe joiners don't result in unsafe strings (#11377)
+ 'join05': (r'{{ a|join:var }}', {'a': ['alpha', 'beta & me'], 'var': ' & '}, 'alpha &amp; beta &amp; me'),
+ 'join06': (r'{{ a|join:var }}', {'a': ['alpha', 'beta & me'], 'var': mark_safe(' & ')}, 'alpha & beta &amp; me'),
+ 'join07': (r'{{ a|join:var|lower }}', {'a': ['Alpha', 'Beta & me'], 'var': ' & ' }, 'alpha &amp; beta &amp; me'),
+ 'join08': (r'{{ a|join:var|lower }}', {'a': ['Alpha', 'Beta & me'], 'var': mark_safe(' & ')}, 'alpha & beta &amp; me'),
+
'date01': (r'{{ d|date:"m" }}', {'d': datetime(2008, 1, 1)}, '01'),
'date02': (r'{{ d|date }}', {'d': datetime(2008, 1, 1)}, 'Jan. 1, 2008'),
#Ticket 9520: Make sure |date doesn't blow up on non-dates
@@ -341,5 +346,5 @@ def get_filter_tests():
'add04': (r'{{ i|add:"16" }}', {'i': 'not_an_int'}, 'not_an_int16'),
'add05': (r'{{ l1|add:l2 }}', {'l1': [1, 2], 'l2': [3, 4]}, '[1, 2, 3, 4]'),
'add06': (r'{{ t1|add:t2 }}', {'t1': (3, 4), 't2': (1, 2)}, '(3, 4, 1, 2)'),
- 'add07': (r'{{ d|add:t }}', {'d': date(2000, 1, 1), 't': timedelta(10)}, '2000-01-11'),
+ 'add07': (r'{{ d|add:t }}', {'d': date(2000, 1, 1), 't': timedelta(10)}, 'Jan. 11, 2000'),
}
diff --git a/tests/regressiontests/templates/loaders.py b/tests/regressiontests/templates/loaders.py
index 64a0dc6505..caa3faa6bc 100644
--- a/tests/regressiontests/templates/loaders.py
+++ b/tests/regressiontests/templates/loaders.py
@@ -15,9 +15,11 @@ import pkg_resources
import imp
import StringIO
import os.path
+import warnings
from django.template import TemplateDoesNotExist, Context
from django.template.loaders.eggs import load_template_source as lts_egg
+from django.template.loaders.eggs import Loader as EggLoader
from django.template import loader
# Mock classes and objects for pkg_resources functions.
@@ -53,7 +55,33 @@ def create_egg(name, resources):
egg._resources = resources
sys.modules[name] = egg
-class EggLoader(unittest.TestCase):
+class DeprecatedEggLoaderTest(unittest.TestCase):
+ "Test the deprecated load_template_source interface to the egg loader"
+ def setUp(self):
+ pkg_resources._provider_factories[MockLoader] = MockProvider
+
+ self.empty_egg = create_egg("egg_empty", {})
+ self.egg_1 = create_egg("egg_1", {
+ os.path.normcase('templates/y.html') : StringIO.StringIO("y"),
+ os.path.normcase('templates/x.txt') : StringIO.StringIO("x"),
+ })
+ self._old_installed_apps = settings.INSTALLED_APPS
+ settings.INSTALLED_APPS = []
+ warnings.simplefilter("ignore", PendingDeprecationWarning)
+
+ def tearDown(self):
+ settings.INSTALLED_APPS = self._old_installed_apps
+ warnings.resetwarnings()
+
+ def test_existing(self):
+ "A template can be loaded from an egg"
+ settings.INSTALLED_APPS = ['egg_1']
+ contents, template_name = lts_egg("y.html")
+ self.assertEqual(contents, "y")
+ self.assertEqual(template_name, "egg:egg_1:templates/y.html")
+
+
+class EggLoaderTest(unittest.TestCase):
def setUp(self):
pkg_resources._provider_factories[MockLoader] = MockProvider
@@ -71,24 +99,28 @@ class EggLoader(unittest.TestCase):
def test_empty(self):
"Loading any template on an empty egg should fail"
settings.INSTALLED_APPS = ['egg_empty']
- self.assertRaises(TemplateDoesNotExist, lts_egg, "not-existing.html")
+ egg_loader = EggLoader()
+ self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "not-existing.html")
def test_non_existing(self):
"Template loading fails if the template is not in the egg"
settings.INSTALLED_APPS = ['egg_1']
- self.assertRaises(TemplateDoesNotExist, lts_egg, "not-existing.html")
+ egg_loader = EggLoader()
+ self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "not-existing.html")
def test_existing(self):
"A template can be loaded from an egg"
settings.INSTALLED_APPS = ['egg_1']
- contents, template_name = lts_egg("y.html")
+ egg_loader = EggLoader()
+ contents, template_name = egg_loader.load_template_source("y.html")
self.assertEqual(contents, "y")
self.assertEqual(template_name, "egg:egg_1:templates/y.html")
def test_not_installed(self):
"Loading an existent template from an egg not included in INSTALLED_APPS should fail"
settings.INSTALLED_APPS = []
- self.assertRaises(TemplateDoesNotExist, lts_egg, "y.html")
+ egg_loader = EggLoader()
+ self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "y.html")
class CachedLoader(unittest.TestCase):
def setUp(self):
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
index 5902e8d5e7..bbbcae30eb 100644
--- a/tests/regressiontests/templates/tests.py
+++ b/tests/regressiontests/templates/tests.py
@@ -506,6 +506,17 @@ class Templates(unittest.TestCase):
'basic-syntax28': ("{{ a.b }}", {'a': SilentGetItemClass()}, ('', 'INVALID')),
'basic-syntax29': ("{{ a.b }}", {'a': SilentAttrClass()}, ('', 'INVALID')),
+ # Something that starts like a number but has an extra lookup works as a lookup.
+ 'basic-syntax30': ("{{ 1.2.3 }}", {"1": {"2": {"3": "d"}}}, "d"),
+ 'basic-syntax31': ("{{ 1.2.3 }}", {"1": {"2": ("a", "b", "c", "d")}}, "d"),
+ 'basic-syntax32': ("{{ 1.2.3 }}", {"1": (("x", "x", "x", "x"), ("y", "y", "y", "y"), ("a", "b", "c", "d"))}, "d"),
+ 'basic-syntax33': ("{{ 1.2.3 }}", {"1": ("xxxx", "yyyy", "abcd")}, "d"),
+ 'basic-syntax34': ("{{ 1.2.3 }}", {"1": ({"x": "x"}, {"y": "y"}, {"z": "z", "3": "d"})}, "d"),
+
+ # Numbers are numbers even if their digits are in the context.
+ 'basic-syntax35': ("{{ 1 }}", {"1": "abc"}, "1"),
+ 'basic-syntax36': ("{{ 1.2 }}", {"1": "abc"}, "1.2"),
+
# List-index syntax allows a template to access a certain item of a subscriptable object.
'list-index01': ("{{ var.1 }}", {"var": ["first item", "second item"]}, "second item"),
@@ -592,7 +603,7 @@ class Templates(unittest.TestCase):
#filters should accept empty string constants
'filter-syntax20': ('{{ ""|default_if_none:"was none" }}', {}, ""),
-
+
### COMMENT SYNTAX ########################################################
'comment-syntax01': ("{# this is hidden #}hello", {}, "hello"),
'comment-syntax02': ("{# this is hidden #}hello{# foo #}", {}, "hello"),
@@ -690,6 +701,7 @@ class Templates(unittest.TestCase):
'for-tag-unpack11': ("{% for x,y,z in items %}{{ x }}:{{ y }},{{ z }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, ("one:1,/two:2,/", "one:1,INVALID/two:2,INVALID/")),
'for-tag-unpack12': ("{% for x,y,z in items %}{{ x }}:{{ y }},{{ z }}/{% endfor %}", {"items": (('one', 1, 'carrot'), ('two', 2))}, ("one:1,carrot/two:2,/", "one:1,carrot/two:2,INVALID/")),
'for-tag-unpack13': ("{% for x,y,z in items %}{{ x }}:{{ y }},{{ z }}/{% endfor %}", {"items": (('one', 1, 'carrot'), ('two', 2, 'cheese'))}, ("one:1,carrot/two:2,cheese/", "one:1,carrot/two:2,cheese/")),
+ 'for-tag-unpack14': ("{% for x,y in items %}{{ x }}:{{ y }}/{% endfor %}", {"items": (1, 2)}, (":/:/", "INVALID:INVALID/INVALID:INVALID/")),
'for-tag-empty01': ("{% for val in values %}{{ val }}{% empty %}empty text{% endfor %}", {"values": [1, 2, 3]}, "123"),
'for-tag-empty02': ("{% for val in values %}{{ val }}{% empty %}values array empty{% endfor %}", {"values": []}, "values array empty"),
'for-tag-empty03': ("{% for val in values %}{{ val }}{% empty %}values array not found{% endfor %}", {}, "values array not found"),
@@ -1285,7 +1297,8 @@ class Templates(unittest.TestCase):
# Regression test for #11270.
'cache17': ('{% load cache %}{% cache 10 long_cache_key poem %}Some Content{% endcache %}', {'poem': 'Oh freddled gruntbuggly/Thy micturations are to me/As plurdled gabbleblotchits/On a lurgid bee/That mordiously hath bitled out/Its earted jurtles/Into a rancid festering/Or else I shall rend thee in the gobberwarts with my blurglecruncheon/See if I dont.'}, 'Some Content'),
-
+
+
### AUTOESCAPE TAG ##############################################
'autoescape-tag01': ("{% autoescape off %}hello{% endautoescape %}", {}, "hello"),
'autoescape-tag02': ("{% autoescape off %}{{ first }}{% endautoescape %}", {"first": "<b>hello</b>"}, "<b>hello</b>"),
@@ -1314,6 +1327,23 @@ class Templates(unittest.TestCase):
# implementation details (fortunately, the (no)autoescape block
# tags can be used in those cases)
'autoescape-filtertag01': ("{{ first }}{% filter safe %}{{ first }} x<y{% endfilter %}", {"first": "<a>"}, template.TemplateSyntaxError),
+
+ # ifqeual compares unescaped vales.
+ 'autoescape-ifequal01': ('{% ifequal var "this & that" %}yes{% endifequal %}', { "var": "this & that" }, "yes" ),
+
+ # Arguments to filters are 'safe' and manipulate their input unescaped.
+ 'autoescape-filters01': ('{{ var|cut:"&" }}', { "var": "this & that" }, "this that" ),
+ 'autoescape-filters02': ('{{ var|join:" & \" }}', { "var": ("Tom", "Dick", "Harry") }, "Tom & Dick & Harry" ),
+
+ # Literal strings are safe.
+ 'autoescape-literals01': ('{{ "this & that" }}',{}, "this & that" ),
+
+ # Iterating over strings outputs safe characters.
+ 'autoescape-stringiterations01': ('{% for l in var %}{{ l }},{% endfor %}', {'var': 'K&R'}, "K,&amp;,R," ),
+
+ # Escape requirement survives lookup.
+ 'autoescape-lookup01': ('{{ var.key }}', { "var": {"key": "this & that" }}, "this &amp; that" ),
+
}
diff --git a/tests/regressiontests/test_client_regress/models.py b/tests/regressiontests/test_client_regress/models.py
index 6da7ae4445..22b59e54a9 100644
--- a/tests/regressiontests/test_client_regress/models.py
+++ b/tests/regressiontests/test_client_regress/models.py
@@ -11,6 +11,7 @@ from django.core.urlresolvers import reverse
from django.core.exceptions import SuspiciousOperation
from django.template import TemplateDoesNotExist, TemplateSyntaxError, Context
from django.template import loader
+from django.test.client import encode_file
class AssertContainsTests(TestCase):
def setUp(self):
@@ -34,20 +35,20 @@ class AssertContainsTests(TestCase):
try:
self.assertContains(response, 'text', status_code=999)
except AssertionError, e:
- self.assertEquals(str(e), "Couldn't retrieve page: Response code was 200 (expected 999)")
+ self.assertEquals(str(e), "Couldn't retrieve content: Response code was 200 (expected 999)")
try:
self.assertContains(response, 'text', status_code=999, msg_prefix='abc')
except AssertionError, e:
- self.assertEquals(str(e), "abc: Couldn't retrieve page: Response code was 200 (expected 999)")
+ self.assertEquals(str(e), "abc: Couldn't retrieve content: Response code was 200 (expected 999)")
try:
self.assertNotContains(response, 'text', status_code=999)
except AssertionError, e:
- self.assertEquals(str(e), "Couldn't retrieve page: Response code was 200 (expected 999)")
+ self.assertEquals(str(e), "Couldn't retrieve content: Response code was 200 (expected 999)")
try:
self.assertNotContains(response, 'text', status_code=999, msg_prefix='abc')
except AssertionError, e:
- self.assertEquals(str(e), "abc: Couldn't retrieve page: Response code was 200 (expected 999)")
+ self.assertEquals(str(e), "abc: Couldn't retrieve content: Response code was 200 (expected 999)")
try:
self.assertNotContains(response, 'once')
@@ -619,6 +620,7 @@ class ContextTests(TestCase):
"Context variables can be retrieved from a single context"
response = self.client.get("/test_client_regress/request_data/", data={'foo':'whiz'})
self.assertEqual(response.context.__class__, Context)
+ self.assertTrue('get-foo' in response.context)
self.assertEqual(response.context['get-foo'], 'whiz')
self.assertEqual(response.context['request-foo'], 'whiz')
self.assertEqual(response.context['data'], 'sausage')
@@ -634,6 +636,7 @@ class ContextTests(TestCase):
response = self.client.get("/test_client_regress/request_data_extended/", data={'foo':'whiz'})
self.assertEqual(response.context.__class__, ContextList)
self.assertEqual(len(response.context), 2)
+ self.assertTrue('get-foo' in response.context)
self.assertEqual(response.context['get-foo'], 'whiz')
self.assertEqual(response.context['request-foo'], 'whiz')
self.assertEqual(response.context['data'], 'bacon')
@@ -821,3 +824,40 @@ class UnicodePayloadTests(TestCase):
response = self.client.post("/test_client_regress/parse_unicode_json/", json,
content_type="application/json; charset=koi8-r")
self.assertEqual(response.content, json.encode('koi8-r'))
+
+class DummyFile(object):
+ def __init__(self, filename):
+ self.name = filename
+ def read(self):
+ return 'TEST_FILE_CONTENT'
+
+class UploadedFileEncodingTest(TestCase):
+ def test_file_encoding(self):
+ encoded_file = encode_file('TEST_BOUNDARY', 'TEST_KEY', DummyFile('test_name.bin'))
+ self.assertEqual('--TEST_BOUNDARY', encoded_file[0])
+ self.assertEqual('Content-Disposition: form-data; name="TEST_KEY"; filename="test_name.bin"', encoded_file[1])
+ self.assertEqual('TEST_FILE_CONTENT', encoded_file[-1])
+
+ def test_guesses_content_type_on_file_encoding(self):
+ self.assertEqual('Content-Type: application/octet-stream',
+ encode_file('IGNORE', 'IGNORE', DummyFile("file.bin"))[2])
+ self.assertEqual('Content-Type: text/plain',
+ encode_file('IGNORE', 'IGNORE', DummyFile("file.txt"))[2])
+ self.assertEqual('Content-Type: application/zip',
+ encode_file('IGNORE', 'IGNORE', DummyFile("file.zip"))[2])
+ self.assertEqual('Content-Type: application/octet-stream',
+ encode_file('IGNORE', 'IGNORE', DummyFile("file.unknown"))[2])
+
+class RequestHeadersTest(TestCase):
+ def test_client_headers(self):
+ "A test client can receive custom headers"
+ response = self.client.get("/test_client_regress/check_headers/", HTTP_X_ARG_CHECK='Testing 123')
+ self.assertEquals(response.content, "HTTP_X_ARG_CHECK: Testing 123")
+ self.assertEquals(response.status_code, 200)
+
+ def test_client_headers_redirect(self):
+ "Test client headers are preserved through redirects"
+ response = self.client.get("/test_client_regress/check_headers_redirect/", follow=True, HTTP_X_ARG_CHECK='Testing 123')
+ self.assertEquals(response.content, "HTTP_X_ARG_CHECK: Testing 123")
+ self.assertRedirects(response, '/test_client_regress/check_headers/',
+ status_code=301, target_status_code=200)
diff --git a/tests/regressiontests/test_client_regress/urls.py b/tests/regressiontests/test_client_regress/urls.py
index 99eb7b70be..650d80b909 100644
--- a/tests/regressiontests/test_client_regress/urls.py
+++ b/tests/regressiontests/test_client_regress/urls.py
@@ -24,4 +24,6 @@ urlpatterns = patterns('',
(r'^request_methods/$', views.request_methods_view),
(r'^check_unicode/$', views.return_unicode),
(r'^parse_unicode_json/$', views.return_json_file),
+ (r'^check_headers/$', views.check_headers),
+ (r'^check_headers_redirect/$', redirect_to, {'url': '/test_client_regress/check_headers/'}),
)
diff --git a/tests/regressiontests/test_client_regress/views.py b/tests/regressiontests/test_client_regress/views.py
index 5ba7cc30a9..40aa61fca7 100644
--- a/tests/regressiontests/test_client_regress/views.py
+++ b/tests/regressiontests/test_client_regress/views.py
@@ -86,3 +86,8 @@ def return_json_file(request):
mimetype='application/json; charset=' + charset)
response['Content-Disposition'] = 'attachment; filename=testfile.json'
return response
+
+def check_headers(request):
+ "A view that responds with value of the X-ARG-CHECK header"
+ return HttpResponse('HTTP_X_ARG_CHECK: %s' % request.META.get('HTTP_X_ARG_CHECK', 'Undefined'))
+
diff --git a/tests/regressiontests/urlpatterns_reverse/included_named_urls.py b/tests/regressiontests/urlpatterns_reverse/included_named_urls.py
new file mode 100644
index 0000000000..b3f7903b41
--- /dev/null
+++ b/tests/regressiontests/urlpatterns_reverse/included_named_urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls.defaults import *
+from views import empty_view
+
+urlpatterns = patterns('',
+ url(r'^$', empty_view, name="named-url3"),
+ url(r'^extra/(?P<extra>\w+)/$', empty_view, name="named-url4"),
+ url(r'^(?P<one>\d+)|(?P<two>\d+)/$', empty_view),
+ (r'^included/', include('regressiontests.urlpatterns_reverse.included_named_urls2')),
+)
+
diff --git a/tests/regressiontests/urlpatterns_reverse/included_named_urls2.py b/tests/regressiontests/urlpatterns_reverse/included_named_urls2.py
new file mode 100644
index 0000000000..96c42c3e4d
--- /dev/null
+++ b/tests/regressiontests/urlpatterns_reverse/included_named_urls2.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import *
+from views import empty_view
+
+urlpatterns = patterns('',
+ url(r'^$', empty_view, name="named-url5"),
+ url(r'^extra/(?P<extra>\w+)/$', empty_view, name="named-url6"),
+ url(r'^(?P<one>\d+)|(?P<two>\d+)/$', empty_view),
+)
+
diff --git a/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py
index 073190657c..16887e2a9b 100644
--- a/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py
+++ b/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py
@@ -1,5 +1,6 @@
from django.conf.urls.defaults import *
from namespace_urls import URLObject
+from views import view_class_instance
testobj3 = URLObject('testapp', 'test-ns3')
@@ -7,7 +8,13 @@ urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
url(r'^normal/$', 'empty_view', name='inc-normal-view'),
url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-normal-view'),
+ url(r'^mixed_args/(\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-mixed-args'),
+ url(r'^no_kwargs/(\d+)/(\d+)/$', 'empty_view', name='inc-no-kwargs'),
+
+ url(r'^view_class/(?P<arg1>\d+)/(?P<arg2>\d+)/$', view_class_instance, name='inc-view-class'),
+
(r'^test3/', include(testobj3.urls)),
(r'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')),
+ (r'^ns-included4/', include('regressiontests.urlpatterns_reverse.namespace_urls', namespace='inc-ns4')),
)
diff --git a/tests/regressiontests/urlpatterns_reverse/named_urls.py b/tests/regressiontests/urlpatterns_reverse/named_urls.py
new file mode 100644
index 0000000000..d8a61a106c
--- /dev/null
+++ b/tests/regressiontests/urlpatterns_reverse/named_urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import *
+from views import empty_view
+
+urlpatterns = patterns('',
+ url(r'^$', empty_view, name="named-url1"),
+ url(r'^extra/(?P<extra>\w+)/$', empty_view, name="named-url2"),
+ url(r'^(?P<one>\d+)|(?P<two>\d+)/$', empty_view),
+ (r'^included/', include('regressiontests.urlpatterns_reverse.included_named_urls')),
+)
diff --git a/tests/regressiontests/urlpatterns_reverse/namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py
index 27cc7f7a22..3d34049932 100644
--- a/tests/regressiontests/urlpatterns_reverse/namespace_urls.py
+++ b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py
@@ -1,4 +1,5 @@
from django.conf.urls.defaults import *
+from views import view_class_instance
class URLObject(object):
def __init__(self, app_name, namespace):
@@ -23,6 +24,14 @@ urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
url(r'^normal/$', 'empty_view', name='normal-view'),
url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='normal-view'),
+ url(r'^mixed_args/(\d+)/(?P<arg2>\d+)/$', 'empty_view', name='mixed-args'),
+ url(r'^no_kwargs/(\d+)/(\d+)/$', 'empty_view', name='no-kwargs'),
+
+ url(r'^view_class/(?P<arg1>\d+)/(?P<arg2>\d+)/$', view_class_instance, name='view-class'),
+
+ (r'^unnamed/normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view'),
+ (r'^unnamed/view_class/(?P<arg1>\d+)/(?P<arg2>\d+)/$', view_class_instance),
+
(r'^test1/', include(testobj1.urls)),
(r'^test2/', include(testobj2.urls)),
(r'^default/', include(default_testobj.urls)),
diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py
index 3fcc935da2..a0b98a88d3 100644
--- a/tests/regressiontests/urlpatterns_reverse/tests.py
+++ b/tests/regressiontests/urlpatterns_reverse/tests.py
@@ -18,7 +18,9 @@ import unittest
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
-from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404
+from django.core.urlresolvers import reverse, resolve, NoReverseMatch,\
+ Resolver404, ResolverMatch,\
+ RegexURLResolver, RegexURLPattern
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
from django.shortcuts import redirect
from django.test import TestCase
@@ -26,6 +28,41 @@ from django.test import TestCase
import urlconf_outer
import urlconf_inner
import middleware
+import views
+
+resolve_test_data = (
+ # These entries are in the format: (path, url_name, app_name, namespace, view_func, args, kwargs)
+ # Simple case
+ ('/normal/42/37/', 'normal-view', None, '', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
+ ('/view_class/42/37/', 'view-class', None, '', views.view_class_instance, tuple(), {'arg1': '42', 'arg2': '37'}),
+ ('/included/normal/42/37/', 'inc-normal-view', None, '', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
+ ('/included/view_class/42/37/', 'inc-view-class', None, '', views.view_class_instance, tuple(), {'arg1': '42', 'arg2': '37'}),
+
+ # Unnamed args are dropped if you have *any* kwargs in a pattern
+ ('/mixed_args/42/37/', 'mixed-args', None, '', views.empty_view, tuple(), {'arg2': '37'}),
+ ('/included/mixed_args/42/37/', 'inc-mixed-args', None, '', views.empty_view, tuple(), {'arg2': '37'}),
+
+ # Unnamed views will be resolved to the function/class name
+ ('/unnamed/normal/42/37/', 'regressiontests.urlpatterns_reverse.views.empty_view', None, '', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
+ ('/unnamed/view_class/42/37/', 'regressiontests.urlpatterns_reverse.views.ViewClass', None, '', views.view_class_instance, tuple(), {'arg1': '42', 'arg2': '37'}),
+
+ # If you have no kwargs, you get an args list.
+ ('/no_kwargs/42/37/', 'no-kwargs', None, '', views.empty_view, ('42','37'), {}),
+ ('/included/no_kwargs/42/37/', 'inc-no-kwargs', None, '', views.empty_view, ('42','37'), {}),
+
+ # Namespaces
+ ('/test1/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns1', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
+ ('/included/test3/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
+ ('/ns-included1/normal/42/37/', 'inc-normal-view', None, 'inc-ns1', views.empty_view, tuple(), {'arg1': '42', 'arg2': '37'}),
+ ('/included/test3/inner/42/37/', 'urlobject-view', 'testapp', 'test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
+ ('/default/inner/42/37/', 'urlobject-view', 'testapp', 'testapp', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
+ ('/other2/inner/42/37/', 'urlobject-view', 'nodefault', 'other-ns2', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
+ ('/other1/inner/42/37/', 'urlobject-view', 'nodefault', 'other-ns1', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
+
+ # Nested namespaces
+ ('/ns-included1/test3/inner/42/37/', 'urlobject-view', 'testapp', 'inc-ns1:test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
+ ('/ns-included1/ns-included4/ns-included2/test3/inner/42/37/', 'urlobject-view', 'testapp', 'inc-ns1:inc-ns4:inc-ns2:test-ns3', 'empty_view', tuple(), {'arg1': '42', 'arg2': '37'}),
+)
test_data = (
('places', '/places/3/', [3], {}),
@@ -119,6 +156,10 @@ class URLPatternReverse(TestCase):
else:
self.assertEquals(got, expected)
+ def test_reverse_none(self):
+ # Reversing None should raise an error, not return the last un-named view.
+ self.assertRaises(NoReverseMatch, reverse, None)
+
class ResolverTests(unittest.TestCase):
def test_non_regex(self):
"""
@@ -133,6 +174,42 @@ class ResolverTests(unittest.TestCase):
self.assertRaises(Resolver404, resolve, 'a')
self.assertRaises(Resolver404, resolve, '\\')
self.assertRaises(Resolver404, resolve, '.')
+
+ def test_404_tried_urls_have_names(self):
+ """
+ Verifies that the list of URLs that come back from a Resolver404
+ exception contains a list in the right format for printing out in
+ the DEBUG 404 page with both the patterns and URL names, if available.
+ """
+ urls = 'regressiontests.urlpatterns_reverse.named_urls'
+ # this list matches the expected URL types and names returned when
+ # you try to resolve a non-existent URL in the first level of included
+ # URLs in named_urls.py (e.g., '/included/non-existent-url')
+ url_types_names = [
+ [{'type': RegexURLPattern, 'name': 'named-url1'}],
+ [{'type': RegexURLPattern, 'name': 'named-url2'}],
+ [{'type': RegexURLPattern, 'name': None}],
+ [{'type': RegexURLResolver}, {'type': RegexURLPattern, 'name': 'named-url3'}],
+ [{'type': RegexURLResolver}, {'type': RegexURLPattern, 'name': 'named-url4'}],
+ [{'type': RegexURLResolver}, {'type': RegexURLPattern, 'name': None}],
+ [{'type': RegexURLResolver}, {'type': RegexURLResolver}],
+ ]
+ try:
+ resolve('/included/non-existent-url', urlconf=urls)
+ self.fail('resolve did not raise a 404')
+ except Resolver404, e:
+ # make sure we at least matched the root ('/') url resolver:
+ self.assertTrue('tried' in e.args[0])
+ tried = e.args[0]['tried']
+ 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']), '%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)
+ else:
+ self.assertEqual(t.name, e['name'], 'Wrong URL name. Expected "%s", got "%s".' % (e['name'], t.name))
class ReverseShortcutTests(TestCase):
urls = 'regressiontests.urlpatterns_reverse.urls'
@@ -229,6 +306,12 @@ class NamespaceTests(TestCase):
self.assertEquals('/ns-included1/test3/inner/37/42/', reverse('inc-ns1:test-ns3:urlobject-view', args=[37,42]))
self.assertEquals('/ns-included1/test3/inner/42/37/', reverse('inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
+ def test_nested_namespace_pattern(self):
+ "Namespaces can be nested"
+ self.assertEquals('/ns-included1/ns-included4/ns-included1/test3/inner/', reverse('inc-ns1:inc-ns4:inc-ns1:test-ns3:urlobject-view'))
+ self.assertEquals('/ns-included1/ns-included4/ns-included1/test3/inner/37/42/', reverse('inc-ns1:inc-ns4:inc-ns1:test-ns3:urlobject-view', args=[37,42]))
+ self.assertEquals('/ns-included1/ns-included4/ns-included1/test3/inner/42/37/', reverse('inc-ns1:inc-ns4:inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
+
def test_app_lookup_object(self):
"A default application namespace can be used for lookup"
self.assertEquals('/default/inner/', reverse('testapp:urlobject-view'))
@@ -311,9 +394,51 @@ class ErrorHandlerResolutionTests(TestCase):
self.assertEqual(self.callable_resolver.resolve404(), handler)
self.assertEqual(self.callable_resolver.resolve500(), handler)
+class DefaultErrorHandlerTests(TestCase):
+ urls = 'regressiontests.urlpatterns_reverse.urls_without_full_import'
+
+ def test_default_handler(self):
+ "If the urls.py doesn't specify handlers, the defaults are used"
+ try:
+ response = self.client.get('/test/')
+ self.assertEquals(response.status_code, 404)
+ except AttributeError:
+ self.fail("Shouldn't get an AttributeError due to undefined 404 handler")
+
+ try:
+ self.assertRaises(ValueError, self.client.get, '/bad_view/')
+ except AttributeError:
+ self.fail("Shouldn't get an AttributeError due to undefined 500 handler")
+
class NoRootUrlConfTests(TestCase):
"""Tests for handler404 and handler500 if urlconf is None"""
urls = None
def test_no_handler_exception(self):
self.assertRaises(ImproperlyConfigured, self.client.get, '/test/me/')
+
+class ResolverMatchTests(TestCase):
+ urls = 'regressiontests.urlpatterns_reverse.namespace_urls'
+
+ def test_urlpattern_resolve(self):
+ for path, name, app_name, namespace, func, args, kwargs in resolve_test_data:
+ # Test legacy support for extracting "function, args, kwargs"
+ match_func, match_args, match_kwargs = resolve(path)
+ self.assertEqual(match_func, func)
+ self.assertEqual(match_args, args)
+ self.assertEqual(match_kwargs, kwargs)
+
+ # Test ResolverMatch capabilities.
+ match = resolve(path)
+ self.assertEqual(match.__class__, ResolverMatch)
+ self.assertEqual(match.url_name, name)
+ self.assertEqual(match.args, args)
+ self.assertEqual(match.kwargs, kwargs)
+ self.assertEqual(match.app_name, app_name)
+ self.assertEqual(match.namespace, namespace)
+ self.assertEqual(match.func, func)
+
+ # ... and for legacy purposes:
+ self.assertEquals(match[0], func)
+ self.assertEquals(match[1], args)
+ self.assertEquals(match[2], kwargs)
diff --git a/tests/regressiontests/urlpatterns_reverse/urls_without_full_import.py b/tests/regressiontests/urlpatterns_reverse/urls_without_full_import.py
new file mode 100644
index 0000000000..75a195ed14
--- /dev/null
+++ b/tests/regressiontests/urlpatterns_reverse/urls_without_full_import.py
@@ -0,0 +1,10 @@
+# A URLs file that doesn't use the default
+# from django.conf.urls.defaults import *
+# import pattern.
+from django.conf.urls.defaults import patterns, url
+from views import empty_view, bad_view
+
+urlpatterns = patterns('',
+ url(r'^test_view/$', empty_view, name="test_view"),
+ url(r'^bad_view/$', bad_view, name="bad_view"),
+)
diff --git a/tests/regressiontests/urlpatterns_reverse/views.py b/tests/regressiontests/urlpatterns_reverse/views.py
index 99c00bde70..fdd742382c 100644
--- a/tests/regressiontests/urlpatterns_reverse/views.py
+++ b/tests/regressiontests/urlpatterns_reverse/views.py
@@ -1,8 +1,19 @@
+from django.http import HttpResponse
+
def empty_view(request, *args, **kwargs):
- pass
+ return HttpResponse('')
def kwargs_view(request, arg1=1, arg2=2):
- pass
+ return HttpResponse('')
def absolute_kwargs_view(request, arg1=1, arg2=2):
- pass
+ return HttpResponse('')
+
+class ViewClass(object):
+ def __call__(self, request, *args, **kwargs):
+ return HttpResponse('')
+
+view_class_instance = ViewClass()
+
+def bad_view(request, *args, **kwargs):
+ raise ValueError("I don't think I'm getting good value for this view")
diff --git a/tests/regressiontests/utils/timesince.py b/tests/regressiontests/utils/timesince.py
index 04878b272a..5a54bf4c8c 100644
--- a/tests/regressiontests/utils/timesince.py
+++ b/tests/regressiontests/utils/timesince.py
@@ -88,11 +88,11 @@ u'0 minutes'
u'0 minutes'
# Timesince should work with both date objects (#9672)
->>> today = datetime.date.today()
->>> timeuntil(today+oneday, today)
-u'1 day'
->>> timeuntil(today-oneday, today)
-u'0 minutes'
->>> timeuntil(today+oneweek, today)
-u'1 week'
+>>> today = datetime.date.today()
+>>> timeuntil(today+oneday, today)
+u'1 day'
+>>> timeuntil(today-oneday, today)
+u'0 minutes'
+>>> timeuntil(today+oneweek, today)
+u'1 week'
"""
diff --git a/tests/regressiontests/views/tests/__init__.py b/tests/regressiontests/views/tests/__init__.py
index 697968ee52..edd533e175 100644
--- a/tests/regressiontests/views/tests/__init__.py
+++ b/tests/regressiontests/views/tests/__init__.py
@@ -2,6 +2,7 @@ from debug import *
from defaults import *
from generic.create_update import *
from generic.date_based import *
+from generic.simple import *
from i18n import *
from specials import *
from static import *
diff --git a/tests/regressiontests/views/tests/generic/simple.py b/tests/regressiontests/views/tests/generic/simple.py
new file mode 100644
index 0000000000..f94b3da439
--- /dev/null
+++ b/tests/regressiontests/views/tests/generic/simple.py
@@ -0,0 +1,38 @@
+# coding: utf-8
+
+from django.test import TestCase
+
+class RedirectToTest(TestCase):
+ def test_redirect_to_returns_permanent_redirect(self):
+ "simple.redirect_to returns a permanent redirect (301) by default"
+ response = self.client.get('/views/simple/redirect_to/')
+ self.assertEqual(response.status_code, 301)
+ self.assertEqual('http://testserver/views/simple/target/', response['Location'])
+
+ def test_redirect_to_can_return_a_temporary_redirect(self):
+ "simple.redirect_to returns a temporary redirect (302) when explicitely asked to"
+ response = self.client.get('/views/simple/redirect_to_temp/')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual('http://testserver/views/simple/target/', response['Location'])
+
+ def test_redirect_to_on_empty_url_returns_gone(self):
+ "simple.redirect_to returns resource gone (410) when given a None url"
+ response = self.client.get('/views/simple/redirect_to_none/')
+ self.assertEqual(response.status_code, 410)
+
+ def test_redirect_to_allows_formatted_url_string(self):
+ "simple.redirect_to uses string interpolation on target url for keyword args"
+ response = self.client.get('/views/simple/redirect_to_arg/42/')
+ self.assertEqual(response.status_code, 301)
+ self.assertEqual('http://testserver/views/simple/target_arg/42/', response['Location'])
+
+ def test_redirect_to_allows_query_string_to_be_passed(self):
+ "simple.redirect_to configured with query_string=True passes on any query string"
+ # the default is to not forward the query string
+ response = self.client.get('/views/simple/redirect_to/?param1=foo&param2=bar')
+ self.assertEqual(response.status_code, 301)
+ self.assertEqual('http://testserver/views/simple/target/', response['Location'])
+ # views configured with query_string=True however passes the query string along
+ response = self.client.get('/views/simple/redirect_to_query/?param1=foo&param2=bar')
+ self.assertEqual(response.status_code, 301)
+ self.assertEqual('http://testserver/views/simple/target/?param1=foo&param2=bar', response['Location'])
diff --git a/tests/regressiontests/views/urls.py b/tests/regressiontests/views/urls.py
index f5675d0e28..b42700baeb 100644
--- a/tests/regressiontests/views/urls.py
+++ b/tests/regressiontests/views/urls.py
@@ -77,7 +77,6 @@ urlpatterns += patterns('django.views.generic.date_based',
)
# crud generic views.
-
urlpatterns += patterns('django.views.generic.create_update',
(r'^create_update/member/create/article/$', 'create_object',
dict(login_required=True, model=Article)),
@@ -123,3 +122,12 @@ urlpatterns += patterns('regressiontests.views.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'),
)
+
+# simple generic views.
+urlpatterns += patterns('django.views.generic.simple',
+ (r'^simple/redirect_to/$', 'redirect_to', dict(url='/views/simple/target/')),
+ (r'^simple/redirect_to_temp/$', 'redirect_to', dict(url='/views/simple/target/', permanent=False)),
+ (r'^simple/redirect_to_none/$', 'redirect_to', dict(url=None)),
+ (r'^simple/redirect_to_arg/(?P<id>\d+)/$', 'redirect_to', dict(url='/views/simple/target_arg/%(id)s/')),
+ (r'^simple/redirect_to_query/$', 'redirect_to', dict(url='/views/simple/target/', query_string=True)),
+)