diff options
Diffstat (limited to 'parts/django/tests/regressiontests')
560 files changed, 45642 insertions, 0 deletions
diff --git a/parts/django/tests/regressiontests/__init__.py b/parts/django/tests/regressiontests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/__init__.py diff --git a/parts/django/tests/regressiontests/admin_changelist/__init__.py b/parts/django/tests/regressiontests/admin_changelist/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_changelist/__init__.py diff --git a/parts/django/tests/regressiontests/admin_changelist/models.py b/parts/django/tests/regressiontests/admin_changelist/models.py new file mode 100644 index 0000000..f030a78 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_changelist/models.py @@ -0,0 +1,9 @@ +from django.db import models +from django.contrib import admin + +class Parent(models.Model): + name = models.CharField(max_length=128) + +class Child(models.Model): + parent = models.ForeignKey(Parent, editable=False) + name = models.CharField(max_length=30, blank=True)
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/admin_changelist/tests.py b/parts/django/tests/regressiontests/admin_changelist/tests.py new file mode 100644 index 0000000..96b36f8 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_changelist/tests.py @@ -0,0 +1,103 @@ +from django.contrib import admin +from django.contrib.admin.options import IncorrectLookupParameters +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(TransactionTestCase): + def test_select_related_preserved(self): + """ + Regression test for #10348: ChangeList.get_query_set() shouldn't + overwrite a custom select_related provided by ModelAdmin.queryset(). + """ + m = ChildAdmin(Child, admin.site) + cl = ChangeList(MockRequest(), 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) + self.assertEqual(cl.query_set.query.select_related, {'parent': {'name': {}}}) + + def test_result_list_html(self): + """ + 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) + request = MockRequest() + m = ChildAdmin(Child, admin.site) + 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) + cl.formset = None + template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}') + context = Context({'cl': cl}) + table_output = template.render(context) + 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.assertFalse(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'] + m.list_display_links = ['id'] + m.list_editable = ['name'] + 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) + template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}') + context = Context({'cl': cl}) + table_output = template.render(context) + # 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.assertFalse(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.assertFalse('<td>%s</td>' % editable_name_field == -1, + 'Failed to find "name" list_editable field in: %s' % table_output) + + def test_result_list_editable(self): + """ + Regression test for #14312: list_editable with pagination + """ + + new_parent = Parent.objects.create(name='parent') + for i in range(200): + new_child = Child.objects.create(name='name %s' % i, parent=new_parent) + request = MockRequest() + request.GET['p'] = -1 # Anything outside range + m = ChildAdmin(Child, admin.site) + + # Test with list_editable fields + m.list_display = ['id', 'name', 'parent'] + m.list_display_links = ['id'] + m.list_editable = ['name'] + self.assertRaises(IncorrectLookupParameters, lambda: \ + 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)) + + +class ChildAdmin(admin.ModelAdmin): + list_display = ['name', 'parent'] + def queryset(self, request): + return super(ChildAdmin, self).queryset(request).select_related("parent__name") + +class MockRequest(object): + GET = {} diff --git a/parts/django/tests/regressiontests/admin_inlines/__init__.py b/parts/django/tests/regressiontests/admin_inlines/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_inlines/__init__.py diff --git a/parts/django/tests/regressiontests/admin_inlines/fixtures/admin-views-users.xml b/parts/django/tests/regressiontests/admin_inlines/fixtures/admin-views-users.xml new file mode 100644 index 0000000..aba8f4a --- /dev/null +++ b/parts/django/tests/regressiontests/admin_inlines/fixtures/admin-views-users.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="100" model="auth.user"> + <field type="CharField" name="username">super</field> + <field type="CharField" name="first_name">Super</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">super@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">True</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> +</django-objects> diff --git a/parts/django/tests/regressiontests/admin_inlines/models.py b/parts/django/tests/regressiontests/admin_inlines/models.py new file mode 100644 index 0000000..4e5c4e3 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_inlines/models.py @@ -0,0 +1,125 @@ +""" +Testing of admin inline formsets. + +""" +from django.db import models +from django.contrib import admin +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic + +class Parent(models.Model): + name = models.CharField(max_length=50) + + def __unicode__(self): + return self.name + +class Teacher(models.Model): + name = models.CharField(max_length=50) + + def __unicode__(self): + return self.name + +class Child(models.Model): + name = models.CharField(max_length=50) + teacher = models.ForeignKey(Teacher) + + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + parent = generic.GenericForeignKey() + + def __unicode__(self): + return u'I am %s, a child of %s' % (self.name, self.parent) + +class Book(models.Model): + name = models.CharField(max_length=50) + +class Author(models.Model): + name = models.CharField(max_length=50) + books = models.ManyToManyField(Book) + +class BookInline(admin.TabularInline): + model = Author.books.through + +class AuthorAdmin(admin.ModelAdmin): + inlines = [BookInline] + +admin.site.register(Author, AuthorAdmin) + +class Holder(models.Model): + dummy = models.IntegerField() + + +class Inner(models.Model): + dummy = models.IntegerField() + holder = models.ForeignKey(Holder) + readonly = models.CharField("Inner readonly label", max_length=1) + + +class InnerInline(admin.StackedInline): + model = Inner + can_delete = False + readonly_fields = ('readonly',) # For bug #13174 tests. + + +class Holder2(models.Model): + dummy = models.IntegerField() + + +class Inner2(models.Model): + dummy = models.IntegerField() + holder = models.ForeignKey(Holder2) + +class HolderAdmin(admin.ModelAdmin): + + class Media: + js = ('my_awesome_admin_scripts.js',) + +class InnerInline2(admin.StackedInline): + model = Inner2 + + class Media: + js = ('my_awesome_inline_scripts.js',) + +class Holder3(models.Model): + dummy = models.IntegerField() + + +class Inner3(models.Model): + dummy = models.IntegerField() + holder = models.ForeignKey(Holder3) + +class InnerInline3(admin.StackedInline): + model = Inner3 + + class Media: + js = ('my_awesome_inline_scripts.js',) + +# Test bug #12561 and #12778 +# only ModelAdmin media +admin.site.register(Holder, HolderAdmin, inlines=[InnerInline]) +# ModelAdmin and Inline media +admin.site.register(Holder2, HolderAdmin, inlines=[InnerInline2]) +# only Inline media +admin.site.register(Holder3, inlines=[InnerInline3]) + +# Models for #12749 + +class Person(models.Model): + firstname = models.CharField(max_length=15) + +class OutfitItem(models.Model): + name = models.CharField(max_length=15) + +class Fashionista(models.Model): + person = models.OneToOneField(Person, primary_key=True) + weaknesses = models.ManyToManyField(OutfitItem, through='ShoppingWeakness', blank=True) + +class ShoppingWeakness(models.Model): + fashionista = models.ForeignKey(Fashionista) + item = models.ForeignKey(OutfitItem) + +class InlineWeakness(admin.TabularInline): + model = ShoppingWeakness + extra = 1 + +admin.site.register(Fashionista, inlines=[InlineWeakness]) diff --git a/parts/django/tests/regressiontests/admin_inlines/tests.py b/parts/django/tests/regressiontests/admin_inlines/tests.py new file mode 100644 index 0000000..b10474d --- /dev/null +++ b/parts/django/tests/regressiontests/admin_inlines/tests.py @@ -0,0 +1,121 @@ +from django.contrib.admin.helpers import InlineAdminForm +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +# local test models +from models import (Holder, Inner, InnerInline, Holder2, Inner2, Holder3, + Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child) + + +class TestInline(TestCase): + fixtures = ['admin-views-users.xml'] + + def setUp(self): + holder = Holder(dummy=13) + holder.save() + Inner(dummy=42, holder=holder).save() + self.change_url = '/test_admin/admin/admin_inlines/holder/%i/' % holder.id + + result = self.client.login(username='super', password='secret') + self.assertEqual(result, True) + + def tearDown(self): + self.client.logout() + + def test_can_delete(self): + """ + can_delete should be passed to inlineformset factory. + """ + response = self.client.get(self.change_url) + inner_formset = response.context[-1]['inline_admin_formsets'][0].formset + expected = InnerInline.can_delete + actual = inner_formset.can_delete + self.assertEqual(expected, actual, 'can_delete must be equal') + + def test_readonly_stacked_inline_label(self): + """Bug #13174.""" + holder = Holder.objects.create(dummy=42) + inner = Inner.objects.create(holder=holder, dummy=42, readonly='') + response = self.client.get('/test_admin/admin/admin_inlines/holder/%i/' + % holder.id) + self.assertContains(response, '<label>Inner readonly label:</label>') + + def test_many_to_many_inlines(self): + "Autogenerated many-to-many inlines are displayed correctly (#13407)" + response = self.client.get('/test_admin/admin/admin_inlines/author/add/') + # The heading for the m2m inline block uses the right text + self.assertContains(response, '<h2>Author-book relationships</h2>') + # The "add another" label is correct + self.assertContains(response, 'Add another Author-Book Relationship') + # The '+' is dropped from the autogenerated form prefix (Author_books+) + self.assertContains(response, 'id="id_Author_books-TOTAL_FORMS"') + + def test_inline_primary(self): + person = Person.objects.create(firstname='Imelda') + item = OutfitItem.objects.create(name='Shoes') + # Imelda likes shoes, but can't cary her own bags. + data = { + 'shoppingweakness_set-TOTAL_FORMS': 1, + 'shoppingweakness_set-INITIAL_FORMS': 0, + 'shoppingweakness_set-MAX_NUM_FORMS': 0, + '_save': u'Save', + 'person': person.id, + 'max_weight': 0, + 'shoppingweakness_set-0-item': item.id, + } + response = self.client.post('/test_admin/admin/admin_inlines/fashionista/add/', data) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(Fashionista.objects.filter(person__firstname='Imelda')), 1) + +class TestInlineMedia(TestCase): + fixtures = ['admin-views-users.xml'] + + def setUp(self): + + result = self.client.login(username='super', password='secret') + self.assertEqual(result, True) + + def tearDown(self): + self.client.logout() + + def test_inline_media_only_base(self): + holder = Holder(dummy=13) + holder.save() + Inner(dummy=42, holder=holder).save() + change_url = '/test_admin/admin/admin_inlines/holder/%i/' % holder.id + response = self.client.get(change_url) + self.assertContains(response, 'my_awesome_admin_scripts.js') + + def test_inline_media_only_inline(self): + holder = Holder3(dummy=13) + holder.save() + Inner3(dummy=42, holder=holder).save() + change_url = '/test_admin/admin/admin_inlines/holder3/%i/' % holder.id + response = self.client.get(change_url) + self.assertContains(response, 'my_awesome_inline_scripts.js') + + def test_all_inline_media(self): + holder = Holder2(dummy=13) + holder.save() + Inner2(dummy=42, holder=holder).save() + change_url = '/test_admin/admin/admin_inlines/holder2/%i/' % holder.id + response = self.client.get(change_url) + self.assertContains(response, 'my_awesome_admin_scripts.js') + self.assertContains(response, 'my_awesome_inline_scripts.js') + +class TestInlineAdminForm(TestCase): + + def test_immutable_content_type(self): + """Regression for #9362 + The problem depends only on InlineAdminForm and its "original" + argument, so we can safely set the other arguments to None/{}. We just + need to check that the content_type argument of Child isn't altered by + the internals of the inline form.""" + + sally = Teacher.objects.create(name='Sally') + john = Parent.objects.create(name='John') + joe = Child.objects.create(name='Joe', teacher=sally, parent=john) + + iaf = InlineAdminForm(None, None, {}, {}, joe) + parent_ct = ContentType.objects.get_for_model(Parent) + self.assertEqual(iaf.original.content_type, parent_ct) diff --git a/parts/django/tests/regressiontests/admin_ordering/__init__.py b/parts/django/tests/regressiontests/admin_ordering/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_ordering/__init__.py diff --git a/parts/django/tests/regressiontests/admin_ordering/models.py b/parts/django/tests/regressiontests/admin_ordering/models.py new file mode 100644 index 0000000..ad63685 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_ordering/models.py @@ -0,0 +1,10 @@ +# coding: utf-8 +from django.db import models + +class Band(models.Model): + name = models.CharField(max_length=100) + bio = models.TextField() + rank = models.IntegerField() + + class Meta: + ordering = ('name',) diff --git a/parts/django/tests/regressiontests/admin_ordering/tests.py b/parts/django/tests/regressiontests/admin_ordering/tests.py new file mode 100644 index 0000000..f63f202 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_ordering/tests.py @@ -0,0 +1,39 @@ +from django.test import TestCase +from django.contrib.admin.options import ModelAdmin + +from models import Band + +class TestAdminOrdering(TestCase): + """ + Let's make sure that ModelAdmin.queryset uses the ordering we define in + ModelAdmin rather that ordering defined in the model's inner Meta + class. + """ + + def setUp(self): + b1 = Band(name='Aerosmith', bio='', rank=3) + b1.save() + b2 = Band(name='Radiohead', bio='', rank=1) + b2.save() + b3 = Band(name='Van Halen', bio='', rank=2) + b3.save() + + def test_default_ordering(self): + """ + The default ordering should be by name, as specified in the inner Meta + class. + """ + ma = ModelAdmin(Band, None) + names = [b.name for b in ma.queryset(None)] + self.assertEqual([u'Aerosmith', u'Radiohead', u'Van Halen'], names) + + def test_specified_ordering(self): + """ + Let's use a custom ModelAdmin that changes the ordering, and make sure + it actually changes. + """ + class BandAdmin(ModelAdmin): + ordering = ('rank',) # default ordering is ('name',) + ma = BandAdmin(Band, None) + names = [b.name for b in ma.queryset(None)] + self.assertEqual([u'Radiohead', u'Van Halen', u'Aerosmith'], names) diff --git a/parts/django/tests/regressiontests/admin_registration/__init__.py b/parts/django/tests/regressiontests/admin_registration/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_registration/__init__.py diff --git a/parts/django/tests/regressiontests/admin_registration/models.py b/parts/django/tests/regressiontests/admin_registration/models.py new file mode 100644 index 0000000..4a2d4e9 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_registration/models.py @@ -0,0 +1,11 @@ +""" +Tests for various ways of registering models with the admin site. +""" + +from django.db import models + +class Person(models.Model): + name = models.CharField(max_length=200) + +class Place(models.Model): + name = models.CharField(max_length=200) diff --git a/parts/django/tests/regressiontests/admin_registration/tests.py b/parts/django/tests/regressiontests/admin_registration/tests.py new file mode 100644 index 0000000..e2a5d7e --- /dev/null +++ b/parts/django/tests/regressiontests/admin_registration/tests.py @@ -0,0 +1,54 @@ +from django.test import TestCase + +from django.contrib import admin + +from models import Person, Place + +class NameAdmin(admin.ModelAdmin): + list_display = ['name'] + save_on_top = True + +class TestRegistration(TestCase): + def setUp(self): + self.site = admin.AdminSite() + + def test_bare_registration(self): + self.site.register(Person) + self.assertTrue( + isinstance(self.site._registry[Person], admin.options.ModelAdmin) + ) + + def test_registration_with_model_admin(self): + self.site.register(Person, NameAdmin) + self.assertTrue( + isinstance(self.site._registry[Person], NameAdmin) + ) + + def test_prevent_double_registration(self): + self.site.register(Person) + self.assertRaises(admin.sites.AlreadyRegistered, + self.site.register, + Person) + + def test_registration_with_star_star_options(self): + self.site.register(Person, search_fields=['name']) + self.assertEqual(self.site._registry[Person].search_fields, ['name']) + + def test_star_star_overrides(self): + self.site.register(Person, NameAdmin, + search_fields=["name"], list_display=['__str__']) + self.assertEqual(self.site._registry[Person].search_fields, ['name']) + self.assertEqual(self.site._registry[Person].list_display, + ['action_checkbox', '__str__']) + self.assertTrue(self.site._registry[Person].save_on_top) + + def test_iterable_registration(self): + self.site.register([Person, Place], search_fields=['name']) + self.assertTrue( + isinstance(self.site._registry[Person], admin.options.ModelAdmin) + ) + self.assertEqual(self.site._registry[Person].search_fields, ['name']) + self.assertTrue( + isinstance(self.site._registry[Place], admin.options.ModelAdmin) + ) + self.assertEqual(self.site._registry[Place].search_fields, ['name']) diff --git a/parts/django/tests/regressiontests/admin_scripts/__init__.py b/parts/django/tests/regressiontests/admin_scripts/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/__init__.py diff --git a/parts/django/tests/regressiontests/admin_scripts/app_with_import/__init__.py b/parts/django/tests/regressiontests/admin_scripts/app_with_import/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/app_with_import/__init__.py diff --git a/parts/django/tests/regressiontests/admin_scripts/app_with_import/models.py b/parts/django/tests/regressiontests/admin_scripts/app_with_import/models.py new file mode 100644 index 0000000..06ca0e8 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/app_with_import/models.py @@ -0,0 +1,7 @@ +from django.contrib.comments.models import Comment +from django.db import models + +# Regression for #13368. This is an example of a model +# that imports a class that has an abstract base class. +class CommentScore(models.Model): + comment = models.OneToOneField(Comment, primary_key=True) diff --git a/parts/django/tests/regressiontests/admin_scripts/broken_app/__init__.py b/parts/django/tests/regressiontests/admin_scripts/broken_app/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/broken_app/__init__.py diff --git a/parts/django/tests/regressiontests/admin_scripts/broken_app/models.py b/parts/django/tests/regressiontests/admin_scripts/broken_app/models.py new file mode 100644 index 0000000..f37f1ef --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/broken_app/models.py @@ -0,0 +1 @@ +from django.db import modelz diff --git a/parts/django/tests/regressiontests/admin_scripts/complex_app/__init__.py b/parts/django/tests/regressiontests/admin_scripts/complex_app/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/complex_app/__init__.py diff --git a/parts/django/tests/regressiontests/admin_scripts/complex_app/admin/__init__.py b/parts/django/tests/regressiontests/admin_scripts/complex_app/admin/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/complex_app/admin/__init__.py diff --git a/parts/django/tests/regressiontests/admin_scripts/complex_app/admin/foo.py b/parts/django/tests/regressiontests/admin_scripts/complex_app/admin/foo.py new file mode 100644 index 0000000..1b933d0 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/complex_app/admin/foo.py @@ -0,0 +1,3 @@ +from django.contrib import admin +from admin_scripts.complex_app.models.foo import Foo +admin.site.register(Foo) diff --git a/parts/django/tests/regressiontests/admin_scripts/complex_app/models/__init__.py b/parts/django/tests/regressiontests/admin_scripts/complex_app/models/__init__.py new file mode 100644 index 0000000..a4d6d95 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/complex_app/models/__init__.py @@ -0,0 +1,4 @@ +from admin_scripts.complex_app.models.bar import Bar +from admin_scripts.complex_app.models.foo import Foo + +__all__ = ['Foo', 'Bar'] diff --git a/parts/django/tests/regressiontests/admin_scripts/complex_app/models/bar.py b/parts/django/tests/regressiontests/admin_scripts/complex_app/models/bar.py new file mode 100644 index 0000000..a5d8853 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/complex_app/models/bar.py @@ -0,0 +1,7 @@ +from django.db import models + +from admin_scripts.complex_app.admin import foo +class Bar(models.Model): + name = models.CharField(max_length=5) + class Meta: + app_label = 'complex_app' diff --git a/parts/django/tests/regressiontests/admin_scripts/complex_app/models/foo.py b/parts/django/tests/regressiontests/admin_scripts/complex_app/models/foo.py new file mode 100644 index 0000000..70c285e --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/complex_app/models/foo.py @@ -0,0 +1,6 @@ +from django.db import models + +class Foo(models.Model): + name = models.CharField(max_length=5) + class Meta: + app_label = 'complex_app' diff --git a/parts/django/tests/regressiontests/admin_scripts/management/__init__.py b/parts/django/tests/regressiontests/admin_scripts/management/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/management/__init__.py diff --git a/parts/django/tests/regressiontests/admin_scripts/management/commands/__init__.py b/parts/django/tests/regressiontests/admin_scripts/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/management/commands/__init__.py diff --git a/parts/django/tests/regressiontests/admin_scripts/management/commands/app_command.py b/parts/django/tests/regressiontests/admin_scripts/management/commands/app_command.py new file mode 100644 index 0000000..f72e912 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/management/commands/app_command.py @@ -0,0 +1,10 @@ +from django.core.management.base import AppCommand + +class Command(AppCommand): + help = 'Test Application-based commands' + requires_model_validation = False + args = '[appname ...]' + + def handle_app(self, app, **options): + print 'EXECUTE:AppCommand app=%s, options=%s' % (app, sorted(options.items())) + diff --git a/parts/django/tests/regressiontests/admin_scripts/management/commands/base_command.py b/parts/django/tests/regressiontests/admin_scripts/management/commands/base_command.py new file mode 100644 index 0000000..438f703 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/management/commands/base_command.py @@ -0,0 +1,15 @@ +from django.core.management.base import BaseCommand +from optparse import make_option + +class Command(BaseCommand): + option_list = BaseCommand.option_list + ( + make_option('--option_a','-a', action='store', dest='option_a', default='1'), + make_option('--option_b','-b', action='store', dest='option_b', default='2'), + make_option('--option_c','-c', action='store', dest='option_c', default='3'), + ) + help = 'Test basic commands' + requires_model_validation = False + args = '[labels ...]' + + def handle(self, *labels, **options): + print 'EXECUTE:BaseCommand labels=%s, options=%s' % (labels, sorted(options.items())) diff --git a/parts/django/tests/regressiontests/admin_scripts/management/commands/label_command.py b/parts/django/tests/regressiontests/admin_scripts/management/commands/label_command.py new file mode 100644 index 0000000..2b735c8 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/management/commands/label_command.py @@ -0,0 +1,9 @@ +from django.core.management.base import LabelCommand + +class Command(LabelCommand): + help = "Test Label-based commands" + requires_model_validation = False + args = '<label>' + + def handle_label(self, label, **options): + print 'EXECUTE:LabelCommand label=%s, options=%s' % (label, sorted(options.items())) diff --git a/parts/django/tests/regressiontests/admin_scripts/management/commands/noargs_command.py b/parts/django/tests/regressiontests/admin_scripts/management/commands/noargs_command.py new file mode 100644 index 0000000..683eb7a --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/management/commands/noargs_command.py @@ -0,0 +1,9 @@ +from django.core.management.base import NoArgsCommand + +class Command(NoArgsCommand): + help = "Test No-args commands" + requires_model_validation = False + + + def handle_noargs(self, **options): + print 'EXECUTE:NoArgsCommand options=%s' % sorted(options.items()) diff --git a/parts/django/tests/regressiontests/admin_scripts/models.py b/parts/django/tests/regressiontests/admin_scripts/models.py new file mode 100644 index 0000000..96a40d2 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/models.py @@ -0,0 +1,12 @@ +from django.db import models + +class Article(models.Model): + headline = models.CharField(max_length=100, default='Default headline') + pub_date = models.DateTimeField() + + def __unicode__(self): + return self.headline + + class Meta: + ordering = ('-pub_date', 'headline') +
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/admin_scripts/simple_app/__init__.py b/parts/django/tests/regressiontests/admin_scripts/simple_app/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/simple_app/__init__.py diff --git a/parts/django/tests/regressiontests/admin_scripts/simple_app/models.py b/parts/django/tests/regressiontests/admin_scripts/simple_app/models.py new file mode 100644 index 0000000..65b30ed --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/simple_app/models.py @@ -0,0 +1 @@ +from admin_scripts.complex_app.models.bar import Bar diff --git a/parts/django/tests/regressiontests/admin_scripts/tests.py b/parts/django/tests/regressiontests/admin_scripts/tests.py new file mode 100644 index 0000000..2a7f302 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_scripts/tests.py @@ -0,0 +1,1199 @@ +""" +A series of tests to establish that the command-line managment tools work as +advertised - especially with regards to the handling of the DJANGO_SETTINGS_MODULE +and default settings.py files. +""" +import os +import unittest +import shutil +import sys +import re + +from django import conf, bin, get_version +from django.conf import settings + + +class AdminScriptTestCase(unittest.TestCase): + def write_settings(self, filename, apps=None, is_dir=False, sdict=None): + test_dir = os.path.dirname(os.path.dirname(__file__)) + if is_dir: + settings_dir = os.path.join(test_dir,filename) + os.mkdir(settings_dir) + settings_file = open(os.path.join(settings_dir,'__init__.py'), 'w') + else: + settings_file = open(os.path.join(test_dir, filename), 'w') + settings_file.write('# Settings file automatically generated by regressiontests.admin_scripts test case\n') + exports = [ + 'DATABASES', + 'ROOT_URLCONF' + ] + for s in exports: + if hasattr(settings, s): + o = getattr(settings, s) + if not isinstance(o, dict): + o = "'%s'" % o + settings_file.write("%s = %s\n" % (s, o)) + + if apps is None: + apps = ['django.contrib.auth', 'django.contrib.contenttypes', 'admin_scripts'] + + if apps: + settings_file.write("INSTALLED_APPS = %s\n" % apps) + + if sdict: + for k, v in sdict.items(): + settings_file.write("%s = %s\n" % (k, v)) + + settings_file.close() + + def remove_settings(self, filename, is_dir=False): + test_dir = os.path.dirname(os.path.dirname(__file__)) + full_name = os.path.join(test_dir, filename) + if is_dir: + shutil.rmtree(full_name) + else: + os.remove(full_name) + + # Also try to remove the compiled file; if it exists, it could + # mess up later tests that depend upon the .py file not existing + try: + if sys.platform.startswith('java'): + # Jython produces module$py.class files + os.remove(re.sub(r'\.py$', '$py.class', full_name)) + else: + # CPython produces module.pyc files + os.remove(full_name + 'c') + except OSError: + pass + + def _ext_backend_paths(self): + """ + Returns the paths for any external backend packages. + """ + paths = [] + first_package_re = re.compile(r'(^[^\.]+)\.') + for backend in settings.DATABASES.values(): + result = first_package_re.findall(backend['ENGINE']) + if result and result != 'django': + backend_pkg = __import__(result[0]) + backend_dir = os.path.dirname(backend_pkg.__file__) + paths.append(os.path.dirname(backend_dir)) + return paths + + def run_test(self, script, args, settings_file=None, apps=None): + test_dir = os.path.dirname(os.path.dirname(__file__)) + project_dir = os.path.dirname(test_dir) + base_dir = os.path.dirname(project_dir) + ext_backend_base_dirs = self._ext_backend_paths() + + # Remember the old environment + old_django_settings_module = os.environ.get('DJANGO_SETTINGS_MODULE', None) + if sys.platform.startswith('java'): + python_path_var_name = 'JYTHONPATH' + else: + python_path_var_name = 'PYTHONPATH' + + old_python_path = os.environ.get(python_path_var_name, None) + old_cwd = os.getcwd() + + # Set the test environment + if settings_file: + os.environ['DJANGO_SETTINGS_MODULE'] = settings_file + elif 'DJANGO_SETTINGS_MODULE' in os.environ: + del os.environ['DJANGO_SETTINGS_MODULE'] + python_path = [test_dir, base_dir] + python_path.extend(ext_backend_base_dirs) + os.environ[python_path_var_name] = os.pathsep.join(python_path) + + # Build the command line + executable = sys.executable + arg_string = ' '.join(['%s' % arg for arg in args]) + if ' ' in executable: + cmd = '""%s" "%s" %s"' % (executable, script, arg_string) + else: + cmd = '%s "%s" %s' % (executable, script, arg_string) + + # Move to the test directory and run + os.chdir(test_dir) + try: + from subprocess import Popen, PIPE + p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) + stdin, stdout, stderr = (p.stdin, p.stdout, p.stderr) + p.wait() + except ImportError: + stdin, stdout, stderr = os.popen3(cmd) + out, err = stdout.read(), stderr.read() + + # Restore the old environment + if old_django_settings_module: + os.environ['DJANGO_SETTINGS_MODULE'] = old_django_settings_module + if old_python_path: + os.environ[python_path_var_name] = old_python_path + # Move back to the old working directory + os.chdir(old_cwd) + + return out, err + + def run_django_admin(self, args, settings_file=None): + 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): + conf_dir = os.path.dirname(conf.__file__) + template_manage_py = os.path.join(conf_dir, 'project_template', 'manage.py') + + test_dir = os.path.dirname(os.path.dirname(__file__)) + test_manage_py = os.path.join(test_dir, 'manage.py') + shutil.copyfile(template_manage_py, test_manage_py) + + stdout, stderr = self.run_test('./manage.py', args, settings_file) + + # Cleanup - remove the generated manage.py script + os.remove(test_manage_py) + + return stdout, stderr + + def assertNoOutput(self, stream): + "Utility assertion: assert that the given stream is empty" + self.assertEquals(len(stream), 0, "Stream should be empty: actually contains '%s'" % stream) + def assertOutput(self, stream, msg): + "Utility assertion: assert that the given message exists in the output" + self.assertTrue(msg in stream, "'%s' does not match actual output text '%s'" % (msg, stream)) + +########################################################################## +# DJANGO ADMIN TESTS +# This first series of test classes checks the environment processing +# of the django-admin.py script +########################################################################## + + +class DjangoAdminNoSettings(AdminScriptTestCase): + "A series of tests for django-admin.py when there is no settings.py file." + + def test_builtin_command(self): + "no settings: django-admin builtin commands fail with an import error when no settings provided" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + + def test_builtin_with_bad_settings(self): + "no settings: django-admin builtin commands fail if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_builtin_with_bad_environment(self): + "no settings: django-admin builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'bad_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + +class DjangoAdminDefaultSettings(AdminScriptTestCase): + """A series of tests for django-admin.py when using a settings.py file that + contains the test application. + """ + def setUp(self): + self.write_settings('settings.py') + + def tearDown(self): + self.remove_settings('settings.py') + + def test_builtin_command(self): + "default: django-admin builtin commands fail with an import error when no settings provided" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + + def test_builtin_with_settings(self): + "default: django-admin builtin commands succeed if settings are provided as argument" + args = ['sqlall','--settings=settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_environment(self): + "default: django-admin builtin commands succeed if settings are provided in the environment" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'settings') + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_bad_settings(self): + "default: django-admin builtin commands fail if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_builtin_with_bad_environment(self): + "default: django-admin builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'bad_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_custom_command(self): + "default: django-admin can't execute user commands if it isn't provided settings" + args = ['noargs_command'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + + def test_custom_command_with_settings(self): + "default: django-admin can execute user commands if settings are provided as argument" + args = ['noargs_command', '--settings=settings'] + out, err = self.run_django_admin(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + def test_custom_command_with_environment(self): + "default: django-admin can execute user commands if settings are provided in environment" + args = ['noargs_command'] + out, err = self.run_django_admin(args,'settings') + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + +class DjangoAdminFullPathDefaultSettings(AdminScriptTestCase): + """A series of tests for django-admin.py when using a settings.py file that + contains the test application specified using a full path. + """ + def setUp(self): + self.write_settings('settings.py', ['django.contrib.auth', 'django.contrib.contenttypes', 'regressiontests.admin_scripts']) + + def tearDown(self): + self.remove_settings('settings.py') + + def test_builtin_command(self): + "fulldefault: django-admin builtin commands fail with an import error when no settings provided" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + + def test_builtin_with_settings(self): + "fulldefault: django-admin builtin commands succeed if a settings file is provided" + args = ['sqlall','--settings=settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_environment(self): + "fulldefault: django-admin builtin commands succeed if the environment contains settings" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'settings') + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_bad_settings(self): + "fulldefault: django-admin builtin commands fail if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_builtin_with_bad_environment(self): + "fulldefault: django-admin builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'bad_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_custom_command(self): + "fulldefault: django-admin can't execute user commands unless settings are provided" + args = ['noargs_command'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + + def test_custom_command_with_settings(self): + "fulldefault: django-admin can execute user commands if settings are provided as argument" + args = ['noargs_command', '--settings=settings'] + out, err = self.run_django_admin(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + def test_custom_command_with_environment(self): + "fulldefault: django-admin can execute user commands if settings are provided in environment" + args = ['noargs_command'] + out, err = self.run_django_admin(args,'settings') + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + +class DjangoAdminMinimalSettings(AdminScriptTestCase): + """A series of tests for django-admin.py when using a settings.py file that + doesn't contain the test application. + """ + def setUp(self): + self.write_settings('settings.py', apps=['django.contrib.auth','django.contrib.contenttypes']) + + def tearDown(self): + self.remove_settings('settings.py') + + def test_builtin_command(self): + "minimal: django-admin builtin commands fail with an import error when no settings provided" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + + def test_builtin_with_settings(self): + "minimal: django-admin builtin commands fail if settings are provided as argument" + args = ['sqlall','--settings=settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, 'App with label admin_scripts could not be found') + + def test_builtin_with_environment(self): + "minimal: django-admin builtin commands fail if settings are provided in the environment" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'settings') + self.assertNoOutput(out) + self.assertOutput(err, 'App with label admin_scripts could not be found') + + def test_builtin_with_bad_settings(self): + "minimal: django-admin builtin commands fail if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_builtin_with_bad_environment(self): + "minimal: django-admin builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'bad_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_custom_command(self): + "minimal: django-admin can't execute user commands unless settings are provided" + args = ['noargs_command'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + + def test_custom_command_with_settings(self): + "minimal: django-admin can't execute user commands, even if settings are provided as argument" + args = ['noargs_command', '--settings=settings'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + + def test_custom_command_with_environment(self): + "minimal: django-admin can't execute user commands, even if settings are provided in environment" + args = ['noargs_command'] + out, err = self.run_django_admin(args,'settings') + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + +class DjangoAdminAlternateSettings(AdminScriptTestCase): + """A series of tests for django-admin.py when using a settings file + with a name other than 'settings.py'. + """ + def setUp(self): + self.write_settings('alternate_settings.py') + + def tearDown(self): + self.remove_settings('alternate_settings.py') + + def test_builtin_command(self): + "alternate: django-admin builtin commands fail with an import error when no settings provided" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + + def test_builtin_with_settings(self): + "alternate: django-admin builtin commands succeed if settings are provided as argument" + args = ['sqlall','--settings=alternate_settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_environment(self): + "alternate: django-admin builtin commands succeed if settings are provided in the environment" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'alternate_settings') + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_bad_settings(self): + "alternate: django-admin builtin commands fail if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_builtin_with_bad_environment(self): + "alternate: django-admin builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'bad_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_custom_command(self): + "alternate: django-admin can't execute user commands unless settings are provided" + args = ['noargs_command'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + + def test_custom_command_with_settings(self): + "alternate: django-admin can execute user commands if settings are provided as argument" + args = ['noargs_command', '--settings=alternate_settings'] + out, err = self.run_django_admin(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + def test_custom_command_with_environment(self): + "alternate: django-admin can execute user commands if settings are provided in environment" + args = ['noargs_command'] + out, err = self.run_django_admin(args,'alternate_settings') + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + +class DjangoAdminMultipleSettings(AdminScriptTestCase): + """A series of tests for django-admin.py when multiple settings files + (including the default 'settings.py') are available. The default settings + file is insufficient for performing the operations described, so the + alternate settings must be used by the running script. + """ + def setUp(self): + self.write_settings('settings.py', apps=['django.contrib.auth','django.contrib.contenttypes']) + self.write_settings('alternate_settings.py') + + def tearDown(self): + self.remove_settings('settings.py') + self.remove_settings('alternate_settings.py') + + def test_builtin_command(self): + "alternate: django-admin builtin commands fail with an import error when no settings provided" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + + def test_builtin_with_settings(self): + "alternate: django-admin builtin commands succeed if settings are provided as argument" + args = ['sqlall','--settings=alternate_settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_environment(self): + "alternate: django-admin builtin commands succeed if settings are provided in the environment" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'alternate_settings') + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_bad_settings(self): + "alternate: django-admin builtin commands fail if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_builtin_with_bad_environment(self): + "alternate: django-admin builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'bad_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_custom_command(self): + "alternate: django-admin can't execute user commands unless settings are provided" + args = ['noargs_command'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + + def test_custom_command_with_settings(self): + "alternate: django-admin can't execute user commands, even if settings are provided as argument" + args = ['noargs_command', '--settings=alternate_settings'] + out, err = self.run_django_admin(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + def test_custom_command_with_environment(self): + "alternate: django-admin can't execute user commands, even if settings are provided in environment" + args = ['noargs_command'] + out, err = self.run_django_admin(args,'alternate_settings') + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + +class DjangoAdminSettingsDirectory(AdminScriptTestCase): + """ + A series of tests for django-admin.py when the settings file is in a + directory. (see #9751). + """ + + def setUp(self): + self.write_settings('settings', is_dir=True) + + def tearDown(self): + self.remove_settings('settings', is_dir=True) + + def test_setup_environ(self): + "directory: startapp creates the correct directory" + test_dir = os.path.dirname(os.path.dirname(__file__)) + args = ['startapp','settings_test'] + out, err = self.run_django_admin(args,'settings') + self.assertNoOutput(err) + self.assert_(os.path.exists(os.path.join(test_dir, 'settings_test'))) + shutil.rmtree(os.path.join(test_dir, 'settings_test')) + + def test_builtin_command(self): + "directory: django-admin builtin commands fail with an import error when no settings provided" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + + def test_builtin_with_bad_settings(self): + "directory: django-admin builtin commands fail if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_builtin_with_bad_environment(self): + "directory: django-admin builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'bad_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_custom_command(self): + "directory: django-admin can't execute user commands unless settings are provided" + args = ['noargs_command'] + out, err = self.run_django_admin(args) + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + + def test_builtin_with_settings(self): + "directory: django-admin builtin commands succeed if settings are provided as argument" + args = ['sqlall','--settings=settings', 'admin_scripts'] + out, err = self.run_django_admin(args) + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_environment(self): + "directory: django-admin builtin commands succeed if settings are provided in the environment" + args = ['sqlall','admin_scripts'] + out, err = self.run_django_admin(args,'settings') + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + +########################################################################## +# MANAGE.PY TESTS +# This next series of test classes checks the environment processing +# of the generated manage.py script +########################################################################## + +class ManageNoSettings(AdminScriptTestCase): + "A series of tests for manage.py when there is no settings.py file." + + def test_builtin_command(self): + "no settings: manage.py builtin commands fail with an import error when no settings provided" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") + + def test_builtin_with_bad_settings(self): + "no settings: manage.py builtin commands fail if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") + + def test_builtin_with_bad_environment(self): + "no settings: manage.py builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args,'bad_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") + + +class ManageDefaultSettings(AdminScriptTestCase): + """A series of tests for manage.py when using a settings.py file that + contains the test application. + """ + def setUp(self): + self.write_settings('settings.py') + + def tearDown(self): + self.remove_settings('settings.py') + + def test_builtin_command(self): + "default: manage.py builtin commands succeed when default settings are appropriate" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_settings(self): + "default: manage.py builtin commands succeed if settings are provided as argument" + args = ['sqlall','--settings=settings', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_environment(self): + "default: manage.py builtin commands succeed if settings are provided in the environment" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args,'settings') + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_bad_settings(self): + "default: manage.py builtin commands succeed if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_builtin_with_bad_environment(self): + "default: manage.py builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args,'bad_settings') + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_custom_command(self): + "default: manage.py can execute user commands when default settings are appropriate" + args = ['noargs_command'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + def test_custom_command_with_settings(self): + "default: manage.py can execute user commands when settings are provided as argument" + args = ['noargs_command', '--settings=settings'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + def test_custom_command_with_environment(self): + "default: manage.py can execute user commands when settings are provided in environment" + args = ['noargs_command'] + out, err = self.run_manage(args,'settings') + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + +class ManageFullPathDefaultSettings(AdminScriptTestCase): + """A series of tests for manage.py when using a settings.py file that + contains the test application specified using a full path. + """ + def setUp(self): + self.write_settings('settings.py', ['django.contrib.auth', 'django.contrib.contenttypes', 'regressiontests.admin_scripts']) + + def tearDown(self): + self.remove_settings('settings.py') + + def test_builtin_command(self): + "fulldefault: manage.py builtin commands succeed when default settings are appropriate" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_settings(self): + "fulldefault: manage.py builtin commands succeed if settings are provided as argument" + args = ['sqlall','--settings=settings', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_environment(self): + "fulldefault: manage.py builtin commands succeed if settings are provided in the environment" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args,'settings') + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_bad_settings(self): + "fulldefault: manage.py builtin commands succeed if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_builtin_with_bad_environment(self): + "fulldefault: manage.py builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args,'bad_settings') + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_custom_command(self): + "fulldefault: manage.py can execute user commands when default settings are appropriate" + args = ['noargs_command'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + def test_custom_command_with_settings(self): + "fulldefault: manage.py can execute user commands when settings are provided as argument" + args = ['noargs_command', '--settings=settings'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + def test_custom_command_with_environment(self): + "fulldefault: manage.py can execute user commands when settings are provided in environment" + args = ['noargs_command'] + out, err = self.run_manage(args,'settings') + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + +class ManageMinimalSettings(AdminScriptTestCase): + """A series of tests for manage.py when using a settings.py file that + doesn't contain the test application. + """ + def setUp(self): + self.write_settings('settings.py', apps=['django.contrib.auth','django.contrib.contenttypes']) + + def tearDown(self): + self.remove_settings('settings.py') + + def test_builtin_command(self): + "minimal: manage.py builtin commands fail with an import error when no settings provided" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, 'App with label admin_scripts could not be found') + + def test_builtin_with_settings(self): + "minimal: manage.py builtin commands fail if settings are provided as argument" + args = ['sqlall','--settings=settings', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, 'App with label admin_scripts could not be found') + + def test_builtin_with_environment(self): + "minimal: manage.py builtin commands fail if settings are provided in the environment" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args,'settings') + self.assertNoOutput(out) + self.assertOutput(err, 'App with label admin_scripts could not be found') + + def test_builtin_with_bad_settings(self): + "minimal: manage.py builtin commands fail if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_builtin_with_bad_environment(self): + "minimal: manage.py builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args,'bad_settings') + self.assertNoOutput(out) + self.assertOutput(err, 'App with label admin_scripts could not be found') + + def test_custom_command(self): + "minimal: manage.py can't execute user commands without appropriate settings" + args = ['noargs_command'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + + def test_custom_command_with_settings(self): + "minimal: manage.py can't execute user commands, even if settings are provided as argument" + args = ['noargs_command', '--settings=settings'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + + def test_custom_command_with_environment(self): + "minimal: manage.py can't execute user commands, even if settings are provided in environment" + args = ['noargs_command'] + out, err = self.run_manage(args,'settings') + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + +class ManageAlternateSettings(AdminScriptTestCase): + """A series of tests for manage.py when using a settings file + with a name other than 'settings.py'. + """ + def setUp(self): + self.write_settings('alternate_settings.py') + + def tearDown(self): + self.remove_settings('alternate_settings.py') + + def test_builtin_command(self): + "alternate: manage.py builtin commands fail with an import error when no default settings provided" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") + + def test_builtin_with_settings(self): + "alternate: manage.py builtin commands fail if settings are provided as argument but no defaults" + args = ['sqlall','--settings=alternate_settings', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") + + def test_builtin_with_environment(self): + "alternate: manage.py builtin commands fail if settings are provided in the environment but no defaults" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args,'alternate_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") + + def test_builtin_with_bad_settings(self): + "alternate: manage.py builtin commands fail if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") + + def test_builtin_with_bad_environment(self): + "alternate: manage.py builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args,'bad_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") + + def test_custom_command(self): + "alternate: manage.py can't execute user commands" + args = ['noargs_command'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") + + def test_custom_command_with_settings(self): + "alternate: manage.py can't execute user commands, even if settings are provided as argument" + args = ['noargs_command', '--settings=alternate_settings'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") + + def test_custom_command_with_environment(self): + "alternate: manage.py can't execute user commands, even if settings are provided in environment" + args = ['noargs_command'] + out, err = self.run_manage(args,'alternate_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") + + +class ManageMultipleSettings(AdminScriptTestCase): + """A series of tests for manage.py when multiple settings files + (including the default 'settings.py') are available. The default settings + file is insufficient for performing the operations described, so the + alternate settings must be used by the running script. + """ + def setUp(self): + self.write_settings('settings.py', apps=['django.contrib.auth','django.contrib.contenttypes']) + self.write_settings('alternate_settings.py') + + def tearDown(self): + self.remove_settings('settings.py') + self.remove_settings('alternate_settings.py') + + def test_builtin_command(self): + "multiple: manage.py builtin commands fail with an import error when no settings provided" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, 'App with label admin_scripts could not be found.') + + def test_builtin_with_settings(self): + "multiple: manage.py builtin commands succeed if settings are provided as argument" + args = ['sqlall','--settings=alternate_settings', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, 'CREATE TABLE') + + def test_builtin_with_environment(self): + "multiple: manage.py builtin commands fail if settings are provided in the environment" + # FIXME: This doesn't seem to be the correct output. + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args,'alternate_settings') + self.assertNoOutput(out) + self.assertOutput(err, 'App with label admin_scripts could not be found.') + + def test_builtin_with_bad_settings(self): + "multiple: manage.py builtin commands fail if settings file (from argument) doesn't exist" + args = ['sqlall','--settings=bad_settings', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Could not import settings 'bad_settings'") + + def test_builtin_with_bad_environment(self): + "multiple: manage.py builtin commands fail if settings file (from environment) doesn't exist" + args = ['sqlall','admin_scripts'] + out, err = self.run_manage(args,'bad_settings') + self.assertNoOutput(out) + self.assertOutput(err, "App with label admin_scripts could not be found") + + def test_custom_command(self): + "multiple: manage.py can't execute user commands using default settings" + args = ['noargs_command'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + + def test_custom_command_with_settings(self): + "multiple: manage.py can execute user commands if settings are provided as argument" + args = ['noargs_command', '--settings=alternate_settings'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand") + + def test_custom_command_with_environment(self): + "multiple: manage.py can execute user commands if settings are provided in environment" + args = ['noargs_command'] + out, err = self.run_manage(args,'alternate_settings') + self.assertNoOutput(out) + self.assertOutput(err, "Unknown command: 'noargs_command'") + + +class ManageValidate(AdminScriptTestCase): + def tearDown(self): + self.remove_settings('settings.py') + + def test_nonexistent_app(self): + "manage.py validate reports an error on a non-existent app in INSTALLED_APPS" + self.write_settings('settings.py', apps=['admin_scriptz.broken_app'], sdict={'USE_I18N': False}) + args = ['validate'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, 'No module named admin_scriptz') + + def test_broken_app(self): + "manage.py validate reports an ImportError if an app's models.py raises one on import" + self.write_settings('settings.py', apps=['admin_scripts.broken_app']) + args = ['validate'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, 'ImportError') + + def test_complex_app(self): + "manage.py validate does not raise an ImportError validating a complex app with nested calls to load_app" + self.write_settings('settings.py', + apps=['admin_scripts.complex_app', 'admin_scripts.simple_app'], + sdict={'DEBUG': True}) + args = ['validate'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, '0 errors found') + + def test_app_with_import(self): + "manage.py validate does not raise errors when an app imports a base class that itself has an abstract base" + self.write_settings('settings.py', + apps=['admin_scripts.app_with_import', 'django.contrib.comments'], + sdict={'DEBUG': True}) + args = ['validate'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, '0 errors found') + +########################################################################## +# COMMAND PROCESSING TESTS +# Check that user-space commands are correctly handled - in particular, +# that arguments to the commands are correctly parsed and processed. +########################################################################## + +class CommandTypes(AdminScriptTestCase): + "Tests for the various types of base command types that can be defined." + def setUp(self): + self.write_settings('settings.py') + + def tearDown(self): + self.remove_settings('settings.py') + + def test_version(self): + "--version is handled as a special case" + args = ['--version'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + # Only check the first part of the version number + self.assertOutput(out, get_version().split('-')[0]) + + def test_help(self): + "--help is handled as a special case" + args = ['--help'] + out, err = self.run_manage(args) + if sys.version_info < (2, 5): + self.assertOutput(out, "usage: manage.py subcommand [options] [args]") + else: + self.assertOutput(out, "Usage: manage.py subcommand [options] [args]") + self.assertOutput(err, "Type 'manage.py help <subcommand>' for help on a specific subcommand.") + + def test_specific_help(self): + "--help can be used on a specific command" + args = ['sqlall','--help'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "Prints the CREATE TABLE, custom SQL and CREATE INDEX SQL statements for the given model module name(s).") + + def test_base_command(self): + "User BaseCommands can execute when a label is provided" + args = ['base_command','testlabel'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + + def test_base_command_no_label(self): + "User BaseCommands can execute when no labels are provided" + args = ['base_command'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=(), options=[('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + + def test_base_command_multiple_label(self): + "User BaseCommands can execute when no labels are provided" + args = ['base_command','testlabel','anotherlabel'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel', 'anotherlabel'), options=[('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + + def test_base_command_with_option(self): + "User BaseCommands can execute with options when a label is provided" + args = ['base_command','testlabel','--option_a=x'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + + def test_base_command_with_options(self): + "User BaseCommands can execute with multiple options when a label is provided" + args = ['base_command','testlabel','-a','x','--option_b=y'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + + def test_noargs(self): + "NoArg Commands can be executed" + args = ['noargs_command'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + + def test_noargs_with_args(self): + "NoArg Commands raise an error if an argument is provided" + args = ['noargs_command','argument'] + out, err = self.run_manage(args) + self.assertOutput(err, "Error: Command doesn't accept any arguments") + + def test_app_command(self): + "User AppCommands can execute when a single app name is provided" + args = ['app_command', 'auth'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:AppCommand app=<module 'django.contrib.auth.models'") + self.assertOutput(out, os.sep.join(['django','contrib','auth','models.py'])) + self.assertOutput(out, "'>, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + + def test_app_command_no_apps(self): + "User AppCommands raise an error when no app name is provided" + args = ['app_command'] + out, err = self.run_manage(args) + self.assertOutput(err, 'Error: Enter at least one appname.') + + def test_app_command_multiple_apps(self): + "User AppCommands raise an error when multiple app names are provided" + args = ['app_command','auth','contenttypes'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:AppCommand app=<module 'django.contrib.auth.models'") + self.assertOutput(out, os.sep.join(['django','contrib','auth','models.py'])) + self.assertOutput(out, "'>, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:AppCommand app=<module 'django.contrib.contenttypes.models'") + self.assertOutput(out, os.sep.join(['django','contrib','contenttypes','models.py'])) + self.assertOutput(out, "'>, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + + def test_app_command_invalid_appname(self): + "User AppCommands can execute when a single app name is provided" + args = ['app_command', 'NOT_AN_APP'] + out, err = self.run_manage(args) + self.assertOutput(err, "App with label NOT_AN_APP could not be found") + + def test_app_command_some_invalid_appnames(self): + "User AppCommands can execute when some of the provided app names are invalid" + args = ['app_command', 'auth', 'NOT_AN_APP'] + out, err = self.run_manage(args) + self.assertOutput(err, "App with label NOT_AN_APP could not be found") + + def test_label_command(self): + "User LabelCommands can execute when a label is provided" + args = ['label_command','testlabel'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:LabelCommand label=testlabel, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + + def test_label_command_no_label(self): + "User LabelCommands raise an error if no label is provided" + args = ['label_command'] + out, err = self.run_manage(args) + self.assertOutput(err, 'Enter at least one label') + + def test_label_command_multiple_label(self): + "User LabelCommands are executed multiple times if multiple labels are provided" + args = ['label_command','testlabel','anotherlabel'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:LabelCommand label=testlabel, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:LabelCommand label=anotherlabel, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + +class ArgumentOrder(AdminScriptTestCase): + """Tests for 2-stage argument parsing scheme. + + django-admin command arguments are parsed in 2 parts; the core arguments + (--settings, --traceback and --pythonpath) are parsed using a Lax parser. + This Lax parser ignores any unknown options. Then the full settings are + passed to the command parser, which extracts commands of interest to the + individual command. + """ + def setUp(self): + self.write_settings('settings.py', apps=['django.contrib.auth','django.contrib.contenttypes']) + self.write_settings('alternate_settings.py') + + def tearDown(self): + self.remove_settings('settings.py') + self.remove_settings('alternate_settings.py') + + def test_setting_then_option(self): + "Options passed after settings are correctly handled" + args = ['base_command','testlabel','--settings=alternate_settings','--option_a=x'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") + + def test_setting_then_short_option(self): + "Short options passed after settings are correctly handled" + args = ['base_command','testlabel','--settings=alternate_settings','--option_a=x'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") + + def test_option_then_setting(self): + "Options passed before settings are correctly handled" + args = ['base_command','testlabel','--option_a=x','--settings=alternate_settings'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") + + def test_short_option_then_setting(self): + "Short options passed before settings are correctly handled" + args = ['base_command','testlabel','-a','x','--settings=alternate_settings'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") + + def test_option_then_setting_then_option(self): + "Options are correctly handled when they are passed before and after a setting" + args = ['base_command','testlabel','--option_a=x','--settings=alternate_settings','--option_b=y'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") diff --git a/parts/django/tests/regressiontests/admin_util/__init__.py b/parts/django/tests/regressiontests/admin_util/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_util/__init__.py diff --git a/parts/django/tests/regressiontests/admin_util/models.py b/parts/django/tests/regressiontests/admin_util/models.py new file mode 100644 index 0000000..3191a55 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_util/models.py @@ -0,0 +1,33 @@ +from django.db import models + +class Article(models.Model): + """ + A simple Article model for testing + """ + site = models.ForeignKey('sites.Site', related_name="admin_articles") + title = models.CharField(max_length=100) + title2 = models.CharField(max_length=100, verbose_name="another name") + created = models.DateTimeField() + + def test_from_model(self): + return "nothing" + + def test_from_model_with_override(self): + return "nothing" + test_from_model_with_override.short_description = "not What you Expect" + +class Count(models.Model): + num = models.PositiveSmallIntegerField() + +class Event(models.Model): + date = models.DateTimeField(auto_now_add=True) + +class Location(models.Model): + event = models.OneToOneField(Event, verbose_name='awesome event') + +class Guest(models.Model): + event = models.OneToOneField(Event) + name = models.CharField(max_length=255) + + class Meta: + verbose_name = "awesome guest" diff --git a/parts/django/tests/regressiontests/admin_util/tests.py b/parts/django/tests/regressiontests/admin_util/tests.py new file mode 100644 index 0000000..7476d10 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_util/tests.py @@ -0,0 +1,239 @@ +from datetime import datetime +import unittest + +from django.conf import settings +from django.db import models +from django.utils.formats import localize +from django.test import TestCase + +from django.contrib import admin +from django.contrib.admin.util import display_for_field, label_for_field, lookup_field +from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE +from django.contrib.sites.models import Site +from django.contrib.admin.util import NestedObjects + +from models import Article, Count, Event, Location + + +class NestedObjectsTests(TestCase): + """ + Tests for ``NestedObject`` utility collection. + + """ + def setUp(self): + self.n = NestedObjects() + self.objs = [Count.objects.create(num=i) for i in range(5)] + + def _check(self, target): + self.assertEquals(self.n.nested(lambda obj: obj.num), target) + + def _add(self, obj, parent=None): + # don't bother providing the extra args that NestedObjects ignores + self.n.add(None, None, obj, None, parent) + + def test_unrelated_roots(self): + self._add(self.objs[0]) + self._add(self.objs[1]) + self._add(self.objs[2], self.objs[1]) + + self._check([0, 1, [2]]) + + def test_siblings(self): + self._add(self.objs[0]) + self._add(self.objs[1], self.objs[0]) + self._add(self.objs[2], self.objs[0]) + + self._check([0, [1, 2]]) + + def test_duplicate_instances(self): + self._add(self.objs[0]) + self._add(self.objs[1]) + dupe = Count.objects.get(num=1) + self._add(dupe, self.objs[0]) + + self._check([0, 1]) + + def test_non_added_parent(self): + self._add(self.objs[0], self.objs[1]) + + self._check([0]) + + def test_cyclic(self): + self._add(self.objs[0], self.objs[2]) + self._add(self.objs[1], self.objs[0]) + self._add(self.objs[2], self.objs[1]) + self._add(self.objs[0], self.objs[2]) + + self._check([0, [1, [2]]]) + + +class UtilTests(unittest.TestCase): + def test_values_from_lookup_field(self): + """ + Regression test for #12654: lookup_field + """ + SITE_NAME = 'example.com' + TITLE_TEXT = 'Some title' + CREATED_DATE = datetime.min + ADMIN_METHOD = 'admin method' + SIMPLE_FUNCTION = 'function' + INSTANCE_ATTRIBUTE = 'attr' + + class MockModelAdmin(object): + def get_admin_value(self, obj): + return ADMIN_METHOD + + simple_function = lambda obj: SIMPLE_FUNCTION + + article = Article( + site=Site(domain=SITE_NAME), + title=TITLE_TEXT, + created=CREATED_DATE, + ) + article.non_field = INSTANCE_ATTRIBUTE + + verifications = ( + ('site', SITE_NAME), + ('created', localize(CREATED_DATE)), + ('title', TITLE_TEXT), + ('get_admin_value', ADMIN_METHOD), + (simple_function, SIMPLE_FUNCTION), + ('test_from_model', article.test_from_model()), + ('non_field', INSTANCE_ATTRIBUTE) + ) + + mock_admin = MockModelAdmin() + for name, value in verifications: + field, attr, resolved_value = lookup_field(name, article, mock_admin) + + if field is not None: + resolved_value = display_for_field(resolved_value, field) + + self.assertEqual(value, resolved_value) + + def test_null_display_for_field(self): + """ + Regression test for #12550: display_for_field should handle None + value. + """ + display_value = display_for_field(None, models.CharField()) + self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE) + + display_value = display_for_field(None, models.CharField( + choices=( + (None, "test_none"), + ) + )) + self.assertEqual(display_value, "test_none") + + display_value = display_for_field(None, models.DateField()) + self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE) + + display_value = display_for_field(None, models.TimeField()) + self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE) + + # Regression test for #13071: NullBooleanField has special + # handling. + display_value = display_for_field(None, models.NullBooleanField()) + expected = u'<img src="%simg/admin/icon-unknown.gif" alt="None" />' % settings.ADMIN_MEDIA_PREFIX + self.assertEqual(display_value, expected) + + display_value = display_for_field(None, models.DecimalField()) + self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE) + + display_value = display_for_field(None, models.FloatField()) + self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE) + + def test_label_for_field(self): + """ + Tests for label_for_field + """ + self.assertEquals( + label_for_field("title", Article), + "title" + ) + self.assertEquals( + label_for_field("title2", Article), + "another name" + ) + self.assertEquals( + label_for_field("title2", Article, return_attr=True), + ("another name", None) + ) + + self.assertEquals( + label_for_field("__unicode__", Article), + "article" + ) + self.assertEquals( + label_for_field("__str__", Article), + "article" + ) + + self.assertRaises( + AttributeError, + lambda: label_for_field("unknown", Article) + ) + + def test_callable(obj): + return "nothing" + self.assertEquals( + label_for_field(test_callable, Article), + "Test callable" + ) + self.assertEquals( + label_for_field(test_callable, Article, return_attr=True), + ("Test callable", test_callable) + ) + + self.assertEquals( + label_for_field("test_from_model", Article), + "Test from model" + ) + self.assertEquals( + label_for_field("test_from_model", Article, return_attr=True), + ("Test from model", Article.test_from_model) + ) + self.assertEquals( + label_for_field("test_from_model_with_override", Article), + "not What you Expect" + ) + + self.assertEquals( + label_for_field(lambda x: "nothing", Article), + "--" + ) + + class MockModelAdmin(object): + def test_from_model(self, obj): + return "nothing" + test_from_model.short_description = "not Really the Model" + + self.assertEquals( + label_for_field("test_from_model", Article, model_admin=MockModelAdmin), + "not Really the Model" + ) + self.assertEquals( + label_for_field("test_from_model", Article, + model_admin = MockModelAdmin, + return_attr = True + ), + ("not Really the Model", MockModelAdmin.test_from_model) + ) + + def test_related_name(self): + """ + Regression test for #13963 + """ + self.assertEquals( + label_for_field('location', Event, return_attr=True), + ('location', None), + ) + self.assertEquals( + label_for_field('event', Location, return_attr=True), + ('awesome event', None), + ) + self.assertEquals( + label_for_field('guest', Event, return_attr=True), + ('awesome guest', None), + ) diff --git a/parts/django/tests/regressiontests/admin_validation/__init__.py b/parts/django/tests/regressiontests/admin_validation/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_validation/__init__.py diff --git a/parts/django/tests/regressiontests/admin_validation/models.py b/parts/django/tests/regressiontests/admin_validation/models.py new file mode 100644 index 0000000..24387cc --- /dev/null +++ b/parts/django/tests/regressiontests/admin_validation/models.py @@ -0,0 +1,47 @@ +""" +Tests of ModelAdmin validation logic. +""" + +from django.db import models + + +class Album(models.Model): + title = models.CharField(max_length=150) + + +class Song(models.Model): + title = models.CharField(max_length=150) + album = models.ForeignKey(Album) + original_release = models.DateField(editable=False) + + class Meta: + ordering = ('title',) + + def __unicode__(self): + return self.title + + def readonly_method_on_model(self): + # does nothing + pass + + +class TwoAlbumFKAndAnE(models.Model): + album1 = models.ForeignKey(Album, related_name="album1_set") + album2 = models.ForeignKey(Album, related_name="album2_set") + e = models.CharField(max_length=1) + + +class Author(models.Model): + name = models.CharField(max_length=100) + + +class Book(models.Model): + name = models.CharField(max_length=100) + subtitle = models.CharField(max_length=100) + price = models.FloatField() + authors = models.ManyToManyField(Author, through='AuthorsBooks') + + +class AuthorsBooks(models.Model): + author = models.ForeignKey(Author) + book = models.ForeignKey(Book) diff --git a/parts/django/tests/regressiontests/admin_validation/tests.py b/parts/django/tests/regressiontests/admin_validation/tests.py new file mode 100644 index 0000000..1872ca5 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_validation/tests.py @@ -0,0 +1,247 @@ +from django.contrib import admin +from django import forms +from django.contrib.admin.validation import validate, validate_inline, \ + ImproperlyConfigured +from django.test import TestCase + +from models import Song, Book, Album, TwoAlbumFKAndAnE + +class SongForm(forms.ModelForm): + pass + +class ValidFields(admin.ModelAdmin): + form = SongForm + fields = ['title'] + +class InvalidFields(admin.ModelAdmin): + form = SongForm + fields = ['spam'] + +class ValidationTestCase(TestCase): + def assertRaisesMessage(self, exc, msg, func, *args, **kwargs): + try: + func(*args, **kwargs) + except Exception, e: + self.assertEqual(msg, str(e)) + self.assertTrue(isinstance(e, exc), "Expected %s, got %s" % (exc, type(e))) + + def test_readonly_and_editable(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ["original_release"] + fieldsets = [ + (None, { + "fields": ["title", "original_release"], + }), + ] + validate(SongAdmin, Song) + + def test_custom_modelforms_with_fields_fieldsets(self): + """ + # Regression test for #8027: custom ModelForms with fields/fieldsets + """ + validate(ValidFields, Song) + self.assertRaisesMessage(ImproperlyConfigured, + "'InvalidFields.fields' refers to field 'spam' that is missing from the form.", + validate, + InvalidFields, Song) + + def test_exclude_values(self): + """ + Tests for basic validation of 'exclude' option values (#12689) + """ + class ExcludedFields1(admin.ModelAdmin): + exclude = ('foo') + self.assertRaisesMessage(ImproperlyConfigured, + "'ExcludedFields1.exclude' must be a list or tuple.", + validate, + ExcludedFields1, Book) + + def test_exclude_duplicate_values(self): + class ExcludedFields2(admin.ModelAdmin): + exclude = ('name', 'name') + self.assertRaisesMessage(ImproperlyConfigured, + "There are duplicate field(s) in ExcludedFields2.exclude", + validate, + ExcludedFields2, Book) + + def test_exclude_in_inline(self): + class ExcludedFieldsInline(admin.TabularInline): + model = Song + exclude = ('foo') + + class ExcludedFieldsAlbumAdmin(admin.ModelAdmin): + model = Album + inlines = [ExcludedFieldsInline] + + self.assertRaisesMessage(ImproperlyConfigured, + "'ExcludedFieldsInline.exclude' must be a list or tuple.", + validate, + ExcludedFieldsAlbumAdmin, Album) + + def test_exclude_inline_model_admin(self): + """ + # Regression test for #9932 - exclude in InlineModelAdmin + # should not contain the ForeignKey field used in ModelAdmin.model + """ + class SongInline(admin.StackedInline): + model = Song + exclude = ['album'] + + class AlbumAdmin(admin.ModelAdmin): + model = Album + inlines = [SongInline] + + self.assertRaisesMessage(ImproperlyConfigured, + "SongInline cannot exclude the field 'album' - this is the foreign key to the parent model Album.", + validate, + AlbumAdmin, Album) + + def test_fk_exclusion(self): + """ + Regression test for #11709 - when testing for fk excluding (when exclude is + given) make sure fk_name is honored or things blow up when there is more + than one fk to the parent model. + """ + class TwoAlbumFKAndAnEInline(admin.TabularInline): + model = TwoAlbumFKAndAnE + exclude = ("e",) + fk_name = "album1" + validate_inline(TwoAlbumFKAndAnEInline, None, Album) + + def test_inline_self_validation(self): + class TwoAlbumFKAndAnEInline(admin.TabularInline): + model = TwoAlbumFKAndAnE + + self.assertRaisesMessage(Exception, + "<class 'regressiontests.admin_validation.models.TwoAlbumFKAndAnE'> has more than 1 ForeignKey to <class 'regressiontests.admin_validation.models.Album'>", + validate_inline, + TwoAlbumFKAndAnEInline, None, Album) + + def test_inline_with_specified(self): + class TwoAlbumFKAndAnEInline(admin.TabularInline): + model = TwoAlbumFKAndAnE + fk_name = "album1" + validate_inline(TwoAlbumFKAndAnEInline, None, Album) + + def test_readonly(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("title",) + + validate(SongAdmin, Song) + + def test_readonly_on_method(self): + def my_function(obj): + pass + + class SongAdmin(admin.ModelAdmin): + readonly_fields = (my_function,) + + validate(SongAdmin, Song) + + def test_readonly_on_modeladmin(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("readonly_method_on_modeladmin",) + + def readonly_method_on_modeladmin(self, obj): + pass + + validate(SongAdmin, Song) + + def test_readonly_method_on_model(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("readonly_method_on_model",) + + validate(SongAdmin, Song) + + def test_nonexistant_field(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("title", "nonexistant") + + self.assertRaisesMessage(ImproperlyConfigured, + "SongAdmin.readonly_fields[1], 'nonexistant' is not a callable or an attribute of 'SongAdmin' or found in the model 'Song'.", + validate, + SongAdmin, Song) + + def test_extra(self): + class SongAdmin(admin.ModelAdmin): + def awesome_song(self, instance): + if instance.title == "Born to Run": + return "Best Ever!" + return "Status unknown." + validate(SongAdmin, Song) + + def test_readonly_lambda(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = (lambda obj: "test",) + + validate(SongAdmin, Song) + + def test_graceful_m2m_fail(self): + """ + Regression test for #12203/#12237 - Fail more gracefully when a M2M field that + specifies the 'through' option is included in the 'fields' or the 'fieldsets' + ModelAdmin options. + """ + + class BookAdmin(admin.ModelAdmin): + fields = ['authors'] + + self.assertRaisesMessage(ImproperlyConfigured, + "'BookAdmin.fields' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model.", + validate, + BookAdmin, Book) + + def test_cannon_include_through(self): + class FieldsetBookAdmin(admin.ModelAdmin): + fieldsets = ( + ('Header 1', {'fields': ('name',)}), + ('Header 2', {'fields': ('authors',)}), + ) + self.assertRaisesMessage(ImproperlyConfigured, + "'FieldsetBookAdmin.fieldsets[1][1]['fields']' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model.", + validate, + FieldsetBookAdmin, Book) + + def test_nested_fieldsets(self): + class NestedFieldsetAdmin(admin.ModelAdmin): + fieldsets = ( + ('Main', {'fields': ('price', ('name', 'subtitle'))}), + ) + validate(NestedFieldsetAdmin, Book) + + def test_explicit_through_override(self): + """ + Regression test for #12209 -- If the explicitly provided through model + is specified as a string, the admin should still be able use + Model.m2m_field.through + """ + + class AuthorsInline(admin.TabularInline): + model = Book.authors.through + + class BookAdmin(admin.ModelAdmin): + inlines = [AuthorsInline] + + # If the through model is still a string (and hasn't been resolved to a model) + # the validation will fail. + validate(BookAdmin, Book) + + def test_non_model_fields(self): + """ + Regression for ensuring ModelAdmin.fields can contain non-model fields + that broke with r11737 + """ + class SongForm(forms.ModelForm): + extra_data = forms.CharField() + class Meta: + model = Song + + class FieldsOnFormOnlyAdmin(admin.ModelAdmin): + form = SongForm + fields = ['title', 'extra_data'] + + validate(FieldsOnFormOnlyAdmin, Song) + + + + diff --git a/parts/django/tests/regressiontests/admin_views/__init__.py b/parts/django/tests/regressiontests/admin_views/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/__init__.py diff --git a/parts/django/tests/regressiontests/admin_views/customadmin.py b/parts/django/tests/regressiontests/admin_views/customadmin.py new file mode 100644 index 0000000..34e39ef --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/customadmin.py @@ -0,0 +1,34 @@ +""" +A second, custom AdminSite -- see tests.CustomAdminSiteTests. +""" +from django.conf.urls.defaults import patterns +from django.contrib import admin +from django.http import HttpResponse + +import models + +class Admin2(admin.AdminSite): + login_template = 'custom_admin/login.html' + logout_template = 'custom_admin/logout.html' + index_template = 'custom_admin/index.html' + password_change_template = 'custom_admin/password_change_form.html' + password_change_done_template = 'custom_admin/password_change_done.html' + + # A custom index view. + def index(self, request, extra_context=None): + return super(Admin2, self).index(request, {'foo': '*bar*'}) + + def get_urls(self): + return patterns('', + (r'^my_view/$', self.admin_view(self.my_view)), + ) + super(Admin2, self).get_urls() + + def my_view(self, request): + return HttpResponse("Django is a magical pony!") + +site = Admin2(name="admin2") + +site.register(models.Article, models.ArticleAdmin) +site.register(models.Section, inlines=[models.ArticleInline]) +site.register(models.Thing, models.ThingAdmin) +site.register(models.Fabric, models.FabricAdmin) diff --git a/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-actions.xml b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-actions.xml new file mode 100644 index 0000000..316e750 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-actions.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="1" model="admin_views.subscriber"> + <field type="CharField" name="name">John Doe</field> + <field type="CharField" name="email">john@example.org</field> + </object> + <object pk="2" model="admin_views.subscriber"> + <field type="CharField" name="name">Max Mustermann</field> + <field type="CharField" name="email">max@example.org</field> + </object> + <object pk="1" model="admin_views.externalsubscriber"> + <field type="CharField" name="name">John Doe</field> + <field type="CharField" name="email">john@example.org</field> + </object> +</django-objects> diff --git a/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-colors.xml b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-colors.xml new file mode 100644 index 0000000..e121356 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-colors.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="1" model="admin_views.color"> + <field type="CharField" name="value">Red</field> + <field type="BooleanField" name="warm">1</field> + </object> + <object pk="2" model="admin_views.color"> + <field type="CharField" name="value">Orange</field> + <field type="BooleanField" name="warm">1</field> + </object> + <object pk="3" model="admin_views.color"> + <field type="CharField" name="value">Blue</field> + <field type="BooleanField" name="warm">0</field> + </object> + <object pk="4" model="admin_views.color"> + <field type="CharField" name="value">Green</field> + <field type="BooleanField" name="warm">0</field> + </object> +</django-objects> diff --git a/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-fabrics.xml b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-fabrics.xml new file mode 100644 index 0000000..485bb27 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-fabrics.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="1" model="admin_views.fabric"> + <field type="CharField" name="surface">x</field> + </object> + <object pk="2" model="admin_views.fabric"> + <field type="CharField" name="surface">y</field> + </object> + <object pk="3" model="admin_views.fabric"> + <field type="CharField" name="surface">plain</field> + </object> +</django-objects> diff --git a/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-person.xml b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-person.xml new file mode 100644 index 0000000..ff00fd2 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-person.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="1" model="admin_views.person"> + <field type="CharField" name="name">John Mauchly</field> + <field type="IntegerField" name="gender">1</field> + <field type="BooleanField" name="alive">True</field> + </object> + <object pk="2" model="admin_views.person"> + <field type="CharField" name="name">Grace Hopper</field> + <field type="IntegerField" name="gender">1</field> + <field type="BooleanField" name="alive">False</field> + </object> + <object pk="3" model="admin_views.person"> + <field type="CharField" name="name">Guido van Rossum</field> + <field type="IntegerField" name="gender">1</field> + <field type="BooleanField" name="alive">True</field> + </object> +</django-objects> diff --git a/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-unicode.xml b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-unicode.xml new file mode 100644 index 0000000..5652aa1 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-unicode.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="100" model="auth.user"> + <field type="CharField" name="username">super</field> + <field type="CharField" name="first_name">Super</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">super@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">True</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="1" model="admin_views.book"> + <field type="CharField" name="name">Lærdommer</field> + </object> + <object pk="1" model="admin_views.promo"> + <field type="CharField" name="name"><Promo for Lærdommer></field> + <field to="admin_views.book" name="book" rel="ManyToOneRel">1</field> + </object> + <object pk="1" model="admin_views.chapter"> + <field type="CharField" name="title">Norske bostaver æøå skaper problemer</field> + <field type="TextField" name="content"><p>Svært frustrerende med UnicodeDecodeErro</p></field> + <field to="admin_views.book" name="book" rel="ManyToOneRel">1</field> + </object> + <object pk="2" model="admin_views.chapter"> + <field type="CharField" name="title">Kjærlighet</field> + <field type="TextField" name="content"><p>La kjærligheten til de lidende seire.</p></field> + <field to="admin_views.book" name="book" rel="ManyToOneRel">1</field> + </object> + <object pk="3" model="admin_views.chapter"> + <field type="CharField" name="title">Kjærlighet</field> + <field type="TextField" name="content"><p>Noe innhold</p></field> + <field to="admin_views.book" name="book" rel="ManyToOneRel">1</field> + </object> + <object pk="1" model="admin_views.chapterxtra1"> + <field type="CharField" name="xtra"><Xtra(1) Norske bostaver æøå skaper problemer></field> + <field to="admin_views.chapter" name="chap" rel="OneToOneRel">1</field> + </object> + <object pk="2" model="admin_views.chapterxtra1"> + <field type="CharField" name="xtra"><Xtra(1) Kjærlighet></field> + <field to="admin_views.chapter" name="chap" rel="OneToOneRel">2</field> + </object> + <object pk="3" model="admin_views.chapterxtra1"> + <field type="CharField" name="xtra"><Xtra(1) Kjærlighet></field> + <field to="admin_views.chapter" name="chap" rel="OneToOneRel">3</field> + </object> + <object pk="1" model="admin_views.chapterxtra2"> + <field type="CharField" name="xtra"><Xtra(2) Norske bostaver æøå skaper problemer></field> + <field to="admin_views.chapter" name="chap" rel="OneToOneRel">1</field> + </object> + <object pk="2" model="admin_views.chapterxtra2"> + <field type="CharField" name="xtra"><Xtra(2) Kjærlighet></field> + <field to="admin_views.chapter" name="chap" rel="OneToOneRel">2</field> + </object> + <object pk="3" model="admin_views.chapterxtra2"> + <field type="CharField" name="xtra"><Xtra(2) Kjærlighet></field> + <field to="admin_views.chapter" name="chap" rel="OneToOneRel">3</field> + </object> +</django-objects> diff --git a/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-users.xml b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-users.xml new file mode 100644 index 0000000..f1ff296 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/fixtures/admin-views-users.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="100" model="auth.user"> + <field type="CharField" name="username">super</field> + <field type="CharField" name="first_name">Super</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">super@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">True</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="101" model="auth.user"> + <field type="CharField" name="username">adduser</field> + <field type="CharField" name="first_name">Add</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">auser@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">False</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="102" model="auth.user"> + <field type="CharField" name="username">changeuser</field> + <field type="CharField" name="first_name">Change</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">cuser@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">False</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="103" model="auth.user"> + <field type="CharField" name="username">deleteuser</field> + <field type="CharField" name="first_name">Delete</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">duser@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">False</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="104" model="auth.user"> + <field type="CharField" name="username">joepublic</field> + <field type="CharField" name="first_name">Joe</field> + <field type="CharField" name="last_name">Public</field> + <field type="CharField" name="email">joepublic@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">False</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">False</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="1" model="admin_views.section"> + <field type="CharField" name="name">Test section</field> + </object> + <object pk="1" model="admin_views.article"> + <field type="TextField" name="content"><p>Middle content</p></field> + <field type="DateTimeField" name="date">2008-03-18 11:54:58</field> + <field to="admin_views.section" name="section" rel="ManyToOneRel">1</field> + </object> + <object pk="2" model="admin_views.article"> + <field type="TextField" name="content"><p>Oldest content</p></field> + <field type="DateTimeField" name="date">2000-03-18 11:54:58</field> + <field to="admin_views.section" name="section" rel="ManyToOneRel">1</field> + </object> + <object pk="3" model="admin_views.article"> + <field type="TextField" name="content"><p>Newest content</p></field> + <field type="DateTimeField" name="date">2009-03-18 11:54:58</field> + <field to="admin_views.section" name="section" rel="ManyToOneRel">1</field> + </object> + + +</django-objects>
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/admin_views/fixtures/deleted-objects.xml b/parts/django/tests/regressiontests/admin_views/fixtures/deleted-objects.xml new file mode 100644 index 0000000..92e43db --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/fixtures/deleted-objects.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="1" model="admin_views.villain"> + <field type="CharField" name="name">Adam</field> + </object> + <object pk="2" model="admin_views.villain"> + <field type="CharField" name="name">Sue</field> + </object> + <object pk="3" model="admin_views.villain"> + <field type="CharField" name="name">Bob</field> + </object> + <object pk="3" model="admin_views.supervillain"> + </object> + <object pk="1" model="admin_views.plot"> + <field type="CharField" name="name">World Domination</field> + <field type="ForeignKey" name="team_leader">1</field> + <field type="ForeignKey" name="contact">2</field> + </object> + <object pk="2" model="admin_views.plot"> + <field type="CharField" name="name">World Peace</field> + <field type="ForeignKey" name="team_leader">2</field> + <field type="ForeignKey" name="contact">2</field> + </object> + <object pk="1" model="admin_views.plotdetails"> + <field type="CharField" name="details">almost finished</field> + <field type="ForeignKey" name="plot">1</field> + </object> + <object pk="1" model="admin_views.secrethideout"> + <field type="CharField" name="location">underground bunker</field> + <field type="ForeignKey" name="villain">1</field> + </object> + <object pk="2" model="admin_views.secrethideout"> + <field type="CharField" name="location">floating castle</field> + <field type="ForeignKey" name="villain">3</field> + </object> + <object pk="1" model="admin_views.supersecrethideout"> + <field type="CharField" name="location">super floating castle!</field> + <field type="ForeignKey" name="supervillain">3</field> + </object> + <object pk="1" model="admin_views.cyclicone"> + <field type="CharField" name="name">I am recursive</field> + <field type="ForeignKey" name="two">1</field> + </object> + <object pk="1" model="admin_views.cyclictwo"> + <field type="CharField" name="name">I am recursive too</field> + <field type="ForeignKey" name="one">1</field> + </object> + <object pk="3" model="admin_views.plot"> + <field type="CharField" name="name">Corn Conspiracy</field> + <field type="ForeignKey" name="team_leader">1</field> + <field type="ForeignKey" name="contact">1</field> + </object> +</django-objects> diff --git a/parts/django/tests/regressiontests/admin_views/fixtures/multiple-child-classes.json b/parts/django/tests/regressiontests/admin_views/fixtures/multiple-child-classes.json new file mode 100644 index 0000000..5cadf4c --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/fixtures/multiple-child-classes.json @@ -0,0 +1,107 @@ +[ + { + "pk": 1, + "model": "admin_views.title", + "fields": + { + } + }, + + { + "pk": 2, + "model": "admin_views.title", + "fields": + { + } + }, + + { + "pk": 3, + "model": "admin_views.title", + "fields": + { + } + }, + + { + "pk": 4, + "model": "admin_views.title", + "fields": + { + } + }, + + { + "pk": 1, + "model": "admin_views.titletranslation", + "fields": + { + "text": "Bar", + "title": 1 + } + }, + + { + "pk": 2, + "model": "admin_views.titletranslation", + "fields": + { + "text": "Foo", + "title": 2 + } + }, + + { + "pk": 3, + "model": "admin_views.titletranslation", + "fields": + { + "text": "Few", + "title": 3 + } + }, + + { + "pk": 4, + "model": "admin_views.titletranslation", + "fields": + { + "text": "Bas", + "title": 4 + } + }, + + { + "pk": 1, + "model": "admin_views.recommender", + "fields": + { + } + }, + + { + "pk": 4, + "model": "admin_views.recommender", + "fields": + { + } + }, + + { + "pk": 2, + "model": "admin_views.recommendation", + "fields": + { + "recommender": 1 + } + }, + + { + "pk": 3, + "model": "admin_views.recommendation", + "fields": + { + "recommender": 4 + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/admin_views/fixtures/string-primary-key.xml b/parts/django/tests/regressiontests/admin_views/fixtures/string-primary-key.xml new file mode 100644 index 0000000..8e1dbf0 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/fixtures/string-primary-key.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="1" model="admin_views.modelwithstringprimarykey"> + <field type="CharField" name="id"><![CDATA[abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 -_.!~*'() ;/?:@&=+$, <>#%" {}|\^[]`]]></field> + </object> +</django-objects>
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/admin_views/models.py b/parts/django/tests/regressiontests/admin_views/models.py new file mode 100644 index 0000000..191b4f3 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/models.py @@ -0,0 +1,636 @@ +# -*- coding: utf-8 -*- +import datetime +import tempfile +import os + +from django.contrib import admin +from django.core.files.storage import FileSystemStorage +from django.contrib.admin.views.main import ChangeList +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 + +class Section(models.Model): + """ + A simple section that links to articles, to test linking to related items + in admin views. + """ + name = models.CharField(max_length=100) + +class Article(models.Model): + """ + A simple article to test admin views. Test backwards compatibility. + """ + title = models.CharField(max_length=100) + content = models.TextField() + date = models.DateTimeField() + section = models.ForeignKey(Section, null=True, blank=True) + + def __unicode__(self): + return self.title + + def model_year(self): + return self.date.year + model_year.admin_order_field = 'date' + model_year.short_description = '' + +class Book(models.Model): + """ + A simple book that has chapters. + """ + name = models.CharField(max_length=100, verbose_name=u'¿Name?') + + def __unicode__(self): + return self.name + +class Promo(models.Model): + name = models.CharField(max_length=100, verbose_name=u'¿Name?') + book = models.ForeignKey(Book) + + def __unicode__(self): + return self.name + +class Chapter(models.Model): + title = models.CharField(max_length=100, verbose_name=u'¿Title?') + content = models.TextField() + book = models.ForeignKey(Book) + + def __unicode__(self): + return self.title + + class Meta: + # Use a utf-8 bytestring to ensure it works (see #11710) + verbose_name = '¿Chapter?' + +class ChapterXtra1(models.Model): + chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?') + xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?') + + def __unicode__(self): + return u'¿Xtra1: %s' % self.xtra + +class ChapterXtra2(models.Model): + chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?') + xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?') + + def __unicode__(self): + return u'¿Xtra2: %s' % self.xtra + +def callable_year(dt_value): + return dt_value.year +callable_year.admin_order_field = 'date' + +class ArticleInline(admin.TabularInline): + model = Article + +class ChapterInline(admin.TabularInline): + model = Chapter + +class ArticleAdmin(admin.ModelAdmin): + list_display = ('content', 'date', callable_year, 'model_year', 'modeladmin_year') + list_filter = ('date', 'section') + + def changelist_view(self, request): + "Test that extra_context works" + return super(ArticleAdmin, self).changelist_view( + request, extra_context={ + 'extra_var': 'Hello!' + } + ) + + def modeladmin_year(self, obj): + return obj.date.year + modeladmin_year.admin_order_field = 'date' + modeladmin_year.short_description = None + +class CustomArticle(models.Model): + content = models.TextField() + date = models.DateTimeField() + +class CustomArticleAdmin(admin.ModelAdmin): + """ + Tests various hooks for using custom templates and contexts. + """ + change_list_template = 'custom_admin/change_list.html' + change_form_template = 'custom_admin/change_form.html' + add_form_template = 'custom_admin/add_form.html' + object_history_template = 'custom_admin/object_history.html' + delete_confirmation_template = 'custom_admin/delete_confirmation.html' + delete_selected_confirmation_template = 'custom_admin/delete_selected_confirmation.html' + + def changelist_view(self, request): + "Test that extra_context works" + return super(CustomArticleAdmin, self).changelist_view( + request, extra_context={ + 'extra_var': 'Hello!' + } + ) + +class ModelWithStringPrimaryKey(models.Model): + id = models.CharField(max_length=255, primary_key=True) + + def __unicode__(self): + return self.id + +class Color(models.Model): + value = models.CharField(max_length=10) + warm = models.BooleanField() + def __unicode__(self): + return self.value + +class Thing(models.Model): + title = models.CharField(max_length=20) + color = models.ForeignKey(Color, limit_choices_to={'warm': True}) + def __unicode__(self): + return self.title + +class ThingAdmin(admin.ModelAdmin): + list_filter = ('color',) + +class Fabric(models.Model): + NG_CHOICES = ( + ('Textured', ( + ('x', 'Horizontal'), + ('y', 'Vertical'), + ) + ), + ('plain', 'Smooth'), + ) + surface = models.CharField(max_length=20, choices=NG_CHOICES) + +class FabricAdmin(admin.ModelAdmin): + list_display = ('surface',) + list_filter = ('surface',) + +class Person(models.Model): + GENDER_CHOICES = ( + (1, "Male"), + (2, "Female"), + ) + name = models.CharField(max_length=100) + gender = models.IntegerField(choices=GENDER_CHOICES) + alive = models.BooleanField() + + def __unicode__(self): + return self.name + + class Meta: + ordering = ["id"] + +class BasePersonModelFormSet(BaseModelFormSet): + def clean(self): + for person_dict in self.cleaned_data: + person = person_dict.get('id') + alive = person_dict.get('alive') + if person and alive and person.name == "Grace Hopper": + raise forms.ValidationError, "Grace is not a Zombie" + +class PersonAdmin(admin.ModelAdmin): + list_display = ('name', 'gender', 'alive') + list_editable = ('gender', 'alive') + list_filter = ('gender',) + search_fields = (u'name',) + ordering = ["id"] + save_as = True + + def get_changelist_formset(self, request, **kwargs): + return super(PersonAdmin, self).get_changelist_formset(request, + formset=BasePersonModelFormSet, **kwargs) + + +class Persona(models.Model): + """ + A simple persona associated with accounts, to test inlining of related + accounts which inherit from a common accounts class. + """ + name = models.CharField(blank=False, max_length=80) + def __unicode__(self): + return self.name + +class Account(models.Model): + """ + A simple, generic account encapsulating the information shared by all + types of accounts. + """ + username = models.CharField(blank=False, max_length=80) + persona = models.ForeignKey(Persona, related_name="accounts") + servicename = u'generic service' + + def __unicode__(self): + return "%s: %s" % (self.servicename, self.username) + +class FooAccount(Account): + """A service-specific account of type Foo.""" + servicename = u'foo' + +class BarAccount(Account): + """A service-specific account of type Bar.""" + servicename = u'bar' + +class FooAccountAdmin(admin.StackedInline): + model = FooAccount + extra = 1 + +class BarAccountAdmin(admin.StackedInline): + model = BarAccount + extra = 1 + +class PersonaAdmin(admin.ModelAdmin): + inlines = ( + FooAccountAdmin, + BarAccountAdmin + ) + +class Subscriber(models.Model): + name = models.CharField(blank=False, max_length=80) + email = models.EmailField(blank=False, max_length=175) + + def __unicode__(self): + return "%s (%s)" % (self.name, self.email) + +class SubscriberAdmin(admin.ModelAdmin): + actions = ['mail_admin'] + + def mail_admin(self, request, selected): + EmailMessage( + 'Greetings from a ModelAdmin action', + 'This is the test email from a admin action', + 'from@example.com', + ['to@example.com'] + ).send() + +class ExternalSubscriber(Subscriber): + pass + +class OldSubscriber(Subscriber): + pass + +def external_mail(modeladmin, request, selected): + EmailMessage( + 'Greetings from a function action', + 'This is the test email from a function action', + 'from@example.com', + ['to@example.com'] + ).send() + +def redirect_to(modeladmin, request, selected): + from django.http import HttpResponseRedirect + return HttpResponseRedirect('/some-where-else/') + +class ExternalSubscriberAdmin(admin.ModelAdmin): + actions = [external_mail, redirect_to] + +class Media(models.Model): + name = models.CharField(max_length=60) + +class Podcast(Media): + release_date = models.DateField() + +class PodcastAdmin(admin.ModelAdmin): + list_display = ('name', 'release_date') + list_editable = ('release_date',) + + ordering = ('name',) + +class Vodcast(Media): + media = models.OneToOneField(Media, primary_key=True, parent_link=True) + released = models.BooleanField(default=False) + +class VodcastAdmin(admin.ModelAdmin): + list_display = ('name', 'released') + list_editable = ('released',) + + ordering = ('name',) + +class Parent(models.Model): + name = models.CharField(max_length=128) + +class Child(models.Model): + parent = models.ForeignKey(Parent, editable=False) + name = models.CharField(max_length=30, blank=True) + +class ChildInline(admin.StackedInline): + model = Child + +class ParentAdmin(admin.ModelAdmin): + model = Parent + inlines = [ChildInline] + +class EmptyModel(models.Model): + def __unicode__(self): + return "Primary key = %s" % self.id + +class EmptyModelAdmin(admin.ModelAdmin): + def queryset(self, request): + return super(EmptyModelAdmin, self).queryset(request).filter(pk__gt=1) + +class OldSubscriberAdmin(admin.ModelAdmin): + actions = None + +temp_storage = FileSystemStorage(tempfile.mkdtemp()) +UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload') + +class Gallery(models.Model): + name = models.CharField(max_length=100) + +class Picture(models.Model): + name = models.CharField(max_length=100) + image = models.FileField(storage=temp_storage, upload_to='test_upload') + gallery = models.ForeignKey(Gallery, related_name="pictures") + +class PictureInline(admin.TabularInline): + model = Picture + extra = 1 + +class GalleryAdmin(admin.ModelAdmin): + inlines = [PictureInline] + +class PictureAdmin(admin.ModelAdmin): + pass + +class Language(models.Model): + iso = models.CharField(max_length=5, primary_key=True) + name = models.CharField(max_length=50) + english_name = models.CharField(max_length=50) + shortlist = models.BooleanField(default=False) + + class Meta: + ordering = ('iso',) + +class LanguageAdmin(admin.ModelAdmin): + list_display = ['iso', 'shortlist', 'english_name', 'name'] + list_editable = ['shortlist'] + +# a base class for Recommender and Recommendation +class Title(models.Model): + pass + +class TitleTranslation(models.Model): + title = models.ForeignKey(Title) + text = models.CharField(max_length=100) + +class Recommender(Title): + pass + +class Recommendation(Title): + recommender = models.ForeignKey(Recommender) + +class RecommendationAdmin(admin.ModelAdmin): + search_fields = ('titletranslation__text', 'recommender__titletranslation__text',) + +class Collector(models.Model): + name = models.CharField(max_length=100) + +class Widget(models.Model): + owner = models.ForeignKey(Collector) + name = models.CharField(max_length=100) + +class DooHickey(models.Model): + code = models.CharField(max_length=10, primary_key=True) + owner = models.ForeignKey(Collector) + name = models.CharField(max_length=100) + +class Grommet(models.Model): + code = models.AutoField(primary_key=True) + owner = models.ForeignKey(Collector) + name = models.CharField(max_length=100) + +class Whatsit(models.Model): + index = models.IntegerField(primary_key=True) + owner = models.ForeignKey(Collector) + name = models.CharField(max_length=100) + +class Doodad(models.Model): + name = models.CharField(max_length=100) + +class FancyDoodad(Doodad): + owner = models.ForeignKey(Collector) + expensive = models.BooleanField(default=True) + +class WidgetInline(admin.StackedInline): + model = Widget + +class DooHickeyInline(admin.StackedInline): + model = DooHickey + +class GrommetInline(admin.StackedInline): + model = Grommet + +class WhatsitInline(admin.StackedInline): + model = Whatsit + +class FancyDoodadInline(admin.StackedInline): + model = FancyDoodad + +class Category(models.Model): + collector = models.ForeignKey(Collector) + order = models.PositiveIntegerField() + + class Meta: + ordering = ('order',) + + def __unicode__(self): + return u'%s:o%s' % (self.id, self.order) + +class CategoryAdmin(admin.ModelAdmin): + list_display = ('id', 'collector', 'order') + list_editable = ('order',) + +class CategoryInline(admin.StackedInline): + model = Category + +class CollectorAdmin(admin.ModelAdmin): + inlines = [ + WidgetInline, DooHickeyInline, GrommetInline, WhatsitInline, + FancyDoodadInline, CategoryInline + ] + +class Link(models.Model): + posted = models.DateField( + default=lambda: datetime.date.today() - datetime.timedelta(days=7) + ) + url = models.URLField() + post = models.ForeignKey("Post") + + +class LinkInline(admin.TabularInline): + model = Link + extra = 1 + + readonly_fields = ("posted",) + + +class Post(models.Model): + title = models.CharField(max_length=100) + content = models.TextField() + posted = models.DateField(default=datetime.date.today) + public = models.NullBooleanField() + + def awesomeness_level(self): + return "Very awesome." + +class PostAdmin(admin.ModelAdmin): + list_display = ['title', 'public'] + readonly_fields = ('posted', 'awesomeness_level', 'coolness', 'value', lambda obj: "foo") + + inlines = [ + LinkInline + ] + + def coolness(self, instance): + if instance.pk: + return "%d amount of cool." % instance.pk + else: + return "Unkown coolness." + + def value(self, instance): + return 1000 + value.short_description = 'Value in $US' + +class Gadget(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class CustomChangeList(ChangeList): + def get_query_set(self): + return self.root_query_set.filter(pk=9999) # Does not exist + +class GadgetAdmin(admin.ModelAdmin): + def get_changelist(self, request, **kwargs): + return CustomChangeList + +class Villain(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class SuperVillain(Villain): + pass + +class FunkyTag(models.Model): + "Because we all know there's only one real use case for GFKs." + name = models.CharField(max_length=25) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + + def __unicode__(self): + return self.name + +class Plot(models.Model): + name = models.CharField(max_length=100) + team_leader = models.ForeignKey(Villain, related_name='lead_plots') + contact = models.ForeignKey(Villain, related_name='contact_plots') + tags = generic.GenericRelation(FunkyTag) + + def __unicode__(self): + return self.name + +class PlotDetails(models.Model): + details = models.CharField(max_length=100) + plot = models.OneToOneField(Plot) + + def __unicode__(self): + return self.details + +class SecretHideout(models.Model): + """ Secret! Not registered with the admin! """ + location = models.CharField(max_length=100) + villain = models.ForeignKey(Villain) + + def __unicode__(self): + return self.location + +class SuperSecretHideout(models.Model): + """ Secret! Not registered with the admin! """ + location = models.CharField(max_length=100) + supervillain = models.ForeignKey(SuperVillain) + + def __unicode__(self): + return self.location + +class CyclicOne(models.Model): + name = models.CharField(max_length=25) + two = models.ForeignKey('CyclicTwo') + + def __unicode__(self): + return self.name + +class CyclicTwo(models.Model): + name = models.CharField(max_length=25) + one = models.ForeignKey(CyclicOne) + + def __unicode__(self): + return self.name + +class Topping(models.Model): + name = models.CharField(max_length=20) + +class Pizza(models.Model): + name = models.CharField(max_length=20) + toppings = models.ManyToManyField('Topping') + +class PizzaAdmin(admin.ModelAdmin): + readonly_fields = ('toppings',) + +class Album(models.Model): + owner = models.ForeignKey(User) + title = models.CharField(max_length=30) + +class AlbumAdmin(admin.ModelAdmin): + list_filter = ['title'] + +admin.site.register(Article, ArticleAdmin) +admin.site.register(CustomArticle, CustomArticleAdmin) +admin.site.register(Section, save_as=True, inlines=[ArticleInline]) +admin.site.register(ModelWithStringPrimaryKey) +admin.site.register(Color) +admin.site.register(Thing, ThingAdmin) +admin.site.register(Person, PersonAdmin) +admin.site.register(Persona, PersonaAdmin) +admin.site.register(Subscriber, SubscriberAdmin) +admin.site.register(ExternalSubscriber, ExternalSubscriberAdmin) +admin.site.register(OldSubscriber, OldSubscriberAdmin) +admin.site.register(Podcast, PodcastAdmin) +admin.site.register(Vodcast, VodcastAdmin) +admin.site.register(Parent, ParentAdmin) +admin.site.register(EmptyModel, EmptyModelAdmin) +admin.site.register(Fabric, FabricAdmin) +admin.site.register(Gallery, GalleryAdmin) +admin.site.register(Picture, PictureAdmin) +admin.site.register(Language, LanguageAdmin) +admin.site.register(Recommendation, RecommendationAdmin) +admin.site.register(Recommender) +admin.site.register(Collector, CollectorAdmin) +admin.site.register(Category, CategoryAdmin) +admin.site.register(Post, PostAdmin) +admin.site.register(Gadget, GadgetAdmin) +admin.site.register(Villain) +admin.site.register(SuperVillain) +admin.site.register(Plot) +admin.site.register(PlotDetails) +admin.site.register(CyclicOne) +admin.site.register(CyclicTwo) + +# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. +# That way we cover all four cases: +# related ForeignKey object registered in admin +# related ForeignKey object not registered in admin +# related OneToOne object registered in admin +# related OneToOne object not registered in admin +# when deleting Book so as exercise all four troublesome (w.r.t escaping +# and calling force_unicode to avoid problems on Python 2.3) paths through +# contrib.admin.util's get_deleted_objects function. +admin.site.register(Book, inlines=[ChapterInline]) +admin.site.register(Promo) +admin.site.register(ChapterXtra1) +admin.site.register(Pizza, PizzaAdmin) +admin.site.register(Topping) +admin.site.register(Album, AlbumAdmin) diff --git a/parts/django/tests/regressiontests/admin_views/tests.py b/parts/django/tests/regressiontests/admin_views/tests.py new file mode 100644 index 0000000..d3467dd --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/tests.py @@ -0,0 +1,2287 @@ +# coding: utf-8 + +import re +import datetime + +from django.conf import settings +from django.core.exceptions import SuspiciousOperation +from django.core.files import temp as tempfile +# Register auth models with the admin. +from django.contrib.auth import REDIRECT_FIELD_NAME, admin +from django.contrib.auth.models import User, Permission, UNUSABLE_PASSWORD +from django.contrib.contenttypes.models import ContentType +from django.contrib.admin.models import LogEntry, DELETION +from django.contrib.admin.sites import LOGIN_FORM_KEY +from django.contrib.admin.util import quote +from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME +from django.forms.util import ErrorList +from django.test import TestCase +from django.utils import formats +from django.utils.cache import get_max_age +from django.utils.encoding import iri_to_uri +from django.utils.html import escape +from django.utils.translation import activate, deactivate +import django.template.context + +# local test models +from models import Article, BarAccount, CustomArticle, EmptyModel, \ + FooAccount, Gallery, ModelWithStringPrimaryKey, \ + Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \ + Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \ + Category, Post, Plot, FunkyTag + + +class AdminViewBasicTest(TestCase): + fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml'] + + # Store the bit of the URL where the admin is registered as a class + # variable. That way we can test a second AdminSite just by subclassing + # this test case and changing urlbit. + urlbit = 'admin' + + def setUp(self): + self.old_language_code = settings.LANGUAGE_CODE + self.client.login(username='super', password='secret') + + def tearDown(self): + settings.LANGUAGE_CODE = self.old_language_code + self.client.logout() + + def testTrailingSlashRequired(self): + """ + If you leave off the trailing slash, app should redirect and add it. + """ + request = self.client.get('/test_admin/%s/admin_views/article/add' % self.urlbit) + self.assertRedirects(request, + '/test_admin/%s/admin_views/article/add/' % self.urlbit, status_code=301 + ) + + def testBasicAddGet(self): + """ + A smoke test to ensure GET on the add_view works. + """ + response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit) + self.assertEqual(response.status_code, 200) + + def testAddWithGETArgs(self): + response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit, {'name': 'My Section'}) + self.assertEqual(response.status_code, 200) + self.assertTrue( + 'value="My Section"' in response.content, + "Couldn't find an input with the right value in the response." + ) + + def testBasicEditGet(self): + """ + A smoke test to ensure GET on the change_view works. + """ + response = self.client.get('/test_admin/%s/admin_views/section/1/' % self.urlbit) + self.assertEqual(response.status_code, 200) + + def testBasicEditGetStringPK(self): + """ + A smoke test to ensure GET on the change_view works (returns an HTTP + 404 error, see #11191) when passing a string as the PK argument for a + model with an integer PK field. + """ + response = self.client.get('/test_admin/%s/admin_views/section/abc/' % self.urlbit) + self.assertEqual(response.status_code, 404) + + def testBasicAddPost(self): + """ + A smoke test to ensure POST on add_view works. + """ + post_data = { + "name": u"Another Section", + # inline data + "article_set-TOTAL_FORMS": u"3", + "article_set-INITIAL_FORMS": u"0", + "article_set-MAX_NUM_FORMS": u"0", + } + response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data) + self.assertEqual(response.status_code, 302) # redirect somewhere + + # Post data for edit inline + inline_post_data = { + "name": u"Test section", + # inline data + "article_set-TOTAL_FORMS": u"6", + "article_set-INITIAL_FORMS": u"3", + "article_set-MAX_NUM_FORMS": u"0", + "article_set-0-id": u"1", + # there is no title in database, give one here or formset will fail. + "article_set-0-title": u"Norske bostaver æøå skaper problemer", + "article_set-0-content": u"<p>Middle content</p>", + "article_set-0-date_0": u"2008-03-18", + "article_set-0-date_1": u"11:54:58", + "article_set-0-section": u"1", + "article_set-1-id": u"2", + "article_set-1-title": u"Need a title.", + "article_set-1-content": u"<p>Oldest content</p>", + "article_set-1-date_0": u"2000-03-18", + "article_set-1-date_1": u"11:54:58", + "article_set-2-id": u"3", + "article_set-2-title": u"Need a title.", + "article_set-2-content": u"<p>Newest content</p>", + "article_set-2-date_0": u"2009-03-18", + "article_set-2-date_1": u"11:54:58", + "article_set-3-id": u"", + "article_set-3-title": u"", + "article_set-3-content": u"", + "article_set-3-date_0": u"", + "article_set-3-date_1": u"", + "article_set-4-id": u"", + "article_set-4-title": u"", + "article_set-4-content": u"", + "article_set-4-date_0": u"", + "article_set-4-date_1": u"", + "article_set-5-id": u"", + "article_set-5-title": u"", + "article_set-5-content": u"", + "article_set-5-date_0": u"", + "article_set-5-date_1": u"", + } + + def testBasicEditPost(self): + """ + A smoke test to ensure POST on edit_view works. + """ + response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, self.inline_post_data) + self.assertEqual(response.status_code, 302) # redirect somewhere + + def testEditSaveAs(self): + """ + Test "save as". + """ + post_data = self.inline_post_data.copy() + post_data.update({ + '_saveasnew': u'Save+as+new', + "article_set-1-section": u"1", + "article_set-2-section": u"1", + "article_set-3-section": u"1", + "article_set-4-section": u"1", + "article_set-5-section": u"1", + }) + response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, post_data) + self.assertEqual(response.status_code, 302) # redirect somewhere + + def testChangeListSortingCallable(self): + """ + Ensure we can sort on a list_display field that is a callable + (column 2 is callable_year in ArticleAdmin) + """ + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 2}) + self.assertEqual(response.status_code, 200) + self.assertTrue( + response.content.index('Oldest content') < response.content.index('Middle content') and + response.content.index('Middle content') < response.content.index('Newest content'), + "Results of sorting on callable are out of order." + ) + + def testChangeListSortingModel(self): + """ + Ensure we can sort on a list_display field that is a Model method + (colunn 3 is 'model_year' in ArticleAdmin) + """ + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'dsc', 'o': 3}) + self.assertEqual(response.status_code, 200) + self.assertTrue( + response.content.index('Newest content') < response.content.index('Middle content') and + response.content.index('Middle content') < response.content.index('Oldest content'), + "Results of sorting on Model method are out of order." + ) + + def testChangeListSortingModelAdmin(self): + """ + Ensure we can sort on a list_display field that is a ModelAdmin method + (colunn 4 is 'modeladmin_year' in ArticleAdmin) + """ + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 4}) + self.assertEqual(response.status_code, 200) + self.assertTrue( + response.content.index('Oldest content') < response.content.index('Middle content') and + response.content.index('Middle content') < response.content.index('Newest content'), + "Results of sorting on ModelAdmin method are out of order." + ) + + def testLimitedFilter(self): + """Ensure admin changelist filters do not contain objects excluded via limit_choices_to.""" + response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit) + self.assertEqual(response.status_code, 200) + self.assertTrue( + '<div id="changelist-filter">' in response.content, + "Expected filter not found in changelist view." + ) + self.assertFalse( + '<a href="?color__id__exact=3">Blue</a>' in response.content, + "Changelist filter not correctly limited by limit_choices_to." + ) + + def testIncorrectLookupParameters(self): + """Ensure incorrect lookup parameters are handled gracefully.""" + response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'notarealfield': '5'}) + self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit) + response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'}) + self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit) + + def testIsNullLookups(self): + """Ensure is_null is handled correctly.""" + Article.objects.create(title="I Could Go Anywhere", content="Versatile", date=datetime.datetime.now()) + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit) + self.assertTrue('4 articles' in response.content, '"4 articles" missing from response') + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': 'false'}) + self.assertTrue('3 articles' in response.content, '"3 articles" missing from response') + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': 'true'}) + self.assertTrue('1 article' in response.content, '"1 article" missing from response') + + def testLogoutAndPasswordChangeURLs(self): + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit) + self.assertFalse('<a href="/test_admin/%s/logout/">' % self.urlbit not in response.content) + self.assertFalse('<a href="/test_admin/%s/password_change/">' % self.urlbit not in response.content) + + def testNamedGroupFieldChoicesChangeList(self): + """ + Ensures the admin changelist shows correct values in the relevant column + for rows corresponding to instances of a model in which a named group + has been used in the choices option of a field. + """ + response = self.client.get('/test_admin/%s/admin_views/fabric/' % self.urlbit) + self.assertEqual(response.status_code, 200) + self.assertTrue( + '<a href="1/">Horizontal</a>' in response.content and + '<a href="2/">Vertical</a>' in response.content, + "Changelist table isn't showing the right human-readable values set by a model field 'choices' option named group." + ) + + def testNamedGroupFieldChoicesFilter(self): + """ + Ensures the filter UI shows correctly when at least one named group has + been used in the choices option of a model field. + """ + response = self.client.get('/test_admin/%s/admin_views/fabric/' % self.urlbit) + self.assertEqual(response.status_code, 200) + self.assertTrue( + '<div id="changelist-filter">' in response.content, + "Expected filter not found in changelist view." + ) + self.assertTrue( + '<a href="?surface__exact=x">Horizontal</a>' in response.content and + '<a href="?surface__exact=y">Vertical</a>' in response.content, + "Changelist filter isn't showing options contained inside a model field 'choices' option named group." + ) + + def testChangeListNullBooleanDisplay(self): + Post.objects.create(public=None) + # This hard-codes the URl because it'll fail if it runs + # against the 'admin2' custom admin (which doesn't have the + # Post model). + response = self.client.get("/test_admin/admin/admin_views/post/") + self.assertTrue('icon-unknown.gif' in response.content) + + def testI18NLanguageNonEnglishDefault(self): + """ + Check if the Javascript i18n view returns an empty language catalog + if the default language is non-English but the selected language + is English. See #13388 and #3594 for more details. + """ + settings.LANGUAGE_CODE = 'fr' + activate('en-us') + response = self.client.get('/test_admin/admin/jsi18n/') + self.assertNotContains(response, 'Choisir une heure') + deactivate() + + def testI18NLanguageNonEnglishFallback(self): + """ + Makes sure that the fallback language is still working properly + in cases where the selected language cannot be found. + """ + settings.LANGUAGE_CODE = 'fr' + activate('none') + response = self.client.get('/test_admin/admin/jsi18n/') + self.assertContains(response, 'Choisir une heure') + deactivate() + + def test_disallowed_filtering(self): + self.assertRaises(SuspiciousOperation, + self.client.get, "/test_admin/admin/admin_views/album/?owner__email__startswith=fuzzy" + ) + +class SaveAsTests(TestCase): + fixtures = ['admin-views-users.xml','admin-views-person.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def test_save_as_duplication(self): + """Ensure save as actually creates a new person""" + post_data = {'_saveasnew':'', 'name':'John M', 'gender':1} + response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data) + self.assertEqual(len(Person.objects.filter(name='John M')), 1) + self.assertEqual(len(Person.objects.filter(id=1)), 1) + + def test_save_as_display(self): + """ + Ensure that 'save as' is displayed when activated and after submitting + invalid data aside save_as_new will not show us a form to overwrite the + initial model. + """ + response = self.client.get('/test_admin/admin/admin_views/person/1/') + self.assert_(response.context['save_as']) + post_data = {'_saveasnew':'', 'name':'John M', 'gender':3, 'alive':'checked'} + response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data) + self.assertEqual(response.context['form_url'], '../add/') + +class CustomModelAdminTest(AdminViewBasicTest): + urlbit = "admin2" + + def testCustomAdminSiteLoginTemplate(self): + self.client.logout() + request = self.client.get('/test_admin/admin2/') + self.assertTemplateUsed(request, 'custom_admin/login.html') + self.assert_('Hello from a custom login template' in request.content) + + def testCustomAdminSiteLogoutTemplate(self): + request = self.client.get('/test_admin/admin2/logout/') + self.assertTemplateUsed(request, 'custom_admin/logout.html') + self.assert_('Hello from a custom logout template' in request.content) + + def testCustomAdminSiteIndexViewAndTemplate(self): + request = self.client.get('/test_admin/admin2/') + self.assertTemplateUsed(request, 'custom_admin/index.html') + self.assert_('Hello from a custom index template *bar*' in request.content) + + def testCustomAdminSitePasswordChangeTemplate(self): + request = self.client.get('/test_admin/admin2/password_change/') + self.assertTemplateUsed(request, 'custom_admin/password_change_form.html') + self.assert_('Hello from a custom password change form template' in request.content) + + def testCustomAdminSitePasswordChangeDoneTemplate(self): + request = self.client.get('/test_admin/admin2/password_change/done/') + self.assertTemplateUsed(request, 'custom_admin/password_change_done.html') + self.assert_('Hello from a custom password change done template' in request.content) + + def testCustomAdminSiteView(self): + self.client.login(username='super', password='secret') + response = self.client.get('/test_admin/%s/my_view/' % self.urlbit) + self.assert_(response.content == "Django is a magical pony!", response.content) + +def get_perm(Model, perm): + """Return the permission object, for the Model""" + ct = ContentType.objects.get_for_model(Model) + return Permission.objects.get(content_type=ct, codename=perm) + +class AdminViewPermissionsTest(TestCase): + """Tests for Admin Views Permissions.""" + + fixtures = ['admin-views-users.xml'] + + def setUp(self): + """Test setup.""" + # Setup permissions, for our users who can add, change, and delete. + # We can't put this into the fixture, because the content type id + # and the permission id could be different on each run of the test. + + opts = Article._meta + + # User who can add Articles + add_user = User.objects.get(username='adduser') + add_user.user_permissions.add(get_perm(Article, + opts.get_add_permission())) + + # User who can change Articles + change_user = User.objects.get(username='changeuser') + change_user.user_permissions.add(get_perm(Article, + opts.get_change_permission())) + + # User who can delete Articles + delete_user = User.objects.get(username='deleteuser') + delete_user.user_permissions.add(get_perm(Article, + opts.get_delete_permission())) + + delete_user.user_permissions.add(get_perm(Section, + Section._meta.get_delete_permission())) + + # login POST dicts + self.super_login = { + LOGIN_FORM_KEY: 1, + 'username': 'super', + 'password': 'secret'} + self.super_email_login = { + LOGIN_FORM_KEY: 1, + 'username': 'super@example.com', + 'password': 'secret'} + self.super_email_bad_login = { + LOGIN_FORM_KEY: 1, + 'username': 'super@example.com', + 'password': 'notsecret'} + self.adduser_login = { + LOGIN_FORM_KEY: 1, + 'username': 'adduser', + 'password': 'secret'} + self.changeuser_login = { + LOGIN_FORM_KEY: 1, + 'username': 'changeuser', + 'password': 'secret'} + self.deleteuser_login = { + LOGIN_FORM_KEY: 1, + 'username': 'deleteuser', + 'password': 'secret'} + self.joepublic_login = { + LOGIN_FORM_KEY: 1, + 'username': 'joepublic', + 'password': 'secret'} + self.no_username_login = { + LOGIN_FORM_KEY: 1, + 'password': 'secret'} + + def testLogin(self): + """ + Make sure only staff members can log in. + + Successful posts to the login page will redirect to the orignal url. + Unsuccessfull attempts will continue to render the login page with + a 200 status code. + """ + # Super User + request = self.client.get('/test_admin/admin/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.super_login) + self.assertRedirects(login, '/test_admin/admin/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + + # Test if user enters e-mail address + request = self.client.get('/test_admin/admin/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.super_email_login) + self.assertContains(login, "Your e-mail address is not your username") + # only correct passwords get a username hint + login = self.client.post('/test_admin/admin/', self.super_email_bad_login) + self.assertContains(login, "Please enter a correct username and password") + new_user = User(username='jondoe', password='secret', email='super@example.com') + new_user.save() + # check to ensure if there are multiple e-mail addresses a user doesn't get a 500 + login = self.client.post('/test_admin/admin/', self.super_email_login) + self.assertContains(login, "Please enter a correct username and password") + + # Add User + request = self.client.get('/test_admin/admin/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.adduser_login) + self.assertRedirects(login, '/test_admin/admin/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + + # Change User + request = self.client.get('/test_admin/admin/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.changeuser_login) + self.assertRedirects(login, '/test_admin/admin/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + + # Delete User + request = self.client.get('/test_admin/admin/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.deleteuser_login) + self.assertRedirects(login, '/test_admin/admin/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + + # Regular User should not be able to login. + request = self.client.get('/test_admin/admin/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.joepublic_login) + self.assertEqual(login.status_code, 200) + self.assertContains(login, "Please enter a correct username and password.") + + # Requests without username should not return 500 errors. + request = self.client.get('/test_admin/admin/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.no_username_login) + self.assertEqual(login.status_code, 200) + form = login.context[0].get('form') + self.assert_(login.context[0].get('error_message')) + + def testLoginSuccessfullyRedirectsToOriginalUrl(self): + request = self.client.get('/test_admin/admin/') + self.assertEqual(request.status_code, 200) + query_string = 'the-answer=42' + redirect_url = '/test_admin/admin/?%s' % query_string + new_next = {REDIRECT_FIELD_NAME: redirect_url} + login = self.client.post('/test_admin/admin/', dict(self.super_login, **new_next), QUERY_STRING=query_string) + self.assertRedirects(login, redirect_url) + + def testAddView(self): + """Test add view restricts access and actually adds items.""" + + add_dict = {'title' : 'Døm ikke', + 'content': '<p>great article</p>', + 'date_0': '2008-03-18', 'date_1': '10:54:39', + 'section': 1} + + # Change User should not have access to add articles + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.changeuser_login) + # make sure the view removes test cookie + self.assertEqual(self.client.session.test_cookie_worked(), False) + request = self.client.get('/test_admin/admin/admin_views/article/add/') + self.assertEqual(request.status_code, 403) + # Try POST just to make sure + post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) + self.assertEqual(post.status_code, 403) + self.assertEqual(Article.objects.all().count(), 3) + self.client.get('/test_admin/admin/logout/') + + # Add user may login and POST to add view, then redirect to admin root + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.adduser_login) + addpage = self.client.get('/test_admin/admin/admin_views/article/add/') + self.assertEqual(addpage.status_code, 200) + change_list_link = '<a href="../">Articles</a> ›' + self.assertFalse(change_list_link in addpage.content, + 'User restricted to add permission is given link to change list view in breadcrumbs.') + post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) + self.assertRedirects(post, '/test_admin/admin/') + self.assertEqual(Article.objects.all().count(), 4) + self.client.get('/test_admin/admin/logout/') + + # Super can add too, but is redirected to the change list view + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.super_login) + addpage = self.client.get('/test_admin/admin/admin_views/article/add/') + self.assertEqual(addpage.status_code, 200) + self.assertFalse(change_list_link not in addpage.content, + 'Unrestricted user is not given link to change list view in breadcrumbs.') + post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) + self.assertRedirects(post, '/test_admin/admin/admin_views/article/') + self.assertEqual(Article.objects.all().count(), 5) + self.client.get('/test_admin/admin/logout/') + + # 8509 - if a normal user is already logged in, it is possible + # to change user into the superuser without error + login = self.client.login(username='joepublic', password='secret') + # Check and make sure that if user expires, data still persists + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.super_login) + # make sure the view removes test cookie + self.assertEqual(self.client.session.test_cookie_worked(), False) + + def testChangeView(self): + """Change view should restrict access and allow users to edit items.""" + + change_dict = {'title' : 'Ikke fordømt', + 'content': '<p>edited article</p>', + 'date_0': '2008-03-18', 'date_1': '10:54:39', + 'section': 1} + + # add user shoud not be able to view the list of article or change any of them + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.adduser_login) + request = self.client.get('/test_admin/admin/admin_views/article/') + self.assertEqual(request.status_code, 403) + request = self.client.get('/test_admin/admin/admin_views/article/1/') + self.assertEqual(request.status_code, 403) + post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) + self.assertEqual(post.status_code, 403) + self.client.get('/test_admin/admin/logout/') + + # change user can view all items and edit them + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.changeuser_login) + request = self.client.get('/test_admin/admin/admin_views/article/') + self.assertEqual(request.status_code, 200) + request = self.client.get('/test_admin/admin/admin_views/article/1/') + self.assertEqual(request.status_code, 200) + post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) + self.assertRedirects(post, '/test_admin/admin/admin_views/article/') + self.assertEqual(Article.objects.get(pk=1).content, '<p>edited article</p>') + + # one error in form should produce singular error message, multiple errors plural + change_dict['title'] = '' + post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) + self.assertEqual(request.status_code, 200) + self.assertTrue('Please correct the error below.' in post.content, + 'Singular error message not found in response to post with one error.') + change_dict['content'] = '' + post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) + self.assertEqual(request.status_code, 200) + self.assertTrue('Please correct the errors below.' in post.content, + 'Plural error message not found in response to post with multiple errors.') + self.client.get('/test_admin/admin/logout/') + + def testCustomModelAdminTemplates(self): + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.super_login) + + # Test custom change list template with custom extra context + request = self.client.get('/test_admin/admin/admin_views/customarticle/') + self.assertEqual(request.status_code, 200) + self.assert_("var hello = 'Hello!';" in request.content) + self.assertTemplateUsed(request, 'custom_admin/change_list.html') + + # Test custom add form template + request = self.client.get('/test_admin/admin/admin_views/customarticle/add/') + self.assertTemplateUsed(request, 'custom_admin/add_form.html') + + # Add an article so we can test delete, change, and history views + post = self.client.post('/test_admin/admin/admin_views/customarticle/add/', { + 'content': '<p>great article</p>', + 'date_0': '2008-03-18', + 'date_1': '10:54:39' + }) + self.assertRedirects(post, '/test_admin/admin/admin_views/customarticle/') + self.assertEqual(CustomArticle.objects.all().count(), 1) + + # Test custom delete, change, and object history templates + # Test custom change form template + request = self.client.get('/test_admin/admin/admin_views/customarticle/1/') + self.assertTemplateUsed(request, 'custom_admin/change_form.html') + request = self.client.get('/test_admin/admin/admin_views/customarticle/1/delete/') + self.assertTemplateUsed(request, 'custom_admin/delete_confirmation.html') + request = self.client.post('/test_admin/admin/admin_views/customarticle/', data={ + 'index': 0, + 'action': ['delete_selected'], + '_selected_action': ['1'], + }) + self.assertTemplateUsed(request, 'custom_admin/delete_selected_confirmation.html') + request = self.client.get('/test_admin/admin/admin_views/customarticle/1/history/') + self.assertTemplateUsed(request, 'custom_admin/object_history.html') + + self.client.get('/test_admin/admin/logout/') + + def testDeleteView(self): + """Delete view should restrict access and actually delete items.""" + + delete_dict = {'post': 'yes'} + + # add user shoud not be able to delete articles + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.adduser_login) + request = self.client.get('/test_admin/admin/admin_views/article/1/delete/') + self.assertEqual(request.status_code, 403) + post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict) + self.assertEqual(post.status_code, 403) + self.assertEqual(Article.objects.all().count(), 3) + self.client.get('/test_admin/admin/logout/') + + # Delete user can delete + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.deleteuser_login) + response = self.client.get('/test_admin/admin/admin_views/section/1/delete/') + # test response contains link to related Article + self.assertContains(response, "admin_views/article/1/") + + response = self.client.get('/test_admin/admin/admin_views/article/1/delete/') + self.assertEqual(response.status_code, 200) + post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict) + self.assertRedirects(post, '/test_admin/admin/') + self.assertEqual(Article.objects.all().count(), 2) + article_ct = ContentType.objects.get_for_model(Article) + logged = LogEntry.objects.get(content_type=article_ct, action_flag=DELETION) + self.assertEqual(logged.object_id, u'1') + self.client.get('/test_admin/admin/logout/') + + def testDisabledPermissionsWhenLoggedIn(self): + self.client.login(username='super', password='secret') + superuser = User.objects.get(username='super') + superuser.is_active = False + superuser.save() + + response = self.client.get('/test_admin/admin/') + self.assertContains(response, 'id="login-form"') + self.assertNotContains(response, 'Log out') + + response = self.client.get('/test_admin/admin/secure-view/') + self.assertContains(response, 'id="login-form"') + + +class AdminViewDeletedObjectsTest(TestCase): + fixtures = ['admin-views-users.xml', 'deleted-objects.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def test_nesting(self): + """ + Objects should be nested to display the relationships that + cause them to be scheduled for deletion. + """ + pattern = re.compile(r"""<li>Plot: <a href=".+/admin_views/plot/1/">World Domination</a>\s*<ul>\s*<li>Plot details: <a href=".+/admin_views/plotdetails/1/">almost finished</a>""") + response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1)) + self.assertTrue(pattern.search(response.content)) + + def test_cyclic(self): + """ + Cyclic relationships should still cause each object to only be + listed once. + + """ + one = """<li>Cyclic one: <a href="/test_admin/admin/admin_views/cyclicone/1/">I am recursive</a>""" + two = """<li>Cyclic two: <a href="/test_admin/admin/admin_views/cyclictwo/1/">I am recursive too</a>""" + response = self.client.get('/test_admin/admin/admin_views/cyclicone/%s/delete/' % quote(1)) + + self.assertContains(response, one, 1) + self.assertContains(response, two, 1) + + def test_perms_needed(self): + self.client.logout() + delete_user = User.objects.get(username='deleteuser') + delete_user.user_permissions.add(get_perm(Plot, + Plot._meta.get_delete_permission())) + + self.assertTrue(self.client.login(username='deleteuser', + password='secret')) + + response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(1)) + self.assertContains(response, "your account doesn't have permission to delete the following types of objects") + self.assertContains(response, "<li>plot details</li>") + + + def test_not_registered(self): + should_contain = """<li>Secret hideout: underground bunker""" + response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1)) + self.assertContains(response, should_contain, 1) + + def test_multiple_fkeys_to_same_model(self): + """ + If a deleted object has two relationships from another model, + both of those should be followed in looking for related + objects to delete. + + """ + should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/1/">World Domination</a>""" + response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1)) + self.assertContains(response, should_contain) + response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2)) + self.assertContains(response, should_contain) + + def test_multiple_fkeys_to_same_instance(self): + """ + If a deleted object has two relationships pointing to it from + another object, the other object should still only be listed + once. + + """ + should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/2/">World Peace</a></li>""" + response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2)) + self.assertContains(response, should_contain, 1) + + def test_inheritance(self): + """ + In the case of an inherited model, if either the child or + parent-model instance is deleted, both instances are listed + for deletion, as well as any relationships they have. + + """ + should_contain = [ + """<li>Villain: <a href="/test_admin/admin/admin_views/villain/3/">Bob</a>""", + """<li>Super villain: <a href="/test_admin/admin/admin_views/supervillain/3/">Bob</a>""", + """<li>Secret hideout: floating castle""", + """<li>Super secret hideout: super floating castle!""" + ] + response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(3)) + for should in should_contain: + self.assertContains(response, should, 1) + response = self.client.get('/test_admin/admin/admin_views/supervillain/%s/delete/' % quote(3)) + for should in should_contain: + self.assertContains(response, should, 1) + + def test_generic_relations(self): + """ + If a deleted object has GenericForeignKeys pointing to it, + those objects should be listed for deletion. + + """ + plot = Plot.objects.get(pk=3) + tag = FunkyTag.objects.create(content_object=plot, name='hott') + should_contain = """<li>Funky tag: hott""" + response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(3)) + self.assertContains(response, should_contain) + +class AdminViewStringPrimaryKeyTest(TestCase): + fixtures = ['admin-views-users.xml', 'string-primary-key.xml'] + + def __init__(self, *args): + super(AdminViewStringPrimaryKeyTest, self).__init__(*args) + self.pk = """abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 -_.!~*'() ;/?:@&=+$, <>#%" {}|\^[]`""" + + def setUp(self): + self.client.login(username='super', password='secret') + content_type_pk = ContentType.objects.get_for_model(ModelWithStringPrimaryKey).pk + LogEntry.objects.log_action(100, content_type_pk, self.pk, self.pk, 2, change_message='') + + def tearDown(self): + self.client.logout() + + def test_get_history_view(self): + "Retrieving the history for the object using urlencoded form of primary key should work" + response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/history/' % quote(self.pk)) + self.assertContains(response, escape(self.pk)) + self.assertEqual(response.status_code, 200) + + def test_get_change_view(self): + "Retrieving the object using urlencoded form of primary key should work" + response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(self.pk)) + self.assertContains(response, escape(self.pk)) + self.assertEqual(response.status_code, 200) + + def test_changelist_to_changeform_link(self): + "The link from the changelist referring to the changeform of the object should be quoted" + response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/') + should_contain = """<th><a href="%s/">%s</a></th></tr>""" % (quote(self.pk), escape(self.pk)) + self.assertContains(response, should_contain) + + def test_recentactions_link(self): + "The link from the recent actions list referring to the changeform of the object should be quoted" + response = self.client.get('/test_admin/admin/') + should_contain = """<a href="admin_views/modelwithstringprimarykey/%s/">%s</a>""" % (quote(self.pk), escape(self.pk)) + self.assertContains(response, should_contain) + + def test_recentactions_without_content_type(self): + "If a LogEntry is missing content_type it will not display it in span tag under the hyperlink." + response = self.client.get('/test_admin/admin/') + should_contain = """<a href="admin_views/modelwithstringprimarykey/%s/">%s</a>""" % (quote(self.pk), escape(self.pk)) + self.assertContains(response, should_contain) + should_contain = "Model with string primary key" # capitalized in Recent Actions + self.assertContains(response, should_contain) + logentry = LogEntry.objects.get(content_type__name__iexact=should_contain) + # http://code.djangoproject.com/ticket/10275 + # if the log entry doesn't have a content type it should still be + # possible to view the Recent Actions part + logentry.content_type = None + logentry.save() + + counted_presence_before = response.content.count(should_contain) + response = self.client.get('/test_admin/admin/') + counted_presence_after = response.content.count(should_contain) + self.assertEquals(counted_presence_before - 1, + counted_presence_after) + + def test_deleteconfirmation_link(self): + "The link from the delete confirmation page referring back to the changeform of the object should be quoted" + response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk)) + # this URL now comes through reverse(), thus iri_to_uri encoding + should_contain = """/%s/">%s</a>""" % (iri_to_uri(quote(self.pk)), escape(self.pk)) + self.assertContains(response, should_contain) + + def test_url_conflicts_with_add(self): + "A model with a primary key that ends with add should be visible" + add_model = ModelWithStringPrimaryKey(id="i have something to add") + add_model.save() + response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(add_model.pk)) + should_contain = """<h1>Change model with string primary key</h1>""" + self.assertContains(response, should_contain) + + def test_url_conflicts_with_delete(self): + "A model with a primary key that ends with delete should be visible" + delete_model = ModelWithStringPrimaryKey(id="delete") + delete_model.save() + response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(delete_model.pk)) + should_contain = """<h1>Change model with string primary key</h1>""" + self.assertContains(response, should_contain) + + def test_url_conflicts_with_history(self): + "A model with a primary key that ends with history should be visible" + history_model = ModelWithStringPrimaryKey(id="history") + history_model.save() + response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(history_model.pk)) + should_contain = """<h1>Change model with string primary key</h1>""" + self.assertContains(response, should_contain) + + +class SecureViewTest(TestCase): + fixtures = ['admin-views-users.xml'] + + def setUp(self): + # login POST dicts + self.super_login = { + LOGIN_FORM_KEY: 1, + 'username': 'super', + 'password': 'secret'} + self.super_email_login = { + LOGIN_FORM_KEY: 1, + 'username': 'super@example.com', + 'password': 'secret'} + self.super_email_bad_login = { + LOGIN_FORM_KEY: 1, + 'username': 'super@example.com', + 'password': 'notsecret'} + self.adduser_login = { + LOGIN_FORM_KEY: 1, + 'username': 'adduser', + 'password': 'secret'} + self.changeuser_login = { + LOGIN_FORM_KEY: 1, + 'username': 'changeuser', + 'password': 'secret'} + self.deleteuser_login = { + LOGIN_FORM_KEY: 1, + 'username': 'deleteuser', + 'password': 'secret'} + self.joepublic_login = { + LOGIN_FORM_KEY: 1, + 'username': 'joepublic', + 'password': 'secret'} + + def tearDown(self): + self.client.logout() + + def test_secure_view_shows_login_if_not_logged_in(self): + "Ensure that we see the login form" + response = self.client.get('/test_admin/admin/secure-view/' ) + self.assertTemplateUsed(response, 'admin/login.html') + + def test_secure_view_login_successfully_redirects_to_original_url(self): + request = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(request.status_code, 200) + query_string = 'the-answer=42' + redirect_url = '/test_admin/admin/secure-view/?%s' % query_string + new_next = {REDIRECT_FIELD_NAME: redirect_url} + login = self.client.post('/test_admin/admin/secure-view/', dict(self.super_login, **new_next), QUERY_STRING=query_string) + self.assertRedirects(login, redirect_url) + + def test_staff_member_required_decorator_works_as_per_admin_login(self): + """ + Make sure only staff members can log in. + + Successful posts to the login page will redirect to the orignal url. + Unsuccessfull attempts will continue to render the login page with + a 200 status code. + """ + # Super User + request = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/secure-view/', self.super_login) + self.assertRedirects(login, '/test_admin/admin/secure-view/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + # make sure the view removes test cookie + self.assertEqual(self.client.session.test_cookie_worked(), False) + + # Test if user enters e-mail address + request = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login) + self.assertContains(login, "Your e-mail address is not your username") + # only correct passwords get a username hint + login = self.client.post('/test_admin/admin/secure-view/', self.super_email_bad_login) + self.assertContains(login, "Please enter a correct username and password") + new_user = User(username='jondoe', password='secret', email='super@example.com') + new_user.save() + # check to ensure if there are multiple e-mail addresses a user doesn't get a 500 + login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login) + self.assertContains(login, "Please enter a correct username and password") + + # Add User + request = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/secure-view/', self.adduser_login) + self.assertRedirects(login, '/test_admin/admin/secure-view/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + + # Change User + request = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/secure-view/', self.changeuser_login) + self.assertRedirects(login, '/test_admin/admin/secure-view/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + + # Delete User + request = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/secure-view/', self.deleteuser_login) + self.assertRedirects(login, '/test_admin/admin/secure-view/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + + # Regular User should not be able to login. + request = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/secure-view/', self.joepublic_login) + self.assertEqual(login.status_code, 200) + # Login.context is a list of context dicts we just need to check the first one. + self.assert_(login.context[0].get('error_message')) + + # 8509 - if a normal user is already logged in, it is possible + # to change user into the superuser without error + login = self.client.login(username='joepublic', password='secret') + # Check and make sure that if user expires, data still persists + self.client.get('/test_admin/admin/secure-view/') + self.client.post('/test_admin/admin/secure-view/', self.super_login) + # make sure the view removes test cookie + self.assertEqual(self.client.session.test_cookie_worked(), False) + +class AdminViewUnicodeTest(TestCase): + fixtures = ['admin-views-unicode.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def testUnicodeEdit(self): + """ + A test to ensure that POST on edit_view handles non-ascii characters. + """ + post_data = { + "name": u"Test lærdommer", + # inline data + "chapter_set-TOTAL_FORMS": u"6", + "chapter_set-INITIAL_FORMS": u"3", + "chapter_set-MAX_NUM_FORMS": u"0", + "chapter_set-0-id": u"1", + "chapter_set-0-title": u"Norske bostaver æøå skaper problemer", + "chapter_set-0-content": u"<p>Svært frustrerende med UnicodeDecodeError</p>", + "chapter_set-1-id": u"2", + "chapter_set-1-title": u"Kjærlighet.", + "chapter_set-1-content": u"<p>La kjærligheten til de lidende seire.</p>", + "chapter_set-2-id": u"3", + "chapter_set-2-title": u"Need a title.", + "chapter_set-2-content": u"<p>Newest content</p>", + "chapter_set-3-id": u"", + "chapter_set-3-title": u"", + "chapter_set-3-content": u"", + "chapter_set-4-id": u"", + "chapter_set-4-title": u"", + "chapter_set-4-content": u"", + "chapter_set-5-id": u"", + "chapter_set-5-title": u"", + "chapter_set-5-content": u"", + } + + response = self.client.post('/test_admin/admin/admin_views/book/1/', post_data) + self.assertEqual(response.status_code, 302) # redirect somewhere + + def testUnicodeDelete(self): + """ + Ensure that the delete_view handles non-ascii characters + """ + delete_dict = {'post': 'yes'} + response = self.client.get('/test_admin/admin/admin_views/book/1/delete/') + self.assertEqual(response.status_code, 200) + response = self.client.post('/test_admin/admin/admin_views/book/1/delete/', delete_dict) + self.assertRedirects(response, '/test_admin/admin/admin_views/book/') + + +class AdminViewListEditable(TestCase): + fixtures = ['admin-views-users.xml', 'admin-views-person.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def test_inheritance(self): + Podcast.objects.create(name="This Week in Django", + release_date=datetime.date.today()) + response = self.client.get('/test_admin/admin/admin_views/podcast/') + self.assertEqual(response.status_code, 200) + + def test_inheritance_2(self): + Vodcast.objects.create(name="This Week in Django", released=True) + response = self.client.get('/test_admin/admin/admin_views/vodcast/') + self.assertEqual(response.status_code, 200) + + def test_custom_pk(self): + Language.objects.create(iso='en', name='English', english_name='English') + response = self.client.get('/test_admin/admin/admin_views/language/') + self.assertEqual(response.status_code, 200) + + def test_changelist_input_html(self): + response = self.client.get('/test_admin/admin/admin_views/person/') + # 2 inputs per object(the field and the hidden id field) = 6 + # 3 management hidden fields = 3 + # 4 action inputs (3 regular checkboxes, 1 checkbox to select all) + # main form submit button = 1 + # search field and search submit button = 2 + # CSRF field = 1 + # field to track 'select all' across paginated views = 1 + # 6 + 3 + 4 + 1 + 2 + 1 + 1 = 18 inputs + self.assertEqual(response.content.count("<input"), 18) + # 1 select per object = 3 selects + self.assertEqual(response.content.count("<select"), 4) + + def test_post_messages(self): + # Ticket 12707: Saving inline editable should not show admin + # action warnings + data = { + "form-TOTAL_FORMS": "3", + "form-INITIAL_FORMS": "3", + "form-MAX_NUM_FORMS": "0", + + "form-0-gender": "1", + "form-0-id": "1", + + "form-1-gender": "2", + "form-1-id": "2", + + "form-2-alive": "checked", + "form-2-gender": "1", + "form-2-id": "3", + + "_save": "Save", + } + response = self.client.post('/test_admin/admin/admin_views/person/', + data, follow=True) + self.assertEqual(len(response.context['messages']), 1) + + def test_post_submission(self): + data = { + "form-TOTAL_FORMS": "3", + "form-INITIAL_FORMS": "3", + "form-MAX_NUM_FORMS": "0", + + "form-0-gender": "1", + "form-0-id": "1", + + "form-1-gender": "2", + "form-1-id": "2", + + "form-2-alive": "checked", + "form-2-gender": "1", + "form-2-id": "3", + + "_save": "Save", + } + self.client.post('/test_admin/admin/admin_views/person/', data) + + self.assertEqual(Person.objects.get(name="John Mauchly").alive, False) + self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 2) + + # test a filtered page + data = { + "form-TOTAL_FORMS": "2", + "form-INITIAL_FORMS": "2", + "form-MAX_NUM_FORMS": "0", + + "form-0-id": "1", + "form-0-gender": "1", + "form-0-alive": "checked", + + "form-1-id": "3", + "form-1-gender": "1", + "form-1-alive": "checked", + + "_save": "Save", + } + self.client.post('/test_admin/admin/admin_views/person/?gender__exact=1', data) + + self.assertEqual(Person.objects.get(name="John Mauchly").alive, True) + + # test a searched page + data = { + "form-TOTAL_FORMS": "1", + "form-INITIAL_FORMS": "1", + "form-MAX_NUM_FORMS": "0", + + "form-0-id": "1", + "form-0-gender": "1", + + "_save": "Save", + } + self.client.post('/test_admin/admin/admin_views/person/?q=mauchly', data) + + self.assertEqual(Person.objects.get(name="John Mauchly").alive, False) + + def test_non_form_errors(self): + # test if non-form errors are handled; ticket #12716 + data = { + "form-TOTAL_FORMS": "1", + "form-INITIAL_FORMS": "1", + "form-MAX_NUM_FORMS": "0", + + "form-0-id": "2", + "form-0-alive": "1", + "form-0-gender": "2", + + # Ensure that the form processing understands this as a list_editable "Save" + # and not an action "Go". + "_save": "Save", + } + response = self.client.post('/test_admin/admin/admin_views/person/', data) + self.assertContains(response, "Grace is not a Zombie") + + def test_non_form_errors_is_errorlist(self): + # test if non-form errors are correctly handled; ticket #12878 + data = { + "form-TOTAL_FORMS": "1", + "form-INITIAL_FORMS": "1", + "form-MAX_NUM_FORMS": "0", + + "form-0-id": "2", + "form-0-alive": "1", + "form-0-gender": "2", + + "_save": "Save", + } + response = self.client.post('/test_admin/admin/admin_views/person/', data) + non_form_errors = response.context['cl'].formset.non_form_errors() + self.assert_(isinstance(non_form_errors, ErrorList)) + self.assertEqual(str(non_form_errors), str(ErrorList(["Grace is not a Zombie"]))) + + def test_list_editable_ordering(self): + collector = Collector.objects.create(id=1, name="Frederick Clegg") + + Category.objects.create(id=1, order=1, collector=collector) + Category.objects.create(id=2, order=2, collector=collector) + Category.objects.create(id=3, order=0, collector=collector) + Category.objects.create(id=4, order=0, collector=collector) + + # NB: The order values must be changed so that the items are reordered. + data = { + "form-TOTAL_FORMS": "4", + "form-INITIAL_FORMS": "4", + "form-MAX_NUM_FORMS": "0", + + "form-0-order": "14", + "form-0-id": "1", + "form-0-collector": "1", + + "form-1-order": "13", + "form-1-id": "2", + "form-1-collector": "1", + + "form-2-order": "1", + "form-2-id": "3", + "form-2-collector": "1", + + "form-3-order": "0", + "form-3-id": "4", + "form-3-collector": "1", + + # Ensure that the form processing understands this as a list_editable "Save" + # and not an action "Go". + "_save": "Save", + } + response = self.client.post('/test_admin/admin/admin_views/category/', data) + # Successful post will redirect + self.assertEqual(response.status_code, 302) + + # Check that the order values have been applied to the right objects + self.assertEqual(Category.objects.get(id=1).order, 14) + self.assertEqual(Category.objects.get(id=2).order, 13) + self.assertEqual(Category.objects.get(id=3).order, 1) + self.assertEqual(Category.objects.get(id=4).order, 0) + + def test_list_editable_action_submit(self): + # List editable changes should not be executed if the action "Go" button is + # used to submit the form. + data = { + "form-TOTAL_FORMS": "3", + "form-INITIAL_FORMS": "3", + "form-MAX_NUM_FORMS": "0", + + "form-0-gender": "1", + "form-0-id": "1", + + "form-1-gender": "2", + "form-1-id": "2", + + "form-2-alive": "checked", + "form-2-gender": "1", + "form-2-id": "3", + + "index": "0", + "_selected_action": [u'3'], + "action": [u'', u'delete_selected'], + } + self.client.post('/test_admin/admin/admin_views/person/', data) + + self.assertEqual(Person.objects.get(name="John Mauchly").alive, True) + self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 1) + + def test_list_editable_action_choices(self): + # List editable changes should be executed if the "Save" button is + # used to submit the form - any action choices should be ignored. + data = { + "form-TOTAL_FORMS": "3", + "form-INITIAL_FORMS": "3", + "form-MAX_NUM_FORMS": "0", + + "form-0-gender": "1", + "form-0-id": "1", + + "form-1-gender": "2", + "form-1-id": "2", + + "form-2-alive": "checked", + "form-2-gender": "1", + "form-2-id": "3", + + "_save": "Save", + "_selected_action": [u'1'], + "action": [u'', u'delete_selected'], + } + self.client.post('/test_admin/admin/admin_views/person/', data) + + self.assertEqual(Person.objects.get(name="John Mauchly").alive, False) + self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 2) + + + + +class AdminSearchTest(TestCase): + fixtures = ['admin-views-users','multiple-child-classes'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def test_search_on_sibling_models(self): + "Check that a search that mentions sibling models" + response = self.client.get('/test_admin/admin/admin_views/recommendation/?q=bar') + # confirm the search returned 1 object + self.assertContains(response, "\n1 recommendation\n") + +class AdminInheritedInlinesTest(TestCase): + fixtures = ['admin-views-users.xml',] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def testInline(self): + "Ensure that inline models which inherit from a common parent are correctly handled by admin." + + foo_user = u"foo username" + bar_user = u"bar username" + + name_re = re.compile('name="(.*?)"') + + # test the add case + response = self.client.get('/test_admin/admin/admin_views/persona/add/') + names = name_re.findall(response.content) + # make sure we have no duplicate HTML names + self.assertEqual(len(names), len(set(names))) + + # test the add case + post_data = { + "name": u"Test Name", + # inline data + "accounts-TOTAL_FORMS": u"1", + "accounts-INITIAL_FORMS": u"0", + "accounts-MAX_NUM_FORMS": u"0", + "accounts-0-username": foo_user, + "accounts-2-TOTAL_FORMS": u"1", + "accounts-2-INITIAL_FORMS": u"0", + "accounts-2-MAX_NUM_FORMS": u"0", + "accounts-2-0-username": bar_user, + } + + response = self.client.post('/test_admin/admin/admin_views/persona/add/', post_data) + self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(Persona.objects.count(), 1) + self.assertEqual(FooAccount.objects.count(), 1) + self.assertEqual(BarAccount.objects.count(), 1) + self.assertEqual(FooAccount.objects.all()[0].username, foo_user) + self.assertEqual(BarAccount.objects.all()[0].username, bar_user) + self.assertEqual(Persona.objects.all()[0].accounts.count(), 2) + + # test the edit case + + response = self.client.get('/test_admin/admin/admin_views/persona/1/') + names = name_re.findall(response.content) + # make sure we have no duplicate HTML names + self.assertEqual(len(names), len(set(names))) + + post_data = { + "name": u"Test Name", + + "accounts-TOTAL_FORMS": "2", + "accounts-INITIAL_FORMS": u"1", + "accounts-MAX_NUM_FORMS": u"0", + + "accounts-0-username": "%s-1" % foo_user, + "accounts-0-account_ptr": "1", + "accounts-0-persona": "1", + + "accounts-2-TOTAL_FORMS": u"2", + "accounts-2-INITIAL_FORMS": u"1", + "accounts-2-MAX_NUM_FORMS": u"0", + + "accounts-2-0-username": "%s-1" % bar_user, + "accounts-2-0-account_ptr": "2", + "accounts-2-0-persona": "1", + } + response = self.client.post('/test_admin/admin/admin_views/persona/1/', post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Persona.objects.count(), 1) + self.assertEqual(FooAccount.objects.count(), 1) + self.assertEqual(BarAccount.objects.count(), 1) + self.assertEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user) + self.assertEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user) + self.assertEqual(Persona.objects.all()[0].accounts.count(), 2) + +from django.core import mail + +class AdminActionsTest(TestCase): + fixtures = ['admin-views-users.xml', 'admin-views-actions.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def test_model_admin_custom_action(self): + "Tests a custom action defined in a ModelAdmin method" + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action' : 'mail_admin', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) + self.assertEquals(len(mail.outbox), 1) + self.assertEquals(mail.outbox[0].subject, 'Greetings from a ModelAdmin action') + + def test_model_admin_default_delete_action(self): + "Tests the default delete action defined as a ModelAdmin method" + action_data = { + ACTION_CHECKBOX_NAME: [1, 2], + 'action' : 'delete_selected', + 'index': 0, + } + delete_confirmation_data = { + ACTION_CHECKBOX_NAME: [1, 2], + 'action' : 'delete_selected', + 'post': 'yes', + } + confirmation = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) + self.assertContains(confirmation, "Are you sure you want to delete the selected subscriber objects") + self.assertTrue(confirmation.content.count(ACTION_CHECKBOX_NAME) == 2) + response = self.client.post('/test_admin/admin/admin_views/subscriber/', delete_confirmation_data) + self.assertEqual(Subscriber.objects.count(), 0) + + def test_custom_function_mail_action(self): + "Tests a custom action defined in a function" + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action' : 'external_mail', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) + self.assertEquals(len(mail.outbox), 1) + self.assertEquals(mail.outbox[0].subject, 'Greetings from a function action') + + def test_custom_function_action_with_redirect(self): + "Tests a custom action defined in a function" + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action' : 'redirect_to', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) + self.assertEqual(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/') + self.assertEquals(response.context["action_form"], None) + self.assert_( + '<input type="checkbox" class="action-select"' not in response.content, + "Found an unexpected action toggle checkboxbox in response" + ) + self.assert_('action-checkbox-column' not in response.content, + "Found unexpected action-checkbox-column class in response") + + def test_model_without_action_still_has_jquery(self): + "Tests that a ModelAdmin without any actions still gets jQuery included in page" + response = self.client.get('/test_admin/admin/admin_views/oldsubscriber/') + self.assertEquals(response.context["action_form"], None) + self.assert_('jquery.min.js' in response.content, + "jQuery missing from admin pages for model with no admin actions" + ) + + def test_action_column_class(self): + "Tests that the checkbox column class is present in the response" + response = self.client.get('/test_admin/admin/admin_views/subscriber/') + self.assertNotEqual(response.context["action_form"], None) + self.assert_('action-checkbox-column' in response.content, + "Expected an action-checkbox-column in response") + + def test_multiple_actions_form(self): + """ + Test that actions come from the form whose submit button was pressed (#10618). + """ + action_data = { + ACTION_CHECKBOX_NAME: [1], + # Two different actions selected on the two forms... + 'action': ['external_mail', 'delete_selected'], + # ...but we clicked "go" on the top form. + 'index': 0 + } + response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) + + # Send mail, don't delete. + self.assertEquals(len(mail.outbox), 1) + self.assertEquals(mail.outbox[0].subject, 'Greetings from a function action') + + def test_user_message_on_none_selected(self): + """ + User should see a warning when 'Go' is pressed and no items are selected. + """ + action_data = { + ACTION_CHECKBOX_NAME: [], + 'action' : 'delete_selected', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) + msg = """Items must be selected in order to perform actions on them. No items have been changed.""" + self.assertContains(response, msg) + self.assertEqual(Subscriber.objects.count(), 2) + + def test_user_message_on_no_action(self): + """ + User should see a warning when 'Go' is pressed and no action is selected. + """ + action_data = { + ACTION_CHECKBOX_NAME: [1, 2], + 'action' : '', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) + msg = """No action selected.""" + self.assertContains(response, msg) + self.assertEqual(Subscriber.objects.count(), 2) + + def test_selection_counter(self): + """ + Check if the selection counter is there. + """ + response = self.client.get('/test_admin/admin/admin_views/subscriber/') + self.assertContains(response, '0 of 2 selected') + + +class TestCustomChangeList(TestCase): + fixtures = ['admin-views-users.xml'] + urlbit = 'admin' + + def setUp(self): + result = self.client.login(username='super', password='secret') + self.assertEqual(result, True) + + def tearDown(self): + self.client.logout() + + def test_custom_changelist(self): + """ + Validate that a custom ChangeList class can be used (#9749) + """ + # Insert some data + post_data = {"name": u"First Gadget"} + response = self.client.post('/test_admin/%s/admin_views/gadget/add/' % self.urlbit, post_data) + self.assertEqual(response.status_code, 302) # redirect somewhere + # Hit the page once to get messages out of the queue message list + response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit) + # Ensure that that data is still not visible on the page + response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit) + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, 'First Gadget') + + +class TestInlineNotEditable(TestCase): + fixtures = ['admin-views-users.xml'] + + def setUp(self): + result = self.client.login(username='super', password='secret') + self.assertEqual(result, True) + + def tearDown(self): + self.client.logout() + + def test(self): + """ + InlineModelAdmin broken? + """ + response = self.client.get('/test_admin/admin/admin_views/parent/add/') + self.assertEqual(response.status_code, 200) + +class AdminCustomQuerysetTest(TestCase): + fixtures = ['admin-views-users.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + self.pks = [EmptyModel.objects.create().id for i in range(3)] + + def test_changelist_view(self): + response = self.client.get('/test_admin/admin/admin_views/emptymodel/') + for i in self.pks: + if i > 1: + self.assertContains(response, 'Primary key = %s' % i) + else: + self.assertNotContains(response, 'Primary key = %s' % i) + + def test_change_view(self): + for i in self.pks: + response = self.client.get('/test_admin/admin/admin_views/emptymodel/%s/' % i) + if i > 1: + self.assertEqual(response.status_code, 200) + else: + self.assertEqual(response.status_code, 404) + +class AdminInlineFileUploadTest(TestCase): + fixtures = ['admin-views-users.xml', 'admin-views-actions.xml'] + urlbit = 'admin' + + def setUp(self): + self.client.login(username='super', password='secret') + + # Set up test Picture and Gallery. + # These must be set up here instead of in fixtures in order to allow Picture + # to use a NamedTemporaryFile. + tdir = tempfile.gettempdir() + file1 = tempfile.NamedTemporaryFile(suffix=".file1", dir=tdir) + file1.write('a' * (2 ** 21)) + filename = file1.name + file1.close() + g = Gallery(name="Test Gallery") + g.save() + p = Picture(name="Test Picture", image=filename, gallery=g) + p.save() + + def tearDown(self): + self.client.logout() + + def test_inline_file_upload_edit_validation_error_post(self): + """ + Test that inline file uploads correctly display prior data (#10002). + """ + post_data = { + "name": u"Test Gallery", + "pictures-TOTAL_FORMS": u"2", + "pictures-INITIAL_FORMS": u"1", + "pictures-MAX_NUM_FORMS": u"0", + "pictures-0-id": u"1", + "pictures-0-gallery": u"1", + "pictures-0-name": "Test Picture", + "pictures-0-image": "", + "pictures-1-id": "", + "pictures-1-gallery": "1", + "pictures-1-name": "Test Picture 2", + "pictures-1-image": "", + } + response = self.client.post('/test_admin/%s/admin_views/gallery/1/' % self.urlbit, post_data) + self.assertTrue(response._container[0].find("Currently:") > -1) + + +class AdminInlineTests(TestCase): + fixtures = ['admin-views-users.xml'] + + def setUp(self): + self.post_data = { + "name": u"Test Name", + + "widget_set-TOTAL_FORMS": "3", + "widget_set-INITIAL_FORMS": u"0", + "widget_set-MAX_NUM_FORMS": u"0", + "widget_set-0-id": "", + "widget_set-0-owner": "1", + "widget_set-0-name": "", + "widget_set-1-id": "", + "widget_set-1-owner": "1", + "widget_set-1-name": "", + "widget_set-2-id": "", + "widget_set-2-owner": "1", + "widget_set-2-name": "", + + "doohickey_set-TOTAL_FORMS": "3", + "doohickey_set-INITIAL_FORMS": u"0", + "doohickey_set-MAX_NUM_FORMS": u"0", + "doohickey_set-0-owner": "1", + "doohickey_set-0-code": "", + "doohickey_set-0-name": "", + "doohickey_set-1-owner": "1", + "doohickey_set-1-code": "", + "doohickey_set-1-name": "", + "doohickey_set-2-owner": "1", + "doohickey_set-2-code": "", + "doohickey_set-2-name": "", + + "grommet_set-TOTAL_FORMS": "3", + "grommet_set-INITIAL_FORMS": u"0", + "grommet_set-MAX_NUM_FORMS": u"0", + "grommet_set-0-code": "", + "grommet_set-0-owner": "1", + "grommet_set-0-name": "", + "grommet_set-1-code": "", + "grommet_set-1-owner": "1", + "grommet_set-1-name": "", + "grommet_set-2-code": "", + "grommet_set-2-owner": "1", + "grommet_set-2-name": "", + + "whatsit_set-TOTAL_FORMS": "3", + "whatsit_set-INITIAL_FORMS": u"0", + "whatsit_set-MAX_NUM_FORMS": u"0", + "whatsit_set-0-owner": "1", + "whatsit_set-0-index": "", + "whatsit_set-0-name": "", + "whatsit_set-1-owner": "1", + "whatsit_set-1-index": "", + "whatsit_set-1-name": "", + "whatsit_set-2-owner": "1", + "whatsit_set-2-index": "", + "whatsit_set-2-name": "", + + "fancydoodad_set-TOTAL_FORMS": "3", + "fancydoodad_set-INITIAL_FORMS": u"0", + "fancydoodad_set-MAX_NUM_FORMS": u"0", + "fancydoodad_set-0-doodad_ptr": "", + "fancydoodad_set-0-owner": "1", + "fancydoodad_set-0-name": "", + "fancydoodad_set-0-expensive": "on", + "fancydoodad_set-1-doodad_ptr": "", + "fancydoodad_set-1-owner": "1", + "fancydoodad_set-1-name": "", + "fancydoodad_set-1-expensive": "on", + "fancydoodad_set-2-doodad_ptr": "", + "fancydoodad_set-2-owner": "1", + "fancydoodad_set-2-name": "", + "fancydoodad_set-2-expensive": "on", + + "category_set-TOTAL_FORMS": "3", + "category_set-INITIAL_FORMS": "0", + "category_set-MAX_NUM_FORMS": "0", + "category_set-0-order": "", + "category_set-0-id": "", + "category_set-0-collector": "1", + "category_set-1-order": "", + "category_set-1-id": "", + "category_set-1-collector": "1", + "category_set-2-order": "", + "category_set-2-id": "", + "category_set-2-collector": "1", + } + + result = self.client.login(username='super', password='secret') + self.assertEqual(result, True) + self.collector = Collector(pk=1,name='John Fowles') + self.collector.save() + + def tearDown(self): + self.client.logout() + + def test_simple_inline(self): + "A simple model can be saved as inlines" + # First add a new inline + self.post_data['widget_set-0-name'] = "Widget 1" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Widget.objects.count(), 1) + self.assertEqual(Widget.objects.all()[0].name, "Widget 1") + + # Check that the PK link exists on the rendered form + response = self.client.get('/test_admin/admin/admin_views/collector/1/') + self.assertContains(response, 'name="widget_set-0-id"') + + # Now resave that inline + self.post_data['widget_set-INITIAL_FORMS'] = "1" + self.post_data['widget_set-0-id'] = "1" + self.post_data['widget_set-0-name'] = "Widget 1" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Widget.objects.count(), 1) + self.assertEqual(Widget.objects.all()[0].name, "Widget 1") + + # Now modify that inline + self.post_data['widget_set-INITIAL_FORMS'] = "1" + self.post_data['widget_set-0-id'] = "1" + self.post_data['widget_set-0-name'] = "Widget 1 Updated" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Widget.objects.count(), 1) + self.assertEqual(Widget.objects.all()[0].name, "Widget 1 Updated") + + def test_explicit_autofield_inline(self): + "A model with an explicit autofield primary key can be saved as inlines. Regression for #8093" + # First add a new inline + self.post_data['grommet_set-0-name'] = "Grommet 1" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Grommet.objects.count(), 1) + self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1") + + # Check that the PK link exists on the rendered form + response = self.client.get('/test_admin/admin/admin_views/collector/1/') + self.assertContains(response, 'name="grommet_set-0-code"') + + # Now resave that inline + self.post_data['grommet_set-INITIAL_FORMS'] = "1" + self.post_data['grommet_set-0-code'] = "1" + self.post_data['grommet_set-0-name'] = "Grommet 1" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Grommet.objects.count(), 1) + self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1") + + # Now modify that inline + self.post_data['grommet_set-INITIAL_FORMS'] = "1" + self.post_data['grommet_set-0-code'] = "1" + self.post_data['grommet_set-0-name'] = "Grommet 1 Updated" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Grommet.objects.count(), 1) + self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1 Updated") + + def test_char_pk_inline(self): + "A model with a character PK can be saved as inlines. Regression for #10992" + # First add a new inline + self.post_data['doohickey_set-0-code'] = "DH1" + self.post_data['doohickey_set-0-name'] = "Doohickey 1" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(DooHickey.objects.count(), 1) + self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1") + + # Check that the PK link exists on the rendered form + response = self.client.get('/test_admin/admin/admin_views/collector/1/') + self.assertContains(response, 'name="doohickey_set-0-code"') + + # Now resave that inline + self.post_data['doohickey_set-INITIAL_FORMS'] = "1" + self.post_data['doohickey_set-0-code'] = "DH1" + self.post_data['doohickey_set-0-name'] = "Doohickey 1" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(DooHickey.objects.count(), 1) + self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1") + + # Now modify that inline + self.post_data['doohickey_set-INITIAL_FORMS'] = "1" + self.post_data['doohickey_set-0-code'] = "DH1" + self.post_data['doohickey_set-0-name'] = "Doohickey 1 Updated" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(DooHickey.objects.count(), 1) + self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1 Updated") + + def test_integer_pk_inline(self): + "A model with an integer PK can be saved as inlines. Regression for #10992" + # First add a new inline + self.post_data['whatsit_set-0-index'] = "42" + self.post_data['whatsit_set-0-name'] = "Whatsit 1" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Whatsit.objects.count(), 1) + self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1") + + # Check that the PK link exists on the rendered form + response = self.client.get('/test_admin/admin/admin_views/collector/1/') + self.assertContains(response, 'name="whatsit_set-0-index"') + + # Now resave that inline + self.post_data['whatsit_set-INITIAL_FORMS'] = "1" + self.post_data['whatsit_set-0-index'] = "42" + self.post_data['whatsit_set-0-name'] = "Whatsit 1" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Whatsit.objects.count(), 1) + self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1") + + # Now modify that inline + self.post_data['whatsit_set-INITIAL_FORMS'] = "1" + self.post_data['whatsit_set-0-index'] = "42" + self.post_data['whatsit_set-0-name'] = "Whatsit 1 Updated" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Whatsit.objects.count(), 1) + self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1 Updated") + + def test_inherited_inline(self): + "An inherited model can be saved as inlines. Regression for #11042" + # First add a new inline + self.post_data['fancydoodad_set-0-name'] = "Fancy Doodad 1" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(FancyDoodad.objects.count(), 1) + self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1") + + # Check that the PK link exists on the rendered form + response = self.client.get('/test_admin/admin/admin_views/collector/1/') + self.assertContains(response, 'name="fancydoodad_set-0-doodad_ptr"') + + # Now resave that inline + self.post_data['fancydoodad_set-INITIAL_FORMS'] = "1" + self.post_data['fancydoodad_set-0-doodad_ptr'] = "1" + self.post_data['fancydoodad_set-0-name'] = "Fancy Doodad 1" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(FancyDoodad.objects.count(), 1) + self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1") + + # Now modify that inline + self.post_data['fancydoodad_set-INITIAL_FORMS'] = "1" + self.post_data['fancydoodad_set-0-doodad_ptr'] = "1" + self.post_data['fancydoodad_set-0-name'] = "Fancy Doodad 1 Updated" + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(FancyDoodad.objects.count(), 1) + self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1 Updated") + + def test_ordered_inline(self): + """Check that an inline with an editable ordering fields is + updated correctly. Regression for #10922""" + # Create some objects with an initial ordering + Category.objects.create(id=1, order=1, collector=self.collector) + Category.objects.create(id=2, order=2, collector=self.collector) + Category.objects.create(id=3, order=0, collector=self.collector) + Category.objects.create(id=4, order=0, collector=self.collector) + + # NB: The order values must be changed so that the items are reordered. + self.post_data.update({ + "name": "Frederick Clegg", + + "category_set-TOTAL_FORMS": "7", + "category_set-INITIAL_FORMS": "4", + "category_set-MAX_NUM_FORMS": "0", + + "category_set-0-order": "14", + "category_set-0-id": "1", + "category_set-0-collector": "1", + + "category_set-1-order": "13", + "category_set-1-id": "2", + "category_set-1-collector": "1", + + "category_set-2-order": "1", + "category_set-2-id": "3", + "category_set-2-collector": "1", + + "category_set-3-order": "0", + "category_set-3-id": "4", + "category_set-3-collector": "1", + + "category_set-4-order": "", + "category_set-4-id": "", + "category_set-4-collector": "1", + + "category_set-5-order": "", + "category_set-5-id": "", + "category_set-5-collector": "1", + + "category_set-6-order": "", + "category_set-6-id": "", + "category_set-6-collector": "1", + }) + response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) + # Successful post will redirect + self.assertEqual(response.status_code, 302) + + # Check that the order values have been applied to the right objects + self.assertEqual(self.collector.category_set.count(), 4) + self.assertEqual(Category.objects.get(id=1).order, 14) + self.assertEqual(Category.objects.get(id=2).order, 13) + self.assertEqual(Category.objects.get(id=3).order, 1) + self.assertEqual(Category.objects.get(id=4).order, 0) + + +class NeverCacheTests(TestCase): + fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def testAdminIndex(self): + "Check the never-cache status of the main index" + response = self.client.get('/test_admin/admin/') + self.assertEqual(get_max_age(response), 0) + + def testAppIndex(self): + "Check the never-cache status of an application index" + response = self.client.get('/test_admin/admin/admin_views/') + self.assertEqual(get_max_age(response), 0) + + def testModelIndex(self): + "Check the never-cache status of a model index" + response = self.client.get('/test_admin/admin/admin_views/fabric/') + self.assertEqual(get_max_age(response), 0) + + def testModelAdd(self): + "Check the never-cache status of a model add page" + response = self.client.get('/test_admin/admin/admin_views/fabric/add/') + self.assertEqual(get_max_age(response), 0) + + def testModelView(self): + "Check the never-cache status of a model edit page" + response = self.client.get('/test_admin/admin/admin_views/section/1/') + self.assertEqual(get_max_age(response), 0) + + def testModelHistory(self): + "Check the never-cache status of a model history page" + response = self.client.get('/test_admin/admin/admin_views/section/1/history/') + self.assertEqual(get_max_age(response), 0) + + def testModelDelete(self): + "Check the never-cache status of a model delete page" + response = self.client.get('/test_admin/admin/admin_views/section/1/delete/') + self.assertEqual(get_max_age(response), 0) + + def testLogin(self): + "Check the never-cache status of login views" + self.client.logout() + response = self.client.get('/test_admin/admin/') + self.assertEqual(get_max_age(response), 0) + + def testLogout(self): + "Check the never-cache status of logout view" + response = self.client.get('/test_admin/admin/logout/') + self.assertEqual(get_max_age(response), 0) + + def testPasswordChange(self): + "Check the never-cache status of the password change view" + self.client.logout() + response = self.client.get('/test_admin/password_change/') + self.assertEqual(get_max_age(response), None) + + def testPasswordChangeDone(self): + "Check the never-cache status of the password change done view" + response = self.client.get('/test_admin/admin/password_change/done/') + self.assertEqual(get_max_age(response), None) + + def testJsi18n(self): + "Check the never-cache status of the Javascript i18n view" + response = self.client.get('/test_admin/admin/jsi18n/') + self.assertEqual(get_max_age(response), None) + + +class ReadonlyTest(TestCase): + fixtures = ['admin-views-users.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def test_readonly_get(self): + response = self.client.get('/test_admin/admin/admin_views/post/add/') + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, 'name="posted"') + # 3 fields + 2 submit buttons + 4 inline management form fields, + 2 + # hidden fields for inlines + 1 field for the inline + 2 empty form + self.assertEqual(response.content.count("<input"), 14) + self.assertContains(response, formats.localize(datetime.date.today())) + self.assertContains(response, + "<label>Awesomeness level:</label>") + self.assertContains(response, "Very awesome.") + self.assertContains(response, "Unkown coolness.") + self.assertContains(response, "foo") + self.assertContains(response, + formats.localize(datetime.date.today() - datetime.timedelta(days=7)) + ) + + self.assertContains(response, '<div class="form-row coolness">') + self.assertContains(response, '<div class="form-row awesomeness_level">') + self.assertContains(response, '<div class="form-row posted">') + self.assertContains(response, '<div class="form-row value">') + self.assertContains(response, '<div class="form-row ">') + + p = Post.objects.create(title="I worked on readonly_fields", content="Its good stuff") + response = self.client.get('/test_admin/admin/admin_views/post/%d/' % p.pk) + self.assertContains(response, "%d amount of cool" % p.pk) + + def test_readonly_post(self): + data = { + "title": "Django Got Readonly Fields", + "content": "This is an incredible development.", + "link_set-TOTAL_FORMS": "1", + "link_set-INITIAL_FORMS": "0", + "link_set-MAX_NUM_FORMS": "0", + } + response = self.client.post('/test_admin/admin/admin_views/post/add/', data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Post.objects.count(), 1) + p = Post.objects.get() + self.assertEqual(p.posted, datetime.date.today()) + + data["posted"] = "10-8-1990" # some date that's not today + response = self.client.post('/test_admin/admin/admin_views/post/add/', data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Post.objects.count(), 2) + p = Post.objects.order_by('-id')[0] + self.assertEqual(p.posted, datetime.date.today()) + + def test_readonly_manytomany(self): + "Regression test for #13004" + response = self.client.get('/test_admin/admin/admin_views/pizza/add/') + self.assertEqual(response.status_code, 200) + +class UserAdminTest(TestCase): + """ + Tests user CRUD functionality. + """ + fixtures = ['admin-views-users.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def test_save_button(self): + user_count = User.objects.count() + response = self.client.post('/test_admin/admin/auth/user/add/', { + 'username': 'newuser', + 'password1': 'newpassword', + 'password2': 'newpassword', + }) + new_user = User.objects.order_by('-id')[0] + self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk) + self.assertEqual(User.objects.count(), user_count + 1) + self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD) + + def test_save_continue_editing_button(self): + user_count = User.objects.count() + response = self.client.post('/test_admin/admin/auth/user/add/', { + 'username': 'newuser', + 'password1': 'newpassword', + 'password2': 'newpassword', + '_continue': '1', + }) + new_user = User.objects.order_by('-id')[0] + self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk) + self.assertEqual(User.objects.count(), user_count + 1) + self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD) + + def test_password_mismatch(self): + response = self.client.post('/test_admin/admin/auth/user/add/', { + 'username': 'newuser', + 'password1': 'newpassword', + 'password2': 'mismatch', + }) + self.assertEqual(response.status_code, 200) + adminform = response.context['adminform'] + self.assertTrue('password' not in adminform.form.errors) + self.assertEqual(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.assertEqual(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"') + self.assertNotContains(response, 'name="_addanother"') + + def test_save_add_another_button(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.assertEqual(User.objects.count(), user_count + 1) + self.assertNotEqual(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="comments-get_comment_count">get_comment_count</h3>') + self.assertContains(response, '<li><a href="#comments-get_comment_count">get_comment_count</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 + +class ValidXHTMLTests(TestCase): + fixtures = ['admin-views-users.xml'] + urlbit = 'admin' + + def setUp(self): + self._context_processors = None + self._use_i18n, settings.USE_I18N = settings.USE_I18N, False + if 'django.core.context_processors.i18n' in settings.TEMPLATE_CONTEXT_PROCESSORS: + self._context_processors = settings.TEMPLATE_CONTEXT_PROCESSORS + cp = list(settings.TEMPLATE_CONTEXT_PROCESSORS) + cp.remove('django.core.context_processors.i18n') + settings.TEMPLATE_CONTEXT_PROCESSORS = tuple(cp) + # Force re-evaluation of the contex processor list + django.template.context._standard_context_processors = None + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + if self._context_processors is not None: + settings.TEMPLATE_CONTEXT_PROCESSORS = self._context_processors + # Force re-evaluation of the contex processor list + django.template.context._standard_context_processors = None + settings.USE_I18N = self._use_i18n + + def testLangNamePresent(self): + response = self.client.get('/test_admin/%s/admin_views/' % self.urlbit) + self.assertFalse(' lang=""' in response.content) + self.assertFalse(' xml:lang=""' in response.content) diff --git a/parts/django/tests/regressiontests/admin_views/urls.py b/parts/django/tests/regressiontests/admin_views/urls.py new file mode 100644 index 0000000..f3f1fbd --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls.defaults import * +from django.contrib import admin +import views +import customadmin + +urlpatterns = patterns('', + (r'^admin/doc/', include('django.contrib.admindocs.urls')), + (r'^admin/secure-view/$', views.secure_view), + (r'^admin/', include(admin.site.urls)), + (r'^admin2/', include(customadmin.site.urls)), +) diff --git a/parts/django/tests/regressiontests/admin_views/views.py b/parts/django/tests/regressiontests/admin_views/views.py new file mode 100644 index 0000000..732b253 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_views/views.py @@ -0,0 +1,6 @@ +from django.contrib.admin.views.decorators import staff_member_required +from django.http import HttpResponse + +def secure_view(request): + return HttpResponse('%s' % request.POST) +secure_view = staff_member_required(secure_view)
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/admin_widgets/__init__.py b/parts/django/tests/regressiontests/admin_widgets/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_widgets/__init__.py diff --git a/parts/django/tests/regressiontests/admin_widgets/fixtures/admin-widgets-users.xml b/parts/django/tests/regressiontests/admin_widgets/fixtures/admin-widgets-users.xml new file mode 100644 index 0000000..b851562 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_widgets/fixtures/admin-widgets-users.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="100" model="auth.user"> + <field type="CharField" name="username">super</field> + <field type="CharField" name="first_name">Super</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">super@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">True</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="101" model="auth.user"> + <field type="CharField" name="username">testser</field> + <field type="CharField" name="first_name">Add</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">auser@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">False</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + + <object pk="1" model="admin_widgets.car"> + <field to="auth.user" name="owner" rel="ManyToOneRel">100</field> + <field type="CharField" name="make">Volkswagon</field> + <field type="CharField" name="model">Passat</field> + </object> + <object pk="2" model="admin_widgets.car"> + <field to="auth.user" name="owner" rel="ManyToOneRel">101</field> + <field type="CharField" name="make">BMW</field> + <field type="CharField" name="model">M3</field> + </object> + +</django-objects>
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/admin_widgets/models.py b/parts/django/tests/regressiontests/admin_widgets/models.py new file mode 100644 index 0000000..450c3e6 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_widgets/models.py @@ -0,0 +1,68 @@ +from django.db import models +from django.contrib.auth.models import User + +class MyFileField(models.FileField): + pass + +class Member(models.Model): + name = models.CharField(max_length=100) + birthdate = models.DateTimeField(blank=True, null=True) + gender = models.CharField(max_length=1, blank=True, choices=[('M','Male'), ('F', 'Female')]) + + def __unicode__(self): + return self.name + +class Band(models.Model): + name = models.CharField(max_length=100) + members = models.ManyToManyField(Member) + + def __unicode__(self): + return self.name + +class Album(models.Model): + band = models.ForeignKey(Band) + name = models.CharField(max_length=100) + cover_art = models.FileField(upload_to='albums') + backside_art = MyFileField(upload_to='albums_back', null=True) + + def __unicode__(self): + return self.name + +class HiddenInventoryManager(models.Manager): + def get_query_set(self): + return super(HiddenInventoryManager, self).get_query_set().filter(hidden=False) + +class Inventory(models.Model): + barcode = models.PositiveIntegerField(unique=True) + parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True) + name = models.CharField(blank=False, max_length=20) + hidden = models.BooleanField(default=False) + + # see #9258 + default_manager = models.Manager() + objects = HiddenInventoryManager() + + def __unicode__(self): + return self.name + +class Event(models.Model): + band = models.ForeignKey(Band, limit_choices_to=models.Q(pk__gt=0)) + start_date = models.DateField(blank=True, null=True) + start_time = models.TimeField(blank=True, null=True) + description = models.TextField(blank=True) + link = models.URLField(blank=True) + min_age = models.IntegerField(blank=True, null=True) + +class Car(models.Model): + owner = models.ForeignKey(User) + make = models.CharField(max_length=30) + model = models.CharField(max_length=30) + + def __unicode__(self): + return u"%s %s" % (self.make, self.model) + +class CarTire(models.Model): + """ + A single car tire. This to test that a user can only select their own cars. + """ + car = models.ForeignKey(Car) diff --git a/parts/django/tests/regressiontests/admin_widgets/tests.py b/parts/django/tests/regressiontests/admin_widgets/tests.py new file mode 100644 index 0000000..51d883f --- /dev/null +++ b/parts/django/tests/regressiontests/admin_widgets/tests.py @@ -0,0 +1,316 @@ +# encoding: utf-8 + +from datetime import datetime +from unittest import TestCase + +from django import forms +from django.conf import settings +from django.contrib import admin +from django.contrib.admin import widgets +from django.contrib.admin.widgets import FilteredSelectMultiple, AdminSplitDateTime +from django.contrib.admin.widgets import (AdminFileWidget, ForeignKeyRawIdWidget, + ManyToManyRawIdWidget) +from django.core.files.storage import default_storage +from django.core.files.uploadedfile import SimpleUploadedFile +from django.db.models import DateField +from django.test import TestCase as DjangoTestCase +from django.utils.html import conditional_escape +from django.utils.translation import activate, deactivate + +import models + + +class AdminFormfieldForDBFieldTests(TestCase): + """ + Tests for correct behavior of ModelAdmin.formfield_for_dbfield + """ + + def assertFormfield(self, model, fieldname, widgetclass, **admin_overrides): + """ + Helper to call formfield_for_dbfield for a given model and field name + and verify that the returned formfield is appropriate. + """ + # Override any settings on the model admin + class MyModelAdmin(admin.ModelAdmin): pass + for k in admin_overrides: + setattr(MyModelAdmin, k, admin_overrides[k]) + + # Construct the admin, and ask it for a formfield + ma = MyModelAdmin(model, admin.site) + ff = ma.formfield_for_dbfield(model._meta.get_field(fieldname), request=None) + + # "unwrap" the widget wrapper, if needed + if isinstance(ff.widget, widgets.RelatedFieldWidgetWrapper): + widget = ff.widget.widget + else: + widget = ff.widget + + # Check that we got a field of the right type + self.assert_( + isinstance(widget, widgetclass), + "Wrong widget for %s.%s: expected %s, got %s" % \ + (model.__class__.__name__, fieldname, widgetclass, type(widget)) + ) + + # Return the formfield so that other tests can continue + return ff + + def testDateField(self): + self.assertFormfield(models.Event, 'start_date', widgets.AdminDateWidget) + + def testDateTimeField(self): + self.assertFormfield(models.Member, 'birthdate', widgets.AdminSplitDateTime) + + def testTimeField(self): + self.assertFormfield(models.Event, 'start_time', widgets.AdminTimeWidget) + + def testTextField(self): + self.assertFormfield(models.Event, 'description', widgets.AdminTextareaWidget) + + def testURLField(self): + self.assertFormfield(models.Event, 'link', widgets.AdminURLFieldWidget) + + def testIntegerField(self): + self.assertFormfield(models.Event, 'min_age', widgets.AdminIntegerFieldWidget) + + def testCharField(self): + self.assertFormfield(models.Member, 'name', widgets.AdminTextInputWidget) + + def testFileField(self): + self.assertFormfield(models.Album, 'cover_art', widgets.AdminFileWidget) + + def testForeignKey(self): + self.assertFormfield(models.Event, 'band', forms.Select) + + def testRawIDForeignKey(self): + self.assertFormfield(models.Event, 'band', widgets.ForeignKeyRawIdWidget, + raw_id_fields=['band']) + + def testRadioFieldsForeignKey(self): + ff = self.assertFormfield(models.Event, 'band', widgets.AdminRadioSelect, + radio_fields={'band':admin.VERTICAL}) + self.assertEqual(ff.empty_label, None) + + def testManyToMany(self): + self.assertFormfield(models.Band, 'members', forms.SelectMultiple) + + def testRawIDManyTOMany(self): + self.assertFormfield(models.Band, 'members', widgets.ManyToManyRawIdWidget, + raw_id_fields=['members']) + + def testFilteredManyToMany(self): + self.assertFormfield(models.Band, 'members', widgets.FilteredSelectMultiple, + filter_vertical=['members']) + + def testFormfieldOverrides(self): + self.assertFormfield(models.Event, 'start_date', forms.TextInput, + formfield_overrides={DateField: {'widget': forms.TextInput}}) + + def testFieldWithChoices(self): + self.assertFormfield(models.Member, 'gender', forms.Select) + + def testChoicesWithRadioFields(self): + self.assertFormfield(models.Member, 'gender', widgets.AdminRadioSelect, + radio_fields={'gender':admin.VERTICAL}) + + def testInheritance(self): + self.assertFormfield(models.Album, 'backside_art', widgets.AdminFileWidget) + + +class AdminFormfieldForDBFieldWithRequestTests(DjangoTestCase): + fixtures = ["admin-widgets-users.xml"] + + def testFilterChoicesByRequestUser(self): + """ + Ensure the user can only see their own cars in the foreign key dropdown. + """ + self.client.login(username="super", password="secret") + response = self.client.get("/widget_admin/admin_widgets/cartire/add/") + self.assert_("BMW M3" not in response.content) + self.assert_("Volkswagon Passat" in response.content) + + +class AdminForeignKeyWidgetChangeList(DjangoTestCase): + fixtures = ["admin-widgets-users.xml"] + admin_root = '/widget_admin' + + def setUp(self): + self.client.login(username="super", password="secret") + + def tearDown(self): + self.client.logout() + + def test_changelist_foreignkey(self): + response = self.client.get('%s/admin_widgets/car/' % self.admin_root) + self.assertTrue('%s/auth/user/add/' % self.admin_root in response.content) + + +class AdminForeignKeyRawIdWidget(DjangoTestCase): + fixtures = ["admin-widgets-users.xml"] + admin_root = '/widget_admin' + + def setUp(self): + self.client.login(username="super", password="secret") + + def tearDown(self): + self.client.logout() + + def test_nonexistent_target_id(self): + band = models.Band.objects.create(name='Bogey Blues') + pk = band.pk + band.delete() + post_data = { + "band": u'%s' % pk, + } + # Try posting with a non-existent pk in a raw id field: this + # should result in an error message, not a server exception. + response = self.client.post('%s/admin_widgets/event/add/' % self.admin_root, + 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.') + + +class FilteredSelectMultipleWidgetTest(TestCase): + def test_render(self): + w = FilteredSelectMultiple('test', False) + self.assertEqual( + conditional_escape(w.render('test', 'test')), + '<select multiple="multiple" name="test" class="selectfilter">\n</select><script type="text/javascript">addEvent(window, "load", function(e) {SelectFilter.init("id_test", "test", 0, "%(ADMIN_MEDIA_PREFIX)s"); });</script>\n' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX} + ) + + def test_stacked_render(self): + w = FilteredSelectMultiple('test', True) + self.assertEqual( + conditional_escape(w.render('test', 'test')), + '<select multiple="multiple" name="test" class="selectfilterstacked">\n</select><script type="text/javascript">addEvent(window, "load", function(e) {SelectFilter.init("id_test", "test", 1, "%(ADMIN_MEDIA_PREFIX)s"); });</script>\n' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX} + ) + + +class AdminSplitDateTimeWidgetTest(TestCase): + def test_render(self): + w = AdminSplitDateTime() + self.assertEqual( + conditional_escape(w.render('test', datetime(2007, 12, 1, 9, 30))), + '<p class="datetime">Date: <input value="2007-12-01" type="text" class="vDateField" name="test_0" size="10" /><br />Time: <input value="09:30:00" type="text" class="vTimeField" name="test_1" size="8" /></p>', + ) + + def test_localization(self): + w = AdminSplitDateTime() + + activate('de-at') + old_USE_L10N = settings.USE_L10N + settings.USE_L10N = True + w.is_localized = True + self.assertEqual( + conditional_escape(w.render('test', datetime(2007, 12, 1, 9, 30))), + '<p class="datetime">Datum: <input value="01.12.2007" type="text" class="vDateField" name="test_0" size="10" /><br />Zeit: <input value="09:30:00" type="text" class="vTimeField" name="test_1" size="8" /></p>', + ) + deactivate() + settings.USE_L10N = old_USE_L10N + + +class AdminFileWidgetTest(DjangoTestCase): + def test_render(self): + band = models.Band.objects.create(name='Linkin Park') + album = band.album_set.create( + name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg' + ) + + w = AdminFileWidget() + self.assertEqual( + conditional_escape(w.render('test', album.cover_art)), + 'Currently: <a target="_blank" href="%(STORAGE_URL)salbums/hybrid_theory.jpg">albums\hybrid_theory.jpg</a> <br />Change: <input type="file" name="test" />' % {'STORAGE_URL': default_storage.url('')}, + ) + + self.assertEqual( + conditional_escape(w.render('test', SimpleUploadedFile('test', 'content'))), + '<input type="file" name="test" />', + ) + + +class ForeignKeyRawIdWidgetTest(DjangoTestCase): + def test_render(self): + band = models.Band.objects.create(name='Linkin Park') + band.album_set.create( + name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg' + ) + rel = models.Album._meta.get_field('band').rel + + w = ForeignKeyRawIdWidget(rel) + self.assertEqual( + conditional_escape(w.render('test', band.pk, attrs={})), + '<input type="text" name="test" value="%(bandpk)s" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/band/?t=id" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong>Linkin Park</strong>' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX, "bandpk": band.pk}, + ) + + def test_relations_to_non_primary_key(self): + # Check that ForeignKeyRawIdWidget works with fields which aren't + # related to the model's primary key. + apple = models.Inventory.objects.create(barcode=86, name='Apple') + models.Inventory.objects.create(barcode=22, name='Pear') + core = models.Inventory.objects.create( + barcode=87, name='Core', parent=apple + ) + rel = models.Inventory._meta.get_field('parent').rel + w = ForeignKeyRawIdWidget(rel) + self.assertEqual( + w.render('test', core.parent_id, attrs={}), + '<input type="text" name="test" value="86" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong>Apple</strong>' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX}, + ) + + + def test_proper_manager_for_label_lookup(self): + # see #9258 + rel = models.Inventory._meta.get_field('parent').rel + w = ForeignKeyRawIdWidget(rel) + + hidden = models.Inventory.objects.create( + barcode=93, name='Hidden', hidden=True + ) + child_of_hidden = models.Inventory.objects.create( + barcode=94, name='Child of hidden', parent=hidden + ) + self.assertEqual( + w.render('test', child_of_hidden.parent_id, attrs={}), + '<input type="text" name="test" value="93" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/inventory/?t=barcode" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong>Hidden</strong>' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX}, + ) + + +class ManyToManyRawIdWidgetTest(DjangoTestCase): + def test_render(self): + band = models.Band.objects.create(name='Linkin Park') + band.album_set.create( + name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg' + ) + + m1 = models.Member.objects.create(name='Chester') + m2 = models.Member.objects.create(name='Mike') + band.members.add(m1, m2) + rel = models.Band._meta.get_field('members').rel + + w = ManyToManyRawIdWidget(rel) + self.assertEqual( + conditional_escape(w.render('test', [m1.pk, m2.pk], attrs={})), + '<input type="text" name="test" value="%(m1pk)s,%(m2pk)s" class="vManyToManyRawIdAdminField" /><a href="../../../admin_widgets/member/" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a>' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX, "m1pk": m1.pk, "m2pk": m2.pk}, + ) + + self.assertEqual( + conditional_escape(w.render('test', [m1.pk])), + '<input type="text" name="test" value="%(m1pk)s" class="vManyToManyRawIdAdminField" /><a href="../../../admin_widgets/member/" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a>' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX, "m1pk": m1.pk}, + ) + + self.assertEqual(w._has_changed(None, None), False) + self.assertEqual(w._has_changed([], None), False) + self.assertEqual(w._has_changed(None, [u'1']), True) + self.assertEqual(w._has_changed([1, 2], [u'1', u'2']), False) + self.assertEqual(w._has_changed([1, 2], [u'1']), True) + self.assertEqual(w._has_changed([1, 2], [u'1', u'3']), True) diff --git a/parts/django/tests/regressiontests/admin_widgets/urls.py b/parts/django/tests/regressiontests/admin_widgets/urls.py new file mode 100644 index 0000000..af73d53 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_widgets/urls.py @@ -0,0 +1,7 @@ + +from django.conf.urls.defaults import * +import widgetadmin + +urlpatterns = patterns('', + (r'^', include(widgetadmin.site.urls)), +) diff --git a/parts/django/tests/regressiontests/admin_widgets/widgetadmin.py b/parts/django/tests/regressiontests/admin_widgets/widgetadmin.py new file mode 100644 index 0000000..6f15d92 --- /dev/null +++ b/parts/django/tests/regressiontests/admin_widgets/widgetadmin.py @@ -0,0 +1,30 @@ +""" + +""" +from django.contrib import admin + +import models + +class WidgetAdmin(admin.AdminSite): + pass + +class CarAdmin(admin.ModelAdmin): + list_display = ['make', 'model', 'owner'] + list_editable = ['owner'] + +class CarTireAdmin(admin.ModelAdmin): + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == "car": + kwargs["queryset"] = models.Car.objects.filter(owner=request.user) + return db_field.formfield(**kwargs) + return super(CarTireAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) + +class EventAdmin(admin.ModelAdmin): + raw_id_fields = ['band'] + +site = WidgetAdmin(name='widget-admin') + +site.register(models.User) +site.register(models.Car, CarAdmin) +site.register(models.CarTire, CarTireAdmin) +site.register(models.Event, EventAdmin) diff --git a/parts/django/tests/regressiontests/aggregation_regress/__init__.py b/parts/django/tests/regressiontests/aggregation_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/aggregation_regress/__init__.py diff --git a/parts/django/tests/regressiontests/aggregation_regress/fixtures/initial_data.json b/parts/django/tests/regressiontests/aggregation_regress/fixtures/initial_data.json new file mode 100644 index 0000000..597ac04 --- /dev/null +++ b/parts/django/tests/regressiontests/aggregation_regress/fixtures/initial_data.json @@ -0,0 +1,257 @@ +[ + { + "pk": 1, + "model": "aggregation_regress.publisher", + "fields": { + "name": "Apress", + "num_awards": 3 + } + }, + { + "pk": 2, + "model": "aggregation_regress.publisher", + "fields": { + "name": "Sams", + "num_awards": 1 + } + }, + { + "pk": 3, + "model": "aggregation_regress.publisher", + "fields": { + "name": "Prentice Hall", + "num_awards": 7 + } + }, + { + "pk": 4, + "model": "aggregation_regress.publisher", + "fields": { + "name": "Morgan Kaufmann", + "num_awards": 9 + } + }, + { + "pk": 5, + "model": "aggregation_regress.publisher", + "fields": { + "name": "Jonno's House of Books", + "num_awards": 0 + } + }, + { + "pk": 1, + "model": "aggregation_regress.book", + "fields": { + "publisher": 1, + "isbn": "159059725", + "name": "The Definitive Guide to Django: Web Development Done Right", + "price": "30.00", + "rating": 4.5, + "authors": [1, 2], + "contact": 1, + "pages": 447, + "pubdate": "2007-12-6" + } + }, + { + "pk": 2, + "model": "aggregation_regress.book", + "fields": { + "publisher": 2, + "isbn": "067232959", + "name": "Sams Teach Yourself Django in 24 Hours", + "price": "23.09", + "rating": 3.0, + "authors": [3], + "contact": 3, + "pages": 528, + "pubdate": "2008-3-3" + } + }, + { + "pk": 3, + "model": "aggregation_regress.book", + "fields": { + "publisher": 1, + "isbn": "159059996", + "name": "Practical Django Projects", + "price": "29.69", + "rating": 4.0, + "authors": [4], + "contact": 4, + "pages": 300, + "pubdate": "2008-6-23" + } + }, + { + "pk": 4, + "model": "aggregation_regress.book", + "fields": { + "publisher": 3, + "isbn": "013235613", + "name": "Python Web Development with Django", + "price": "29.69", + "rating": 4.0, + "authors": [5, 6, 7], + "contact": 5, + "pages": 350, + "pubdate": "2008-11-3" + } + }, + { + "pk": 5, + "model": "aggregation_regress.book", + "fields": { + "publisher": 3, + "isbn": "013790395", + "name": "Artificial Intelligence: A Modern Approach", + "price": "82.80", + "rating": 4.0, + "authors": [8, 9], + "contact": 8, + "pages": 1132, + "pubdate": "1995-1-15" + } + }, + { + "pk": 6, + "model": "aggregation_regress.book", + "fields": { + "publisher": 4, + "isbn": "155860191", + "name": "Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp", + "price": "75.00", + "rating": 5.0, + "authors": [8], + "contact": 8, + "pages": 946, + "pubdate": "1991-10-15" + } + }, + { + "pk": 1, + "model": "aggregation_regress.store", + "fields": { + "books": [1, 2, 3, 4, 5, 6], + "name": "Amazon.com", + "original_opening": "1994-4-23 9:17:42", + "friday_night_closing": "23:59:59" + } + }, + { + "pk": 2, + "model": "aggregation_regress.store", + "fields": { + "books": [1, 3, 5, 6], + "name": "Books.com", + "original_opening": "2001-3-15 11:23:37", + "friday_night_closing": "23:59:59" + } + }, + { + "pk": 3, + "model": "aggregation_regress.store", + "fields": { + "books": [3, 4, 6], + "name": "Mamma and Pappa's Books", + "original_opening": "1945-4-25 16:24:14", + "friday_night_closing": "21:30:00" + } + }, + { + "pk": 1, + "model": "aggregation_regress.author", + "fields": { + "age": 34, + "friends": [2, 4], + "name": "Adrian Holovaty" + } + }, + { + "pk": 2, + "model": "aggregation_regress.author", + "fields": { + "age": 35, + "friends": [1, 7], + "name": "Jacob Kaplan-Moss" + } + }, + { + "pk": 3, + "model": "aggregation_regress.author", + "fields": { + "age": 45, + "friends": [], + "name": "Brad Dayley" + } + }, + { + "pk": 4, + "model": "aggregation_regress.author", + "fields": { + "age": 29, + "friends": [1], + "name": "James Bennett" + } + }, + { + "pk": 5, + "model": "aggregation_regress.author", + "fields": { + "age": 37, + "friends": [6, 7], + "name": "Jeffrey Forcier" + } + }, + { + "pk": 6, + "model": "aggregation_regress.author", + "fields": { + "age": 29, + "friends": [5, 7], + "name": "Paul Bissex" + } + }, + { + "pk": 7, + "model": "aggregation_regress.author", + "fields": { + "age": 25, + "friends": [2, 5, 6], + "name": "Wesley J. Chun" + } + }, + { + "pk": 8, + "model": "aggregation_regress.author", + "fields": { + "age": 57, + "friends": [9], + "name": "Peter Norvig" + } + }, + { + "pk": 9, + "model": "aggregation_regress.author", + "fields": { + "age": 46, + "friends": [8], + "name": "Stuart Russell" + } + }, + { + "pk": 5, + "model": "aggregation_regress.hardbackbook", + "fields": { + "weight": 4.5 + } + }, + { + "pk": 6, + "model": "aggregation_regress.hardbackbook", + "fields": { + "weight": 3.7 + } + } +] diff --git a/parts/django/tests/regressiontests/aggregation_regress/models.py b/parts/django/tests/regressiontests/aggregation_regress/models.py new file mode 100644 index 0000000..ccef9a5 --- /dev/null +++ b/parts/django/tests/regressiontests/aggregation_regress/models.py @@ -0,0 +1,65 @@ +# coding: utf-8 +from django.db import models + + +class Author(models.Model): + name = models.CharField(max_length=100) + age = models.IntegerField() + friends = models.ManyToManyField('self', blank=True) + + def __unicode__(self): + return self.name + + +class Publisher(models.Model): + name = models.CharField(max_length=255) + num_awards = models.IntegerField() + + def __unicode__(self): + return self.name + + +class Book(models.Model): + isbn = models.CharField(max_length=9) + name = models.CharField(max_length=255) + pages = models.IntegerField() + rating = models.FloatField() + price = models.DecimalField(decimal_places=2, max_digits=6) + authors = models.ManyToManyField(Author) + contact = models.ForeignKey(Author, related_name='book_contact_set') + publisher = models.ForeignKey(Publisher) + pubdate = models.DateField() + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + + +class Store(models.Model): + name = models.CharField(max_length=255) + books = models.ManyToManyField(Book) + original_opening = models.DateTimeField() + friday_night_closing = models.TimeField() + + 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) diff --git a/parts/django/tests/regressiontests/aggregation_regress/tests.py b/parts/django/tests/regressiontests/aggregation_regress/tests.py new file mode 100644 index 0000000..4d5a2a0 --- /dev/null +++ b/parts/django/tests/regressiontests/aggregation_regress/tests.py @@ -0,0 +1,757 @@ +import datetime +import pickle +from decimal import Decimal +from operator import attrgetter + +from django.conf import settings +from django.core.exceptions import FieldError +from django.db import DEFAULT_DB_ALIAS +from django.db.models import Count, Max, Avg, Sum, StdDev, Variance, F +from django.test import TestCase, Approximate + +from models import Author, Book, Publisher, Clues, Entries, HardbackBook + + +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): + """ + Regression test for #12822: DatabaseError: aggregates not allowed in + WHERE clause + + Tests that the subselect works and returns results equivalent to a + query with the IDs listed. + + Before the corresponding fix for this bug, this test passed in 1.1 and + failed in 1.2-beta (trunk). + """ + qs = Book.objects.values('contact').annotate(Max('id')) + qs = qs.order_by('contact').values_list('id__max', flat=True) + # don't do anything with the queryset (qs) before including it as a + # subquery + books = Book.objects.order_by('id') + qs1 = books.filter(id__in=qs) + qs2 = books.filter(id__in=list(qs)) + self.assertEqual(list(qs1), list(qs2)) + + def test_aggregates_in_where_clause_pre_eval(self): + """ + Regression test for #12822: DatabaseError: aggregates not allowed in + WHERE clause + + Same as the above test, but evaluates the queryset for the subquery + before it's used as a subquery. + + Before the corresponding fix for this bug, this test failed in both + 1.1 and 1.2-beta (trunk). + """ + qs = Book.objects.values('contact').annotate(Max('id')) + qs = qs.order_by('contact').values_list('id__max', flat=True) + # force the queryset (qs) for the subquery to be evaluated in its + # current state + list(qs) + books = Book.objects.order_by('id') + qs1 = books.filter(id__in=qs) + qs2 = books.filter(id__in=list(qs)) + self.assertEqual(list(qs1), list(qs2)) + + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.oracle': + def test_annotate_with_extra(self): + """ + Regression test for #11916: Extra params + aggregation creates + incorrect SQL. + """ + #oracle doesn't support subqueries in group by clause + shortest_book_sql = """ + SELECT name + FROM aggregation_regress_book b + WHERE b.publisher_id = aggregation_regress_publisher.id + ORDER BY b.pages + LIMIT 1 + """ + # tests that this query does not raise a DatabaseError due to the full + # subselect being (erroneously) added to the GROUP BY parameters + qs = Publisher.objects.extra(select={ + 'name_of_shortest_book': shortest_book_sql, + }).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_duplicate_alias(self): + # Regression for #11256 - duplicating a default alias raises ValueError. + self.assertRaises(ValueError, Book.objects.all().annotate, Avg('authors__age'), authors__age__avg=Avg('authors__age')) + + def test_field_name_conflict(self): + # Regression for #11256 - providing an aggregate name that conflicts with a field name on the model raises ValueError + self.assertRaises(ValueError, Author.objects.annotate, age=Avg('friends__age')) + + def test_m2m_name_conflict(self): + # Regression for #11256 - providing an aggregate name that conflicts with an m2m name on the model raises ValueError + self.assertRaises(ValueError, Author.objects.annotate, friends=Count('friends')) + + def test_reverse_relation_name_conflict(self): + # Regression for #11256 - providing an aggregate name that conflicts with a reverse-related name on the model raises ValueError + self.assertRaises(ValueError, Author.objects.annotate, book_contact_set=Avg('friends__age')) + + 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')) + 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')) + ) + + def test_empty_filter_count(self): + self.assertEqual( + Author.objects.filter(id__in=[]).annotate(Count("friends")).count(), + 0 + ) + + def test_empty_filter_aggregate(self): + self.assertEqual( + Author.objects.filter(id__in=[]).annotate(Count("friends")).aggregate(Count("pk")), + {"pk__count": None} + ) + + def test_annotate_and_join(self): + self.assertEqual( + Author.objects.annotate(c=Count("friends__name")).exclude(friends__name="Joe").count(), + Author.objects.count() + ) + + def test_f_expression_annotation(self): + # Books with less than 200 pages per author. + qs = Book.objects.values("name").annotate( + n_authors=Count("authors") + ).filter( + pages__lt=F("n_authors") * 200 + ).values_list("pk") + self.assertQuerysetEqual( + Book.objects.filter(pk__in=qs), [ + "Python Web Development with Django" + ], + attrgetter("name") + ) + + def test_values_annotate_values(self): + qs = Book.objects.values("name").annotate( + n_authors=Count("authors") + ).values_list("pk", flat=True) + self.assertEqual(list(qs), list(Book.objects.values_list("pk", flat=True))) + + def test_having_group_by(self): + # Test that when a field occurs on the LHS of a HAVING clause that it + # appears correctly in the GROUP BY clause + qs = Book.objects.values_list("name").annotate( + n_authors=Count("authors") + ).filter( + pages__gt=F("n_authors") + ).values_list("name", flat=True) + # Results should be the same, all Books have more pages than authors + self.assertEqual( + list(qs), list(Book.objects.values_list("name", flat=True)) + ) + + 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/parts/django/tests/regressiontests/app_loading/__init__.py b/parts/django/tests/regressiontests/app_loading/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/__init__.py diff --git a/parts/django/tests/regressiontests/app_loading/eggs/brokenapp.egg b/parts/django/tests/regressiontests/app_loading/eggs/brokenapp.egg Binary files differnew file mode 100755 index 0000000..8aca671 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/eggs/brokenapp.egg diff --git a/parts/django/tests/regressiontests/app_loading/eggs/modelapp.egg b/parts/django/tests/regressiontests/app_loading/eggs/modelapp.egg Binary files differnew file mode 100755 index 0000000..c2370b5 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/eggs/modelapp.egg diff --git a/parts/django/tests/regressiontests/app_loading/eggs/nomodelapp.egg b/parts/django/tests/regressiontests/app_loading/eggs/nomodelapp.egg Binary files differnew file mode 100755 index 0000000..5b8d217 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/eggs/nomodelapp.egg diff --git a/parts/django/tests/regressiontests/app_loading/eggs/omelet.egg b/parts/django/tests/regressiontests/app_loading/eggs/omelet.egg Binary files differnew file mode 100755 index 0000000..bd1c687 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/eggs/omelet.egg diff --git a/parts/django/tests/regressiontests/app_loading/models.py b/parts/django/tests/regressiontests/app_loading/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/models.py diff --git a/parts/django/tests/regressiontests/app_loading/parent/__init__.py b/parts/django/tests/regressiontests/app_loading/parent/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/parent/__init__.py diff --git a/parts/django/tests/regressiontests/app_loading/parent/app/__init__.py b/parts/django/tests/regressiontests/app_loading/parent/app/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/parent/app/__init__.py diff --git a/parts/django/tests/regressiontests/app_loading/parent/app1/__init__.py b/parts/django/tests/regressiontests/app_loading/parent/app1/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/parent/app1/__init__.py diff --git a/parts/django/tests/regressiontests/app_loading/parent/app_2/__init__.py b/parts/django/tests/regressiontests/app_loading/parent/app_2/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/parent/app_2/__init__.py diff --git a/parts/django/tests/regressiontests/app_loading/test_settings.py b/parts/django/tests/regressiontests/app_loading/test_settings.py new file mode 100644 index 0000000..e956311 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/test_settings.py @@ -0,0 +1,3 @@ +INSTALLED_APPS = ( + 'parent.*', +) diff --git a/parts/django/tests/regressiontests/app_loading/tests.py b/parts/django/tests/regressiontests/app_loading/tests.py new file mode 100644 index 0000000..4fb60b2 --- /dev/null +++ b/parts/django/tests/regressiontests/app_loading/tests.py @@ -0,0 +1,83 @@ +import copy +import os +import sys +import time +from unittest import TestCase + +from django.conf import Settings +from django.db.models.loading import cache, load_app + + +class InstalledAppsGlobbingTest(TestCase): + def setUp(self): + self.OLD_SYS_PATH = sys.path[:] + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + self.OLD_TZ = os.environ.get("TZ") + + def test_globbing(self): + settings = Settings('test_settings') + self.assertEquals(settings.INSTALLED_APPS, ['parent.app', 'parent.app1', 'parent.app_2']) + + def tearDown(self): + sys.path = self.OLD_SYS_PATH + if hasattr(time, "tzset") and self.OLD_TZ: + os.environ["TZ"] = self.OLD_TZ + time.tzset() + + +class EggLoadingTest(TestCase): + + def setUp(self): + self.old_path = sys.path[:] + self.egg_dir = '%s/eggs' % os.path.dirname(__file__) + + # This test adds dummy applications to the app cache. These + # need to be removed in order to prevent bad interactions + # with the flush operation in other tests. + self.old_app_models = copy.deepcopy(cache.app_models) + self.old_app_store = copy.deepcopy(cache.app_store) + + def tearDown(self): + sys.path = self.old_path + cache.app_models = self.old_app_models + cache.app_store = self.old_app_store + + def test_egg1(self): + """Models module can be loaded from an app in an egg""" + egg_name = '%s/modelapp.egg' % self.egg_dir + sys.path.append(egg_name) + models = load_app('app_with_models') + self.assertFalse(models is None) + + def test_egg2(self): + """Loading an app from an egg that has no models returns no models (and no error)""" + egg_name = '%s/nomodelapp.egg' % self.egg_dir + sys.path.append(egg_name) + models = load_app('app_no_models') + self.assertTrue(models is None) + + def test_egg3(self): + """Models module can be loaded from an app located under an egg's top-level package""" + egg_name = '%s/omelet.egg' % self.egg_dir + sys.path.append(egg_name) + models = load_app('omelet.app_with_models') + self.assertFalse(models is None) + + def test_egg4(self): + """Loading an app with no models from under the top-level egg package generates no error""" + egg_name = '%s/omelet.egg' % self.egg_dir + sys.path.append(egg_name) + models = load_app('omelet.app_no_models') + self.assertTrue(models is None) + + def test_egg5(self): + """Loading an app from an egg that has an import error in its models module raises that error""" + egg_name = '%s/brokenapp.egg' % self.egg_dir + sys.path.append(egg_name) + self.assertRaises(ImportError, load_app, 'broken_app') + try: + load_app('broken_app') + except ImportError, e: + # Make sure the message is indicating the actual + # problem in the broken app. + self.assertTrue("modelz" in e.args[0]) diff --git a/parts/django/tests/regressiontests/backends/__init__.py b/parts/django/tests/regressiontests/backends/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/backends/__init__.py diff --git a/parts/django/tests/regressiontests/backends/models.py b/parts/django/tests/regressiontests/backends/models.py new file mode 100644 index 0000000..ea7ff96 --- /dev/null +++ b/parts/django/tests/regressiontests/backends/models.py @@ -0,0 +1,37 @@ +from django.db import models + +class Square(models.Model): + root = models.IntegerField() + square = models.PositiveIntegerField() + + 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) + + 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() + + +class Reporter(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name) + + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateField() + reporter = models.ForeignKey(Reporter) + + def __unicode__(self): + return self.headline diff --git a/parts/django/tests/regressiontests/backends/tests.py b/parts/django/tests/regressiontests/backends/tests.py new file mode 100644 index 0000000..10cec89 --- /dev/null +++ b/parts/django/tests/regressiontests/backends/tests.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# Unit and doctests for specific database backends. +import datetime +import unittest + +from django.conf import settings +from django.db import backend, connection, DEFAULT_DB_ALIAS, IntegrityError +from django.db.backends.signals import connection_created +from django.db.backends.postgresql import version as pg_version +from django.test import TestCase, TransactionTestCase + +import models + +class OracleChecks(unittest.TestCase): + + def test_dbms_session(self): + # If the backend is Oracle, test that we can call a standard + # stored procedure through our cursor wrapper. + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.oracle': + convert_unicode = backend.convert_unicode + cursor = connection.cursor() + cursor.callproc(convert_unicode('DBMS_SESSION.SET_IDENTIFIER'), + [convert_unicode('_django_testing!'),]) + return True + else: + return True + + def test_cursor_var(self): + # If the backend is Oracle, test that we can pass cursor variables + # as query parameters. + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.oracle': + cursor = connection.cursor() + var = cursor.var(backend.Database.STRING) + cursor.execute("BEGIN %s := 'X'; END; ", [var]) + self.assertEqual(var.getvalue(), 'X') + + def test_long_string(self): + # If the backend is Oracle, test that we can save a text longer + # than 4000 chars and read it properly + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.oracle': + c = connection.cursor() + c.execute('CREATE TABLE ltext ("TEXT" NCLOB)') + long_str = ''.join([unicode(x) for x in xrange(4000)]) + c.execute('INSERT INTO ltext VALUES (%s)',[long_str]) + c.execute('SELECT text FROM ltext') + row = c.fetchone() + self.assertEquals(long_str, row[0].read()) + c.execute('DROP TABLE ltext') + + def test_client_encoding(self): + # If the backend is Oracle, test that the client encoding is set + # correctly. This was broken under Cygwin prior to r14781. + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.oracle': + c = connection.cursor() # Ensure the connection is initialized. + self.assertEqual(connection.connection.encoding, "UTF-8") + self.assertEqual(connection.connection.nencoding, "UTF-8") + +class DateQuotingTest(TestCase): + + def test_django_date_trunc(self): + """ + Test the custom ``django_date_trunc method``, in particular against + fields which clash with strings passed to it (e.g. 'year') - see + #12818__. + + __: http://code.djangoproject.com/ticket/12818 + + """ + updated = datetime.datetime(2010, 2, 20) + models.SchoolClass.objects.create(year=2009, last_updated=updated) + years = models.SchoolClass.objects.dates('last_updated', 'year') + self.assertEqual(list(years), [datetime.datetime(2010, 1, 1, 0, 0)]) + + def test_django_extract(self): + """ + Test the custom ``django_extract method``, in particular against fields + which clash with strings passed to it (e.g. 'day') - see #12818__. + + __: http://code.djangoproject.com/ticket/12818 + + """ + updated = datetime.datetime(2010, 2, 20) + models.SchoolClass.objects.create(year=2009, last_updated=updated) + 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)" + cursor = connection.cursor() + query = ('INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' % ( + connection.introspection.table_name_converter('backends_square'), + connection.ops.quote_name('root'), + connection.ops.quote_name('square') + )) + self.assertRaises(Exception, cursor.executemany, query, [(1,2,3),]) + self.assertRaises(Exception, cursor.executemany, query, [(1,),]) + +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": + 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 == {}) + + +class BackendTestCase(TestCase): + def test_cursor_executemany(self): + #4896: Test cursor.executemany + cursor = connection.cursor() + qn = connection.ops.quote_name + opts = models.Square._meta + f1, f2 = opts.get_field('root'), opts.get_field('square') + query = ('INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' + % (connection.introspection.table_name_converter(opts.db_table), qn(f1.column), qn(f2.column))) + cursor.executemany(query, [(i, i**2) for i in range(-5, 6)]) + self.assertEqual(models.Square.objects.count(), 11) + for i in range(-5, 6): + square = models.Square.objects.get(root=i) + self.assertEqual(square.square, i**2) + + #4765: executemany with params=[] does nothing + cursor.executemany(query, []) + self.assertEqual(models.Square.objects.count(), 11) + + +# We don't make these tests conditional because that means we would need to +# check and differentiate between: +# * MySQL+InnoDB, MySQL+MYISAM (something we currently can't do). +# * if sqlite3 (if/once we get #14204 fixed) has referential integrity turned +# on or not, something that would be controlled by runtime support and user +# preference. +# verify if its type is django.database.db.IntegrityError. + +class FkConstraintsTests(TransactionTestCase): + + def setUp(self): + # Create a Reporter. + self.r = models.Reporter.objects.create(first_name='John', last_name='Smith') + + def test_integrity_checks_on_creation(self): + """ + Try to create a model instance that violates a FK constraint. If it + fails it should fail with IntegrityError. + """ + a = models.Article(headline="This is a test", pub_date=datetime.datetime(2005, 7, 27), reporter_id=30) + try: + a.save() + except IntegrityError: + pass + + def test_integrity_checks_on_update(self): + """ + Try to update a model instance introducing a FK constraint violation. + If it fails it should fail with IntegrityError. + """ + # Create an Article. + models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r) + # Retrive it from the DB + a = models.Article.objects.get(headline="Test article") + a.reporter_id = 30 + try: + a.save() + except IntegrityError: + pass + def test_unicode_fetches(self): + #6254: fetchone, fetchmany, fetchall return strings as unicode objects + qn = connection.ops.quote_name + models.Person(first_name="John", last_name="Doe").save() + models.Person(first_name="Jane", last_name="Doe").save() + models.Person(first_name="Mary", last_name="Agnelline").save() + models.Person(first_name="Peter", last_name="Parker").save() + models.Person(first_name="Clark", last_name="Kent").save() + opts2 = models.Person._meta + f3, f4 = opts2.get_field('first_name'), opts2.get_field('last_name') + query2 = ('SELECT %s, %s FROM %s ORDER BY %s' + % (qn(f3.column), qn(f4.column), connection.introspection.table_name_converter(opts2.db_table), + qn(f3.column))) + cursor = connection.cursor() + cursor.execute(query2) + self.assertEqual(cursor.fetchone(), (u'Clark', u'Kent')) + self.assertEqual(list(cursor.fetchmany(2)), [(u'Jane', u'Doe'), (u'John', u'Doe')]) + self.assertEqual(list(cursor.fetchall()), [(u'Mary', u'Agnelline'), (u'Peter', u'Parker')]) diff --git a/parts/django/tests/regressiontests/bash_completion/__init__.py b/parts/django/tests/regressiontests/bash_completion/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/regressiontests/bash_completion/__init__.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/regressiontests/bash_completion/management/__init__.py b/parts/django/tests/regressiontests/bash_completion/management/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/regressiontests/bash_completion/management/__init__.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/regressiontests/bash_completion/management/commands/__init__.py b/parts/django/tests/regressiontests/bash_completion/management/commands/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/regressiontests/bash_completion/management/commands/__init__.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/regressiontests/bash_completion/management/commands/test_command.py b/parts/django/tests/regressiontests/bash_completion/management/commands/test_command.py new file mode 100644 index 0000000..5cb8820 --- /dev/null +++ b/parts/django/tests/regressiontests/bash_completion/management/commands/test_command.py @@ -0,0 +1,14 @@ +import sys, os +from optparse import OptionParser, make_option + +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + option_list = BaseCommand.option_list + ( + make_option("--list", action="store_true", dest="list", + help="Print all options"), + ) + + def handle(self, *args, **options): + pass diff --git a/parts/django/tests/regressiontests/bash_completion/models.py b/parts/django/tests/regressiontests/bash_completion/models.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/regressiontests/bash_completion/models.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/regressiontests/bash_completion/tests.py b/parts/django/tests/regressiontests/bash_completion/tests.py new file mode 100644 index 0000000..24c8b1d --- /dev/null +++ b/parts/django/tests/regressiontests/bash_completion/tests.py @@ -0,0 +1,87 @@ +""" +A series of tests to establish that the command-line bash completion works. +""" +import os +import unittest +import sys +import StringIO + +from django.conf import settings +from django.core.management import ManagementUtility + +class BashCompletionTests(unittest.TestCase): + """ + Testing the Python level bash completion code. + This requires setting up the environment as if we got passed data + from bash. + """ + + def setUp(self): + self.old_DJANGO_AUTO_COMPLETE = os.environ.get('DJANGO_AUTO_COMPLETE') + os.environ['DJANGO_AUTO_COMPLETE'] = '1' + self.output = StringIO.StringIO() + self.old_stdout = sys.stdout + sys.stdout = self.output + + def tearDown(self): + sys.stdout = self.old_stdout + if self.old_DJANGO_AUTO_COMPLETE: + os.environ['DJANGO_AUTO_COMPLETE'] = self.old_DJANGO_AUTO_COMPLETE + else: + del os.environ['DJANGO_AUTO_COMPLETE'] + + def _user_input(self, input_str): + os.environ['COMP_WORDS'] = input_str + os.environ['COMP_CWORD'] = str(len(input_str.split()) - 1) + sys.argv = input_str.split(' ') + + def _run_autocomplete(self): + util = ManagementUtility(argv=sys.argv) + try: + util.autocomplete() + except SystemExit: + pass + return self.output.getvalue().strip().split('\n') + + def test_django_admin_py(self): + "django_admin.py will autocomplete option flags" + self._user_input('django-admin.py sqlall --v') + output = self._run_autocomplete() + self.assertEqual(output, ['--verbosity=']) + + def test_manage_py(self): + "manage.py will autocomplete option flags" + self._user_input('manage.py sqlall --v') + output = self._run_autocomplete() + self.assertEqual(output, ['--verbosity=']) + + def test_custom_command(self): + "A custom command can autocomplete option flags" + self._user_input('django-admin.py test_command --l') + output = self._run_autocomplete() + self.assertEqual(output, ['--list']) + + def test_subcommands(self): + "Subcommands can be autocompleted" + self._user_input('django-admin.py sql') + output = self._run_autocomplete() + self.assertEqual(output, ['sql sqlall sqlclear sqlcustom sqlflush sqlindexes sqlinitialdata sqlreset sqlsequencereset']) + + def test_help(self): + "No errors, just an empty list if there are no autocomplete options" + self._user_input('django-admin.py help --') + output = self._run_autocomplete() + self.assertEqual(output, ['']) + + def test_runfcgi(self): + "Command arguments will be autocompleted" + self._user_input('django-admin.py runfcgi h') + output = self._run_autocomplete() + self.assertEqual(output, ['host=']) + + def test_app_completion(self): + "Application names will be autocompleted for an AppCommand" + self._user_input('django-admin.py sqlall a') + output = self._run_autocomplete() + app_labels = [name.split('.')[-1] for name in settings.INSTALLED_APPS] + self.assertEqual(output, sorted(label for label in app_labels if label.startswith('a'))) diff --git a/parts/django/tests/regressiontests/bug639/__init__.py b/parts/django/tests/regressiontests/bug639/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/bug639/__init__.py diff --git a/parts/django/tests/regressiontests/bug639/models.py b/parts/django/tests/regressiontests/bug639/models.py new file mode 100644 index 0000000..b7e3880 --- /dev/null +++ b/parts/django/tests/regressiontests/bug639/models.py @@ -0,0 +1,26 @@ +import tempfile + +from django.db import models +from django.core.files.storage import FileSystemStorage +from django.forms import ModelForm + +temp_storage_dir = tempfile.mkdtemp() +temp_storage = FileSystemStorage(temp_storage_dir) + +class Photo(models.Model): + title = models.CharField(max_length=30) + image = models.FileField(storage=temp_storage, upload_to='tests') + + # Support code for the tests; this keeps track of how many times save() + # gets called on each instance. + def __init__(self, *args, **kwargs): + super(Photo, self).__init__(*args, **kwargs) + self._savecount = 0 + + def save(self, force_insert=False, force_update=False): + super(Photo, self).save(force_insert, force_update) + self._savecount += 1 + +class PhotoForm(ModelForm): + class Meta: + model = Photo diff --git a/parts/django/tests/regressiontests/bug639/test.jpg b/parts/django/tests/regressiontests/bug639/test.jpg Binary files differnew file mode 100644 index 0000000..391b57a --- /dev/null +++ b/parts/django/tests/regressiontests/bug639/test.jpg diff --git a/parts/django/tests/regressiontests/bug639/tests.py b/parts/django/tests/regressiontests/bug639/tests.py new file mode 100644 index 0000000..2cc3a8a --- /dev/null +++ b/parts/django/tests/regressiontests/bug639/tests.py @@ -0,0 +1,41 @@ +""" +Tests for file field behavior, and specifically #639, in which Model.save() +gets called *again* for each FileField. This test will fail if calling a +ModelForm's save() method causes Model.save() to be called more than once. +""" + +import os +import shutil +import unittest + +from django.core.files.uploadedfile import SimpleUploadedFile +from regressiontests.bug639.models import Photo, PhotoForm, temp_storage_dir + +class Bug639Test(unittest.TestCase): + + def testBug639(self): + """ + Simulate a file upload and check how many times Model.save() gets + called. + """ + # Grab an image for testing. + filename = os.path.join(os.path.dirname(__file__), "test.jpg") + img = open(filename, "rb").read() + + # Fake a POST QueryDict and FILES MultiValueDict. + data = {'title': 'Testing'} + files = {"image": SimpleUploadedFile('test.jpg', img, 'image/jpeg')} + + form = PhotoForm(data=data, files=files) + p = form.save() + + # Check the savecount stored on the object (see the model). + self.assertEqual(p._savecount, 1) + + def tearDown(self): + """ + Make sure to delete the "uploaded" file to avoid clogging /tmp. + """ + p = Photo.objects.get() + p.image.delete(save=False) + shutil.rmtree(temp_storage_dir) diff --git a/parts/django/tests/regressiontests/bug8245/__init__.py b/parts/django/tests/regressiontests/bug8245/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/bug8245/__init__.py diff --git a/parts/django/tests/regressiontests/bug8245/admin.py b/parts/django/tests/regressiontests/bug8245/admin.py new file mode 100644 index 0000000..1812269 --- /dev/null +++ b/parts/django/tests/regressiontests/bug8245/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from models import Story + + +admin.site.register(Story) +raise Exception("Bad admin module") diff --git a/parts/django/tests/regressiontests/bug8245/models.py b/parts/django/tests/regressiontests/bug8245/models.py new file mode 100644 index 0000000..5643955 --- /dev/null +++ b/parts/django/tests/regressiontests/bug8245/models.py @@ -0,0 +1,4 @@ +from django.db import models + +class Story(models.Model): + title = models.CharField(max_length=10) diff --git a/parts/django/tests/regressiontests/bug8245/tests.py b/parts/django/tests/regressiontests/bug8245/tests.py new file mode 100644 index 0000000..5aa4a94 --- /dev/null +++ b/parts/django/tests/regressiontests/bug8245/tests.py @@ -0,0 +1,29 @@ +from unittest import TestCase + +from django.contrib import admin + + +class Bug8245Test(TestCase): + """ + Test for bug #8245 - don't raise an AlreadyRegistered exception when using + autodiscover() and an admin.py module contains an error. + """ + def test_bug_8245(self): + # The first time autodiscover is called, we should get our real error. + try: + admin.autodiscover() + except Exception, e: + self.assertEqual(str(e), "Bad admin module") + else: + self.fail( + 'autodiscover should have raised a "Bad admin module" error.') + + # Calling autodiscover again should raise the very same error it did + # the first time, not an AlreadyRegistered error. + try: + admin.autodiscover() + except Exception, e: + self.assertEqual(str(e), "Bad admin module") + else: + self.fail( + 'autodiscover should have raised a "Bad admin module" error.') diff --git a/parts/django/tests/regressiontests/builtin_server/__init__.py b/parts/django/tests/regressiontests/builtin_server/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/builtin_server/__init__.py diff --git a/parts/django/tests/regressiontests/builtin_server/models.py b/parts/django/tests/regressiontests/builtin_server/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/builtin_server/models.py diff --git a/parts/django/tests/regressiontests/builtin_server/tests.py b/parts/django/tests/regressiontests/builtin_server/tests.py new file mode 100644 index 0000000..c3cfef1 --- /dev/null +++ b/parts/django/tests/regressiontests/builtin_server/tests.py @@ -0,0 +1,51 @@ +from unittest import TestCase +from StringIO import StringIO +from django.core.servers.basehttp import ServerHandler + +# +# Tests for #9659: wsgi.file_wrapper in the builtin server. +# We need to mock a couple of of handlers and keep track of what +# gets called when using a couple kinds of WSGI apps. +# + +class DummyHandler(object): + def log_request(*args, **kwargs): + pass + +class FileWrapperHandler(ServerHandler): + def __init__(self, *args, **kwargs): + ServerHandler.__init__(self, *args, **kwargs) + self.request_handler = DummyHandler() + self._used_sendfile = False + + def sendfile(self): + self._used_sendfile = True + return True + +def wsgi_app(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + return ['Hello World!'] + +def wsgi_app_file_wrapper(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + return environ['wsgi.file_wrapper'](StringIO('foo')) + +class WSGIFileWrapperTests(TestCase): + """ + Test that the wsgi.file_wrapper works for the builting server. + """ + + def test_file_wrapper_uses_sendfile(self): + env = {'SERVER_PROTOCOL': 'HTTP/1.0'} + err = StringIO() + handler = FileWrapperHandler(None, StringIO(), err, env) + handler.run(wsgi_app_file_wrapper) + self.assert_(handler._used_sendfile) + + def test_file_wrapper_no_sendfile(self): + env = {'SERVER_PROTOCOL': 'HTTP/1.0'} + err = StringIO() + handler = FileWrapperHandler(None, StringIO(), err, env) + handler.run(wsgi_app) + self.assertFalse(handler._used_sendfile) + self.assertEqual(handler.stdout.getvalue().splitlines()[-1],'Hello World!') diff --git a/parts/django/tests/regressiontests/cache/__init__.py b/parts/django/tests/regressiontests/cache/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/cache/__init__.py diff --git a/parts/django/tests/regressiontests/cache/liberal_backend.py b/parts/django/tests/regressiontests/cache/liberal_backend.py new file mode 100644 index 0000000..5c7e312 --- /dev/null +++ b/parts/django/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/parts/django/tests/regressiontests/cache/models.py b/parts/django/tests/regressiontests/cache/models.py new file mode 100644 index 0000000..643fd22 --- /dev/null +++ b/parts/django/tests/regressiontests/cache/models.py @@ -0,0 +1,11 @@ +from django.db import models +from datetime import datetime + +def expensive_calculation(): + expensive_calculation.num_runs += 1 + return datetime.now() + +class Poll(models.Model): + question = models.CharField(max_length=200) + answer = models.CharField(max_length=200) + pub_date = models.DateTimeField('date published', default=expensive_calculation) diff --git a/parts/django/tests/regressiontests/cache/tests.py b/parts/django/tests/regressiontests/cache/tests.py new file mode 100644 index 0000000..7167e2f --- /dev/null +++ b/parts/django/tests/regressiontests/cache/tests.py @@ -0,0 +1,652 @@ +# -*- coding: utf-8 -*- + +# Unit tests for cache framework +# Uses whatever cache backend is set in the test settings file. + +import os +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, CacheKeyWarning +from django.http import HttpResponse, HttpRequest +from django.middleware.cache import FetchFromCacheMiddleware, UpdateCacheMiddleware +from django.test.utils import get_warnings_state, restore_warnings_state +from django.utils import translation +from django.utils.cache import patch_vary_headers, get_cache_key, learn_cache_key +from django.utils.hashcompat import md5_constructor +from regressiontests.cache.models import Poll, expensive_calculation + +# functions/classes for complex data type tests +def f(): + return 42 +class C: + def m(n): + return 24 + +class DummyCacheTests(unittest.TestCase): + # The Dummy cache backend doesn't really behave like a test backend, + # so it has different test requirements. + def setUp(self): + self.cache = get_cache('dummy://') + + def test_simple(self): + "Dummy cache backend ignores cache set calls" + self.cache.set("key", "value") + self.assertEqual(self.cache.get("key"), None) + + def test_add(self): + "Add doesn't do anything in dummy cache backend" + self.cache.add("addkey1", "value") + result = self.cache.add("addkey1", "newvalue") + self.assertEqual(result, True) + self.assertEqual(self.cache.get("addkey1"), None) + + def test_non_existent(self): + "Non-existent keys aren't found in the dummy cache backend" + self.assertEqual(self.cache.get("does_not_exist"), None) + self.assertEqual(self.cache.get("does_not_exist", "bang!"), "bang!") + + def test_get_many(self): + "get_many returns nothing for the dummy cache backend" + self.cache.set('a', 'a') + self.cache.set('b', 'b') + self.cache.set('c', 'c') + self.cache.set('d', 'd') + self.assertEqual(self.cache.get_many(['a', 'c', 'd']), {}) + self.assertEqual(self.cache.get_many(['a', 'b', 'e']), {}) + + def test_delete(self): + "Cache deletion is transparently ignored on the dummy cache backend" + self.cache.set("key1", "spam") + self.cache.set("key2", "eggs") + self.assertEqual(self.cache.get("key1"), None) + self.cache.delete("key1") + self.assertEqual(self.cache.get("key1"), None) + self.assertEqual(self.cache.get("key2"), None) + + def test_has_key(self): + "The has_key method doesn't ever return True for the dummy cache backend" + self.cache.set("hello1", "goodbye1") + self.assertEqual(self.cache.has_key("hello1"), False) + self.assertEqual(self.cache.has_key("goodbye1"), False) + + def test_in(self): + "The in operator doesn't ever return True for the dummy cache backend" + self.cache.set("hello2", "goodbye2") + self.assertEqual("hello2" in self.cache, False) + self.assertEqual("goodbye2" in self.cache, False) + + def test_incr(self): + "Dummy cache values can't be incremented" + self.cache.set('answer', 42) + self.assertRaises(ValueError, self.cache.incr, 'answer') + self.assertRaises(ValueError, self.cache.incr, 'does_not_exist') + + def test_decr(self): + "Dummy cache values can't be decremented" + self.cache.set('answer', 42) + self.assertRaises(ValueError, self.cache.decr, 'answer') + self.assertRaises(ValueError, self.cache.decr, 'does_not_exist') + + def test_data_types(self): + "All data types are ignored equally by the dummy cache" + stuff = { + 'string' : 'this is a string', + 'int' : 42, + 'list' : [1, 2, 3, 4], + 'tuple' : (1, 2, 3, 4), + 'dict' : {'A': 1, 'B' : 2}, + 'function' : f, + 'class' : C, + } + self.cache.set("stuff", stuff) + self.assertEqual(self.cache.get("stuff"), None) + + def test_expiration(self): + "Expiration has no effect on the dummy cache" + self.cache.set('expire1', 'very quickly', 1) + self.cache.set('expire2', 'very quickly', 1) + self.cache.set('expire3', 'very quickly', 1) + + time.sleep(2) + self.assertEqual(self.cache.get("expire1"), None) + + self.cache.add("expire2", "newvalue") + self.assertEqual(self.cache.get("expire2"), None) + self.assertEqual(self.cache.has_key("expire3"), False) + + def test_unicode(self): + "Unicode values are ignored by the dummy cache" + stuff = { + u'ascii': u'ascii_value', + u'unicode_ascii': u'Iñtërnâtiônàlizætiøn1', + u'Iñtërnâtiônàlizætiøn': u'Iñtërnâtiônàlizætiøn2', + u'ascii': {u'x' : 1 } + } + for (key, value) in stuff.items(): + self.cache.set(key, value) + self.assertEqual(self.cache.get(key), None) + + def test_set_many(self): + "set_many does nothing for the dummy cache backend" + self.cache.set_many({'a': 1, 'b': 2}) + + def test_delete_many(self): + "delete_many does nothing for the dummy cache backend" + self.cache.delete_many(['a', 'b']) + + def test_clear(self): + "clear does nothing for the dummy cache backend" + self.cache.clear() + + +class BaseCacheTests(object): + # A common set of tests to apply to all cache backends + def tearDown(self): + self.cache.clear() + + def test_simple(self): + # Simple cache set/get works + self.cache.set("key", "value") + self.assertEqual(self.cache.get("key"), "value") + + def test_add(self): + # A key can be added to a cache + self.cache.add("addkey1", "value") + result = self.cache.add("addkey1", "newvalue") + self.assertEqual(result, False) + self.assertEqual(self.cache.get("addkey1"), "value") + + def test_non_existent(self): + # Non-existent cache keys return as None/default + # get with non-existent keys + self.assertEqual(self.cache.get("does_not_exist"), None) + self.assertEqual(self.cache.get("does_not_exist", "bang!"), "bang!") + + def test_get_many(self): + # Multiple cache keys can be returned using get_many + self.cache.set('a', 'a') + self.cache.set('b', 'b') + self.cache.set('c', 'c') + self.cache.set('d', 'd') + self.assertEqual(self.cache.get_many(['a', 'c', 'd']), {'a' : 'a', 'c' : 'c', 'd' : 'd'}) + self.assertEqual(self.cache.get_many(['a', 'b', 'e']), {'a' : 'a', 'b' : 'b'}) + + def test_delete(self): + # Cache keys can be deleted + self.cache.set("key1", "spam") + self.cache.set("key2", "eggs") + self.assertEqual(self.cache.get("key1"), "spam") + self.cache.delete("key1") + self.assertEqual(self.cache.get("key1"), None) + self.assertEqual(self.cache.get("key2"), "eggs") + + def test_has_key(self): + # The cache can be inspected for cache keys + self.cache.set("hello1", "goodbye1") + self.assertEqual(self.cache.has_key("hello1"), True) + self.assertEqual(self.cache.has_key("goodbye1"), False) + + def test_in(self): + # The in operator can be used to inspet cache contents + self.cache.set("hello2", "goodbye2") + self.assertEqual("hello2" in self.cache, True) + self.assertEqual("goodbye2" in self.cache, False) + + def test_incr(self): + # Cache values can be incremented + self.cache.set('answer', 41) + self.assertEqual(self.cache.incr('answer'), 42) + self.assertEqual(self.cache.get('answer'), 42) + self.assertEqual(self.cache.incr('answer', 10), 52) + self.assertEqual(self.cache.get('answer'), 52) + self.assertRaises(ValueError, self.cache.incr, 'does_not_exist') + + def test_decr(self): + # Cache values can be decremented + self.cache.set('answer', 43) + self.assertEqual(self.cache.decr('answer'), 42) + self.assertEqual(self.cache.get('answer'), 42) + self.assertEqual(self.cache.decr('answer', 10), 32) + self.assertEqual(self.cache.get('answer'), 32) + self.assertRaises(ValueError, self.cache.decr, 'does_not_exist') + + def test_data_types(self): + # Many different data types can be cached + stuff = { + 'string' : 'this is a string', + 'int' : 42, + 'list' : [1, 2, 3, 4], + 'tuple' : (1, 2, 3, 4), + 'dict' : {'A': 1, 'B' : 2}, + 'function' : f, + 'class' : C, + } + self.cache.set("stuff", stuff) + self.assertEqual(self.cache.get("stuff"), stuff) + + def test_cache_read_for_model_instance(self): + # Don't want fields with callable as default to be called on cache read + expensive_calculation.num_runs = 0 + Poll.objects.all().delete() + my_poll = Poll.objects.create(question="Well?") + self.assertEqual(Poll.objects.count(), 1) + pub_date = my_poll.pub_date + self.cache.set('question', my_poll) + cached_poll = self.cache.get('question') + self.assertEqual(cached_poll.pub_date, pub_date) + # We only want the default expensive calculation run once + self.assertEqual(expensive_calculation.num_runs, 1) + + def test_cache_write_for_model_instance_with_deferred(self): + # Don't want fields with callable as default to be called on cache write + expensive_calculation.num_runs = 0 + Poll.objects.all().delete() + my_poll = Poll.objects.create(question="What?") + self.assertEqual(expensive_calculation.num_runs, 1) + defer_qs = Poll.objects.all().defer('question') + self.assertEqual(defer_qs.count(), 1) + self.assertEqual(expensive_calculation.num_runs, 1) + self.cache.set('deferred_queryset', defer_qs) + # cache set should not re-evaluate default functions + self.assertEqual(expensive_calculation.num_runs, 1) + + def test_cache_read_for_model_instance_with_deferred(self): + # Don't want fields with callable as default to be called on cache read + expensive_calculation.num_runs = 0 + Poll.objects.all().delete() + my_poll = Poll.objects.create(question="What?") + self.assertEqual(expensive_calculation.num_runs, 1) + defer_qs = Poll.objects.all().defer('question') + self.assertEqual(defer_qs.count(), 1) + self.cache.set('deferred_queryset', defer_qs) + self.assertEqual(expensive_calculation.num_runs, 1) + runs_before_cache_read = expensive_calculation.num_runs + cached_polls = self.cache.get('deferred_queryset') + # We only want the default expensive calculation run on creation and set + self.assertEqual(expensive_calculation.num_runs, runs_before_cache_read) + + def test_expiration(self): + # Cache values can be set to expire + self.cache.set('expire1', 'very quickly', 1) + self.cache.set('expire2', 'very quickly', 1) + self.cache.set('expire3', 'very quickly', 1) + + time.sleep(2) + self.assertEqual(self.cache.get("expire1"), None) + + self.cache.add("expire2", "newvalue") + self.assertEqual(self.cache.get("expire2"), "newvalue") + self.assertEqual(self.cache.has_key("expire3"), False) + + def test_unicode(self): + # Unicode values can be cached + stuff = { + u'ascii': u'ascii_value', + u'unicode_ascii': u'Iñtërnâtiônàlizætiøn1', + u'Iñtërnâtiônàlizætiøn': u'Iñtërnâtiônàlizætiøn2', + u'ascii': {u'x' : 1 } + } + for (key, value) in stuff.items(): + self.cache.set(key, value) + self.assertEqual(self.cache.get(key), value) + + def test_binary_string(self): + # Binary strings should be cachable + from zlib import compress, decompress + value = 'value_to_be_compressed' + compressed_value = compress(value) + self.cache.set('binary1', compressed_value) + compressed_result = self.cache.get('binary1') + self.assertEqual(compressed_value, compressed_result) + self.assertEqual(value, decompress(compressed_result)) + + def test_set_many(self): + # Multiple keys can be set using set_many + self.cache.set_many({"key1": "spam", "key2": "eggs"}) + self.assertEqual(self.cache.get("key1"), "spam") + self.assertEqual(self.cache.get("key2"), "eggs") + + def test_set_many_expiration(self): + # set_many takes a second ``timeout`` parameter + self.cache.set_many({"key1": "spam", "key2": "eggs"}, 1) + time.sleep(2) + self.assertEqual(self.cache.get("key1"), None) + self.assertEqual(self.cache.get("key2"), None) + + def test_delete_many(self): + # Multiple keys can be deleted using delete_many + self.cache.set("key1", "spam") + self.cache.set("key2", "eggs") + self.cache.set("key3", "ham") + self.cache.delete_many(["key1", "key2"]) + self.assertEqual(self.cache.get("key1"), None) + self.assertEqual(self.cache.get("key2"), None) + self.assertEqual(self.cache.get("key3"), "ham") + + def test_clear(self): + # The cache can be emptied using clear + self.cache.set("key1", "spam") + self.cache.set("key2", "eggs") + self.cache.clear() + self.assertEqual(self.cache.get("key1"), None) + self.assertEqual(self.cache.get("key2"), None) + + def test_long_timeout(self): + ''' + Using a timeout greater than 30 days makes memcached think + it is an absolute expiration timestamp instead of a relative + offset. Test that we honour this convention. Refs #12399. + ''' + self.cache.set('key1', 'eggs', 60*60*24*30 + 1) #30 days + 1 second + self.assertEqual(self.cache.get('key1'), 'eggs') + + self.cache.add('key2', 'ham', 60*60*24*30 + 1) + self.assertEqual(self.cache.get('key2'), 'ham') + + self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, 60*60*24*30 + 1) + 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_state = get_warnings_state() + warnings.simplefilter("error", CacheKeyWarning) + + try: + # 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') + finally: + restore_warnings_state(_warnings_state) + +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?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://?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 +# need to contain a CACHE_BACKEND setting that points at +# your memcache server. +if settings.CACHE_BACKEND.startswith('memcached://'): + class MemcachedCacheTests(unittest.TestCase, BaseCacheTests): + 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?max_entries=30' % self.dirname) + + def test_hashing(self): + """Test that keys are hashed into subdirectories correctly""" + self.cache.set("foo", "bar") + keyhash = md5_constructor("foo").hexdigest() + keypath = os.path.join(self.dirname, keyhash[:2], keyhash[2:4], keyhash[4:]) + self.assert_(os.path.exists(keypath)) + + def test_subdirectory_removal(self): + """ + Make sure that the created subdirectories are correctly removed when empty. + """ + self.cache.set("foo", "bar") + keyhash = md5_constructor("foo").hexdigest() + keypath = os.path.join(self.dirname, keyhash[:2], keyhash[2:4], keyhash[4:]) + self.assert_(os.path.exists(keypath)) + + self.cache.delete("foo") + self.assert_(not os.path.exists(keypath)) + 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.""" + + def setUp(self): + self.path = '/cache/test/' + self.old_settings_key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX + self.old_middleware_seconds = settings.CACHE_MIDDLEWARE_SECONDS + self.orig_use_i18n = settings.USE_I18N + settings.CACHE_MIDDLEWARE_KEY_PREFIX = 'settingsprefix' + settings.CACHE_MIDDLEWARE_SECONDS = 1 + settings.USE_I18N = False + + def tearDown(self): + settings.CACHE_MIDDLEWARE_KEY_PREFIX = self.old_settings_key_prefix + settings.CACHE_MIDDLEWARE_SECONDS = self.old_middleware_seconds + settings.USE_I18N = self.orig_use_i18n + + def _get_request(self, path): + request = HttpRequest() + request.META = { + 'SERVER_NAME': 'testserver', + 'SERVER_PORT': 80, + } + request.path = request.path_info = "/cache/%s" % path + return request + + def test_patch_vary_headers(self): + headers = ( + # Initial vary, new headers, resulting vary. + (None, ('Accept-Encoding',), 'Accept-Encoding'), + ('Accept-Encoding', ('accept-encoding',), 'Accept-Encoding'), + ('Accept-Encoding', ('ACCEPT-ENCODING',), 'Accept-Encoding'), + ('Cookie', ('Accept-Encoding',), 'Cookie, Accept-Encoding'), + ('Cookie, Accept-Encoding', ('Accept-Encoding',), 'Cookie, Accept-Encoding'), + ('Cookie, Accept-Encoding', ('Accept-Encoding', 'cookie'), 'Cookie, Accept-Encoding'), + (None, ('Accept-Encoding', 'COOKIE'), 'Accept-Encoding, COOKIE'), + ('Cookie, Accept-Encoding', ('Accept-Encoding', 'cookie'), 'Cookie, Accept-Encoding'), + ('Cookie , Accept-Encoding', ('Accept-Encoding', 'cookie'), 'Cookie, Accept-Encoding'), + ) + for initial_vary, newheaders, resulting_vary in headers: + response = HttpResponse() + if initial_vary is not None: + response['Vary'] = initial_vary + patch_vary_headers(response, newheaders) + self.assertEqual(response['Vary'], resulting_vary) + + def test_get_cache_key(self): + request = self._get_request(self.path) + response = HttpResponse() + key_prefix = 'localprefix' + # Expect None if no headers have been set yet. + self.assertEqual(get_cache_key(request), None) + # Set headers to an empty list. + learn_cache_key(request, response) + self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') + # Verify that a specified key_prefix is taken in to account. + learn_cache_key(request, response, key_prefix=key_prefix) + self.assertEqual(get_cache_key(request, key_prefix=key_prefix), 'views.decorators.cache.cache_page.localprefix.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') + + def test_learn_cache_key(self): + request = self._get_request(self.path) + response = HttpResponse() + response['Vary'] = 'Pony' + # Make sure that the Vary header is added to the key hash + learn_cache_key(request, response) + self.assertEqual(get_cache_key(request), 'views.decorators.cache.cache_page.settingsprefix.a8c87a3d8c44853d7f79474f7ffe4ad5.d41d8cd98f00b204e9800998ecf8427e') + +class CacheI18nTest(unittest.TestCase): + + def setUp(self): + self.orig_cache_middleware_seconds = settings.CACHE_MIDDLEWARE_SECONDS + self.orig_cache_middleware_key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX + self.orig_cache_backend = settings.CACHE_BACKEND + self.orig_use_i18n = settings.USE_I18N + self.orig_languages = settings.LANGUAGES + settings.LANGUAGES = ( + ('en', 'English'), + ('es', 'Spanish'), + ) + settings.CACHE_MIDDLEWARE_KEY_PREFIX = 'settingsprefix' + self.path = '/cache/test/' + + def tearDown(self): + settings.CACHE_MIDDLEWARE_SECONDS = self.orig_cache_middleware_seconds + settings.CACHE_MIDDLEWARE_KEY_PREFIX = self.orig_cache_middleware_key_prefix + settings.CACHE_BACKEND = self.orig_cache_backend + settings.USE_I18N = self.orig_use_i18n + settings.LANGUAGES = self.orig_languages + translation.deactivate() + + def _get_request(self): + request = HttpRequest() + request.META = { + 'SERVER_NAME': 'testserver', + 'SERVER_PORT': 80, + } + request.path = request.path_info = self.path + return request + + def _get_request_cache(self): + request = HttpRequest() + request.META = { + 'SERVER_NAME': 'testserver', + 'SERVER_PORT': 80, + } + request.path = request.path_info = self.path + request._cache_update_cache = True + request.method = 'GET' + request.session = {} + return request + + def test_cache_key_i18n(self): + settings.USE_I18N = True + request = self._get_request() + lang = translation.get_language() + response = HttpResponse() + key = learn_cache_key(request, response) + self.assertTrue(key.endswith(lang), "Cache keys should include the language name when i18n is active") + key2 = get_cache_key(request) + self.assertEqual(key, key2) + + def test_cache_key_no_i18n (self): + settings.USE_I18N = False + request = self._get_request() + lang = translation.get_language() + response = HttpResponse() + key = learn_cache_key(request, response) + self.assertFalse(key.endswith(lang), "Cache keys shouldn't include the language name when i18n is inactive") + + def test_middleware(self): + def set_cache(request, lang, msg): + translation.activate(lang) + response = HttpResponse() + response.content= msg + return UpdateCacheMiddleware().process_response(request, response) + + settings.CACHE_MIDDLEWARE_SECONDS = 60 + settings.CACHE_MIDDLEWARE_KEY_PREFIX="test" + settings.CACHE_BACKEND='locmem:///' + settings.USE_I18N = True + en_message ="Hello world!" + es_message ="Hola mundo!" + + request = self._get_request_cache() + set_cache(request, 'en', en_message) + get_cache_data = FetchFromCacheMiddleware().process_request(request) + # Check that we can recover the cache + self.assertNotEqual(get_cache_data.content, None) + self.assertEqual(en_message, get_cache_data.content) + # change the session language and set content + request = self._get_request_cache() + set_cache(request, 'es', es_message) + # change again the language + translation.activate('en') + # retrieve the content from cache + get_cache_data = FetchFromCacheMiddleware().process_request(request) + self.assertEqual(get_cache_data.content, en_message) + # change again the language + translation.activate('es') + get_cache_data = FetchFromCacheMiddleware().process_request(request) + self.assertEqual(get_cache_data.content, es_message) + +if __name__ == '__main__': + unittest.main() diff --git a/parts/django/tests/regressiontests/comment_tests/__init__.py b/parts/django/tests/regressiontests/comment_tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/__init__.py diff --git a/parts/django/tests/regressiontests/comment_tests/custom_comments/__init__.py b/parts/django/tests/regressiontests/comment_tests/custom_comments/__init__.py new file mode 100644 index 0000000..598927e --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/custom_comments/__init__.py @@ -0,0 +1,32 @@ +from django.core import urlresolvers +from regressiontests.comment_tests.custom_comments.models import CustomComment +from regressiontests.comment_tests.custom_comments.forms import CustomCommentForm + +def get_model(): + return CustomComment + +def get_form(): + return CustomCommentForm + +def get_form_target(): + return urlresolvers.reverse( + "regressiontests.comment_tests.custom_comments.views.custom_submit_comment" + ) + +def get_flag_url(c): + return urlresolvers.reverse( + "regressiontests.comment_tests.custom_comments.views.custom_flag_comment", + args=(c.id,) + ) + +def get_delete_url(c): + return urlresolvers.reverse( + "regressiontests.comment_tests.custom_comments.views.custom_delete_comment", + args=(c.id,) + ) + +def get_approve_url(c): + return urlresolvers.reverse( + "regressiontests.comment_tests.custom_comments.views.custom_approve_comment", + args=(c.id,) + ) diff --git a/parts/django/tests/regressiontests/comment_tests/custom_comments/forms.py b/parts/django/tests/regressiontests/comment_tests/custom_comments/forms.py new file mode 100644 index 0000000..b788cdc --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/custom_comments/forms.py @@ -0,0 +1,4 @@ +from django import forms + +class CustomCommentForm(forms.Form): + pass diff --git a/parts/django/tests/regressiontests/comment_tests/custom_comments/models.py b/parts/django/tests/regressiontests/comment_tests/custom_comments/models.py new file mode 100644 index 0000000..592ad79 --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/custom_comments/models.py @@ -0,0 +1,4 @@ +from django.db import models + +class CustomComment(models.Model): + pass diff --git a/parts/django/tests/regressiontests/comment_tests/custom_comments/views.py b/parts/django/tests/regressiontests/comment_tests/custom_comments/views.py new file mode 100644 index 0000000..93cea9d --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/custom_comments/views.py @@ -0,0 +1,13 @@ +from django.http import HttpResponse + +def custom_submit_comment(request): + return HttpResponse("Hello from the custom submit comment view.") + +def custom_flag_comment(request, comment_id): + return HttpResponse("Hello from the custom flag view.") + +def custom_delete_comment(request, comment_id): + return HttpResponse("Hello from the custom delete view.") + +def custom_approve_comment(request, comment_id): + return HttpResponse("Hello from the custom approve view.") diff --git a/parts/django/tests/regressiontests/comment_tests/fixtures/comment_tests.json b/parts/django/tests/regressiontests/comment_tests/fixtures/comment_tests.json new file mode 100644 index 0000000..55e2161 --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/fixtures/comment_tests.json @@ -0,0 +1,53 @@ +[ + { + "model" : "comment_tests.book", + "pk" : 1, + "fields" : { + "dewey_decimal" : "12.34" + } + }, + { + "model" : "comment_tests.author", + "pk" : 1, + "fields" : { + "first_name" : "John", + "last_name" : "Smith" + } + }, + { + "model" : "comment_tests.author", + "pk" : 2, + "fields" : { + "first_name" : "Peter", + "last_name" : "Jones" + } + }, + { + "model" : "comment_tests.article", + "pk" : 1, + "fields" : { + "author" : 1, + "headline" : "Man Bites Dog" + } + }, + { + "model" : "comment_tests.article", + "pk" : 2, + "fields" : { + "author" : 2, + "headline" : "Dog Bites Man" + } + }, + + { + "model" : "auth.user", + "pk" : 100, + "fields" : { + "username" : "normaluser", + "password" : "34ea4aaaf24efcbb4b30d27302f8657f", + "first_name": "Joe", + "last_name": "Normal", + "email": "joe.normal@example.com" + } + } +] diff --git a/parts/django/tests/regressiontests/comment_tests/fixtures/comment_utils.xml b/parts/django/tests/regressiontests/comment_tests/fixtures/comment_utils.xml new file mode 100644 index 0000000..a39bbf6 --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/fixtures/comment_utils.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="1" model="comment_tests.entry"> + <field type="CharField" name="title">ABC</field> + <field type="TextField" name="body">This is the body</field> + <field type="DateField" name="pub_date">2008-01-01</field> + <field type="BooleanField" name="enable_comments">True</field> + </object> + <object pk="2" model="comment_tests.entry"> + <field type="CharField" name="title">XYZ</field> + <field type="TextField" name="body">Text here</field> + <field type="DateField" name="pub_date">2008-01-02</field> + <field type="BooleanField" name="enable_comments">False</field> + </object> +</django-objects> diff --git a/parts/django/tests/regressiontests/comment_tests/models.py b/parts/django/tests/regressiontests/comment_tests/models.py new file mode 100644 index 0000000..8877ea1 --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/models.py @@ -0,0 +1,34 @@ +""" +Comments may be attached to any object. See the comment documentation for +more information. +""" + +from django.db import models +from django.test import TestCase + +class Author(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + + def __str__(self): + return '%s %s' % (self.first_name, self.last_name) + +class Article(models.Model): + author = models.ForeignKey(Author) + headline = models.CharField(max_length=100) + + def __str__(self): + return self.headline + +class Entry(models.Model): + title = models.CharField(max_length=250) + body = models.TextField() + pub_date = models.DateField() + enable_comments = models.BooleanField() + + def __str__(self): + return self.title + +class Book(models.Model): + dewey_decimal = models.DecimalField(primary_key = True, decimal_places=2, max_digits=5) +
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/comment_tests/tests/__init__.py b/parts/django/tests/regressiontests/comment_tests/tests/__init__.py new file mode 100644 index 0000000..449fea4 --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/tests/__init__.py @@ -0,0 +1,89 @@ +from django.contrib.auth.models import User +from django.contrib.comments.forms import CommentForm +from django.contrib.comments.models import Comment +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site +from django.test import TestCase +from regressiontests.comment_tests.models import Article, Author + +# Shortcut +CT = ContentType.objects.get_for_model + +# Helper base class for comment tests that need data. +class CommentTestCase(TestCase): + fixtures = ["comment_tests"] + urls = 'django.contrib.comments.urls' + + def createSomeComments(self): + # Two anonymous comments on two different objects + c1 = Comment.objects.create( + content_type = CT(Article), + object_pk = "1", + user_name = "Joe Somebody", + user_email = "jsomebody@example.com", + user_url = "http://example.com/~joe/", + comment = "First!", + site = Site.objects.get_current(), + ) + c2 = Comment.objects.create( + content_type = CT(Author), + object_pk = "1", + user_name = "Joe Somebody", + user_email = "jsomebody@example.com", + user_url = "http://example.com/~joe/", + comment = "First here, too!", + site = Site.objects.get_current(), + ) + + # Two authenticated comments: one on the same Article, and + # one on a different Author + user = User.objects.create( + username = "frank_nobody", + first_name = "Frank", + last_name = "Nobody", + email = "fnobody@example.com", + password = "", + is_staff = False, + is_active = True, + is_superuser = False, + ) + c3 = Comment.objects.create( + content_type = CT(Article), + object_pk = "1", + user = user, + user_url = "http://example.com/~frank/", + comment = "Damn, I wanted to be first.", + site = Site.objects.get_current(), + ) + c4 = Comment.objects.create( + content_type = CT(Author), + object_pk = "2", + user = user, + user_url = "http://example.com/~frank/", + comment = "You get here first, too?", + site = Site.objects.get_current(), + ) + + return c1, c2, c3, c4 + + def getData(self): + return { + 'name' : 'Jim Bob', + 'email' : 'jim.bob@example.com', + 'url' : '', + 'comment' : 'This is my comment', + } + + def getValidData(self, obj): + f = CommentForm(obj) + d = self.getData() + d.update(f.initial) + return d + +from regressiontests.comment_tests.tests.app_api_tests import * +from regressiontests.comment_tests.tests.model_tests import * +from regressiontests.comment_tests.tests.comment_form_tests import * +from regressiontests.comment_tests.tests.templatetag_tests import * +from regressiontests.comment_tests.tests.comment_view_tests import * +from regressiontests.comment_tests.tests.moderation_view_tests import * +from regressiontests.comment_tests.tests.comment_utils_moderators_tests import * diff --git a/parts/django/tests/regressiontests/comment_tests/tests/app_api_tests.py b/parts/django/tests/regressiontests/comment_tests/tests/app_api_tests.py new file mode 100644 index 0000000..c4d9ebf --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/tests/app_api_tests.py @@ -0,0 +1,71 @@ +from django.conf import settings +from django.contrib import comments +from django.contrib.comments.models import Comment +from django.contrib.comments.forms import CommentForm +from regressiontests.comment_tests.tests import CommentTestCase + +class CommentAppAPITests(CommentTestCase): + """Tests for the "comment app" API""" + + def testGetCommentApp(self): + self.assertEqual(comments.get_comment_app(), comments) + + def testGetForm(self): + self.assertEqual(comments.get_form(), CommentForm) + + def testGetFormTarget(self): + self.assertEqual(comments.get_form_target(), "/post/") + + def testGetFlagURL(self): + c = Comment(id=12345) + self.assertEqual(comments.get_flag_url(c), "/flag/12345/") + + def getGetDeleteURL(self): + c = Comment(id=12345) + self.assertEqual(comments.get_delete_url(c), "/delete/12345/") + + def getGetApproveURL(self): + c = Comment(id=12345) + self.assertEqual(comments.get_approve_url(c), "/approve/12345/") + + +class CustomCommentTest(CommentTestCase): + urls = 'regressiontests.comment_tests.urls' + + def setUp(self): + self.old_comments_app = getattr(settings, 'COMMENTS_APP', None) + settings.COMMENTS_APP = 'regressiontests.comment_tests.custom_comments' + settings.INSTALLED_APPS = list(settings.INSTALLED_APPS) + [settings.COMMENTS_APP,] + + def tearDown(self): + del settings.INSTALLED_APPS[-1] + settings.COMMENTS_APP = self.old_comments_app + if settings.COMMENTS_APP is None: + delattr(settings._wrapped, 'COMMENTS_APP') + + def testGetCommentApp(self): + from regressiontests.comment_tests import custom_comments + self.assertEqual(comments.get_comment_app(), custom_comments) + + def testGetModel(self): + from regressiontests.comment_tests.custom_comments.models import CustomComment + self.assertEqual(comments.get_model(), CustomComment) + + def testGetForm(self): + from regressiontests.comment_tests.custom_comments.forms import CustomCommentForm + self.assertEqual(comments.get_form(), CustomCommentForm) + + def testGetFormTarget(self): + self.assertEqual(comments.get_form_target(), "/post/") + + def testGetFlagURL(self): + c = Comment(id=12345) + self.assertEqual(comments.get_flag_url(c), "/flag/12345/") + + def getGetDeleteURL(self): + c = Comment(id=12345) + self.assertEqual(comments.get_delete_url(c), "/delete/12345/") + + def getGetApproveURL(self): + c = Comment(id=12345) + self.assertEqual(comments.get_approve_url(c), "/approve/12345/") diff --git a/parts/django/tests/regressiontests/comment_tests/tests/comment_form_tests.py b/parts/django/tests/regressiontests/comment_tests/tests/comment_form_tests.py new file mode 100644 index 0000000..8dfcc07 --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/tests/comment_form_tests.py @@ -0,0 +1,84 @@ +import time + +from django.conf import settings +from django.contrib.comments.forms import CommentForm +from django.contrib.comments.models import Comment +from django.utils.hashcompat import sha_constructor + +from regressiontests.comment_tests.models import Article +from regressiontests.comment_tests.tests import CommentTestCase + + +class CommentFormTests(CommentTestCase): + def testInit(self): + f = CommentForm(Article.objects.get(pk=1)) + self.assertEqual(f.initial['content_type'], str(Article._meta)) + self.assertEqual(f.initial['object_pk'], "1") + self.assertNotEqual(f.initial['security_hash'], None) + self.assertNotEqual(f.initial['timestamp'], None) + + def testValidPost(self): + a = Article.objects.get(pk=1) + f = CommentForm(a, data=self.getValidData(a)) + self.assert_(f.is_valid(), f.errors) + return f + + def tamperWithForm(self, **kwargs): + a = Article.objects.get(pk=1) + d = self.getValidData(a) + d.update(kwargs) + f = CommentForm(Article.objects.get(pk=1), data=d) + self.assertFalse(f.is_valid()) + return f + + def testHoneypotTampering(self): + self.tamperWithForm(honeypot="I am a robot") + + def testTimestampTampering(self): + self.tamperWithForm(timestamp=str(time.time() - 28800)) + + def testSecurityHashTampering(self): + self.tamperWithForm(security_hash="Nobody expects the Spanish Inquisition!") + + def testContentTypeTampering(self): + self.tamperWithForm(content_type="auth.user") + + def testObjectPKTampering(self): + self.tamperWithForm(object_pk="3") + + def testSecurityErrors(self): + f = self.tamperWithForm(honeypot="I am a robot") + self.assert_("honeypot" in f.security_errors()) + + def testGetCommentObject(self): + f = self.testValidPost() + c = f.get_comment_object() + self.assert_(isinstance(c, Comment)) + self.assertEqual(c.content_object, Article.objects.get(pk=1)) + self.assertEqual(c.comment, "This is my comment") + c.save() + self.assertEqual(Comment.objects.count(), 1) + + def testProfanities(self): + """Test COMMENTS_ALLOW_PROFANITIES and PROFANITIES_LIST settings""" + a = Article.objects.get(pk=1) + d = self.getValidData(a) + + # Save settings in case other tests need 'em + saved = settings.PROFANITIES_LIST, settings.COMMENTS_ALLOW_PROFANITIES + + # Don't wanna swear in the unit tests if we don't have to... + settings.PROFANITIES_LIST = ["rooster"] + + # Try with COMMENTS_ALLOW_PROFANITIES off + settings.COMMENTS_ALLOW_PROFANITIES = False + f = CommentForm(a, data=dict(d, comment="What a rooster!")) + self.assertFalse(f.is_valid()) + + # Now with COMMENTS_ALLOW_PROFANITIES on + settings.COMMENTS_ALLOW_PROFANITIES = True + f = CommentForm(a, data=dict(d, comment="What a rooster!")) + self.assertTrue(f.is_valid()) + + # Restore settings + settings.PROFANITIES_LIST, settings.COMMENTS_ALLOW_PROFANITIES = saved diff --git a/parts/django/tests/regressiontests/comment_tests/tests/comment_utils_moderators_tests.py b/parts/django/tests/regressiontests/comment_tests/tests/comment_utils_moderators_tests.py new file mode 100644 index 0000000..2e93b8b --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/tests/comment_utils_moderators_tests.py @@ -0,0 +1,75 @@ +from regressiontests.comment_tests.tests import CommentTestCase, CT, Site +from django.contrib.comments.forms import CommentForm +from django.contrib.comments.models import Comment +from django.contrib.comments.moderation import moderator, CommentModerator, AlreadyModerated +from regressiontests.comment_tests.models import Entry +from django.core import mail + +class EntryModerator1(CommentModerator): + email_notification = True + +class EntryModerator2(CommentModerator): + enable_field = 'enable_comments' + +class EntryModerator3(CommentModerator): + auto_close_field = 'pub_date' + close_after = 7 + +class EntryModerator4(CommentModerator): + auto_moderate_field = 'pub_date' + moderate_after = 7 + +class CommentUtilsModeratorTests(CommentTestCase): + fixtures = ["comment_utils.xml"] + + def createSomeComments(self): + # Tests for the moderation signals must actually post data + # through the comment views, because only the comment views + # emit the custom signals moderation listens for. + e = Entry.objects.get(pk=1) + data = self.getValidData(e) + + self.client.post("/post/", data, REMOTE_ADDR="1.2.3.4") + + # We explicitly do a try/except to get the comment we've just + # posted because moderation may have disallowed it, in which + # case we can just return it as None. + try: + c1 = Comment.objects.all()[0] + except IndexError: + c1 = None + + self.client.post("/post/", data, REMOTE_ADDR="1.2.3.4") + + try: + c2 = Comment.objects.all()[0] + except IndexError: + c2 = None + return c1, c2 + + def tearDown(self): + moderator.unregister(Entry) + + def testRegisterExistingModel(self): + moderator.register(Entry, EntryModerator1) + self.assertRaises(AlreadyModerated, moderator.register, Entry, EntryModerator1) + + def testEmailNotification(self): + moderator.register(Entry, EntryModerator1) + self.createSomeComments() + self.assertEquals(len(mail.outbox), 2) + + def testCommentsEnabled(self): + moderator.register(Entry, EntryModerator2) + self.createSomeComments() + self.assertEquals(Comment.objects.all().count(), 1) + + def testAutoCloseField(self): + moderator.register(Entry, EntryModerator3) + self.createSomeComments() + self.assertEquals(Comment.objects.all().count(), 0) + + def testAutoModerateField(self): + moderator.register(Entry, EntryModerator4) + c1, c2 = self.createSomeComments() + self.assertEquals(c2.is_public, False) diff --git a/parts/django/tests/regressiontests/comment_tests/tests/comment_view_tests.py b/parts/django/tests/regressiontests/comment_tests/tests/comment_view_tests.py new file mode 100644 index 0000000..b8a62b4 --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/tests/comment_view_tests.py @@ -0,0 +1,258 @@ +import re +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.comments import signals +from django.contrib.comments.models import Comment +from regressiontests.comment_tests.models import Article, Book +from regressiontests.comment_tests.tests import CommentTestCase + +post_redirect_re = re.compile(r'^http://testserver/posted/\?c=(?P<pk>\d+$)') + +class CommentViewTests(CommentTestCase): + + def testPostCommentHTTPMethods(self): + a = Article.objects.get(pk=1) + data = self.getValidData(a) + response = self.client.get("/post/", data) + self.assertEqual(response.status_code, 405) + self.assertEqual(response["Allow"], "POST") + + def testPostCommentMissingCtype(self): + a = Article.objects.get(pk=1) + data = self.getValidData(a) + del data["content_type"] + response = self.client.post("/post/", data) + self.assertEqual(response.status_code, 400) + + def testPostCommentBadCtype(self): + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data["content_type"] = "Nobody expects the Spanish Inquisition!" + response = self.client.post("/post/", data) + self.assertEqual(response.status_code, 400) + + def testPostCommentMissingObjectPK(self): + a = Article.objects.get(pk=1) + data = self.getValidData(a) + del data["object_pk"] + response = self.client.post("/post/", data) + self.assertEqual(response.status_code, 400) + + def testPostCommentBadObjectPK(self): + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data["object_pk"] = "14" + response = self.client.post("/post/", data) + self.assertEqual(response.status_code, 400) + + def testPostInvalidIntegerPK(self): + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data["comment"] = "This is another comment" + data["object_pk"] = u'\ufffd' + response = self.client.post("/post/", data) + self.assertEqual(response.status_code, 400) + + def testPostInvalidDecimalPK(self): + b = Book.objects.get(pk='12.34') + data = self.getValidData(b) + data["comment"] = "This is another comment" + data["object_pk"] = 'cookies' + response = self.client.post("/post/", data) + self.assertEqual(response.status_code, 400) + + def testCommentPreview(self): + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data["preview"] = "Preview" + response = self.client.post("/post/", data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "comments/preview.html") + + def testHashTampering(self): + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data["security_hash"] = "Nobody expects the Spanish Inquisition!" + response = self.client.post("/post/", data) + self.assertEqual(response.status_code, 400) + + def testDebugCommentErrors(self): + """The debug error template should be shown only if DEBUG is True""" + olddebug = settings.DEBUG + + settings.DEBUG = True + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data["security_hash"] = "Nobody expects the Spanish Inquisition!" + response = self.client.post("/post/", data) + self.assertEqual(response.status_code, 400) + self.assertTemplateUsed(response, "comments/400-debug.html") + + settings.DEBUG = False + response = self.client.post("/post/", data) + self.assertEqual(response.status_code, 400) + self.assertTemplateNotUsed(response, "comments/400-debug.html") + + settings.DEBUG = olddebug + + def testCreateValidComment(self): + a = Article.objects.get(pk=1) + data = self.getValidData(a) + self.response = self.client.post("/post/", data, REMOTE_ADDR="1.2.3.4") + self.assertEqual(self.response.status_code, 302) + self.assertEqual(Comment.objects.count(), 1) + c = Comment.objects.all()[0] + self.assertEqual(c.ip_address, "1.2.3.4") + self.assertEqual(c.comment, "This is my comment") + + def testPostAsAuthenticatedUser(self): + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data['name'] = data['email'] = '' + self.client.login(username="normaluser", password="normaluser") + self.response = self.client.post("/post/", data, REMOTE_ADDR="1.2.3.4") + self.assertEqual(self.response.status_code, 302) + self.assertEqual(Comment.objects.count(), 1) + c = Comment.objects.all()[0] + self.assertEqual(c.ip_address, "1.2.3.4") + u = User.objects.get(username='normaluser') + self.assertEqual(c.user, u) + self.assertEqual(c.user_name, u.get_full_name()) + self.assertEqual(c.user_email, u.email) + + def testPostAsAuthenticatedUserWithoutFullname(self): + """ + Check that the user's name in the comment is populated for + authenticated users without first_name and last_name. + """ + user = User.objects.create_user(username='jane_other', + email='jane@example.com', password='jane_other') + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data['name'] = data['email'] = '' + self.client.login(username="jane_other", password="jane_other") + self.response = self.client.post("/post/", data, REMOTE_ADDR="1.2.3.4") + c = Comment.objects.get(user=user) + self.assertEqual(c.ip_address, "1.2.3.4") + self.assertEqual(c.user_name, 'jane_other') + user.delete() + + def testPreventDuplicateComments(self): + """Prevent posting the exact same comment twice""" + a = Article.objects.get(pk=1) + data = self.getValidData(a) + self.client.post("/post/", data) + self.client.post("/post/", data) + self.assertEqual(Comment.objects.count(), 1) + + # This should not trigger the duplicate prevention + self.client.post("/post/", dict(data, comment="My second comment.")) + self.assertEqual(Comment.objects.count(), 2) + + def testCommentSignals(self): + """Test signals emitted by the comment posting view""" + + # callback + def receive(sender, **kwargs): + self.assertEqual(kwargs['comment'].comment, "This is my comment") + self.assert_('request' in kwargs) + received_signals.append(kwargs.get('signal')) + + # Connect signals and keep track of handled ones + received_signals = [] + expected_signals = [ + signals.comment_will_be_posted, signals.comment_was_posted + ] + for signal in expected_signals: + signal.connect(receive) + + # Post a comment and check the signals + self.testCreateValidComment() + self.assertEqual(received_signals, expected_signals) + + for signal in expected_signals: + signal.disconnect(receive) + + def testWillBePostedSignal(self): + """ + Test that the comment_will_be_posted signal can prevent the comment from + actually getting saved + """ + def receive(sender, **kwargs): return False + signals.comment_will_be_posted.connect(receive, dispatch_uid="comment-test") + a = Article.objects.get(pk=1) + data = self.getValidData(a) + response = self.client.post("/post/", data) + self.assertEqual(response.status_code, 400) + self.assertEqual(Comment.objects.count(), 0) + signals.comment_will_be_posted.disconnect(dispatch_uid="comment-test") + + def testWillBePostedSignalModifyComment(self): + """ + Test that the comment_will_be_posted signal can modify a comment before + it gets posted + """ + def receive(sender, **kwargs): + # a bad but effective spam filter :)... + kwargs['comment'].is_public = False + + signals.comment_will_be_posted.connect(receive) + self.testCreateValidComment() + c = Comment.objects.all()[0] + self.assertFalse(c.is_public) + + def testCommentNext(self): + """Test the different "next" actions the comment view can take""" + a = Article.objects.get(pk=1) + data = self.getValidData(a) + response = self.client.post("/post/", data) + location = response["Location"] + match = post_redirect_re.match(location) + self.assertTrue(match != None, "Unexpected redirect location: %s" % location) + + data["next"] = "/somewhere/else/" + data["comment"] = "This is another comment" + response = self.client.post("/post/", data) + location = response["Location"] + match = re.search(r"^http://testserver/somewhere/else/\?c=\d+$", location) + self.assertTrue(match != None, "Unexpected redirect location: %s" % location) + + def testCommentDoneView(self): + a = Article.objects.get(pk=1) + data = self.getValidData(a) + response = self.client.post("/post/", data) + location = response["Location"] + match = post_redirect_re.match(location) + self.assertTrue(match != None, "Unexpected redirect location: %s" % location) + pk = int(match.group('pk')) + response = self.client.get(location) + self.assertTemplateUsed(response, "comments/posted.html") + self.assertEqual(response.context[0]["comment"], Comment.objects.get(pk=pk)) + + def testCommentNextWithQueryString(self): + """ + The `next` key needs to handle already having a query string (#10585) + """ + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data["next"] = "/somewhere/else/?foo=bar" + data["comment"] = "This is another comment" + response = self.client.post("/post/", data) + location = response["Location"] + match = re.search(r"^http://testserver/somewhere/else/\?foo=bar&c=\d+$", location) + self.assertTrue(match != None, "Unexpected redirect location: %s" % location) + + def testCommentPostRedirectWithInvalidIntegerPK(self): + """ + Tests that attempting to retrieve the location specified in the + post redirect, after adding some invalid data to the expected + querystring it ends with, doesn't cause a server error. + """ + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data["comment"] = "This is another comment" + response = self.client.post("/post/", data) + location = response["Location"] + broken_location = location + u"\ufffd" + response = self.client.get(broken_location) + self.assertEqual(response.status_code, 200) diff --git a/parts/django/tests/regressiontests/comment_tests/tests/model_tests.py b/parts/django/tests/regressiontests/comment_tests/tests/model_tests.py new file mode 100644 index 0000000..2cbf66f --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/tests/model_tests.py @@ -0,0 +1,49 @@ +from django.contrib.comments.models import Comment + +from regressiontests.comment_tests.models import Author, Article +from regressiontests.comment_tests.tests import CommentTestCase + + +class CommentModelTests(CommentTestCase): + def testSave(self): + for c in self.createSomeComments(): + self.assertNotEqual(c.submit_date, None) + + def testUserProperties(self): + c1, c2, c3, c4 = self.createSomeComments() + self.assertEqual(c1.name, "Joe Somebody") + self.assertEqual(c2.email, "jsomebody@example.com") + self.assertEqual(c3.name, "Frank Nobody") + self.assertEqual(c3.url, "http://example.com/~frank/") + self.assertEqual(c1.user, None) + self.assertEqual(c3.user, c4.user) + +class CommentManagerTests(CommentTestCase): + + def testInModeration(self): + """Comments that aren't public are considered in moderation""" + c1, c2, c3, c4 = self.createSomeComments() + c1.is_public = False + c2.is_public = False + c1.save() + c2.save() + moderated_comments = list(Comment.objects.in_moderation().order_by("id")) + self.assertEqual(moderated_comments, [c1, c2]) + + def testRemovedCommentsNotInModeration(self): + """Removed comments are not considered in moderation""" + c1, c2, c3, c4 = self.createSomeComments() + c1.is_public = False + c2.is_public = False + c2.is_removed = True + c1.save() + c2.save() + moderated_comments = list(Comment.objects.in_moderation()) + self.assertEqual(moderated_comments, [c1]) + + def testForModel(self): + c1, c2, c3, c4 = self.createSomeComments() + article_comments = list(Comment.objects.for_model(Article).order_by("id")) + author_comments = list(Comment.objects.for_model(Author.objects.get(pk=1))) + self.assertEqual(article_comments, [c1, c3]) + self.assertEqual(author_comments, [c2]) diff --git a/parts/django/tests/regressiontests/comment_tests/tests/moderation_view_tests.py b/parts/django/tests/regressiontests/comment_tests/tests/moderation_view_tests.py new file mode 100644 index 0000000..e5094ab --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/tests/moderation_view_tests.py @@ -0,0 +1,203 @@ +from django.contrib.auth.models import User, Permission +from django.contrib.comments import signals +from django.contrib.comments.models import Comment, CommentFlag +from django.contrib.contenttypes.models import ContentType + +from regressiontests.comment_tests.tests import CommentTestCase + + +class FlagViewTests(CommentTestCase): + + def testFlagGet(self): + """GET the flag view: render a confirmation page.""" + comments = self.createSomeComments() + pk = comments[0].pk + self.client.login(username="normaluser", password="normaluser") + response = self.client.get("/flag/%d/" % pk) + self.assertTemplateUsed(response, "comments/flag.html") + + def testFlagPost(self): + """POST the flag view: actually flag the view (nice for XHR)""" + comments = self.createSomeComments() + pk = comments[0].pk + self.client.login(username="normaluser", password="normaluser") + response = self.client.post("/flag/%d/" % pk) + self.assertEqual(response["Location"], "http://testserver/flagged/?c=%d" % pk) + c = Comment.objects.get(pk=pk) + self.assertEqual(c.flags.filter(flag=CommentFlag.SUGGEST_REMOVAL).count(), 1) + return c + + def testFlagPostTwice(self): + """Users don't get to flag comments more than once.""" + c = self.testFlagPost() + self.client.post("/flag/%d/" % c.pk) + self.client.post("/flag/%d/" % c.pk) + self.assertEqual(c.flags.filter(flag=CommentFlag.SUGGEST_REMOVAL).count(), 1) + + def testFlagAnon(self): + """GET/POST the flag view while not logged in: redirect to log in.""" + comments = self.createSomeComments() + pk = comments[0].pk + response = self.client.get("/flag/%d/" % pk) + self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/flag/%d/" % pk) + response = self.client.post("/flag/%d/" % pk) + self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/flag/%d/" % pk) + + def testFlaggedView(self): + comments = self.createSomeComments() + pk = comments[0].pk + response = self.client.get("/flagged/", data={"c":pk}) + self.assertTemplateUsed(response, "comments/flagged.html") + + def testFlagSignals(self): + """Test signals emitted by the comment flag view""" + + # callback + def receive(sender, **kwargs): + self.assertEqual(kwargs['flag'].flag, CommentFlag.SUGGEST_REMOVAL) + self.assertEqual(kwargs['request'].user.username, "normaluser") + received_signals.append(kwargs.get('signal')) + + # Connect signals and keep track of handled ones + received_signals = [] + signals.comment_was_flagged.connect(receive) + + # Post a comment and check the signals + self.testFlagPost() + self.assertEqual(received_signals, [signals.comment_was_flagged]) + +def makeModerator(username): + u = User.objects.get(username=username) + ct = ContentType.objects.get_for_model(Comment) + p = Permission.objects.get(content_type=ct, codename="can_moderate") + u.user_permissions.add(p) + +class DeleteViewTests(CommentTestCase): + + def testDeletePermissions(self): + """The delete view should only be accessible to 'moderators'""" + comments = self.createSomeComments() + pk = comments[0].pk + self.client.login(username="normaluser", password="normaluser") + response = self.client.get("/delete/%d/" % pk) + self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/delete/%d/" % pk) + + makeModerator("normaluser") + response = self.client.get("/delete/%d/" % pk) + self.assertEqual(response.status_code, 200) + + def testDeletePost(self): + """POSTing the delete view should mark the comment as removed""" + comments = self.createSomeComments() + pk = comments[0].pk + makeModerator("normaluser") + self.client.login(username="normaluser", password="normaluser") + response = self.client.post("/delete/%d/" % pk) + self.assertEqual(response["Location"], "http://testserver/deleted/?c=%d" % pk) + c = Comment.objects.get(pk=pk) + self.assertTrue(c.is_removed) + self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_DELETION, user__username="normaluser").count(), 1) + + def testDeleteSignals(self): + def receive(sender, **kwargs): + received_signals.append(kwargs.get('signal')) + + # Connect signals and keep track of handled ones + received_signals = [] + signals.comment_was_flagged.connect(receive) + + # Post a comment and check the signals + self.testDeletePost() + self.assertEqual(received_signals, [signals.comment_was_flagged]) + + def testDeletedView(self): + comments = self.createSomeComments() + pk = comments[0].pk + response = self.client.get("/deleted/", data={"c":pk}) + self.assertTemplateUsed(response, "comments/deleted.html") + +class ApproveViewTests(CommentTestCase): + + def testApprovePermissions(self): + """The delete view should only be accessible to 'moderators'""" + comments = self.createSomeComments() + pk = comments[0].pk + self.client.login(username="normaluser", password="normaluser") + response = self.client.get("/approve/%d/" % pk) + self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/approve/%d/" % pk) + + makeModerator("normaluser") + response = self.client.get("/approve/%d/" % pk) + self.assertEqual(response.status_code, 200) + + def testApprovePost(self): + """POSTing the delete view should mark the comment as removed""" + c1, c2, c3, c4 = self.createSomeComments() + c1.is_public = False; c1.save() + + makeModerator("normaluser") + self.client.login(username="normaluser", password="normaluser") + response = self.client.post("/approve/%d/" % c1.pk) + self.assertEqual(response["Location"], "http://testserver/approved/?c=%d" % c1.pk) + c = Comment.objects.get(pk=c1.pk) + self.assertTrue(c.is_public) + self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_APPROVAL, user__username="normaluser").count(), 1) + + def testApproveSignals(self): + def receive(sender, **kwargs): + received_signals.append(kwargs.get('signal')) + + # Connect signals and keep track of handled ones + received_signals = [] + signals.comment_was_flagged.connect(receive) + + # Post a comment and check the signals + self.testApprovePost() + self.assertEqual(received_signals, [signals.comment_was_flagged]) + + def testApprovedView(self): + comments = self.createSomeComments() + pk = comments[0].pk + response = self.client.get("/approved/", data={"c":pk}) + self.assertTemplateUsed(response, "comments/approved.html") + +class AdminActionsTests(CommentTestCase): + urls = "regressiontests.comment_tests.urls_admin" + + def setUp(self): + super(AdminActionsTests, self).setUp() + + # Make "normaluser" a moderator + u = User.objects.get(username="normaluser") + u.is_staff = True + perms = Permission.objects.filter( + content_type__app_label = 'comments', + codename__endswith = 'comment' + ) + for perm in perms: + u.user_permissions.add(perm) + u.save() + + def testActionsNonModerator(self): + comments = self.createSomeComments() + self.client.login(username="normaluser", password="normaluser") + response = self.client.get("/admin/comments/comment/") + self.assertEquals("approve_comments" in response.content, False) + + def testActionsModerator(self): + comments = self.createSomeComments() + makeModerator("normaluser") + self.client.login(username="normaluser", password="normaluser") + response = self.client.get("/admin/comments/comment/") + self.assertEquals("approve_comments" in response.content, True) + + def testActionsDisabledDelete(self): + "Tests a CommentAdmin where 'delete_selected' has been disabled." + comments = self.createSomeComments() + self.client.login(username="normaluser", password="normaluser") + response = self.client.get('/admin2/comments/comment/') + self.assertEqual(response.status_code, 200) + self.assert_( + '<option value="delete_selected">' not in response.content, + "Found an unexpected delete_selected in response" + ) diff --git a/parts/django/tests/regressiontests/comment_tests/tests/templatetag_tests.py b/parts/django/tests/regressiontests/comment_tests/tests/templatetag_tests.py new file mode 100644 index 0000000..29a097a --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/tests/templatetag_tests.py @@ -0,0 +1,97 @@ +from django.contrib.comments.forms import CommentForm +from django.contrib.comments.models import Comment +from django.contrib.contenttypes.models import ContentType +from django.template import Template, Context +from regressiontests.comment_tests.models import Article, Author +from regressiontests.comment_tests.tests import CommentTestCase + +class CommentTemplateTagTests(CommentTestCase): + + def render(self, t, **c): + ctx = Context(c) + out = Template(t).render(ctx) + return ctx, out + + def testCommentFormTarget(self): + ctx, out = self.render("{% load comments %}{% comment_form_target %}") + self.assertEqual(out, "/post/") + + def testGetCommentForm(self, tag=None): + t = "{% load comments %}" + (tag or "{% get_comment_form for comment_tests.article a.id as form %}") + ctx, out = self.render(t, a=Article.objects.get(pk=1)) + self.assertEqual(out, "") + self.assert_(isinstance(ctx["form"], CommentForm)) + + def testGetCommentFormFromLiteral(self): + self.testGetCommentForm("{% get_comment_form for comment_tests.article 1 as form %}") + + def testGetCommentFormFromObject(self): + self.testGetCommentForm("{% get_comment_form for a as form %}") + + def testRenderCommentForm(self, tag=None): + t = "{% load comments %}" + (tag or "{% render_comment_form for comment_tests.article a.id %}") + ctx, out = self.render(t, a=Article.objects.get(pk=1)) + self.assert_(out.strip().startswith("<form action=")) + self.assert_(out.strip().endswith("</form>")) + + def testRenderCommentFormFromLiteral(self): + self.testRenderCommentForm("{% render_comment_form for comment_tests.article 1 %}") + + def testRenderCommentFormFromObject(self): + self.testRenderCommentForm("{% render_comment_form for a %}") + + def testGetCommentCount(self, tag=None): + self.createSomeComments() + t = "{% load comments %}" + (tag or "{% get_comment_count for comment_tests.article a.id as cc %}") + "{{ cc }}" + ctx, out = self.render(t, a=Article.objects.get(pk=1)) + self.assertEqual(out, "2") + + def testGetCommentCountFromLiteral(self): + self.testGetCommentCount("{% get_comment_count for comment_tests.article 1 as cc %}") + + def testGetCommentCountFromObject(self): + self.testGetCommentCount("{% get_comment_count for a as cc %}") + + def testGetCommentList(self, tag=None): + c1, c2, c3, c4 = self.createSomeComments() + t = "{% load comments %}" + (tag or "{% get_comment_list for comment_tests.author a.id as cl %}") + ctx, out = self.render(t, a=Author.objects.get(pk=1)) + self.assertEqual(out, "") + self.assertEqual(list(ctx["cl"]), [c2]) + + def testGetCommentListFromLiteral(self): + self.testGetCommentList("{% get_comment_list for comment_tests.author 1 as cl %}") + + def testGetCommentListFromObject(self): + self.testGetCommentList("{% get_comment_list for a as cl %}") + + def testGetCommentPermalink(self): + c1, c2, c3, c4 = self.createSomeComments() + t = "{% load comments %}{% get_comment_list for comment_tests.author author.id as cl %}" + t += "{% get_comment_permalink cl.0 %}" + ct = ContentType.objects.get_for_model(Author) + author = Author.objects.get(pk=1) + ctx, out = self.render(t, author=author) + self.assertEqual(out, "/cr/%s/%s/#c%s" % (ct.id, author.id, c2.id)) + + def testGetCommentPermalinkFormatted(self): + c1, c2, c3, c4 = self.createSomeComments() + t = "{% load comments %}{% get_comment_list for comment_tests.author author.id as cl %}" + t += "{% get_comment_permalink cl.0 '#c%(id)s-by-%(user_name)s' %}" + ct = ContentType.objects.get_for_model(Author) + author = Author.objects.get(pk=1) + ctx, out = self.render(t, author=author) + self.assertEqual(out, "/cr/%s/%s/#c%s-by-Joe Somebody" % (ct.id, author.id, c2.id)) + + def testRenderCommentList(self, tag=None): + t = "{% load comments %}" + (tag or "{% render_comment_list for comment_tests.article a.id %}") + ctx, out = self.render(t, a=Article.objects.get(pk=1)) + self.assert_(out.strip().startswith("<dl id=\"comments\">")) + self.assert_(out.strip().endswith("</dl>")) + + def testRenderCommentListFromLiteral(self): + self.testRenderCommentList("{% render_comment_list for comment_tests.article 1 %}") + + def testRenderCommentListFromObject(self): + self.testRenderCommentList("{% render_comment_list for a %}") + diff --git a/parts/django/tests/regressiontests/comment_tests/urls.py b/parts/django/tests/regressiontests/comment_tests/urls.py new file mode 100644 index 0000000..0058689 --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('regressiontests.comment_tests.custom_comments.views', + url(r'^post/$', 'custom_submit_comment'), + url(r'^flag/(\d+)/$', 'custom_flag_comment'), + url(r'^delete/(\d+)/$', 'custom_delete_comment'), + url(r'^approve/(\d+)/$', 'custom_approve_comment'), +) + diff --git a/parts/django/tests/regressiontests/comment_tests/urls_admin.py b/parts/django/tests/regressiontests/comment_tests/urls_admin.py new file mode 100644 index 0000000..d7e1a4e --- /dev/null +++ b/parts/django/tests/regressiontests/comment_tests/urls_admin.py @@ -0,0 +1,19 @@ +from django.conf.urls.defaults import * +from django.contrib import admin +from django.contrib.comments.admin import CommentsAdmin +from django.contrib.comments.models import Comment + +# Make a new AdminSite to avoid picking up the deliberately broken admin +# modules in other tests. +admin_site = admin.AdminSite() +admin_site.register(Comment, CommentsAdmin) + +# To demonstrate proper functionality even when ``delete_selected`` is removed. +admin_site2 = admin.AdminSite() +admin_site2.disable_action('delete_selected') +admin_site2.register(Comment, CommentsAdmin) + +urlpatterns = patterns('', + (r'^admin/', include(admin_site.urls)), + (r'^admin2/', include(admin_site2.urls)), +) diff --git a/parts/django/tests/regressiontests/conditional_processing/__init__.py b/parts/django/tests/regressiontests/conditional_processing/__init__.py new file mode 100644 index 0000000..380474e --- /dev/null +++ b/parts/django/tests/regressiontests/conditional_processing/__init__.py @@ -0,0 +1 @@ +# -*- coding:utf-8 -*- diff --git a/parts/django/tests/regressiontests/conditional_processing/models.py b/parts/django/tests/regressiontests/conditional_processing/models.py new file mode 100644 index 0000000..b291aed --- /dev/null +++ b/parts/django/tests/regressiontests/conditional_processing/models.py @@ -0,0 +1,128 @@ +# -*- coding:utf-8 -*- +from datetime import datetime, timedelta +from calendar import timegm + +from django.test import TestCase +from django.utils.http import parse_etags, quote_etag + +FULL_RESPONSE = 'Test conditional get response' +LAST_MODIFIED = datetime(2007, 10, 21, 23, 21, 47) +LAST_MODIFIED_STR = 'Sun, 21 Oct 2007 23:21:47 GMT' +EXPIRED_LAST_MODIFIED_STR = 'Sat, 20 Oct 2007 23:21:47 GMT' +ETAG = 'b4246ffc4f62314ca13147c9d4f76974' +EXPIRED_ETAG = '7fae4cd4b0f81e7d2914700043aa8ed6' + +class ConditionalGet(TestCase): + def assertFullResponse(self, response, check_last_modified=True, check_etag=True): + self.assertEquals(response.status_code, 200) + self.assertEquals(response.content, FULL_RESPONSE) + if check_last_modified: + self.assertEquals(response['Last-Modified'], LAST_MODIFIED_STR) + if check_etag: + self.assertEquals(response['ETag'], '"%s"' % ETAG) + + def assertNotModified(self, response): + self.assertEquals(response.status_code, 304) + self.assertEquals(response.content, '') + + def testWithoutConditions(self): + response = self.client.get('/condition/') + self.assertFullResponse(response) + + def testIfModifiedSince(self): + self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR + response = self.client.get('/condition/') + self.assertNotModified(response) + self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR + response = self.client.get('/condition/') + self.assertFullResponse(response) + + def testIfNoneMatch(self): + self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG + response = self.client.get('/condition/') + self.assertNotModified(response) + self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % EXPIRED_ETAG + response = self.client.get('/condition/') + self.assertFullResponse(response) + + # Several etags in If-None-Match is a bit exotic but why not? + self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s", "%s"' % (ETAG, EXPIRED_ETAG) + response = self.client.get('/condition/') + self.assertNotModified(response) + + def testIfMatch(self): + self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % ETAG + response = self.client.put('/condition/etag/', {'data': ''}) + self.assertEquals(response.status_code, 200) + self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % EXPIRED_ETAG + response = self.client.put('/condition/etag/', {'data': ''}) + self.assertEquals(response.status_code, 412) + + def testBothHeaders(self): + self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR + self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG + response = self.client.get('/condition/') + self.assertNotModified(response) + + self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR + self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG + response = self.client.get('/condition/') + self.assertFullResponse(response) + + self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR + self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % EXPIRED_ETAG + response = self.client.get('/condition/') + self.assertFullResponse(response) + + def testSingleCondition1(self): + self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR + response = self.client.get('/condition/last_modified/') + self.assertNotModified(response) + response = self.client.get('/condition/etag/') + self.assertFullResponse(response, check_last_modified=False) + + def testSingleCondition2(self): + self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG + response = self.client.get('/condition/etag/') + self.assertNotModified(response) + response = self.client.get('/condition/last_modified/') + self.assertFullResponse(response, check_etag=False) + + def testSingleCondition3(self): + self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = EXPIRED_LAST_MODIFIED_STR + response = self.client.get('/condition/last_modified/') + self.assertFullResponse(response, check_etag=False) + + def testSingleCondition4(self): + self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % EXPIRED_ETAG + response = self.client.get('/condition/etag/') + self.assertFullResponse(response, check_last_modified=False) + + def testSingleCondition5(self): + self.client.defaults['HTTP_IF_MODIFIED_SINCE'] = LAST_MODIFIED_STR + response = self.client.get('/condition/last_modified2/') + self.assertNotModified(response) + response = self.client.get('/condition/etag2/') + self.assertFullResponse(response, check_last_modified=False) + + def testSingleCondition6(self): + self.client.defaults['HTTP_IF_NONE_MATCH'] = '"%s"' % ETAG + response = self.client.get('/condition/etag2/') + self.assertNotModified(response) + response = self.client.get('/condition/last_modified2/') + self.assertFullResponse(response, check_etag=False) + + def testInvalidETag(self): + self.client.defaults['HTTP_IF_NONE_MATCH'] = r'"\"' + response = self.client.get('/condition/etag/') + self.assertFullResponse(response, check_last_modified=False) + + +class ETagProcesing(TestCase): + def testParsing(self): + etags = parse_etags(r'"", "etag", "e\"t\"ag", "e\\tag", W/"weak"') + self.assertEquals(etags, ['', 'etag', 'e"t"ag', r'e\tag', 'weak']) + + def testQuoting(self): + quoted_etag = quote_etag(r'e\t"ag') + self.assertEquals(quoted_etag, r'"e\\t\"ag"') diff --git a/parts/django/tests/regressiontests/conditional_processing/urls.py b/parts/django/tests/regressiontests/conditional_processing/urls.py new file mode 100644 index 0000000..4dbe11a --- /dev/null +++ b/parts/django/tests/regressiontests/conditional_processing/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls.defaults import * +import views + +urlpatterns = patterns('', + ('^$', views.index), + ('^last_modified/$', views.last_modified_view1), + ('^last_modified2/$', views.last_modified_view2), + ('^etag/$', views.etag_view1), + ('^etag2/$', views.etag_view2), +) diff --git a/parts/django/tests/regressiontests/conditional_processing/views.py b/parts/django/tests/regressiontests/conditional_processing/views.py new file mode 100644 index 0000000..df49281 --- /dev/null +++ b/parts/django/tests/regressiontests/conditional_processing/views.py @@ -0,0 +1,26 @@ +# -*- coding:utf-8 -*- +from django.views.decorators.http import condition, etag, last_modified +from django.http import HttpResponse + +from models import FULL_RESPONSE, LAST_MODIFIED, ETAG + +def index(request): + return HttpResponse(FULL_RESPONSE) +index = condition(lambda r: ETAG, lambda r: LAST_MODIFIED)(index) + +def last_modified_view1(request): + return HttpResponse(FULL_RESPONSE) +last_modified_view1 = condition(last_modified_func=lambda r: LAST_MODIFIED)(last_modified_view1) + +def last_modified_view2(request): + return HttpResponse(FULL_RESPONSE) +last_modified_view2 = last_modified(lambda r: LAST_MODIFIED)(last_modified_view2) + +def etag_view1(request): + return HttpResponse(FULL_RESPONSE) +etag_view1 = condition(etag_func=lambda r: ETAG)(etag_view1) + +def etag_view2(request): + return HttpResponse(FULL_RESPONSE) +etag_view2 = etag(lambda r: ETAG)(etag_view2) + diff --git a/parts/django/tests/regressiontests/context_processors/__init__.py b/parts/django/tests/regressiontests/context_processors/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/__init__.py diff --git a/parts/django/tests/regressiontests/context_processors/fixtures/context-processors-users.xml b/parts/django/tests/regressiontests/context_processors/fixtures/context-processors-users.xml new file mode 100644 index 0000000..aba8f4a --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/fixtures/context-processors-users.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="100" model="auth.user"> + <field type="CharField" name="username">super</field> + <field type="CharField" name="first_name">Super</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">super@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">True</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> +</django-objects> diff --git a/parts/django/tests/regressiontests/context_processors/models.py b/parts/django/tests/regressiontests/context_processors/models.py new file mode 100644 index 0000000..cde172d --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/models.py @@ -0,0 +1 @@ +# Models file for tests to run. diff --git a/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_access.html b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_access.html new file mode 100644 index 0000000..b5c65db --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_access.html @@ -0,0 +1 @@ +{{ user }} diff --git a/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_messages.html b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_messages.html new file mode 100644 index 0000000..7b7e448 --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_messages.html @@ -0,0 +1 @@ +{% for m in messages %}{{ m }}{% endfor %} diff --git a/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_no_access.html b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_no_access.html new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_no_access.html @@ -0,0 +1 @@ + diff --git a/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_perms.html b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_perms.html new file mode 100644 index 0000000..a5db868 --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_perms.html @@ -0,0 +1 @@ +{% if perms.auth %}Has auth permissions{% endif %} diff --git a/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_test_access.html b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_test_access.html new file mode 100644 index 0000000..a28ff93 --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_test_access.html @@ -0,0 +1 @@ +{% if session_accessed %}Session accessed{% else %}Session not accessed{% endif %} diff --git a/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html new file mode 100644 index 0000000..7ed16d7 --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html @@ -0,0 +1,4 @@ +unicode: {{ user }} +id: {{ user.id }} +username: {{ user.username }} +url: {% url userpage user %} diff --git a/parts/django/tests/regressiontests/context_processors/templates/context_processors/request_attrs.html b/parts/django/tests/regressiontests/context_processors/templates/context_processors/request_attrs.html new file mode 100644 index 0000000..3978e9d --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/templates/context_processors/request_attrs.html @@ -0,0 +1,13 @@ +{% if request %} +Have request +{% else %} +No request +{% endif %} + +{% if request.is_secure %} +Secure +{% else %} +Not secure +{% endif %} + +{{ request.path }} diff --git a/parts/django/tests/regressiontests/context_processors/tests.py b/parts/django/tests/regressiontests/context_processors/tests.py new file mode 100644 index 0000000..0d19bef --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/tests.py @@ -0,0 +1,112 @@ +""" +Tests for Django's bundled context processors. +""" + +from django.conf import settings +from django.contrib.auth import authenticate +from django.db.models import Q +from django.test import TestCase +from django.template import Template + +class RequestContextProcessorTests(TestCase): + """ + Tests for the ``django.core.context_processors.request`` processor. + """ + + urls = 'regressiontests.context_processors.urls' + + def test_request_attributes(self): + """ + Test that the request object is available in the template and that its + attributes can't be overridden by GET and POST parameters (#3828). + """ + url = '/request_attrs/' + # We should have the request object in the template. + response = self.client.get(url) + self.assertContains(response, 'Have request') + # Test is_secure. + response = self.client.get(url) + self.assertContains(response, 'Not secure') + response = self.client.get(url, {'is_secure': 'blah'}) + self.assertContains(response, 'Not secure') + response = self.client.post(url, {'is_secure': 'blah'}) + self.assertContains(response, 'Not secure') + # Test path. + response = self.client.get(url) + self.assertContains(response, url) + response = self.client.get(url, {'path': '/blah/'}) + self.assertContains(response, url) + response = self.client.post(url, {'path': '/blah/'}) + self.assertContains(response, url) + +class AuthContextProcessorTests(TestCase): + """ + Tests for the ``django.contrib.auth.context_processors.auth`` processor + """ + urls = 'regressiontests.context_processors.urls' + fixtures = ['context-processors-users.xml'] + + def test_session_not_accessed(self): + """ + Tests that the session is not accessed simply by including + the auth context processor + """ + response = self.client.get('/auth_processor_no_attr_access/') + self.assertContains(response, "Session not accessed") + + def test_session_is_accessed(self): + """ + Tests that the session is accessed if the auth context processor + is used and relevant attributes accessed. + """ + response = self.client.get('/auth_processor_attr_access/') + self.assertContains(response, "Session accessed") + + def test_perms_attrs(self): + self.client.login(username='super', password='secret') + response = self.client.get('/auth_processor_perms/') + self.assertContains(response, "Has auth permissions") + + def test_message_attrs(self): + self.client.login(username='super', password='secret') + response = self.client.get('/auth_processor_messages/') + self.assertContains(response, "Message 1") + + def test_user_attrs(self): + """ + Test that the lazy objects returned behave just like the wrapped objects. + """ + # These are 'functional' level tests for common use cases. Direct + # testing of the implementation (SimpleLazyObject) is in the 'utils' + # tests. + self.client.login(username='super', password='secret') + user = authenticate(username='super', password='secret') + response = self.client.get('/auth_processor_user/') + self.assertContains(response, "unicode: super") + self.assertContains(response, "id: 100") + self.assertContains(response, "username: super") + # bug #12037 is tested by the {% url %} in the template: + self.assertContains(response, "url: /userpage/super/") + + # See if this object can be used for queries where a Q() comparing + # a user can be used with another Q() (in an AND or OR fashion). + # This simulates what a template tag might do with the user from the + # context. Note that we don't need to execute a query, just build it. + # + # The failure case (bug #12049) on Python 2.4 with a LazyObject-wrapped + # User is a fatal TypeError: "function() takes at least 2 arguments + # (0 given)" deep inside deepcopy(). + # + # Python 2.5 and 2.6 succeeded, but logged internally caught exception + # spew: + # + # Exception RuntimeError: 'maximum recursion depth exceeded while + # calling a Python object' in <type 'exceptions.AttributeError'> + # ignored" + query = Q(user=response.context['user']) & Q(someflag=True) + + # Tests for user equality. This is hard because User defines + # equality in a non-duck-typing way + # See bug #12060 + self.assertEqual(response.context['user'], user) + self.assertEqual(user, response.context['user']) diff --git a/parts/django/tests/regressiontests/context_processors/urls.py b/parts/django/tests/regressiontests/context_processors/urls.py new file mode 100644 index 0000000..30728c8 --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/urls.py @@ -0,0 +1,14 @@ +from django.conf.urls.defaults import * + +import views + + +urlpatterns = patterns('', + (r'^request_attrs/$', views.request_processor), + (r'^auth_processor_no_attr_access/$', views.auth_processor_no_attr_access), + (r'^auth_processor_attr_access/$', views.auth_processor_attr_access), + (r'^auth_processor_user/$', views.auth_processor_user), + (r'^auth_processor_perms/$', views.auth_processor_perms), + (r'^auth_processor_messages/$', views.auth_processor_messages), + url(r'^userpage/(.+)/$', views.userpage, name="userpage"), +) diff --git a/parts/django/tests/regressiontests/context_processors/views.py b/parts/django/tests/regressiontests/context_processors/views.py new file mode 100644 index 0000000..3f2dcb0 --- /dev/null +++ b/parts/django/tests/regressiontests/context_processors/views.py @@ -0,0 +1,37 @@ +from django.core import context_processors +from django.shortcuts import render_to_response +from django.template.context import RequestContext + + +def request_processor(request): + return render_to_response('context_processors/request_attrs.html', + RequestContext(request, {}, processors=[context_processors.request])) + +def auth_processor_no_attr_access(request): + r1 = render_to_response('context_processors/auth_attrs_no_access.html', + RequestContext(request, {}, processors=[context_processors.auth])) + # *After* rendering, we check whether the session was accessed + return render_to_response('context_processors/auth_attrs_test_access.html', + {'session_accessed':request.session.accessed}) + +def auth_processor_attr_access(request): + r1 = render_to_response('context_processors/auth_attrs_access.html', + RequestContext(request, {}, processors=[context_processors.auth])) + return render_to_response('context_processors/auth_attrs_test_access.html', + {'session_accessed':request.session.accessed}) + +def auth_processor_user(request): + return render_to_response('context_processors/auth_attrs_user.html', + RequestContext(request, {}, processors=[context_processors.auth])) + +def auth_processor_perms(request): + return render_to_response('context_processors/auth_attrs_perms.html', + RequestContext(request, {}, processors=[context_processors.auth])) + +def auth_processor_messages(request): + request.user.message_set.create(message="Message 1") + return render_to_response('context_processors/auth_attrs_messages.html', + RequestContext(request, {}, processors=[context_processors.auth])) + +def userpage(request): + pass diff --git a/parts/django/tests/regressiontests/csrf_tests/__init__.py b/parts/django/tests/regressiontests/csrf_tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/csrf_tests/__init__.py diff --git a/parts/django/tests/regressiontests/csrf_tests/models.py b/parts/django/tests/regressiontests/csrf_tests/models.py new file mode 100644 index 0000000..71abcc5 --- /dev/null +++ b/parts/django/tests/regressiontests/csrf_tests/models.py @@ -0,0 +1 @@ +# models.py file for tests to run. diff --git a/parts/django/tests/regressiontests/csrf_tests/tests.py b/parts/django/tests/regressiontests/csrf_tests/tests.py new file mode 100644 index 0000000..9f74fc5 --- /dev/null +++ b/parts/django/tests/regressiontests/csrf_tests/tests.py @@ -0,0 +1,375 @@ +# -*- coding: utf-8 -*- + +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, csrf_view_exempt, requires_csrf_token +from django.core.context_processors import csrf +from django.contrib.sessions.middleware import SessionMiddleware +from django.utils.importlib import import_module +from django.conf import settings +from django.template import RequestContext, Template + +# Response/views used for CsrfResponseMiddleware and CsrfViewMiddleware tests +def post_form_response(): + resp = HttpResponse(content=u""" +<html><body><h1>\u00a1Unicode!<form method="post"><input type="text" /></form></body></html> +""", mimetype="text/html") + return resp + +def post_form_response_non_html(): + resp = post_form_response() + resp["Content-Type"] = "application/xml" + return resp + +def post_form_view(request): + """A view that returns a POST form (without a token)""" + return post_form_response() + +# Response/views used for template tag tests +def _token_template(): + return Template("{% csrf_token %}") + +def _render_csrf_token_template(req): + context = RequestContext(req, processors=[csrf]) + template = _token_template() + return template.render(context) + +def token_view(request): + """A view that uses {% csrf_token %}""" + return HttpResponse(_render_csrf_token_template(request)) + +def non_token_view_using_request_processor(request): + """ + A view that doesn't use the token, but does use the csrf view processor. + """ + context = RequestContext(request, processors=[csrf]) + template = Template("") + return HttpResponse(template.render(context)) + +class TestingHttpRequest(HttpRequest): + """ + A version of HttpRequest that allows us to change some things + more easily + """ + def is_secure(self): + 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 + # the old code that we're to be backwards-compatible with. Don't use the CSRF code + # to generate this hash, or we're merely testing the code against itself and not + # checking backwards-compatibility. This is also the output of (echo -n test1 | md5sum). + _session_token = "5a105e8b9d40e1329780d62ea2265d8a" + _session_id = "1" + _secret_key_for_session_test= "test" + + def _get_GET_no_csrf_cookie_request(self): + return TestingHttpRequest() + + def _get_GET_csrf_cookie_request(self): + req = TestingHttpRequest() + req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id_cookie + return req + + def _get_POST_csrf_cookie_request(self): + req = self._get_GET_csrf_cookie_request() + req.method = "POST" + return req + + def _get_POST_no_csrf_cookie_request(self): + req = self._get_GET_no_csrf_cookie_request() + req.method = "POST" + return req + + def _get_POST_request_with_token(self): + req = self._get_POST_csrf_cookie_request() + req.POST['csrfmiddlewaretoken'] = self._csrf_id + return req + + def _get_POST_session_request_with_token(self): + req = self._get_POST_no_csrf_cookie_request() + req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id + req.POST['csrfmiddlewaretoken'] = self._session_token + return req + + def _get_POST_session_request_no_token(self): + req = self._get_POST_no_csrf_cookie_request() + req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id + return req + + def _check_token_present(self, response, csrf_id=None): + self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % (csrf_id or self._csrf_id)) + + # Check the post processing and outgoing cookie + def test_process_response_no_csrf_cookie(self): + """ + When no prior CSRF cookie exists, check that the cookie is created and a + token is inserted. + """ + req = self._get_GET_no_csrf_cookie_request() + CsrfMiddleware().process_view(req, 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) + # 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 + if only CsrfViewMiddleware is used. + """ + # This is checking that CsrfViewMiddleware has the cookie setting + # code. Most of the other tests use CsrfMiddleware. + req = self._get_GET_no_csrf_cookie_request() + # token_view calls get_token() indirectly + CsrfViewMiddleware().process_view(req, token_view, (), {}) + resp = token_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) + self.assertNotEqual(csrf_cookie, False) + + def test_process_response_get_token_not_used(self): + """ + Check that if get_token() is not called, the view middleware does not + add a cookie. + """ + # This is important to make pages cacheable. Pages which do call + # get_token(), assuming they use the token, are not cacheable because + # the token is specific to the user + req = self._get_GET_no_csrf_cookie_request() + # non_token_view_using_request_processor does not call get_token(), but + # does use the csrf request processor. By using this, we are testing + # that the view processor is properly lazy and doesn't call get_token() + # until needed. + CsrfViewMiddleware().process_view(req, non_token_view_using_request_processor, (), {}) + resp = non_token_view_using_request_processor(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) + self.assertEqual(csrf_cookie, False) + + def test_process_response_existing_csrf_cookie(self): + """ + Check that the token is inserted when a prior CSRF cookie exists + """ + req = self._get_GET_csrf_cookie_request() + CsrfMiddleware().process_view(req, post_form_view, (), {}) + + resp = post_form_response() + resp_content = resp.content # needed because process_response modifies resp + resp2 = CsrfMiddleware().process_response(req, resp) + self.assertNotEqual(resp_content, resp2.content) + self._check_token_present(resp2) + + def test_process_response_non_html(self): + """ + Check the the post-processor does nothing for content-types not in _HTML_TYPES. + """ + req = self._get_GET_no_csrf_cookie_request() + CsrfMiddleware().process_view(req, post_form_view, (), {}) + resp = post_form_response_non_html() + resp_content = resp.content # needed because process_response modifies resp + resp2 = CsrfMiddleware().process_response(req, resp) + self.assertEquals(resp_content, resp2.content) + + def test_process_response_exempt_view(self): + """ + Check that no post processing is done for an exempt view + """ + 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) + + # Check the request processing + def test_process_request_no_session_no_csrf_cookie(self): + """ + Check that if neither a CSRF cookie nor a session cookie are present, + the middleware rejects the incoming request. This will stop login CSRF. + """ + req = self._get_POST_no_csrf_cookie_request() + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(403, req2.status_code) + + def test_process_request_csrf_cookie_no_token(self): + """ + Check that if a CSRF cookie is present but no token, the middleware + rejects the incoming request. + """ + req = self._get_POST_csrf_cookie_request() + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(403, req2.status_code) + + def test_process_request_csrf_cookie_and_token(self): + """ + Check that if both a cookie and a token is present, the middleware lets it through. + """ + req = self._get_POST_request_with_token() + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(None, req2) + + def test_process_request_session_cookie_no_csrf_cookie_token(self): + """ + When no CSRF cookie exists, but the user has a session, check that a token + using the session cookie as a legacy CSRF cookie is accepted. + """ + orig_secret_key = settings.SECRET_KEY + settings.SECRET_KEY = self._secret_key_for_session_test + try: + req = self._get_POST_session_request_with_token() + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(None, req2) + finally: + settings.SECRET_KEY = orig_secret_key + + def test_process_request_session_cookie_no_csrf_cookie_no_token(self): + """ + Check that if a session cookie is present but no token and no CSRF cookie, + the request is rejected. + """ + req = self._get_POST_session_request_no_token() + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(403, req2.status_code) + + def test_process_request_csrf_cookie_no_token_exempt_view(self): + """ + Check that if a CSRF cookie is present and no token, but the csrf_exempt + decorator has been applied to the view, the middleware lets it through + """ + req = self._get_POST_csrf_cookie_request() + req2 = CsrfMiddleware().process_view(req, csrf_exempt(post_form_view), (), {}) + self.assertEquals(None, req2) + + def test_ajax_exemption(self): + """ + Check that AJAX requests are automatically exempted. + """ + req = self._get_POST_csrf_cookie_request() + req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(None, req2) + + # Tests for the template tag method + def test_token_node_no_csrf_cookie(self): + """ + Check that CsrfTokenNode works when no CSRF cookie is set + """ + req = self._get_GET_no_csrf_cookie_request() + 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 + """ + req = self._get_GET_csrf_cookie_request() + CsrfViewMiddleware().process_view(req, token_view, (), {}) + 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_get_token_for_requires_csrf_token_view(self): + """ + Check that get_token works for a view decorated solely with requires_csrf_token + """ + req = self._get_GET_csrf_cookie_request() + resp = requires_csrf_token(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 + the middleware (when one was not already present) + """ + req = self._get_GET_no_csrf_cookie_request() + CsrfViewMiddleware().process_view(req, token_view, (), {}) + resp = token_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies[settings.CSRF_COOKIE_NAME] + self._check_token_present(resp, csrf_id=csrf_cookie.value) + + def test_response_middleware_without_view_middleware(self): + """ + Check that CsrfResponseMiddleware finishes without error if the view middleware + has not been called, as is the case if a request middleware returns a response. + """ + req = self._get_GET_no_csrf_cookie_request() + resp = post_form_view(req) + CsrfMiddleware().process_response(req, resp) + + def test_https_bad_referer(self): + """ + Test that a POST HTTPS request with a bad referer is rejected + """ + req = self._get_POST_request_with_token() + req._is_secure = True + req.META['HTTP_HOST'] = 'www.example.com' + req.META['HTTP_REFERER'] = 'https://www.evil.org/somepage' + req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) + self.assertNotEqual(None, req2) + self.assertEquals(403, req2.status_code) + + def test_https_good_referer(self): + """ + Test that a POST HTTPS request with a good referer is accepted + """ + req = self._get_POST_request_with_token() + req._is_secure = True + req.META['HTTP_HOST'] = 'www.example.com' + req.META['HTTP_REFERER'] = 'https://www.example.com/somepage' + req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(None, req2) diff --git a/parts/django/tests/regressiontests/custom_columns_regress/__init__.py b/parts/django/tests/regressiontests/custom_columns_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/custom_columns_regress/__init__.py diff --git a/parts/django/tests/regressiontests/custom_columns_regress/models.py b/parts/django/tests/regressiontests/custom_columns_regress/models.py new file mode 100644 index 0000000..93de237 --- /dev/null +++ b/parts/django/tests/regressiontests/custom_columns_regress/models.py @@ -0,0 +1,36 @@ +""" +Regression for #9736. + +Checks some pathological column naming to make sure it doesn't break +table creation or queries. + +""" + +from django.db import models + +class Article(models.Model): + Article_ID = models.AutoField(primary_key=True, db_column='Article ID') + headline = models.CharField(max_length=100) + authors = models.ManyToManyField('Author', db_table='my m2m table') + primary_author = models.ForeignKey('Author', db_column='Author ID', related_name='primary_set') + + def __unicode__(self): + return self.headline + + class Meta: + ordering = ('headline',) + +class Author(models.Model): + Author_ID = models.AutoField(primary_key=True, db_column='Author ID') + first_name = models.CharField(max_length=30, db_column='first name') + last_name = models.CharField(max_length=30, db_column='last name') + + def __unicode__(self): + return u'%s %s' % (self.first_name, self.last_name) + + class Meta: + db_table = 'my author table' + ordering = ('last_name','first_name') + + + diff --git a/parts/django/tests/regressiontests/custom_columns_regress/tests.py b/parts/django/tests/regressiontests/custom_columns_regress/tests.py new file mode 100644 index 0000000..8507601 --- /dev/null +++ b/parts/django/tests/regressiontests/custom_columns_regress/tests.py @@ -0,0 +1,84 @@ +from django.test import TestCase +from django.core.exceptions import FieldError + +from models import Author, Article + +def pks(objects): + """ Return pks to be able to compare lists""" + return [o.pk for o in objects] + +class CustomColumnRegression(TestCase): + + def assertRaisesMessage(self, exc, msg, func, *args, **kwargs): + try: + func(*args, **kwargs) + except Exception, e: + self.assertEqual(msg, str(e)) + self.assertTrue(isinstance(e, exc), "Expected %s, got %s" % (exc, type(e))) + + def setUp(self): + self.a1 = Author.objects.create(first_name='John', last_name='Smith') + self.a2 = Author.objects.create(first_name='Peter', last_name='Jones') + self.authors = [self.a1, self.a2] + + def test_basic_creation(self): + art = Article(headline='Django lets you build Web apps easily', primary_author=self.a1) + art.save() + art.authors = [self.a1, self.a2] + + def test_author_querying(self): + self.assertQuerysetEqual( + Author.objects.all().order_by('last_name'), + ['<Author: Peter Jones>', '<Author: John Smith>'] + ) + + def test_author_filtering(self): + self.assertQuerysetEqual( + Author.objects.filter(first_name__exact='John'), + ['<Author: John Smith>'] + ) + + def test_author_get(self): + self.assertEqual(self.a1, Author.objects.get(first_name__exact='John')) + + def test_filter_on_nonexistant_field(self): + self.assertRaisesMessage( + FieldError, + "Cannot resolve keyword 'firstname' into field. Choices are: Author_ID, article, first_name, last_name, primary_set", + Author.objects.filter, + firstname__exact='John' + ) + + def test_author_get_attributes(self): + a = Author.objects.get(last_name__exact='Smith') + self.assertEqual('John', a.first_name) + self.assertEqual('Smith', a.last_name) + self.assertRaisesMessage( + AttributeError, + "'Author' object has no attribute 'firstname'", + getattr, + a, 'firstname' + ) + + self.assertRaisesMessage( + AttributeError, + "'Author' object has no attribute 'last'", + getattr, + a, 'last' + ) + + def test_m2m_table(self): + art = Article.objects.create(headline='Django lets you build Web apps easily', primary_author=self.a1) + art.authors = self.authors + self.assertQuerysetEqual( + art.authors.all().order_by('last_name'), + ['<Author: Peter Jones>', '<Author: John Smith>'] + ) + self.assertQuerysetEqual( + self.a1.article_set.all(), + ['<Article: Django lets you build Web apps easily>'] + ) + self.assertQuerysetEqual( + art.authors.filter(last_name='Jones'), + ['<Author: Peter Jones>'] + ) diff --git a/parts/django/tests/regressiontests/custom_managers_regress/__init__.py b/parts/django/tests/regressiontests/custom_managers_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/custom_managers_regress/__init__.py diff --git a/parts/django/tests/regressiontests/custom_managers_regress/models.py b/parts/django/tests/regressiontests/custom_managers_regress/models.py new file mode 100644 index 0000000..747972b --- /dev/null +++ b/parts/django/tests/regressiontests/custom_managers_regress/models.py @@ -0,0 +1,40 @@ +""" +Regression tests for custom manager classes. +""" + +from django.db import models + +class RestrictedManager(models.Manager): + """ + A manager that filters out non-public instances. + """ + def get_query_set(self): + return super(RestrictedManager, self).get_query_set().filter(is_public=True) + +class RelatedModel(models.Model): + name = models.CharField(max_length=50) + + def __unicode__(self): + return self.name + +class RestrictedModel(models.Model): + name = models.CharField(max_length=50) + is_public = models.BooleanField(default=False) + related = models.ForeignKey(RelatedModel) + + objects = RestrictedManager() + plain_manager = models.Manager() + + def __unicode__(self): + return self.name + +class OneToOneRestrictedModel(models.Model): + name = models.CharField(max_length=50) + is_public = models.BooleanField(default=False) + related = models.OneToOneField(RelatedModel) + + objects = RestrictedManager() + plain_manager = models.Manager() + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/regressiontests/custom_managers_regress/tests.py b/parts/django/tests/regressiontests/custom_managers_regress/tests.py new file mode 100644 index 0000000..6dd668a --- /dev/null +++ b/parts/django/tests/regressiontests/custom_managers_regress/tests.py @@ -0,0 +1,47 @@ +from django.test import TestCase + +from models import RelatedModel, RestrictedModel, OneToOneRestrictedModel + +class CustomManagersRegressTestCase(TestCase): + def test_filtered_default_manager(self): + """Even though the default manager filters out some records, + we must still be able to save (particularly, save by updating + existing records) those filtered instances. This is a + regression test for #8990, #9527""" + related = RelatedModel.objects.create(name="xyzzy") + obj = RestrictedModel.objects.create(name="hidden", related=related) + obj.name = "still hidden" + obj.save() + + # If the hidden object wasn't seen during the save process, + # there would now be two objects in the database. + self.assertEqual(RestrictedModel.plain_manager.count(), 1) + + def test_delete_related_on_filtered_manager(self): + """Deleting related objects should also not be distracted by a + restricted manager on the related object. This is a regression + test for #2698.""" + related = RelatedModel.objects.create(name="xyzzy") + + for name, public in (('one', True), ('two', False), ('three', False)): + RestrictedModel.objects.create(name=name, is_public=public, related=related) + + obj = RelatedModel.objects.get(name="xyzzy") + obj.delete() + + # All of the RestrictedModel instances should have been + # deleted, since they *all* pointed to the RelatedModel. If + # the default manager is used, only the public one will be + # deleted. + self.assertEqual(len(RestrictedModel.plain_manager.all()), 0) + + def test_delete_one_to_one_manager(self): + # The same test case as the last one, but for one-to-one + # models, which are implemented slightly different internally, + # so it's a different code path. + obj = RelatedModel.objects.create(name="xyzzy") + OneToOneRestrictedModel.objects.create(name="foo", is_public=False, related=obj) + obj = RelatedModel.objects.get(name="xyzzy") + obj.delete() + self.assertEqual(len(OneToOneRestrictedModel.plain_manager.all()), 0) + diff --git a/parts/django/tests/regressiontests/datatypes/__init__.py b/parts/django/tests/regressiontests/datatypes/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/datatypes/__init__.py diff --git a/parts/django/tests/regressiontests/datatypes/models.py b/parts/django/tests/regressiontests/datatypes/models.py new file mode 100644 index 0000000..f6fb72d --- /dev/null +++ b/parts/django/tests/regressiontests/datatypes/models.py @@ -0,0 +1,25 @@ +""" +This is a basic model to test saving and loading boolean and date-related +types, which in the past were problematic for some database backends. +""" + +from django.db import models + +class Donut(models.Model): + name = models.CharField(max_length=100) + is_frosted = models.BooleanField(default=False) + has_sprinkles = models.NullBooleanField() + baked_date = models.DateField(null=True) + baked_time = models.TimeField(null=True) + consumed_at = models.DateTimeField(null=True) + review = models.TextField() + + class Meta: + ordering = ('consumed_at',) + + def __str__(self): + return self.name + +class RumBaba(models.Model): + baked_date = models.DateField(auto_now_add=True) + baked_timestamp = models.DateTimeField(auto_now_add=True) diff --git a/parts/django/tests/regressiontests/datatypes/tests.py b/parts/django/tests/regressiontests/datatypes/tests.py new file mode 100644 index 0000000..f7a0447 --- /dev/null +++ b/parts/django/tests/regressiontests/datatypes/tests.py @@ -0,0 +1,93 @@ +import datetime +from django.db import DEFAULT_DB_ALIAS +from django.test import TestCase +from django.utils import tzinfo + +from models import Donut, RumBaba +from django.conf import settings + +class DataTypesTestCase(TestCase): + + def test_boolean_type(self): + d = Donut(name='Apple Fritter') + self.assertFalse(d.is_frosted) + self.assertTrue(d.has_sprinkles is None) + d.has_sprinkles = True + self.assertTrue(d.has_sprinkles) + + d.save() + + d2 = Donut.objects.get(name='Apple Fritter') + self.assertFalse(d2.is_frosted) + self.assertTrue(d2.has_sprinkles) + + def test_date_type(self): + d = Donut(name='Apple Fritter') + d.baked_date = datetime.date(year=1938, month=6, day=4) + d.baked_time = datetime.time(hour=5, minute=30) + d.consumed_at = datetime.datetime(year=2007, month=4, day=20, hour=16, minute=19, second=59) + d.save() + + d2 = Donut.objects.get(name='Apple Fritter') + self.assertEqual(d2.baked_date, datetime.date(1938, 6, 4)) + self.assertEqual(d2.baked_time, datetime.time(5, 30)) + self.assertEqual(d2.consumed_at, datetime.datetime(2007, 4, 20, 16, 19, 59)) + + def test_time_field(self): + #Test for ticket #12059: TimeField wrongly handling datetime.datetime object. + d = Donut(name='Apple Fritter') + d.baked_time = datetime.datetime(year=2007, month=4, day=20, hour=16, minute=19, second=59) + d.save() + + d2 = Donut.objects.get(name='Apple Fritter') + self.assertEqual(d2.baked_time, datetime.time(16, 19, 59)) + + def test_year_boundaries(self): + """Year boundary tests (ticket #3689)""" + d = Donut.objects.create(name='Date Test 2007', + baked_date=datetime.datetime(year=2007, month=12, day=31), + consumed_at=datetime.datetime(year=2007, month=12, day=31, hour=23, minute=59, second=59)) + d1 = Donut.objects.create(name='Date Test 2006', + baked_date=datetime.datetime(year=2006, month=1, day=1), + consumed_at=datetime.datetime(year=2006, month=1, day=1)) + + self.assertEqual("Date Test 2007", + Donut.objects.filter(baked_date__year=2007)[0].name) + + self.assertEqual("Date Test 2006", + Donut.objects.filter(baked_date__year=2006)[0].name) + + d2 = Donut.objects.create(name='Apple Fritter', + consumed_at = datetime.datetime(year=2007, month=4, day=20, hour=16, minute=19, second=59)) + + self.assertEqual([u'Apple Fritter', u'Date Test 2007'], + list(Donut.objects.filter(consumed_at__year=2007).order_by('name').values_list('name', flat=True))) + + self.assertEqual(0, Donut.objects.filter(consumed_at__year=2005).count()) + self.assertEqual(0, Donut.objects.filter(consumed_at__year=2008).count()) + + def test_textfields_unicode(self): + """Regression test for #10238: TextField values returned from the + database should be unicode.""" + d = Donut.objects.create(name=u'Jelly Donut', review=u'Outstanding') + newd = Donut.objects.get(id=d.id) + self.assert_(isinstance(newd.review, unicode)) + + def test_tz_awareness_mysql(self): + """Regression test for #8354: the MySQL backend should raise an error + if given a timezone-aware datetime object.""" + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.mysql': + dt = datetime.datetime(2008, 8, 31, 16, 20, tzinfo=tzinfo.FixedOffset(0)) + d = Donut(name='Bear claw', consumed_at=dt) + self.assertRaises(ValueError, d.save) + # ValueError: MySQL backend does not support timezone-aware datetimes. + + def test_datefield_auto_now_add(self): + """Regression test for #10970, auto_now_add for DateField should store + a Python datetime.date, not a datetime.datetime""" + b = RumBaba.objects.create() + # Verify we didn't break DateTimeField behavior + self.assert_(isinstance(b.baked_timestamp, datetime.datetime)) + # We need to test this this way because datetime.datetime inherits + # from datetime.date: + self.assert_(isinstance(b.baked_date, datetime.date) and not isinstance(b.baked_date, datetime.datetime)) diff --git a/parts/django/tests/regressiontests/db_typecasts/__init__.py b/parts/django/tests/regressiontests/db_typecasts/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/db_typecasts/__init__.py diff --git a/parts/django/tests/regressiontests/db_typecasts/models.py b/parts/django/tests/regressiontests/db_typecasts/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/db_typecasts/models.py diff --git a/parts/django/tests/regressiontests/db_typecasts/tests.py b/parts/django/tests/regressiontests/db_typecasts/tests.py new file mode 100644 index 0000000..8c71c8f --- /dev/null +++ b/parts/django/tests/regressiontests/db_typecasts/tests.py @@ -0,0 +1,62 @@ +# Unit tests for typecast functions in django.db.backends.util + +from django.db.backends import util as typecasts +import datetime, unittest + +TEST_CASES = { + 'typecast_date': ( + ('', None), + (None, None), + ('2005-08-11', datetime.date(2005, 8, 11)), + ('1990-01-01', datetime.date(1990, 1, 1)), + ), + 'typecast_time': ( + ('', None), + (None, None), + ('0:00:00', datetime.time(0, 0)), + ('0:30:00', datetime.time(0, 30)), + ('8:50:00', datetime.time(8, 50)), + ('08:50:00', datetime.time(8, 50)), + ('12:00:00', datetime.time(12, 00)), + ('12:30:00', datetime.time(12, 30)), + ('13:00:00', datetime.time(13, 00)), + ('23:59:00', datetime.time(23, 59)), + ('00:00:12', datetime.time(0, 0, 12)), + ('00:00:12.5', datetime.time(0, 0, 12, 500000)), + ('7:22:13.312', datetime.time(7, 22, 13, 312000)), + ), + 'typecast_timestamp': ( + ('', None), + (None, None), + ('2005-08-11 0:00:00', datetime.datetime(2005, 8, 11)), + ('2005-08-11 0:30:00', datetime.datetime(2005, 8, 11, 0, 30)), + ('2005-08-11 8:50:30', datetime.datetime(2005, 8, 11, 8, 50, 30)), + ('2005-08-11 8:50:30.123', datetime.datetime(2005, 8, 11, 8, 50, 30, 123000)), + ('2005-08-11 8:50:30.9', datetime.datetime(2005, 8, 11, 8, 50, 30, 900000)), + ('2005-08-11 8:50:30.312-05', datetime.datetime(2005, 8, 11, 8, 50, 30, 312000)), + ('2005-08-11 8:50:30.312+02', datetime.datetime(2005, 8, 11, 8, 50, 30, 312000)), + # ticket 14453 + ('2010-10-12 15:29:22.063202', datetime.datetime(2010, 10, 12, 15, 29, 22, 63202)), + ('2010-10-12 15:29:22.063202-03', datetime.datetime(2010, 10, 12, 15, 29, 22, 63202)), + ('2010-10-12 15:29:22.063202+04', datetime.datetime(2010, 10, 12, 15, 29, 22, 63202)), + ('2010-10-12 15:29:22.0632021', datetime.datetime(2010, 10, 12, 15, 29, 22, 63202)), + ('2010-10-12 15:29:22.0632029', datetime.datetime(2010, 10, 12, 15, 29, 22, 63202)), + ), + 'typecast_boolean': ( + (None, None), + ('', False), + ('t', True), + ('f', False), + ('x', False), + ), +} + +class DBTypeCasts(unittest.TestCase): + def test_typeCasts(self): + for k, v in TEST_CASES.items(): + for inpt, expected in v: + got = getattr(typecasts, k)(inpt) + assert got == expected, "In %s: %r doesn't match %r. Got %r instead." % (k, inpt, expected, got) + +if __name__ == '__main__': + unittest.main() diff --git a/parts/django/tests/regressiontests/decorators/__init__.py b/parts/django/tests/regressiontests/decorators/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/decorators/__init__.py diff --git a/parts/django/tests/regressiontests/decorators/models.py b/parts/django/tests/regressiontests/decorators/models.py new file mode 100644 index 0000000..e5a7950 --- /dev/null +++ b/parts/django/tests/regressiontests/decorators/models.py @@ -0,0 +1,2 @@ +# A models.py so that tests run. + diff --git a/parts/django/tests/regressiontests/decorators/tests.py b/parts/django/tests/regressiontests/decorators/tests.py new file mode 100644 index 0000000..ea2e10e --- /dev/null +++ b/parts/django/tests/regressiontests/decorators/tests.py @@ -0,0 +1,141 @@ +from unittest import TestCase +from sys import version_info +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.4 fallback. + +from django.http import HttpResponse, HttpRequest +from django.utils.functional import allow_lazy, lazy, memoize +from django.views.decorators.http import require_http_methods, require_GET, require_POST +from django.views.decorators.vary import vary_on_headers, vary_on_cookie +from django.views.decorators.cache import cache_page, never_cache, cache_control +from django.utils.decorators import method_decorator +from django.contrib.auth.decorators import login_required, permission_required, user_passes_test +from django.contrib.admin.views.decorators import staff_member_required + +def fully_decorated(request): + """Expected __doc__""" + return HttpResponse('<html><body>dummy</body></html>') +fully_decorated.anything = "Expected __dict__" + +# django.views.decorators.http +fully_decorated = require_http_methods(["GET"])(fully_decorated) +fully_decorated = require_GET(fully_decorated) +fully_decorated = require_POST(fully_decorated) + +# django.views.decorators.vary +fully_decorated = vary_on_headers('Accept-language')(fully_decorated) +fully_decorated = vary_on_cookie(fully_decorated) + +# django.views.decorators.cache +fully_decorated = cache_page(60*15)(fully_decorated) +fully_decorated = cache_control(private=True)(fully_decorated) +fully_decorated = never_cache(fully_decorated) + +# django.contrib.auth.decorators +# Apply user_passes_test twice to check #9474 +fully_decorated = user_passes_test(lambda u:True)(fully_decorated) +fully_decorated = login_required(fully_decorated) +fully_decorated = permission_required('change_world')(fully_decorated) + +# django.contrib.admin.views.decorators +fully_decorated = staff_member_required(fully_decorated) + +# django.utils.functional +fully_decorated = memoize(fully_decorated, {}, 1) +fully_decorated = allow_lazy(fully_decorated) +fully_decorated = lazy(fully_decorated) + + +class DecoratorsTest(TestCase): + + def test_attributes(self): + """ + Tests that django decorators set certain attributes of the wrapped + function. + """ + # Only check __name__ on Python 2.4 or later since __name__ can't be + # assigned to in earlier Python versions. + if version_info[0] >= 2 and version_info[1] >= 4: + self.assertEquals(fully_decorated.__name__, 'fully_decorated') + self.assertEquals(fully_decorated.__doc__, 'Expected __doc__') + self.assertEquals(fully_decorated.__dict__['anything'], 'Expected __dict__') + + def test_user_passes_test_composition(self): + """ + Test that the user_passes_test decorator can be applied multiple times + (#9474). + """ + def test1(user): + user.decorators_applied.append('test1') + return True + + def test2(user): + user.decorators_applied.append('test2') + return True + + def callback(request): + return request.user.decorators_applied + + callback = user_passes_test(test1)(callback) + callback = user_passes_test(test2)(callback) + + class DummyUser(object): pass + class DummyRequest(object): pass + + request = DummyRequest() + request.user = DummyUser() + request.user.decorators_applied = [] + response = callback(request) + + self.assertEqual(response, ['test2', 'test1']) + + def test_cache_page_new_style(self): + """ + Test that we can call cache_page the new way + """ + def my_view(request): + return "response" + my_view_cached = cache_page(123)(my_view) + self.assertEqual(my_view_cached(HttpRequest()), "response") + my_view_cached2 = cache_page(123, key_prefix="test")(my_view) + self.assertEqual(my_view_cached2(HttpRequest()), "response") + + def test_cache_page_old_style(self): + """ + Test that we can call cache_page the old way + """ + def my_view(request): + return "response" + my_view_cached = cache_page(my_view, 123) + self.assertEqual(my_view_cached(HttpRequest()), "response") + my_view_cached2 = cache_page(my_view, 123, key_prefix="test") + self.assertEqual(my_view_cached2(HttpRequest()), "response") + my_view_cached3 = cache_page(my_view) + self.assertEqual(my_view_cached3(HttpRequest()), "response") + my_view_cached4 = cache_page()(my_view) + self.assertEqual(my_view_cached4(HttpRequest()), "response") + + +# For testing method_decorator, a decorator that assumes a single argument. +# We will get type arguments if there is a mismatch in the number of arguments. +def simple_dec(func): + def wrapper(arg): + return func("test:" + arg) + return wraps(func)(wrapper) + +simple_dec_m = method_decorator(simple_dec) + + +class MethodDecoratorTests(TestCase): + """ + Tests for method_decorator + """ + def test_method_decorator(self): + class Test(object): + @simple_dec_m + def say(self, arg): + return arg + + self.assertEqual("test:hello", Test().say("hello")) diff --git a/parts/django/tests/regressiontests/defaultfilters/__init__.py b/parts/django/tests/regressiontests/defaultfilters/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/defaultfilters/__init__.py diff --git a/parts/django/tests/regressiontests/defaultfilters/models.py b/parts/django/tests/regressiontests/defaultfilters/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/defaultfilters/models.py diff --git a/parts/django/tests/regressiontests/defaultfilters/tests.py b/parts/django/tests/regressiontests/defaultfilters/tests.py new file mode 100644 index 0000000..0866bfc --- /dev/null +++ b/parts/django/tests/regressiontests/defaultfilters/tests.py @@ -0,0 +1,485 @@ +# -*- coding: utf-8 -*- +import datetime +import unittest + +from django.template.defaultfilters import * + +class DefaultFiltersTests(unittest.TestCase): + + def test_floatformat(self): + self.assertEqual(floatformat(7.7), u'7.7') + self.assertEqual(floatformat(7.0), u'7') + self.assertEqual(floatformat(0.7), u'0.7') + self.assertEqual(floatformat(0.07), u'0.1') + self.assertEqual(floatformat(0.007), u'0.0') + self.assertEqual(floatformat(0.0), u'0') + self.assertEqual(floatformat(7.7, 3), u'7.700') + self.assertEqual(floatformat(6.000000, 3), u'6.000') + self.assertEqual(floatformat(6.200000, 3), u'6.200') + self.assertEqual(floatformat(6.200000, -3), u'6.200') + self.assertEqual(floatformat(13.1031, -3), u'13.103') + self.assertEqual(floatformat(11.1197, -2), u'11.12') + self.assertEqual(floatformat(11.0000, -2), u'11') + self.assertEqual(floatformat(11.000001, -2), u'11.00') + self.assertEqual(floatformat(8.2798, 3), u'8.280') + self.assertEqual(floatformat(u'foo'), u'') + self.assertEqual(floatformat(13.1031, u'bar'), u'13.1031') + self.assertEqual(floatformat(18.125, 2), u'18.13') + self.assertEqual(floatformat(u'foo', u'bar'), u'') + self.assertEqual(floatformat(u'¿Cómo esta usted?'), u'') + self.assertEqual(floatformat(None), u'') + + pos_inf = float(1e30000) + self.assertEqual(floatformat(pos_inf), unicode(pos_inf)) + + neg_inf = float(-1e30000) + self.assertEqual(floatformat(neg_inf), unicode(neg_inf)) + + nan = pos_inf / pos_inf + self.assertEqual(floatformat(nan), unicode(nan)) + + class FloatWrapper(object): + def __init__(self, value): + self.value = value + def __float__(self): + return self.value + + self.assertEqual(floatformat(FloatWrapper(11.000001), -2), u'11.00') + + def test_addslashes(self): + self.assertEqual(addslashes(u'"double quotes" and \'single quotes\''), + u'\\"double quotes\\" and \\\'single quotes\\\'') + + self.assertEqual(addslashes(ur'\ : backslashes, too'), + u'\\\\ : backslashes, too') + + def test_capfirst(self): + self.assertEqual(capfirst(u'hello world'), u'Hello world') + + def test_escapejs(self): + self.assertEqual(escapejs(u'"double quotes" and \'single quotes\''), + u'\\u0022double quotes\\u0022 and \\u0027single quotes\\u0027') + self.assertEqual(escapejs(ur'\ : backslashes, too'), + u'\\u005C : backslashes, too') + self.assertEqual(escapejs(u'and lots of whitespace: \r\n\t\v\f\b'), + u'and lots of whitespace: \\u000D\\u000A\\u0009\\u000B\\u000C\\u0008') + self.assertEqual(escapejs(ur'<script>and this</script>'), + u'\\u003Cscript\\u003Eand this\\u003C/script\\u003E') + self.assertEqual( + escapejs(u'paragraph separator:\u2029and line separator:\u2028'), + u'paragraph separator:\\u2029and line separator:\\u2028') + + def test_fix_ampersands(self): + self.assertEqual(fix_ampersands(u'Jack & Jill & Jeroboam'), + u'Jack & Jill & Jeroboam') + + def test_linenumbers(self): + self.assertEqual(linenumbers(u'line 1\nline 2'), + u'1. line 1\n2. line 2') + self.assertEqual(linenumbers(u'\n'.join([u'x'] * 10)), + u'01. x\n02. x\n03. x\n04. x\n05. x\n06. x\n07. '\ + u'x\n08. x\n09. x\n10. x') + + def test_lower(self): + self.assertEqual(lower('TEST'), u'test') + + # uppercase E umlaut + self.assertEqual(lower(u'\xcb'), u'\xeb') + + def test_make_list(self): + self.assertEqual(make_list('abc'), [u'a', u'b', u'c']) + self.assertEqual(make_list(1234), [u'1', u'2', u'3', u'4']) + + def test_slugify(self): + self.assertEqual(slugify(' Jack & Jill like numbers 1,2,3 and 4 and'\ + ' silly characters ?%.$!/'), + u'jack-jill-like-numbers-123-and-4-and-silly-characters') + + self.assertEqual(slugify(u"Un \xe9l\xe9phant \xe0 l'or\xe9e du bois"), + u'un-elephant-a-loree-du-bois') + + def test_stringformat(self): + self.assertEqual(stringformat(1, u'03d'), u'001') + self.assertEqual(stringformat(1, u'z'), u'') + + def test_title(self): + self.assertEqual(title('a nice title, isn\'t it?'), + u"A Nice Title, Isn't It?") + self.assertEqual(title(u'discoth\xe8que'), u'Discoth\xe8que') + + def test_truncatewords(self): + self.assertEqual( + truncatewords(u'A sentence with a few words in it', 1), u'A ...') + self.assertEqual( + truncatewords(u'A sentence with a few words in it', 5), + u'A sentence with a few ...') + self.assertEqual( + truncatewords(u'A sentence with a few words in it', 100), + u'A sentence with a few words in it') + self.assertEqual( + truncatewords(u'A sentence with a few words in it', + 'not a number'), u'A sentence with a few words in it') + + def test_truncatewords_html(self): + self.assertEqual(truncatewords_html( + u'<p>one <a href="#">two - three <br>four</a> five</p>', 0), u'') + self.assertEqual(truncatewords_html(u'<p>one <a href="#">two - '\ + u'three <br>four</a> five</p>', 2), + u'<p>one <a href="#">two ...</a></p>') + self.assertEqual(truncatewords_html( + u'<p>one <a href="#">two - three <br>four</a> five</p>', 4), + u'<p>one <a href="#">two - three <br>four ...</a></p>') + self.assertEqual(truncatewords_html( + u'<p>one <a href="#">two - three <br>four</a> five</p>', 5), + u'<p>one <a href="#">two - three <br>four</a> five</p>') + self.assertEqual(truncatewords_html( + u'<p>one <a href="#">two - three <br>four</a> five</p>', 100), + u'<p>one <a href="#">two - three <br>four</a> five</p>') + self.assertEqual(truncatewords_html( + u'\xc5ngstr\xf6m was here', 1), u'\xc5ngstr\xf6m ...') + + def test_upper(self): + self.assertEqual(upper(u'Mixed case input'), u'MIXED CASE INPUT') + # lowercase e umlaut + self.assertEqual(upper(u'\xeb'), u'\xcb') + + def test_urlencode(self): + self.assertEqual(urlencode(u'fran\xe7ois & jill'), + u'fran%C3%A7ois%20%26%20jill') + self.assertEqual(urlencode(1), u'1') + + def test_iriencode(self): + self.assertEqual(iriencode(u'S\xf8r-Tr\xf8ndelag'), + u'S%C3%B8r-Tr%C3%B8ndelag') + self.assertEqual(iriencode(urlencode(u'fran\xe7ois & jill')), + u'fran%C3%A7ois%20%26%20jill') + + def test_urlizetrunc(self): + self.assertEqual(urlizetrunc(u'http://short.com/', 20), u'<a href='\ + u'"http://short.com/" rel="nofollow">http://short.com/</a>') + + self.assertEqual(urlizetrunc(u'http://www.google.co.uk/search?hl=en'\ + u'&q=some+long+url&btnG=Search&meta=', 20), u'<a href="http://'\ + u'www.google.co.uk/search?hl=en&q=some+long+url&btnG=Search&'\ + u'meta=" rel="nofollow">http://www.google...</a>') + + self.assertEqual(urlizetrunc('http://www.google.co.uk/search?hl=en'\ + u'&q=some+long+url&btnG=Search&meta=', 20), u'<a href="http://'\ + u'www.google.co.uk/search?hl=en&q=some+long+url&btnG=Search'\ + u'&meta=" rel="nofollow">http://www.google...</a>') + + # Check truncating of URIs which are the exact length + uri = 'http://31characteruri.com/test/' + self.assertEqual(len(uri), 31) + + self.assertEqual(urlizetrunc(uri, 31), + u'<a href="http://31characteruri.com/test/" rel="nofollow">'\ + u'http://31characteruri.com/test/</a>') + + self.assertEqual(urlizetrunc(uri, 30), + u'<a href="http://31characteruri.com/test/" rel="nofollow">'\ + u'http://31characteruri.com/t...</a>') + + self.assertEqual(urlizetrunc(uri, 2), + u'<a href="http://31characteruri.com/test/"'\ + u' rel="nofollow">...</a>') + + def test_urlize(self): + # Check normal urlize + self.assertEqual(urlize('http://google.com'), + u'<a href="http://google.com" rel="nofollow">http://google.com</a>') + self.assertEqual(urlize('http://google.com/'), + u'<a href="http://google.com/" rel="nofollow">http://google.com/</a>') + self.assertEqual(urlize('www.google.com'), + u'<a href="http://www.google.com" rel="nofollow">www.google.com</a>') + self.assertEqual(urlize('djangoproject.org'), + u'<a href="http://djangoproject.org" rel="nofollow">djangoproject.org</a>') + self.assertEqual(urlize('info@djangoproject.org'), + u'<a href="mailto:info@djangoproject.org">info@djangoproject.org</a>') + + # Check urlize with https addresses + self.assertEqual(urlize('https://google.com'), + u'<a href="https://google.com" rel="nofollow">https://google.com</a>') + + def test_wordcount(self): + self.assertEqual(wordcount(''), 0) + self.assertEqual(wordcount(u'oneword'), 1) + self.assertEqual(wordcount(u'lots of words'), 3) + + self.assertEqual(wordwrap(u'this is a long paragraph of text that '\ + u'really needs to be wrapped I\'m afraid', 14), + u"this is a long\nparagraph of\ntext that\nreally needs\nto be "\ + u"wrapped\nI'm afraid") + + self.assertEqual(wordwrap(u'this is a short paragraph of text.\n '\ + u'But this line should be indented', 14), + u'this is a\nshort\nparagraph of\ntext.\n But this\nline '\ + u'should be\nindented') + + self.assertEqual(wordwrap(u'this is a short paragraph of text.\n '\ + u'But this line should be indented',15), u'this is a short\n'\ + u'paragraph of\ntext.\n But this line\nshould be\nindented') + + def test_rjust(self): + self.assertEqual(ljust(u'test', 10), u'test ') + self.assertEqual(ljust(u'test', 3), u'test') + self.assertEqual(rjust(u'test', 10), u' test') + self.assertEqual(rjust(u'test', 3), u'test') + + def test_center(self): + self.assertEqual(center(u'test', 6), u' test ') + + def test_cut(self): + self.assertEqual(cut(u'a string to be mangled', 'a'), + u' string to be mngled') + self.assertEqual(cut(u'a string to be mangled', 'ng'), + u'a stri to be maled') + self.assertEqual(cut(u'a string to be mangled', 'strings'), + u'a string to be mangled') + + def test_force_escape(self): + self.assertEqual( + force_escape(u'<some html & special characters > here'), + u'<some html & special characters > here') + self.assertEqual( + force_escape(u'<some html & special characters > here ĐÅ€£'), + u'<some html & special characters > here'\ + u' \u0110\xc5\u20ac\xa3') + + def test_linebreaks(self): + self.assertEqual(linebreaks(u'line 1'), u'<p>line 1</p>') + self.assertEqual(linebreaks(u'line 1\nline 2'), + u'<p>line 1<br />line 2</p>') + + def test_removetags(self): + self.assertEqual(removetags(u'some <b>html</b> with <script>alert'\ + u'("You smell")</script> disallowed <img /> tags', 'script img'), + u'some <b>html</b> with alert("You smell") disallowed tags') + self.assertEqual(striptags(u'some <b>html</b> with <script>alert'\ + u'("You smell")</script> disallowed <img /> tags'), + u'some html with alert("You smell") disallowed tags') + + def test_dictsort(self): + sorted_dicts = dictsort([{'age': 23, 'name': 'Barbara-Ann'}, + {'age': 63, 'name': 'Ra Ra Rasputin'}, + {'name': 'Jonny B Goode', 'age': 18}], 'age') + + self.assertEqual([sorted(dict.items()) for dict in sorted_dicts], + [[('age', 18), ('name', 'Jonny B Goode')], + [('age', 23), ('name', 'Barbara-Ann')], + [('age', 63), ('name', 'Ra Ra Rasputin')]]) + + def test_dictsortreversed(self): + sorted_dicts = dictsortreversed([{'age': 23, 'name': 'Barbara-Ann'}, + {'age': 63, 'name': 'Ra Ra Rasputin'}, + {'name': 'Jonny B Goode', 'age': 18}], + 'age') + + self.assertEqual([sorted(dict.items()) for dict in sorted_dicts], + [[('age', 63), ('name', 'Ra Ra Rasputin')], + [('age', 23), ('name', 'Barbara-Ann')], + [('age', 18), ('name', 'Jonny B Goode')]]) + + def test_first(self): + self.assertEqual(first([0,1,2]), 0) + self.assertEqual(first(u''), u'') + self.assertEqual(first(u'test'), u't') + + def test_join(self): + self.assertEqual(join([0,1,2], u'glue'), u'0glue1glue2') + + def test_length(self): + self.assertEqual(length(u'1234'), 4) + self.assertEqual(length([1,2,3,4]), 4) + self.assertEqual(length_is([], 0), True) + self.assertEqual(length_is([], 1), False) + self.assertEqual(length_is('a', 1), True) + self.assertEqual(length_is(u'a', 10), False) + + def test_slice(self): + self.assertEqual(slice_(u'abcdefg', u'0'), u'') + self.assertEqual(slice_(u'abcdefg', u'1'), u'a') + self.assertEqual(slice_(u'abcdefg', u'-1'), u'abcdef') + self.assertEqual(slice_(u'abcdefg', u'1:2'), u'b') + self.assertEqual(slice_(u'abcdefg', u'1:3'), u'bc') + self.assertEqual(slice_(u'abcdefg', u'0::2'), u'aceg') + + def test_unordered_list(self): + self.assertEqual(unordered_list([u'item 1', u'item 2']), + u'\t<li>item 1</li>\n\t<li>item 2</li>') + self.assertEqual(unordered_list([u'item 1', [u'item 1.1']]), + u'\t<li>item 1\n\t<ul>\n\t\t<li>item 1.1</li>\n\t</ul>\n\t</li>') + + self.assertEqual( + unordered_list([u'item 1', [u'item 1.1', u'item1.2'], u'item 2']), + u'\t<li>item 1\n\t<ul>\n\t\t<li>item 1.1</li>\n\t\t<li>item1.2'\ + u'</li>\n\t</ul>\n\t</li>\n\t<li>item 2</li>') + + self.assertEqual( + unordered_list([u'item 1', [u'item 1.1', [u'item 1.1.1', + [u'item 1.1.1.1']]]]), + u'\t<li>item 1\n\t<ul>\n\t\t<li>item 1.1\n\t\t<ul>\n\t\t\t<li>'\ + u'item 1.1.1\n\t\t\t<ul>\n\t\t\t\t<li>item 1.1.1.1</li>\n\t\t\t'\ + u'</ul>\n\t\t\t</li>\n\t\t</ul>\n\t\t</li>\n\t</ul>\n\t</li>') + + self.assertEqual(unordered_list( + ['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']]), + u'\t<li>States\n\t<ul>\n\t\t<li>Kansas\n\t\t<ul>\n\t\t\t<li>'\ + u'Lawrence</li>\n\t\t\t<li>Topeka</li>\n\t\t</ul>\n\t\t</li>'\ + u'\n\t\t<li>Illinois</li>\n\t</ul>\n\t</li>') + + class ULItem(object): + def __init__(self, title): + self.title = title + def __unicode__(self): + return u'ulitem-%s' % str(self.title) + + a = ULItem('a') + b = ULItem('b') + self.assertEqual(unordered_list([a,b]), + u'\t<li>ulitem-a</li>\n\t<li>ulitem-b</li>') + + # Old format for unordered lists should still work + self.assertEqual(unordered_list([u'item 1', []]), u'\t<li>item 1</li>') + + self.assertEqual(unordered_list([u'item 1', [[u'item 1.1', []]]]), + u'\t<li>item 1\n\t<ul>\n\t\t<li>item 1.1</li>\n\t</ul>\n\t</li>') + + self.assertEqual(unordered_list([u'item 1', [[u'item 1.1', []], + [u'item 1.2', []]]]), u'\t<li>item 1\n\t<ul>\n\t\t<li>item 1.1'\ + u'</li>\n\t\t<li>item 1.2</li>\n\t</ul>\n\t</li>') + + self.assertEqual(unordered_list(['States', [['Kansas', [['Lawrence', + []], ['Topeka', []]]], ['Illinois', []]]]), u'\t<li>States\n\t'\ + u'<ul>\n\t\t<li>Kansas\n\t\t<ul>\n\t\t\t<li>Lawrence</li>'\ + u'\n\t\t\t<li>Topeka</li>\n\t\t</ul>\n\t\t</li>\n\t\t<li>'\ + u'Illinois</li>\n\t</ul>\n\t</li>') + + def test_add(self): + self.assertEqual(add(u'1', u'2'), 3) + + def test_get_digit(self): + self.assertEqual(get_digit(123, 1), 3) + self.assertEqual(get_digit(123, 2), 2) + self.assertEqual(get_digit(123, 3), 1) + self.assertEqual(get_digit(123, 4), 0) + self.assertEqual(get_digit(123, 0), 123) + self.assertEqual(get_digit(u'xyz', 0), u'xyz') + + def test_date(self): + # real testing of date() is in dateformat.py + self.assertEqual(date(datetime.datetime(2005, 12, 29), u"d F Y"), + u'29 December 2005') + self.assertEqual(date(datetime.datetime(2005, 12, 29), ur'jS o\f F'), + u'29th of December') + + def test_time(self): + # real testing of time() is done in dateformat.py + self.assertEqual(time(datetime.time(13), u"h"), u'01') + self.assertEqual(time(datetime.time(0), u"h"), u'12') + + def test_timesince(self): + # real testing is done in timesince.py, where we can provide our own 'now' + self.assertEqual( + timesince(datetime.datetime.now() - datetime.timedelta(1)), + u'1 day') + + self.assertEqual( + timesince(datetime.datetime(2005, 12, 29), + datetime.datetime(2005, 12, 30)), + u'1 day') + + def test_timeuntil(self): + self.assertEqual( + timeuntil(datetime.datetime.now() + datetime.timedelta(1)), + u'1 day') + + self.assertEqual(timeuntil(datetime.datetime(2005, 12, 30), + datetime.datetime(2005, 12, 29)), + u'1 day') + + def test_default(self): + self.assertEqual(default(u"val", u"default"), u'val') + self.assertEqual(default(None, u"default"), u'default') + self.assertEqual(default(u'', u"default"), u'default') + + def test_if_none(self): + self.assertEqual(default_if_none(u"val", u"default"), u'val') + self.assertEqual(default_if_none(None, u"default"), u'default') + self.assertEqual(default_if_none(u'', u"default"), u'') + + def test_divisibleby(self): + self.assertEqual(divisibleby(4, 2), True) + self.assertEqual(divisibleby(4, 3), False) + + def test_yesno(self): + self.assertEqual(yesno(True), u'yes') + self.assertEqual(yesno(False), u'no') + self.assertEqual(yesno(None), u'maybe') + self.assertEqual(yesno(True, u'certainly,get out of town,perhaps'), + u'certainly') + self.assertEqual(yesno(False, u'certainly,get out of town,perhaps'), + u'get out of town') + self.assertEqual(yesno(None, u'certainly,get out of town,perhaps'), + u'perhaps') + self.assertEqual(yesno(None, u'certainly,get out of town'), + u'get out of town') + + def test_filesizeformat(self): + self.assertEqual(filesizeformat(1023), u'1023 bytes') + self.assertEqual(filesizeformat(1024), u'1.0 KB') + self.assertEqual(filesizeformat(10*1024), u'10.0 KB') + self.assertEqual(filesizeformat(1024*1024-1), u'1024.0 KB') + self.assertEqual(filesizeformat(1024*1024), u'1.0 MB') + self.assertEqual(filesizeformat(1024*1024*50), u'50.0 MB') + self.assertEqual(filesizeformat(1024*1024*1024-1), u'1024.0 MB') + self.assertEqual(filesizeformat(1024*1024*1024), u'1.0 GB') + self.assertEqual(filesizeformat(complex(1,-1)), u'0 bytes') + self.assertEqual(filesizeformat(""), u'0 bytes') + self.assertEqual(filesizeformat(u"\N{GREEK SMALL LETTER ALPHA}"), + u'0 bytes') + + def test_pluralize(self): + self.assertEqual(pluralize(1), u'') + self.assertEqual(pluralize(0), u's') + self.assertEqual(pluralize(2), u's') + self.assertEqual(pluralize([1]), u'') + self.assertEqual(pluralize([]), u's') + self.assertEqual(pluralize([1,2,3]), u's') + self.assertEqual(pluralize(1,u'es'), u'') + self.assertEqual(pluralize(0,u'es'), u'es') + self.assertEqual(pluralize(2,u'es'), u'es') + self.assertEqual(pluralize(1,u'y,ies'), u'y') + self.assertEqual(pluralize(0,u'y,ies'), u'ies') + self.assertEqual(pluralize(2,u'y,ies'), u'ies') + self.assertEqual(pluralize(0,u'y,ies,error'), u'') + + def test_phone2numeric(self): + self.assertEqual(phone2numeric(u'0800 flowers'), u'0800 3569377') + + def test_non_string_input(self): + # Filters shouldn't break if passed non-strings + self.assertEqual(addslashes(123), u'123') + self.assertEqual(linenumbers(123), u'1. 123') + self.assertEqual(lower(123), u'123') + self.assertEqual(make_list(123), [u'1', u'2', u'3']) + self.assertEqual(slugify(123), u'123') + self.assertEqual(title(123), u'123') + self.assertEqual(truncatewords(123, 2), u'123') + self.assertEqual(upper(123), u'123') + self.assertEqual(urlencode(123), u'123') + self.assertEqual(urlize(123), u'123') + self.assertEqual(urlizetrunc(123, 1), u'123') + self.assertEqual(wordcount(123), 1) + self.assertEqual(wordwrap(123, 2), u'123') + self.assertEqual(ljust('123', 4), u'123 ') + self.assertEqual(rjust('123', 4), u' 123') + self.assertEqual(center('123', 5), u' 123 ') + self.assertEqual(center('123', 6), u' 123 ') + self.assertEqual(cut(123, '2'), u'13') + self.assertEqual(escape(123), u'123') + self.assertEqual(linebreaks(123), u'<p>123</p>') + self.assertEqual(linebreaksbr(123), u'123') + self.assertEqual(removetags(123, 'a'), u'123') + self.assertEqual(striptags(123), u'123') + diff --git a/parts/django/tests/regressiontests/defer_regress/__init__.py b/parts/django/tests/regressiontests/defer_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/defer_regress/__init__.py diff --git a/parts/django/tests/regressiontests/defer_regress/models.py b/parts/django/tests/regressiontests/defer_regress/models.py new file mode 100644 index 0000000..8db8c29 --- /dev/null +++ b/parts/django/tests/regressiontests/defer_regress/models.py @@ -0,0 +1,36 @@ +""" +Regression tests for defer() / only() behavior. +""" + +from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.db import connection, models + +class Item(models.Model): + name = models.CharField(max_length=15) + text = models.TextField(default="xyzzy") + value = models.IntegerField() + other_value = models.IntegerField(default=0) + + def __unicode__(self): + return self.name + +class RelatedItem(models.Model): + item = models.ForeignKey(Item) + +class Child(models.Model): + name = models.CharField(max_length=10) + value = models.IntegerField() + +class Leaf(models.Model): + name = models.CharField(max_length=10) + child = models.ForeignKey(Child) + second_child = models.ForeignKey(Child, related_name="other", null=True) + value = models.IntegerField(default=42) + + def __unicode__(self): + return self.name + +class ResolveThis(models.Model): + num = models.FloatField() + name = models.CharField(max_length=16) diff --git a/parts/django/tests/regressiontests/defer_regress/tests.py b/parts/django/tests/regressiontests/defer_regress/tests.py new file mode 100644 index 0000000..05f1279 --- /dev/null +++ b/parts/django/tests/regressiontests/defer_regress/tests.py @@ -0,0 +1,163 @@ +from operator import attrgetter + +from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.contrib.sessions.backends.db import SessionStore +from django.db import connection +from django.db.models.loading import cache +from django.test import TestCase + +from models import ResolveThis, Item, RelatedItem, Child, Leaf + + +class DeferRegressionTest(TestCase): + def assert_num_queries(self, n, func, *args, **kwargs): + old_DEBUG = settings.DEBUG + settings.DEBUG = True + starting_queries = len(connection.queries) + try: + func(*args, **kwargs) + finally: + settings.DEBUG = old_DEBUG + self.assertEqual(starting_queries + n, len(connection.queries)) + + def test_basic(self): + # Deferred fields should really be deferred and not accidentally use + # the field's default value just because they aren't passed to __init__ + + Item.objects.create(name="first", value=42) + obj = Item.objects.only("name", "other_value").get(name="first") + # Accessing "name" doesn't trigger a new database query. Accessing + # "value" or "text" should. + def test(): + self.assertEqual(obj.name, "first") + self.assertEqual(obj.other_value, 0) + self.assert_num_queries(0, test) + + def test(): + self.assertEqual(obj.value, 42) + self.assert_num_queries(1, test) + + def test(): + self.assertEqual(obj.text, "xyzzy") + self.assert_num_queries(1, test) + + def test(): + self.assertEqual(obj.text, "xyzzy") + self.assert_num_queries(0, test) + + # Regression test for #10695. Make sure different instances don't + # inadvertently share data in the deferred descriptor objects. + i = Item.objects.create(name="no I'm first", value=37) + items = Item.objects.only("value").order_by("-value") + self.assertEqual(items[0].name, "first") + self.assertEqual(items[1].name, "no I'm first") + + RelatedItem.objects.create(item=i) + r = RelatedItem.objects.defer("item").get() + self.assertEqual(r.item_id, i.id) + self.assertEqual(r.item, i) + + # Some further checks for select_related() and inherited model + # behaviour (regression for #10710). + c1 = Child.objects.create(name="c1", value=42) + c2 = Child.objects.create(name="c2", value=37) + Leaf.objects.create(name="l1", child=c1, second_child=c2) + + obj = Leaf.objects.only("name", "child").select_related()[0] + self.assertEqual(obj.child.name, "c1") + + self.assertQuerysetEqual( + Leaf.objects.select_related().only("child__name", "second_child__name"), [ + "l1", + ], + attrgetter("name") + ) + + # Models instances with deferred fields should still return the same + # content types as their non-deferred versions (bug #10738). + ctype = ContentType.objects.get_for_model + c1 = ctype(Item.objects.all()[0]) + c2 = ctype(Item.objects.defer("name")[0]) + c3 = ctype(Item.objects.only("name")[0]) + self.assertTrue(c1 is c2 is c3) + + # Regression for #10733 - only() can be used on a model with two + # foreign keys. + results = Leaf.objects.only("name", "child", "second_child").select_related() + self.assertEqual(results[0].child.name, "c1") + self.assertEqual(results[0].second_child.name, "c2") + + results = Leaf.objects.only("name", "child", "second_child", "child__name", "second_child__name").select_related() + self.assertEqual(results[0].child.name, "c1") + self.assertEqual(results[0].second_child.name, "c2") + + # Test for #12163 - Pickling error saving session with unsaved model + # instances. + SESSION_KEY = '2b1189a188b44ad18c35e1baac6ceead' + + item = Item() + item._deferred = False + s = SessionStore(SESSION_KEY) + s.clear() + s["item"] = item + s.save() + + s = SessionStore(SESSION_KEY) + s.modified = True + s.save() + + i2 = s["item"] + self.assertFalse(i2._deferred) + + # Regression for #11936 - loading.get_models should not return deferred + # models by default. + klasses = sorted( + cache.get_models(cache.get_app("defer_regress")), + key=lambda klass: klass.__name__ + ) + self.assertEqual( + klasses, [ + Child, + Item, + Leaf, + RelatedItem, + ResolveThis, + ] + ) + + klasses = sorted( + map( + attrgetter("__name__"), + cache.get_models( + cache.get_app("defer_regress"), include_deferred=True + ), + ) + ) + self.assertEqual( + klasses, [ + "Child", + "Child_Deferred_value", + "Item", + "Item_Deferred_name", + "Item_Deferred_name_other_value_text", + "Item_Deferred_name_other_value_value", + "Item_Deferred_other_value_text_value", + "Item_Deferred_text_value", + "Leaf", + "Leaf_Deferred_child_id_second_child_id_value", + "Leaf_Deferred_name_value", + "Leaf_Deferred_second_child_value", + "Leaf_Deferred_value", + "RelatedItem", + "RelatedItem_Deferred_", + "RelatedItem_Deferred_item_id", + "ResolveThis", + ] + ) + + def test_resolve_columns(self): + rt = ResolveThis.objects.create(num=5.0, name='Foobar') + qs = ResolveThis.objects.defer('num') + self.assertEqual(1, qs.count()) + self.assertEqual('Foobar', qs[0].name) diff --git a/parts/django/tests/regressiontests/delete_regress/__init__.py b/parts/django/tests/regressiontests/delete_regress/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/regressiontests/delete_regress/__init__.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/regressiontests/delete_regress/models.py b/parts/django/tests/regressiontests/delete_regress/models.py new file mode 100644 index 0000000..8109c0a --- /dev/null +++ b/parts/django/tests/regressiontests/delete_regress/models.py @@ -0,0 +1,37 @@ +from django.db import models + +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType + +class Award(models.Model): + name = models.CharField(max_length=25) + object_id = models.PositiveIntegerField() + content_type = models.ForeignKey(ContentType) + content_object = generic.GenericForeignKey() + +class AwardNote(models.Model): + award = models.ForeignKey(Award) + note = models.CharField(max_length=100) + +class Person(models.Model): + name = models.CharField(max_length=25) + awards = generic.GenericRelation(Award) + +class Book(models.Model): + pagecount = models.IntegerField() + +class Toy(models.Model): + name = models.CharField(max_length=50) + +class Child(models.Model): + name = models.CharField(max_length=50) + toys = models.ManyToManyField(Toy, through='PlayedWith') + +class PlayedWith(models.Model): + child = models.ForeignKey(Child) + toy = models.ForeignKey(Toy) + date = models.DateField(db_column='date_col') + +class PlayedWithNote(models.Model): + played = models.ForeignKey(PlayedWith) + note = models.TextField() diff --git a/parts/django/tests/regressiontests/delete_regress/tests.py b/parts/django/tests/regressiontests/delete_regress/tests.py new file mode 100644 index 0000000..26cd3c5 --- /dev/null +++ b/parts/django/tests/regressiontests/delete_regress/tests.py @@ -0,0 +1,116 @@ +import datetime + +from django.conf import settings +from django.db import backend, connection, transaction, DEFAULT_DB_ALIAS +from django.test import TestCase, TransactionTestCase + +from models import Book, Award, AwardNote, Person, Child, Toy, PlayedWith, PlayedWithNote + +# Can't run this test under SQLite, because you can't +# get two connections to an in-memory database. +if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.sqlite3': + class DeleteLockingTest(TransactionTestCase): + def setUp(self): + # Create a second connection to the default database + conn_settings = settings.DATABASES[DEFAULT_DB_ALIAS] + self.conn2 = backend.DatabaseWrapper({ + 'HOST': conn_settings['HOST'], + 'NAME': conn_settings['NAME'], + 'OPTIONS': conn_settings['OPTIONS'], + 'PASSWORD': conn_settings['PASSWORD'], + 'PORT': conn_settings['PORT'], + 'USER': conn_settings['USER'], + 'TIME_ZONE': settings.TIME_ZONE, + }) + + # Put both DB connections into managed transaction mode + transaction.enter_transaction_management() + transaction.managed(True) + self.conn2._enter_transaction_management(True) + + def tearDown(self): + # Close down the second connection. + transaction.leave_transaction_management() + self.conn2.close() + + def test_concurrent_delete(self): + "Deletes on concurrent transactions don't collide and lock the database. Regression for #9479" + + # Create some dummy data + b1 = Book(id=1, pagecount=100) + b2 = Book(id=2, pagecount=200) + b3 = Book(id=3, pagecount=300) + b1.save() + b2.save() + b3.save() + + transaction.commit() + + self.assertEquals(3, Book.objects.count()) + + # Delete something using connection 2. + cursor2 = self.conn2.cursor() + cursor2.execute('DELETE from delete_regress_book WHERE id=1') + self.conn2._commit(); + + # Now perform a queryset delete that covers the object + # deleted in connection 2. This causes an infinite loop + # under MySQL InnoDB unless we keep track of already + # deleted objects. + Book.objects.filter(pagecount__lt=250).delete() + transaction.commit() + self.assertEquals(1, Book.objects.count()) + +class DeleteCascadeTests(TestCase): + def test_generic_relation_cascade(self): + """ + Test that Django cascades deletes through generic-related + objects to their reverse relations. + + This might falsely succeed if the database cascades deletes + itself immediately; the postgresql_psycopg2 backend does not + give such a false success because ForeignKeys are created with + DEFERRABLE INITIALLY DEFERRED, so its internal cascade is + delayed until transaction commit. + + """ + person = Person.objects.create(name='Nelson Mandela') + award = Award.objects.create(name='Nobel', content_object=person) + note = AwardNote.objects.create(note='a peace prize', + award=award) + self.assertEquals(AwardNote.objects.count(), 1) + person.delete() + self.assertEquals(Award.objects.count(), 0) + # first two asserts are just sanity checks, this is the kicker: + self.assertEquals(AwardNote.objects.count(), 0) + + def test_fk_to_m2m_through(self): + """ + Test that if a M2M relationship has an explicitly-specified + through model, and some other model has an FK to that through + model, deletion is cascaded from one of the participants in + the M2M, to the through model, to its related model. + + Like the above test, this could in theory falsely succeed if + the DB cascades deletes itself immediately. + + """ + juan = Child.objects.create(name='Juan') + paints = Toy.objects.create(name='Paints') + played = PlayedWith.objects.create(child=juan, toy=paints, + date=datetime.date.today()) + note = PlayedWithNote.objects.create(played=played, + note='the next Jackson Pollock') + self.assertEquals(PlayedWithNote.objects.count(), 1) + paints.delete() + self.assertEquals(PlayedWith.objects.count(), 0) + # first two asserts just sanity checks, this is the kicker: + self.assertEquals(PlayedWithNote.objects.count(), 0) + +class LargeDeleteTests(TestCase): + def test_large_deletes(self): + "Regression for #13309 -- if the number of objects > chunk size, deletion still occurs" + for x in range(300): + track = Book.objects.create(pagecount=x+100) + Book.objects.all().delete() + self.assertEquals(Book.objects.count(), 0) diff --git a/parts/django/tests/regressiontests/dispatch/__init__.py b/parts/django/tests/regressiontests/dispatch/__init__.py new file mode 100644 index 0000000..679895b --- /dev/null +++ b/parts/django/tests/regressiontests/dispatch/__init__.py @@ -0,0 +1,2 @@ +"""Unit-tests for the dispatch project +""" diff --git a/parts/django/tests/regressiontests/dispatch/models.py b/parts/django/tests/regressiontests/dispatch/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/dispatch/models.py diff --git a/parts/django/tests/regressiontests/dispatch/tests/__init__.py b/parts/django/tests/regressiontests/dispatch/tests/__init__.py new file mode 100644 index 0000000..150bb01 --- /dev/null +++ b/parts/django/tests/regressiontests/dispatch/tests/__init__.py @@ -0,0 +1,6 @@ +""" +Unit-tests for the dispatch project +""" + +from test_saferef import * +from test_dispatcher import * diff --git a/parts/django/tests/regressiontests/dispatch/tests/test_dispatcher.py b/parts/django/tests/regressiontests/dispatch/tests/test_dispatcher.py new file mode 100644 index 0000000..ad3a05f --- /dev/null +++ b/parts/django/tests/regressiontests/dispatch/tests/test_dispatcher.py @@ -0,0 +1,123 @@ +from django.dispatch import Signal +import unittest +import sys +import gc +import django.utils.copycompat as copy + +if sys.platform.startswith('java'): + def garbage_collect(): + """Run the garbage collector and wait a bit to let it do his work""" + import time + gc.collect() + time.sleep(0.1) +else: + def garbage_collect(): + gc.collect() + +def receiver_1_arg(val, **kwargs): + return val + +class Callable(object): + def __call__(self, val, **kwargs): + return val + + def a(self, val, **kwargs): + return val + +a_signal = Signal(providing_args=["val"]) + +class DispatcherTests(unittest.TestCase): + """Test suite for dispatcher (barely started)""" + + def _testIsClean(self, signal): + """Assert that everything has been cleaned up automatically""" + self.assertEqual(signal.receivers, []) + + # force cleanup just in case + signal.receivers = [] + + def testExact(self): + a_signal.connect(receiver_1_arg, sender=self) + expected = [(receiver_1_arg,"test")] + result = a_signal.send(sender=self, val="test") + self.assertEqual(result, expected) + a_signal.disconnect(receiver_1_arg, sender=self) + self._testIsClean(a_signal) + + def testIgnoredSender(self): + a_signal.connect(receiver_1_arg) + expected = [(receiver_1_arg,"test")] + result = a_signal.send(sender=self, val="test") + self.assertEqual(result, expected) + a_signal.disconnect(receiver_1_arg) + self._testIsClean(a_signal) + + def testGarbageCollected(self): + a = Callable() + a_signal.connect(a.a, sender=self) + expected = [] + del a + garbage_collect() + result = a_signal.send(sender=self, val="test") + self.assertEqual(result, expected) + self._testIsClean(a_signal) + + def testMultipleRegistration(self): + a = Callable() + a_signal.connect(a) + a_signal.connect(a) + a_signal.connect(a) + a_signal.connect(a) + a_signal.connect(a) + a_signal.connect(a) + result = a_signal.send(sender=self, val="test") + self.assertEqual(len(result), 1) + self.assertEqual(len(a_signal.receivers), 1) + del a + del result + garbage_collect() + self._testIsClean(a_signal) + + def testUidRegistration(self): + def uid_based_receiver_1(**kwargs): + pass + + def uid_based_receiver_2(**kwargs): + pass + + a_signal.connect(uid_based_receiver_1, dispatch_uid = "uid") + a_signal.connect(uid_based_receiver_2, dispatch_uid = "uid") + self.assertEqual(len(a_signal.receivers), 1) + a_signal.disconnect(dispatch_uid = "uid") + self._testIsClean(a_signal) + + def testRobust(self): + """Test the sendRobust function""" + def fails(val, **kwargs): + raise ValueError('this') + a_signal.connect(fails) + result = a_signal.send_robust(sender=self, val="test") + err = result[0][1] + self.assert_(isinstance(err, ValueError)) + self.assertEqual(err.args, ('this',)) + a_signal.disconnect(fails) + self._testIsClean(a_signal) + + def testDisconnection(self): + receiver_1 = Callable() + receiver_2 = Callable() + receiver_3 = Callable() + a_signal.connect(receiver_1) + a_signal.connect(receiver_2) + a_signal.connect(receiver_3) + a_signal.disconnect(receiver_1) + del receiver_2 + garbage_collect() + a_signal.disconnect(receiver_3) + self._testIsClean(a_signal) + +def getSuite(): + return unittest.makeSuite(DispatcherTests,'test') + +if __name__ == "__main__": + unittest.main() diff --git a/parts/django/tests/regressiontests/dispatch/tests/test_saferef.py b/parts/django/tests/regressiontests/dispatch/tests/test_saferef.py new file mode 100644 index 0000000..c0ec879 --- /dev/null +++ b/parts/django/tests/regressiontests/dispatch/tests/test_saferef.py @@ -0,0 +1,79 @@ +from django.dispatch.saferef import * + +import unittest + +class Test1(object): + def x(self): + pass + +def test2(obj): + pass + +class Test2(object): + def __call__(self, obj): + pass + +class Tester(unittest.TestCase): + def setUp(self): + ts = [] + ss = [] + for x in xrange(5000): + t = Test1() + ts.append(t) + s = safeRef(t.x, self._closure) + ss.append(s) + ts.append(test2) + ss.append(safeRef(test2, self._closure)) + for x in xrange(30): + t = Test2() + ts.append(t) + s = safeRef(t, self._closure) + ss.append(s) + self.ts = ts + self.ss = ss + self.closureCount = 0 + + def tearDown(self): + del self.ts + del self.ss + + def testIn(self): + """Test the "in" operator for safe references (cmp)""" + for t in self.ts[:50]: + self.assert_(safeRef(t.x) in self.ss) + + def testValid(self): + """Test that the references are valid (return instance methods)""" + for s in self.ss: + self.assert_(s()) + + def testShortCircuit (self): + """Test that creation short-circuits to reuse existing references""" + sd = {} + for s in self.ss: + sd[s] = 1 + for t in self.ts: + if hasattr(t, 'x'): + self.assert_(sd.has_key(safeRef(t.x))) + self.assert_(safeRef(t.x) in sd) + else: + self.assert_(sd.has_key(safeRef(t))) + self.assert_(safeRef(t) in sd) + + def testRepresentation (self): + """Test that the reference object's representation works + + XXX Doesn't currently check the results, just that no error + is raised + """ + repr(self.ss[-1]) + + def _closure(self, ref): + """Dumb utility mechanism to increment deletion counter""" + self.closureCount +=1 + +def getSuite(): + return unittest.makeSuite(Tester,'test') + +if __name__ == "__main__": + unittest.main() diff --git a/parts/django/tests/regressiontests/expressions_regress/__init__.py b/parts/django/tests/regressiontests/expressions_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/expressions_regress/__init__.py diff --git a/parts/django/tests/regressiontests/expressions_regress/models.py b/parts/django/tests/regressiontests/expressions_regress/models.py new file mode 100644 index 0000000..f2997ee --- /dev/null +++ b/parts/django/tests/regressiontests/expressions_regress/models.py @@ -0,0 +1,12 @@ +""" +Model for testing arithmetic expressions. +""" +from django.db import models + +class Number(models.Model): + integer = models.IntegerField(db_column='the_integer') + float = models.FloatField(null=True, db_column='the_float') + + def __unicode__(self): + return u'%i, %.3f' % (self.integer, self.float) + diff --git a/parts/django/tests/regressiontests/expressions_regress/tests.py b/parts/django/tests/regressiontests/expressions_regress/tests.py new file mode 100644 index 0000000..a662728 --- /dev/null +++ b/parts/django/tests/regressiontests/expressions_regress/tests.py @@ -0,0 +1,195 @@ +""" +Spanning tests for all the operations that F() expressions can perform. +""" +from django.test import TestCase, Approximate + +from django.conf import settings +from django.db import models, DEFAULT_DB_ALIAS +from django.db.models import F + +from regressiontests.expressions_regress.models import Number + +class ExpressionsRegressTests(TestCase): + + def setUp(self): + Number(integer=-1).save() + Number(integer=42).save() + Number(integer=1337).save() + self.assertEqual(Number.objects.update(float=F('integer')), 3) + + def test_fill_with_value_from_same_object(self): + """ + We can fill a value in all objects with an other value of the + same object. + """ + self.assertQuerysetEqual( + Number.objects.all(), + [ + '<Number: -1, -1.000>', + '<Number: 42, 42.000>', + '<Number: 1337, 1337.000>' + ] + ) + + def test_increment_value(self): + """ + We can increment a value of all objects in a query set. + """ + self.assertEqual( + Number.objects.filter(integer__gt=0) + .update(integer=F('integer') + 1), + 2) + + self.assertQuerysetEqual( + Number.objects.all(), + [ + '<Number: -1, -1.000>', + '<Number: 43, 42.000>', + '<Number: 1338, 1337.000>' + ] + ) + + def test_filter_not_equals_other_field(self): + """ + We can filter for objects, where a value is not equals the value + of an other field. + """ + self.assertEqual( + Number.objects.filter(integer__gt=0) + .update(integer=F('integer') + 1), + 2) + self.assertQuerysetEqual( + Number.objects.exclude(float=F('integer')), + [ + '<Number: 43, 42.000>', + '<Number: 1338, 1337.000>' + ] + ) + + def test_complex_expressions(self): + """ + Complex expressions of different connection types are possible. + """ + n = Number.objects.create(integer=10, float=123.45) + self.assertEqual(Number.objects.filter(pk=n.pk) + .update(float=F('integer') + F('float') * 2), + 1) + + self.assertEqual(Number.objects.get(pk=n.pk).integer, 10) + self.assertEqual(Number.objects.get(pk=n.pk).float, Approximate(256.900, places=3)) + +class ExpressionOperatorTests(TestCase): + def setUp(self): + self.n = Number.objects.create(integer=42, float=15.5) + + def test_lefthand_addition(self): + # LH Addition of floats and integers + Number.objects.filter(pk=self.n.pk).update( + integer=F('integer') + 15, + float=F('float') + 42.7 + ) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 57) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(58.200, places=3)) + + def test_lefthand_subtraction(self): + # LH Subtraction of floats and integers + Number.objects.filter(pk=self.n.pk).update(integer=F('integer') - 15, + float=F('float') - 42.7) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 27) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(-27.200, places=3)) + + def test_lefthand_multiplication(self): + # Multiplication of floats and integers + Number.objects.filter(pk=self.n.pk).update(integer=F('integer') * 15, + float=F('float') * 42.7) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 630) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(661.850, places=3)) + + def test_lefthand_division(self): + # LH Division of floats and integers + Number.objects.filter(pk=self.n.pk).update(integer=F('integer') / 2, + float=F('float') / 42.7) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 21) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(0.363, places=3)) + + def test_lefthand_modulo(self): + # LH Modulo arithmetic on integers + Number.objects.filter(pk=self.n.pk).update(integer=F('integer') % 20) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 2) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) + + def test_lefthand_bitwise_and(self): + # LH Bitwise ands on integers + Number.objects.filter(pk=self.n.pk).update(integer=F('integer') & 56) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 40) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) + + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.oracle': + def test_lefthand_bitwise_or(self): + # LH Bitwise or on integers + Number.objects.filter(pk=self.n.pk).update(integer=F('integer') | 48) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 58) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) + + def test_right_hand_addition(self): + # Right hand operators + Number.objects.filter(pk=self.n.pk).update(integer=15 + F('integer'), + float=42.7 + F('float')) + + # RH Addition of floats and integers + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 57) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(58.200, places=3)) + + def test_right_hand_subtraction(self): + Number.objects.filter(pk=self.n.pk).update(integer=15 - F('integer'), + float=42.7 - F('float')) + + # RH Subtraction of floats and integers + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, -27) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(27.200, places=3)) + + def test_right_hand_multiplication(self): + # RH Multiplication of floats and integers + Number.objects.filter(pk=self.n.pk).update(integer=15 * F('integer'), + float=42.7 * F('float')) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 630) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(661.850, places=3)) + + def test_right_hand_division(self): + # RH Division of floats and integers + Number.objects.filter(pk=self.n.pk).update(integer=640 / F('integer'), + float=42.7 / F('float')) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 15) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(2.755, places=3)) + + def test_right_hand_modulo(self): + # RH Modulo arithmetic on integers + Number.objects.filter(pk=self.n.pk).update(integer=69 % F('integer')) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 27) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) + + def test_right_hand_bitwise_and(self): + # RH Bitwise ands on integers + Number.objects.filter(pk=self.n.pk).update(integer=15 & F('integer')) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 10) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) + + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.oracle': + def test_right_hand_bitwise_or(self): + # RH Bitwise or on integers + Number.objects.filter(pk=self.n.pk).update(integer=15 | F('integer')) + + self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 47) + self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) + diff --git a/parts/django/tests/regressiontests/extra_regress/__init__.py b/parts/django/tests/regressiontests/extra_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/extra_regress/__init__.py diff --git a/parts/django/tests/regressiontests/extra_regress/models.py b/parts/django/tests/regressiontests/extra_regress/models.py new file mode 100644 index 0000000..073157a --- /dev/null +++ b/parts/django/tests/regressiontests/extra_regress/models.py @@ -0,0 +1,40 @@ +import datetime + +import django.utils.copycompat as copy + +from django.contrib.auth.models import User +from django.db import models + +class RevisionableModel(models.Model): + base = models.ForeignKey('self', null=True) + title = models.CharField(blank=True, max_length=255) + when = models.DateTimeField(default=datetime.datetime.now) + + def __unicode__(self): + return u"%s (%s, %s)" % (self.title, self.id, self.base.id) + + def save(self, *args, **kwargs): + super(RevisionableModel, self).save(*args, **kwargs) + if not self.base: + self.base = self + kwargs.pop('force_insert', None) + kwargs.pop('force_update', None) + super(RevisionableModel, self).save(*args, **kwargs) + + def new_revision(self): + new_revision = copy.copy(self) + new_revision.pk = None + return new_revision + +class Order(models.Model): + created_by = models.ForeignKey(User) + text = models.TextField() + +class TestObject(models.Model): + first = models.CharField(max_length=20) + second = models.CharField(max_length=20) + third = models.CharField(max_length=20) + + def __unicode__(self): + return u'TestObject: %s,%s,%s' % (self.first,self.second,self.third) + diff --git a/parts/django/tests/regressiontests/extra_regress/tests.py b/parts/django/tests/regressiontests/extra_regress/tests.py new file mode 100644 index 0000000..ef7cbb8 --- /dev/null +++ b/parts/django/tests/regressiontests/extra_regress/tests.py @@ -0,0 +1,314 @@ +from django.test import TestCase + +from django.utils.datastructures import SortedDict + +from django.contrib.auth.models import User +from regressiontests.extra_regress.models import TestObject, Order, \ + RevisionableModel + +import datetime + +class ExtraRegressTests(TestCase): + + def setUp(self): + self.u = User.objects.create_user( + username="fred", + password="secret", + email="fred@example.com" + ) + + def test_regression_7314_7372(self): + """ + Regression tests for #7314 and #7372 + """ + rm = RevisionableModel.objects.create( + title='First Revision', + when=datetime.datetime(2008, 9, 28, 10, 30, 0) + ) + self.assertEqual(rm.pk, rm.base.pk) + + rm2 = rm.new_revision() + rm2.title = "Second Revision" + rm.when = datetime.datetime(2008, 9, 28, 14, 25, 0) + rm2.save() + + self.assertEqual(rm2.title, 'Second Revision') + self.assertEqual(rm2.base.title, 'First Revision') + + self.assertNotEqual(rm2.pk, rm.pk) + self.assertEqual(rm2.base.pk, rm.pk) + + # Queryset to match most recent revision: + qs = RevisionableModel.objects.extra( + where=["%(table)s.id IN (SELECT MAX(rev.id) FROM %(table)s rev GROUP BY rev.base_id)" % { + 'table': RevisionableModel._meta.db_table, + }] + ) + + self.assertQuerysetEqual(qs, + [('Second Revision', 'First Revision')], + transform=lambda r: (r.title, r.base.title) + ) + + # Queryset to search for string in title: + qs2 = RevisionableModel.objects.filter(title__contains="Revision") + self.assertQuerysetEqual(qs2, + [ + ('First Revision', 'First Revision'), + ('Second Revision', 'First Revision'), + ], + transform=lambda r: (r.title, r.base.title) + ) + + # Following queryset should return the most recent revision: + self.assertQuerysetEqual(qs & qs2, + [('Second Revision', 'First Revision')], + transform=lambda r: (r.title, r.base.title) + ) + + def test_extra_stay_tied(self): + # Extra select parameters should stay tied to their corresponding + # select portions. Applies when portions are updated or otherwise + # moved around. + qs = User.objects.extra( + select=SortedDict((("alpha", "%s"), ("beta", "2"), ("gamma", "%s"))), + select_params=(1, 3) + ) + qs = qs.extra(select={"beta": 4}) + qs = qs.extra(select={"alpha": "%s"}, select_params=[5]) + self.assertEqual( + list(qs.filter(id=self.u.id).values('alpha', 'beta', 'gamma')), + [{'alpha': 5, 'beta': 4, 'gamma': 3}] + ) + + def test_regression_7957(self): + """ + Regression test for #7957: Combining extra() calls should leave the + corresponding parameters associated with the right extra() bit. I.e. + internal dictionary must remain sorted. + """ + self.assertEqual( + User.objects.extra(select={"alpha": "%s"}, select_params=(1,) + ).extra(select={"beta": "%s"}, select_params=(2,))[0].alpha, + 1) + + self.assertEqual( + User.objects.extra(select={"beta": "%s"}, select_params=(1,) + ).extra(select={"alpha": "%s"}, select_params=(2,))[0].alpha, + 2) + + def test_regression_7961(self): + """ + Regression test for #7961: When not using a portion of an + extra(...) in a query, remove any corresponding parameters from the + query as well. + """ + self.assertEqual( + list(User.objects.extra(select={"alpha": "%s"}, select_params=(-6,) + ).filter(id=self.u.id).values_list('id', flat=True)), + [self.u.id] + ) + + def test_regression_8063(self): + """ + Regression test for #8063: limiting a query shouldn't discard any + extra() bits. + """ + qs = User.objects.all().extra(where=['id=%s'], params=[self.u.id]) + self.assertQuerysetEqual(qs, ['<User: fred>']) + self.assertQuerysetEqual(qs[:1], ['<User: fred>']) + + def test_regression_8039(self): + """ + Regression test for #8039: Ordering sometimes removed relevant tables + from extra(). This test is the critical case: ordering uses a table, + but then removes the reference because of an optimisation. The table + should still be present because of the extra() call. + """ + self.assertQuerysetEqual( + Order.objects.extra(where=["username=%s"], + params=["fred"], + tables=["auth_user"] + ).order_by('created_by'), + [] + ) + + def test_regression_8819(self): + """ + Regression test for #8819: Fields in the extra(select=...) list + should be available to extra(order_by=...). + """ + self.assertQuerysetEqual( + User.objects.filter(pk=self.u.id).extra(select={'extra_field': 1}).distinct(), + ['<User: fred>'] + ) + self.assertQuerysetEqual( + User.objects.filter(pk=self.u.id).extra(select={'extra_field': 1}, order_by=['extra_field']), + ['<User: fred>'] + ) + self.assertQuerysetEqual( + User.objects.filter(pk=self.u.id).extra(select={'extra_field': 1}, order_by=['extra_field']).distinct(), + ['<User: fred>'] + ) + + def test_dates_query(self): + """ + When calling the dates() method on a queryset with extra selection + columns, we can (and should) ignore those columns. They don't change + the result and cause incorrect SQL to be produced otherwise. + """ + rm = RevisionableModel.objects.create( + title='First Revision', + when=datetime.datetime(2008, 9, 28, 10, 30, 0) + ) + + self.assertQuerysetEqual( + RevisionableModel.objects.extra(select={"the_answer": 'id'}).dates('when', 'month'), + ['datetime.datetime(2008, 9, 1, 0, 0)'] + ) + + def test_values_with_extra(self): + """ + Regression test for #10256... If there is a values() clause, Extra + columns are only returned if they are explicitly mentioned. + """ + obj = TestObject(first='first', second='second', third='third') + obj.save() + + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values()), + [{'bar': u'second', 'third': u'third', 'second': u'second', 'whiz': u'third', 'foo': u'first', 'id': obj.pk, 'first': u'first'}] + ) + + # Extra clauses after an empty values clause are still included + self.assertEqual( + list(TestObject.objects.values().extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + [{'bar': u'second', 'third': u'third', 'second': u'second', 'whiz': u'third', 'foo': u'first', 'id': obj.pk, 'first': u'first'}] + ) + + # Extra columns are ignored if not mentioned in the values() clause + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('first', 'second')), + [{'second': u'second', 'first': u'first'}] + ) + + # Extra columns after a non-empty values() clause are ignored + self.assertEqual( + list(TestObject.objects.values('first', 'second').extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + [{'second': u'second', 'first': u'first'}] + ) + + # Extra columns can be partially returned + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('first', 'second', 'foo')), + [{'second': u'second', 'foo': u'first', 'first': u'first'}] + ) + + # Also works if only extra columns are included + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('foo', 'whiz')), + [{'foo': u'first', 'whiz': u'third'}] + ) + + # Values list works the same way + # All columns are returned for an empty values_list() + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list()), + [(u'first', u'second', u'third', obj.pk, u'first', u'second', u'third')] + ) + + # Extra columns after an empty values_list() are still included + self.assertEqual( + list(TestObject.objects.values_list().extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + [(u'first', u'second', u'third', obj.pk, u'first', u'second', u'third')] + ) + + # Extra columns ignored completely if not mentioned in values_list() + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first', 'second')), + [(u'first', u'second')] + ) + + # Extra columns after a non-empty values_list() clause are ignored completely + self.assertEqual( + list(TestObject.objects.values_list('first', 'second').extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + [(u'first', u'second')] + ) + + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('second', flat=True)), + [u'second'] + ) + + # Only the extra columns specified in the values_list() are returned + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first', 'second', 'whiz')), + [(u'first', u'second', u'third')] + ) + + # ...also works if only extra columns are included + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('foo','whiz')), + [(u'first', u'third')] + ) + + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz', flat=True)), + [u'third'] + ) + + # ... and values are returned in the order they are specified + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz','foo')), + [(u'third', u'first')] + ) + + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first','id')), + [(u'first', obj.pk)] + ) + + self.assertEqual( + list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz', 'first', 'bar', 'id')), + [(u'third', u'first', u'second', obj.pk)] + ) + + def test_regression_10847(self): + """ + Regression for #10847: the list of extra columns can always be + accurately evaluated. Using an inner query ensures that as_sql() is + producing correct output without requiring full evaluation and + execution of the inner query. + """ + obj = TestObject(first='first', second='second', third='third') + obj.save() + + self.assertEqual( + list(TestObject.objects.extra(select={'extra': 1}).values('pk')), + [{'pk': obj.pk}] + ) + + self.assertQuerysetEqual( + TestObject.objects.filter( + pk__in=TestObject.objects.extra(select={'extra': 1}).values('pk') + ), + ['<TestObject: TestObject: first,second,third>'] + ) + + self.assertEqual( + list(TestObject.objects.values('pk').extra(select={'extra': 1})), + [{'pk': obj.pk}] + ) + + self.assertQuerysetEqual( + TestObject.objects.filter( + pk__in=TestObject.objects.values('pk').extra(select={'extra': 1}) + ), + ['<TestObject: TestObject: first,second,third>'] + ) + + self.assertQuerysetEqual( + TestObject.objects.filter(pk=obj.pk) | + TestObject.objects.extra(where=["id > %s"], params=[obj.pk]), + ['<TestObject: TestObject: first,second,third>'] + ) diff --git a/parts/django/tests/regressiontests/file_storage/__init__.py b/parts/django/tests/regressiontests/file_storage/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/regressiontests/file_storage/__init__.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/regressiontests/file_storage/models.py b/parts/django/tests/regressiontests/file_storage/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/file_storage/models.py diff --git a/parts/django/tests/regressiontests/file_storage/test.png b/parts/django/tests/regressiontests/file_storage/test.png Binary files differnew file mode 100644 index 0000000..4f17cd0 --- /dev/null +++ b/parts/django/tests/regressiontests/file_storage/test.png diff --git a/parts/django/tests/regressiontests/file_storage/test1.png b/parts/django/tests/regressiontests/file_storage/test1.png Binary files differnew file mode 100644 index 0000000..bc98741 --- /dev/null +++ b/parts/django/tests/regressiontests/file_storage/test1.png diff --git a/parts/django/tests/regressiontests/file_storage/tests.py b/parts/django/tests/regressiontests/file_storage/tests.py new file mode 100644 index 0000000..8726181 --- /dev/null +++ b/parts/django/tests/regressiontests/file_storage/tests.py @@ -0,0 +1,382 @@ +# -*- coding: utf-8 -*- +import os +import shutil +import sys +import tempfile +import time +import unittest +from cStringIO import StringIO +from django.conf import settings +from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured +from django.core.files.base import ContentFile, File +from django.core.files.images import get_image_dimensions +from django.core.files.storage import FileSystemStorage, get_storage_class +from django.core.files.uploadedfile import UploadedFile +from unittest import TestCase + +try: + import threading +except ImportError: + import dummy_threading as threading + +# Try to import PIL in either of the two ways it can end up installed. +# Checking for the existence of Image is enough for CPython, but +# for PyPy, you need to check for the underlying modules +try: + from PIL import Image, _imaging +except ImportError: + try: + import Image, _imaging + except ImportError: + Image = None + +class GetStorageClassTests(unittest.TestCase): + def assertRaisesErrorWithMessage(self, error, message, callable, + *args, **kwargs): + self.assertRaises(error, callable, *args, **kwargs) + try: + callable(*args, **kwargs) + except error, e: + self.assertEqual(message, str(e)) + + def test_get_filesystem_storage(self): + """ + get_storage_class returns the class for a storage backend name/path. + """ + self.assertEqual( + get_storage_class('django.core.files.storage.FileSystemStorage'), + FileSystemStorage) + + def test_get_invalid_storage_module(self): + """ + get_storage_class raises an error if the requested import don't exist. + """ + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "NonExistingStorage isn't a storage module.", + get_storage_class, + 'NonExistingStorage') + + def test_get_nonexisting_storage_class(self): + """ + get_storage_class raises an error if the requested class don't exist. + """ + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + 'Storage module "django.core.files.storage" does not define a '\ + '"NonExistingStorage" class.', + get_storage_class, + 'django.core.files.storage.NonExistingStorage') + + def test_get_nonexisting_storage_module(self): + """ + get_storage_class raises an error if the requested module don't exist. + """ + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + 'Error importing storage module django.core.files.non_existing_'\ + 'storage: "No module named non_existing_storage"', + get_storage_class, + 'django.core.files.non_existing_storage.NonExistingStorage') + +class FileStorageTests(unittest.TestCase): + storage_class = FileSystemStorage + + def setUp(self): + self.temp_dir = tempfile.mktemp() + os.makedirs(self.temp_dir) + self.storage = self.storage_class(location=self.temp_dir, + base_url='/test_media_url/') + + def tearDown(self): + shutil.rmtree(self.temp_dir) + + def test_file_access_options(self): + """ + Standard file access options are available, and work as expected. + """ + self.assertFalse(self.storage.exists('storage_test')) + f = self.storage.open('storage_test', 'w') + f.write('storage contents') + f.close() + self.assert_(self.storage.exists('storage_test')) + + f = self.storage.open('storage_test', 'r') + self.assertEqual(f.read(), 'storage contents') + f.close() + + self.storage.delete('storage_test') + self.assertFalse(self.storage.exists('storage_test')) + + def test_file_save_without_name(self): + """ + File storage extracts the filename from the content object if no + name is given explicitly. + """ + self.assertFalse(self.storage.exists('test.file')) + + f = ContentFile('custom contents') + f.name = 'test.file' + + storage_f_name = self.storage.save(None, f) + + self.assertEqual(storage_f_name, f.name) + + self.assert_(os.path.exists(os.path.join(self.temp_dir, f.name))) + + self.storage.delete(storage_f_name) + + def test_file_path(self): + """ + File storage returns the full path of a file + """ + self.assertFalse(self.storage.exists('test.file')) + + f = ContentFile('custom contents') + f_name = self.storage.save('test.file', f) + + self.assertEqual(self.storage.path(f_name), + os.path.join(self.temp_dir, f_name)) + + self.storage.delete(f_name) + + def test_file_url(self): + """ + File storage returns a url to access a given file from the Web. + """ + self.assertEqual(self.storage.url('test.file'), + '%s%s' % (self.storage.base_url, 'test.file')) + + self.storage.base_url = None + self.assertRaises(ValueError, self.storage.url, 'test.file') + + def test_file_with_mixin(self): + """ + File storage can get a mixin to extend the functionality of the + returned file. + """ + self.assertFalse(self.storage.exists('test.file')) + + class TestFileMixin(object): + mixed_in = True + + f = ContentFile('custom contents') + f_name = self.storage.save('test.file', f) + + self.assert_(isinstance( + self.storage.open('test.file', mixin=TestFileMixin), + TestFileMixin + )) + + self.storage.delete('test.file') + + def test_listdir(self): + """ + File storage returns a tuple containing directories and files. + """ + self.assertFalse(self.storage.exists('storage_test_1')) + self.assertFalse(self.storage.exists('storage_test_2')) + self.assertFalse(self.storage.exists('storage_dir_1')) + + f = self.storage.save('storage_test_1', ContentFile('custom content')) + f = self.storage.save('storage_test_2', ContentFile('custom content')) + os.mkdir(os.path.join(self.temp_dir, 'storage_dir_1')) + + dirs, files = self.storage.listdir('') + self.assertEqual(set(dirs), set([u'storage_dir_1'])) + self.assertEqual(set(files), + set([u'storage_test_1', u'storage_test_2'])) + + self.storage.delete('storage_test_1') + self.storage.delete('storage_test_2') + os.rmdir(os.path.join(self.temp_dir, 'storage_dir_1')) + + def test_file_storage_prevents_directory_traversal(self): + """ + File storage prevents directory traversal (files can only be accessed if + they're below the storage location). + """ + self.assertRaises(SuspiciousOperation, self.storage.exists, '..') + self.assertRaises(SuspiciousOperation, self.storage.exists, '/etc/passwd') + +class CustomStorage(FileSystemStorage): + def get_available_name(self, name): + """ + Append numbers to duplicate files rather than underscores, like Trac. + """ + parts = name.split('.') + basename, ext = parts[0], parts[1:] + number = 2 + while self.exists(name): + name = '.'.join([basename, str(number)] + ext) + number += 1 + + return name + +class CustomStorageTests(FileStorageTests): + storage_class = CustomStorage + + def test_custom_get_available_name(self): + first = self.storage.save('custom_storage', ContentFile('custom contents')) + self.assertEqual(first, 'custom_storage') + second = self.storage.save('custom_storage', ContentFile('more contents')) + self.assertEqual(second, 'custom_storage.2') + self.storage.delete(first) + self.storage.delete(second) + +class UnicodeFileNameTests(unittest.TestCase): + def test_unicode_file_names(self): + """ + Regression test for #8156: files with unicode names I can't quite figure + out the encoding situation between doctest and this file, but the actual + repr doesn't matter; it just shouldn't return a unicode object. + """ + uf = UploadedFile(name=u'¿Cómo?',content_type='text') + self.assertEqual(type(uf.__repr__()), str) + +# Tests for a race condition on file saving (#4948). +# This is written in such a way that it'll always pass on platforms +# without threading. + +class SlowFile(ContentFile): + def chunks(self): + time.sleep(1) + return super(ContentFile, self).chunks() + +class FileSaveRaceConditionTest(TestCase): + def setUp(self): + self.storage_dir = tempfile.mkdtemp() + self.storage = FileSystemStorage(self.storage_dir) + self.thread = threading.Thread(target=self.save_file, args=['conflict']) + + def tearDown(self): + shutil.rmtree(self.storage_dir) + + def save_file(self, name): + name = self.storage.save(name, SlowFile("Data")) + + def test_race_condition(self): + self.thread.start() + name = self.save_file('conflict') + self.thread.join() + self.assert_(self.storage.exists('conflict')) + self.assert_(self.storage.exists('conflict_1')) + self.storage.delete('conflict') + self.storage.delete('conflict_1') + +class FileStoragePermissions(TestCase): + def setUp(self): + self.old_perms = settings.FILE_UPLOAD_PERMISSIONS + settings.FILE_UPLOAD_PERMISSIONS = 0666 + self.storage_dir = tempfile.mkdtemp() + self.storage = FileSystemStorage(self.storage_dir) + + def tearDown(self): + settings.FILE_UPLOAD_PERMISSIONS = self.old_perms + shutil.rmtree(self.storage_dir) + + def test_file_upload_permissions(self): + name = self.storage.save("the_file", ContentFile("data")) + actual_mode = os.stat(self.storage.path(name))[0] & 0777 + self.assertEqual(actual_mode, 0666) + + +class FileStoragePathParsing(TestCase): + def setUp(self): + self.storage_dir = tempfile.mkdtemp() + self.storage = FileSystemStorage(self.storage_dir) + + def tearDown(self): + shutil.rmtree(self.storage_dir) + + def test_directory_with_dot(self): + """Regression test for #9610. + + If the directory name contains a dot and the file name doesn't, make + sure we still mangle the file name instead of the directory name. + """ + + self.storage.save('dotted.path/test', ContentFile("1")) + self.storage.save('dotted.path/test', ContentFile("2")) + + self.assertFalse(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path'))) + self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test'))) + self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test_1'))) + + def test_first_character_dot(self): + """ + File names with a dot as their first character don't have an extension, + and the underscore should get added to the end. + """ + self.storage.save('dotted.path/.test', ContentFile("1")) + self.storage.save('dotted.path/.test', ContentFile("2")) + + self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test'))) + # Before 2.6, a leading dot was treated as an extension, and so + # underscore gets added to beginning instead of end. + if sys.version_info < (2, 6): + self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/_1.test'))) + else: + self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test_1'))) + +if Image is not None: + class DimensionClosingBug(TestCase): + """ + Test that get_image_dimensions() properly closes files (#8817) + """ + def test_not_closing_of_files(self): + """ + Open files passed into get_image_dimensions() should stay opened. + """ + empty_io = StringIO() + try: + get_image_dimensions(empty_io) + finally: + self.assert_(not empty_io.closed) + + def test_closing_of_filenames(self): + """ + get_image_dimensions() called with a filename should closed the file. + """ + # We need to inject a modified open() builtin into the images module + # that checks if the file was closed properly if the function is + # called with a filename instead of an file object. + # get_image_dimensions will call our catching_open instead of the + # regular builtin one. + + class FileWrapper(object): + _closed = [] + def __init__(self, f): + self.f = f + def __getattr__(self, name): + return getattr(self.f, name) + def close(self): + self._closed.append(True) + self.f.close() + + def catching_open(*args): + return FileWrapper(open(*args)) + + from django.core.files import images + images.open = catching_open + try: + get_image_dimensions(os.path.join(os.path.dirname(__file__), "test1.png")) + 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, 'rb')) + 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/parts/django/tests/regressiontests/file_uploads/__init__.py b/parts/django/tests/regressiontests/file_uploads/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/file_uploads/__init__.py diff --git a/parts/django/tests/regressiontests/file_uploads/models.py b/parts/django/tests/regressiontests/file_uploads/models.py new file mode 100644 index 0000000..9d02050 --- /dev/null +++ b/parts/django/tests/regressiontests/file_uploads/models.py @@ -0,0 +1,10 @@ +import tempfile +import os +from django.db import models +from django.core.files.storage import FileSystemStorage + +temp_storage = FileSystemStorage(tempfile.mkdtemp()) +UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload') + +class FileModel(models.Model): + testfile = models.FileField(storage=temp_storage, upload_to='test_upload') diff --git a/parts/django/tests/regressiontests/file_uploads/tests.py b/parts/django/tests/regressiontests/file_uploads/tests.py new file mode 100644 index 0000000..99d0b6b --- /dev/null +++ b/parts/django/tests/regressiontests/file_uploads/tests.py @@ -0,0 +1,305 @@ +#! -*- coding: utf-8 -*- +import errno +import os +import shutil +import unittest +from StringIO import StringIO + +from django.core.files import temp as tempfile +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase, client +from django.utils import simplejson +from django.utils.hashcompat import sha_constructor +from django.http.multipartparser import MultiPartParser + +from models import FileModel, temp_storage, UPLOAD_TO +import uploadhandler + + +UNICODE_FILENAME = u'test-0123456789_中文_Orléans.jpg' + +class FileUploadTests(TestCase): + def test_simple_upload(self): + post_data = { + 'name': 'Ringo', + 'file_field': open(__file__), + } + response = self.client.post('/file_uploads/upload/', post_data) + self.assertEqual(response.status_code, 200) + + def test_large_upload(self): + tdir = tempfile.gettempdir() + + file1 = tempfile.NamedTemporaryFile(suffix=".file1", dir=tdir) + file1.write('a' * (2 ** 21)) + file1.seek(0) + + file2 = tempfile.NamedTemporaryFile(suffix=".file2", dir=tdir) + file2.write('a' * (10 * 2 ** 20)) + file2.seek(0) + + post_data = { + 'name': 'Ringo', + 'file_field1': file1, + 'file_field2': file2, + } + + for key in post_data.keys(): + try: + post_data[key + '_hash'] = sha_constructor(post_data[key].read()).hexdigest() + post_data[key].seek(0) + except AttributeError: + post_data[key + '_hash'] = sha_constructor(post_data[key]).hexdigest() + + response = self.client.post('/file_uploads/verify/', post_data) + + self.assertEqual(response.status_code, 200) + + def test_unicode_file_name(self): + tdir = tempfile.gettempdir() + + # This file contains chinese symbols and an accented char in the name. + file1 = open(os.path.join(tdir, UNICODE_FILENAME.encode('utf-8')), 'w+b') + file1.write('b' * (2 ** 10)) + file1.seek(0) + + post_data = { + 'file_unicode': file1, + } + + response = self.client.post('/file_uploads/unicode_name/', post_data) + + file1.close() + try: + os.unlink(file1.name) + except: + pass + + self.assertEqual(response.status_code, 200) + + def test_dangerous_file_names(self): + """Uploaded file names should be sanitized before ever reaching the view.""" + # This test simulates possible directory traversal attacks by a + # malicious uploader We have to do some monkeybusiness here to construct + # a malicious payload with an invalid file name (containing os.sep or + # os.pardir). This similar to what an attacker would need to do when + # trying such an attack. + scary_file_names = [ + "/tmp/hax0rd.txt", # Absolute path, *nix-style. + "C:\\Windows\\hax0rd.txt", # Absolute path, win-syle. + "C:/Windows/hax0rd.txt", # Absolute path, broken-style. + "\\tmp\\hax0rd.txt", # Absolute path, broken in a different way. + "/tmp\\hax0rd.txt", # Absolute path, broken by mixing. + "subdir/hax0rd.txt", # Descendant path, *nix-style. + "subdir\\hax0rd.txt", # Descendant path, win-style. + "sub/dir\\hax0rd.txt", # Descendant path, mixed. + "../../hax0rd.txt", # Relative path, *nix-style. + "..\\..\\hax0rd.txt", # Relative path, win-style. + "../..\\hax0rd.txt" # Relative path, mixed. + ] + + payload = [] + for i, name in enumerate(scary_file_names): + payload.extend([ + '--' + client.BOUNDARY, + 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i, name), + 'Content-Type: application/octet-stream', + '', + 'You got pwnd.' + ]) + payload.extend([ + '--' + client.BOUNDARY + '--', + '', + ]) + + payload = "\r\n".join(payload) + r = { + 'CONTENT_LENGTH': len(payload), + 'CONTENT_TYPE': client.MULTIPART_CONTENT, + 'PATH_INFO': "/file_uploads/echo/", + 'REQUEST_METHOD': 'POST', + 'wsgi.input': client.FakePayload(payload), + } + response = self.client.request(**r) + + # The filenames should have been sanitized by the time it got to the view. + recieved = simplejson.loads(response.content) + for i, name in enumerate(scary_file_names): + got = recieved["file%s" % i] + self.assertEqual(got, "hax0rd.txt") + + def test_filename_overflow(self): + """File names over 256 characters (dangerous on some platforms) get fixed up.""" + name = "%s.txt" % ("f"*500) + payload = "\r\n".join([ + '--' + client.BOUNDARY, + 'Content-Disposition: form-data; name="file"; filename="%s"' % name, + 'Content-Type: application/octet-stream', + '', + 'Oops.' + '--' + client.BOUNDARY + '--', + '', + ]) + r = { + 'CONTENT_LENGTH': len(payload), + 'CONTENT_TYPE': client.MULTIPART_CONTENT, + 'PATH_INFO': "/file_uploads/echo/", + 'REQUEST_METHOD': 'POST', + 'wsgi.input': client.FakePayload(payload), + } + got = simplejson.loads(self.client.request(**r).content) + self.assert_(len(got['file']) < 256, "Got a long file name (%s characters)." % len(got['file'])) + + def test_custom_upload_handler(self): + # A small file (under the 5M quota) + smallfile = tempfile.NamedTemporaryFile() + smallfile.write('a' * (2 ** 21)) + smallfile.seek(0) + + # A big file (over the quota) + bigfile = tempfile.NamedTemporaryFile() + bigfile.write('a' * (10 * 2 ** 20)) + bigfile.seek(0) + + # Small file posting should work. + response = self.client.post('/file_uploads/quota/', {'f': smallfile}) + got = simplejson.loads(response.content) + self.assert_('f' in got) + + # Large files don't go through. + response = self.client.post("/file_uploads/quota/", {'f': bigfile}) + got = simplejson.loads(response.content) + self.assert_('f' not in got) + + def test_broken_custom_upload_handler(self): + f = tempfile.NamedTemporaryFile() + f.write('a' * (2 ** 21)) + f.seek(0) + + # AttributeError: You cannot alter upload handlers after the upload has been processed. + self.assertRaises( + AttributeError, + self.client.post, + '/file_uploads/quota/broken/', + {'f': f} + ) + + def test_fileupload_getlist(self): + file1 = tempfile.NamedTemporaryFile() + file1.write('a' * (2 ** 23)) + file1.seek(0) + + file2 = tempfile.NamedTemporaryFile() + file2.write('a' * (2 * 2 ** 18)) + file2.seek(0) + + file2a = tempfile.NamedTemporaryFile() + file2a.write('a' * (5 * 2 ** 20)) + file2a.seek(0) + + response = self.client.post('/file_uploads/getlist_count/', { + 'file1': file1, + 'field1': u'test', + 'field2': u'test3', + 'field3': u'test5', + 'field4': u'test6', + 'field5': u'test7', + 'file2': (file2, file2a) + }) + got = simplejson.loads(response.content) + + self.assertEqual(got.get('file1'), 1) + self.assertEqual(got.get('file2'), 2) + + def test_file_error_blocking(self): + """ + The server should not block when there are upload errors (bug #8622). + This can happen if something -- i.e. an exception handler -- tries to + access POST while handling an error in parsing POST. This shouldn't + cause an infinite loop! + """ + class POSTAccessingHandler(client.ClientHandler): + """A handler that'll access POST during an exception.""" + def handle_uncaught_exception(self, request, resolver, exc_info): + ret = super(POSTAccessingHandler, self).handle_uncaught_exception(request, resolver, exc_info) + p = request.POST + return ret + + post_data = { + 'name': 'Ringo', + 'file_field': open(__file__), + } + # Maybe this is a little more complicated that it needs to be; but if + # the django.test.client.FakePayload.read() implementation changes then + # this test would fail. So we need to know exactly what kind of error + # it raises when there is an attempt to read more than the available bytes: + try: + client.FakePayload('a').read(2) + except Exception, reference_error: + pass + + # install the custom handler that tries to access request.POST + self.client.handler = POSTAccessingHandler() + + try: + response = self.client.post('/file_uploads/upload_errors/', post_data) + except reference_error.__class__, err: + self.failIf( + str(err) == str(reference_error), + "Caught a repeated exception that'll cause an infinite loop in file uploads." + ) + except Exception, err: + # CustomUploadError is the error that should have been raised + self.assertEqual(err.__class__, uploadhandler.CustomUploadError) + +class DirectoryCreationTests(unittest.TestCase): + """ + Tests for error handling during directory creation + via _save_FIELD_file (ticket #6450) + """ + def setUp(self): + self.obj = FileModel() + if not os.path.isdir(temp_storage.location): + os.makedirs(temp_storage.location) + if os.path.isdir(UPLOAD_TO): + os.chmod(UPLOAD_TO, 0700) + shutil.rmtree(UPLOAD_TO) + + def tearDown(self): + os.chmod(temp_storage.location, 0700) + shutil.rmtree(temp_storage.location) + + def test_readonly_root(self): + """Permission errors are not swallowed""" + os.chmod(temp_storage.location, 0500) + try: + self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', 'x')) + except OSError, err: + self.assertEquals(err.errno, errno.EACCES) + except Exception, err: + self.fail("OSError [Errno %s] not raised." % errno.EACCES) + + def test_not_a_directory(self): + """The correct IOError is raised when the upload directory name exists but isn't a directory""" + # Create a file with the upload directory name + fd = open(UPLOAD_TO, 'w') + fd.close() + try: + self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', 'x')) + except IOError, err: + # The test needs to be done on a specific string as IOError + # is raised even without the patch (just not early enough) + self.assertEquals(err.args[0], + "%s exists and is not a directory." % UPLOAD_TO) + except: + self.fail("IOError not raised") + +class MultiParserTests(unittest.TestCase): + + def test_empty_upload_handlers(self): + # We're not actually parsing here; just checking if the parser properly + # instantiates with empty upload handlers. + parser = MultiPartParser({ + 'CONTENT_TYPE': 'multipart/form-data; boundary=_foo', + 'CONTENT_LENGTH': '1' + }, StringIO('x'), [], 'utf-8') diff --git a/parts/django/tests/regressiontests/file_uploads/uploadhandler.py b/parts/django/tests/regressiontests/file_uploads/uploadhandler.py new file mode 100644 index 0000000..9f3960a --- /dev/null +++ b/parts/django/tests/regressiontests/file_uploads/uploadhandler.py @@ -0,0 +1,34 @@ +""" +Upload handlers to test the upload API. +""" + +from django.core.files.uploadhandler import FileUploadHandler, StopUpload + +class QuotaUploadHandler(FileUploadHandler): + """ + This test upload handler terminates the connection if more than a quota + (5MB) is uploaded. + """ + + QUOTA = 5 * 2**20 # 5 MB + + def __init__(self, request=None): + super(QuotaUploadHandler, self).__init__(request) + self.total_upload = 0 + + def receive_data_chunk(self, raw_data, start): + self.total_upload += len(raw_data) + if self.total_upload >= self.QUOTA: + raise StopUpload(connection_reset=True) + return raw_data + + def file_complete(self, file_size): + return None + +class CustomUploadError(Exception): + pass + +class ErroringUploadHandler(FileUploadHandler): + """A handler that raises an exception.""" + def receive_data_chunk(self, raw_data, start): + raise CustomUploadError("Oops!") diff --git a/parts/django/tests/regressiontests/file_uploads/urls.py b/parts/django/tests/regressiontests/file_uploads/urls.py new file mode 100644 index 0000000..413080e --- /dev/null +++ b/parts/django/tests/regressiontests/file_uploads/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls.defaults import * +import views + +urlpatterns = patterns('', + (r'^upload/$', views.file_upload_view), + (r'^verify/$', views.file_upload_view_verify), + (r'^unicode_name/$', views.file_upload_unicode_name), + (r'^echo/$', views.file_upload_echo), + (r'^quota/$', views.file_upload_quota), + (r'^quota/broken/$', views.file_upload_quota_broken), + (r'^getlist_count/$', views.file_upload_getlist_count), + (r'^upload_errors/$', views.file_upload_errors), +) diff --git a/parts/django/tests/regressiontests/file_uploads/views.py b/parts/django/tests/regressiontests/file_uploads/views.py new file mode 100644 index 0000000..50bc3f8 --- /dev/null +++ b/parts/django/tests/regressiontests/file_uploads/views.py @@ -0,0 +1,114 @@ +import os +from django.core.files.uploadedfile import UploadedFile +from django.http import HttpResponse, HttpResponseServerError +from django.utils import simplejson +from models import FileModel, UPLOAD_TO +from uploadhandler import QuotaUploadHandler, ErroringUploadHandler +from django.utils.hashcompat import sha_constructor +from tests import UNICODE_FILENAME + +def file_upload_view(request): + """ + Check that a file upload can be updated into the POST dictionary without + going pear-shaped. + """ + form_data = request.POST.copy() + form_data.update(request.FILES) + if isinstance(form_data.get('file_field'), UploadedFile) and isinstance(form_data['name'], unicode): + # If a file is posted, the dummy client should only post the file name, + # not the full path. + if os.path.dirname(form_data['file_field'].name) != '': + return HttpResponseServerError() + return HttpResponse('') + else: + return HttpResponseServerError() + +def file_upload_view_verify(request): + """ + Use the sha digest hash to verify the uploaded contents. + """ + form_data = request.POST.copy() + form_data.update(request.FILES) + + for key, value in form_data.items(): + if key.endswith('_hash'): + continue + if key + '_hash' not in form_data: + continue + submitted_hash = form_data[key + '_hash'] + if isinstance(value, UploadedFile): + new_hash = sha_constructor(value.read()).hexdigest() + else: + new_hash = sha_constructor(value).hexdigest() + if new_hash != submitted_hash: + return HttpResponseServerError() + + # Adding large file to the database should succeed + largefile = request.FILES['file_field2'] + obj = FileModel() + obj.testfile.save(largefile.name, largefile) + + return HttpResponse('') + +def file_upload_unicode_name(request): + + # Check to see if unicode name came through properly. + if not request.FILES['file_unicode'].name.endswith(UNICODE_FILENAME): + return HttpResponseServerError() + + response = None + + # Check to make sure the exotic characters are preserved even + # through file save. + uni_named_file = request.FILES['file_unicode'] + obj = FileModel.objects.create(testfile=uni_named_file) + full_name = u'%s/%s' % (UPLOAD_TO, uni_named_file.name) + if not os.path.exists(full_name): + response = HttpResponseServerError() + + # Cleanup the object with its exotic file name immediately. + # (shutil.rmtree used elsewhere in the tests to clean up the + # upload directory has been seen to choke on unicode + # filenames on Windows.) + obj.delete() + + if response: + return response + else: + return HttpResponse('') + +def file_upload_echo(request): + """ + Simple view to echo back info about uploaded files for tests. + """ + r = dict([(k, f.name) for k, f in request.FILES.items()]) + return HttpResponse(simplejson.dumps(r)) + +def file_upload_quota(request): + """ + Dynamically add in an upload handler. + """ + request.upload_handlers.insert(0, QuotaUploadHandler()) + return file_upload_echo(request) + +def file_upload_quota_broken(request): + """ + You can't change handlers after reading FILES; this view shouldn't work. + """ + response = file_upload_echo(request) + request.upload_handlers.insert(0, QuotaUploadHandler()) + return response + +def file_upload_getlist_count(request): + """ + Check the .getlist() function to ensure we receive the correct number of files. + """ + file_counts = {} + + for key in request.FILES.keys(): + file_counts[key] = len(request.FILES.getlist(key)) + return HttpResponse(simplejson.dumps(file_counts)) + +def file_upload_errors(request): + request.upload_handlers.insert(0, ErroringUploadHandler()) + return file_upload_echo(request) diff --git a/parts/django/tests/regressiontests/fixtures_regress/__init__.py b/parts/django/tests/regressiontests/fixtures_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/__init__.py diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/absolute.json b/parts/django/tests/regressiontests/fixtures_regress/fixtures/absolute.json new file mode 100644 index 0000000..37ed3f6 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/absolute.json @@ -0,0 +1,9 @@ +[ + { + "pk": "1", + "model": "fixtures_regress.absolute", + "fields": { + "name": "Load Absolute Path Test" + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/animal.xml b/parts/django/tests/regressiontests/fixtures_regress/fixtures/animal.xml new file mode 100644 index 0000000..0383c60 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/animal.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="10" model="fixtures_regress.animal"> + <field type="CharField" name="name">Emu</field> + <field type="CharField" name="latin_name">Dromaius novaehollandiae</field> + <field type="IntegerField" name="count">42</field> + <field type="FloatField" name="weight">1.2</field> + </object> +</django-objects>
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/bad_fixture1.unkn b/parts/django/tests/regressiontests/fixtures_regress/fixtures/bad_fixture1.unkn new file mode 100644 index 0000000..a8b0a0c --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/bad_fixture1.unkn @@ -0,0 +1 @@ +This data shouldn't load, as it's of an unknown file format.
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/bad_fixture2.xml b/parts/django/tests/regressiontests/fixtures_regress/fixtures/bad_fixture2.xml new file mode 100644 index 0000000..87b809f --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/bad_fixture2.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objcts version="1.0"> + <objct pk="2" model="fixtures.article"> + <field type="CharField" name="headline">Poker on TV is great!</field> + <field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field> + </objct> +</django-objcts>
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/big-fixture.json b/parts/django/tests/regressiontests/fixtures_regress/fixtures/big-fixture.json new file mode 100644 index 0000000..e655fbb --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/big-fixture.json @@ -0,0 +1,83 @@ +[ + { + "pk": 6, + "model": "fixtures_regress.channel", + "fields": { + "name": "Business" + } + }, + + { + "pk": 1, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 1", + "channels": [6] + } + }, + { + "pk": 2, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 2", + "channels": [6] + } + }, + { + "pk": 3, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 3", + "channels": [6] + } + }, + { + "pk": 4, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 4", + "channels": [6] + } + }, + + { + "pk": 5, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 5", + "channels": [6] + } + }, + { + "pk": 6, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 6", + "channels": [6] + } + }, + { + "pk": 7, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 7", + "channels": [6] + } + }, + { + "pk": 8, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 8", + "channels": [6] + } + }, + { + "pk": 9, + "model": "fixtures_regress.article", + "fields": { + "title": "Yet Another Article", + "channels": [6] + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/empty.json b/parts/django/tests/regressiontests/fixtures_regress/fixtures/empty.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/empty.json @@ -0,0 +1 @@ +[]
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/forward_ref_lookup.json b/parts/django/tests/regressiontests/fixtures_regress/fixtures/forward_ref_lookup.json new file mode 100644 index 0000000..fe50c65 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/forward_ref_lookup.json @@ -0,0 +1,32 @@ +[ + { + "pk": "4", + "model": "fixtures_regress.person", + "fields": { + "name": "Neal Stephenson" + } + }, + { + "pk": "2", + "model": "fixtures_regress.store", + "fields": { + "name": "Amazon" + } + }, + { + "pk": "3", + "model": "fixtures_regress.store", + "fields": { + "name": "Borders" + } + }, + { + "pk": 1, + "model": "fixtures_regress.book", + "fields": { + "name": "Cryptonomicon", + "author": ["Neal Stephenson"], + "stores": [["Amazon"], ["Borders"]] + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/model-inheritance.json b/parts/django/tests/regressiontests/fixtures_regress/fixtures/model-inheritance.json new file mode 100644 index 0000000..00c482b --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/model-inheritance.json @@ -0,0 +1,4 @@ +[ + {"pk": 1, "model": "fixtures_regress.parent", "fields": {"name": "fred"}}, + {"pk": 1, "model": "fixtures_regress.child", "fields": {"data": "apple"}} +] diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/nk-inheritance.json b/parts/django/tests/regressiontests/fixtures_regress/fixtures/nk-inheritance.json new file mode 100644 index 0000000..08e5d4f --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/nk-inheritance.json @@ -0,0 +1,18 @@ +[ + { + "pk": 1, + "model": "fixtures_regress.nkchild", + "fields": { + "data": "apple" + } + }, + { + "pk": 1, + "model": "fixtures_regress.reftonkchild", + "fields": { + "text": "my text", + "nk_fk" : ["apple"], + "nk_m2m": [["apple"]] + } + } +] diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/nk-inheritance2.xml b/parts/django/tests/regressiontests/fixtures_regress/fixtures/nk-inheritance2.xml new file mode 100644 index 0000000..7eb17a6 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/nk-inheritance2.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="2" model="fixtures_regress.parent"> + <field type="CharField" name="name">james</field> + </object> + <object pk="2" model="fixtures_regress.nkchild"> + <field type="CharField" name="data">banana</field> + </object> + <object pk="2" model="fixtures_regress.reftonkchild"> + <field type="CharField" name="text">other text</field> + <field to="fixtures_regress.nkchild" name="nk_fk" rel="ManyToOneRel"> + <natural>apple</natural> + </field> + <field to="fixtures_regress.nkchild" name="nk_m2m" rel="ManyToManyRel"> + <object> + <natural>banana</natural> + </object> + <object> + <natural>apple</natural> + </object> + </field> + </object> +</django-objects>
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/non_natural_1.json b/parts/django/tests/regressiontests/fixtures_regress/fixtures/non_natural_1.json new file mode 100644 index 0000000..4bce792 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/non_natural_1.json @@ -0,0 +1,25 @@ +[ + { + "pk": 12, + "model": "fixtures_regress.person", + "fields": { + "name": "Greg Egan" + } + }, + { + "pk": 11, + "model": "fixtures_regress.store", + "fields": { + "name": "Angus and Robertson" + } + }, + { + "pk": 10, + "model": "fixtures_regress.book", + "fields": { + "name": "Permutation City", + "author": 12, + "stores": [11] + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/non_natural_2.xml b/parts/django/tests/regressiontests/fixtures_regress/fixtures/non_natural_2.xml new file mode 100644 index 0000000..280ad37 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/non_natural_2.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="22" model="fixtures_regress.person"> + <field type="CharField" name="name">Orson Scott Card</field> + </object> + <object pk="21" model="fixtures_regress.store"> + <field type="CharField" name="name">Collins Bookstore</field> + </object> + <object pk="20" model="fixtures_regress.book"> + <field type="CharField" name="name">Ender's Game</field> + <field to="fixtures_regress.person" name="author" rel="ManyToOneRel">22</field> + <field to="fixtures_regress.store" name="stores" rel="ManyToManyRel"> + <object pk="21"/> + </field> + </object> +</django-objects>
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/pretty.xml b/parts/django/tests/regressiontests/fixtures_regress/fixtures/pretty.xml new file mode 100644 index 0000000..68e5710 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/pretty.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="1" model="fixtures_regress.stuff"> + <field type="CharField" name="name"> + <None/> + </field> + <field to="auth.user" name="owner" rel="ManyToOneRel"> + <None/> + </field> + </object> +</django-objects>
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/sequence.json b/parts/django/tests/regressiontests/fixtures_regress/fixtures/sequence.json new file mode 100644 index 0000000..60bfcdd --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/sequence.json @@ -0,0 +1,12 @@ +[ + { + "pk": "1", + "model": "fixtures_regress.animal", + "fields": { + "name": "Lion", + "latin_name": "Panthera leo", + "count": 3, + "weight": 1.2 + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/fixtures_regress/fixtures/thingy.json b/parts/django/tests/regressiontests/fixtures_regress/fixtures/thingy.json new file mode 100644 index 0000000..1693177 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/fixtures/thingy.json @@ -0,0 +1,9 @@ +[ + { + "pk": "1", + "model": "fixtures_regress.thingy", + "fields": { + "name": "Whatchamacallit" + } + } +] diff --git a/parts/django/tests/regressiontests/fixtures_regress/models.py b/parts/django/tests/regressiontests/fixtures_regress/models.py new file mode 100644 index 0000000..7465fd2 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/models.py @@ -0,0 +1,225 @@ +from django.db import models, DEFAULT_DB_ALIAS +from django.contrib.auth.models import User +from django.conf import settings + + +class Animal(models.Model): + name = models.CharField(max_length=150) + latin_name = models.CharField(max_length=150) + count = models.IntegerField() + weight = models.FloatField() + + # use a non-default name for the default manager + specimens = models.Manager() + + def __unicode__(self): + return self.name + + +class Plant(models.Model): + name = models.CharField(max_length=150) + + class Meta: + # For testing when upper case letter in app name; regression for #4057 + db_table = "Fixtures_regress_plant" + +class Stuff(models.Model): + name = models.CharField(max_length=20, null=True) + owner = models.ForeignKey(User, null=True) + + def __unicode__(self): + return unicode(self.name) + u' is owned by ' + unicode(self.owner) + + +class Absolute(models.Model): + name = models.CharField(max_length=40) + + load_count = 0 + + def __init__(self, *args, **kwargs): + super(Absolute, self).__init__(*args, **kwargs) + Absolute.load_count += 1 + + +class Parent(models.Model): + name = models.CharField(max_length=10) + + class Meta: + ordering = ('id',) + + +class Child(Parent): + data = models.CharField(max_length=10) + + +# Models to regression test #7572 +class Channel(models.Model): + name = models.CharField(max_length=255) + + +class Article(models.Model): + title = models.CharField(max_length=255) + channels = models.ManyToManyField(Channel) + + class Meta: + ordering = ('id',) + + +# Models to regression test #11428 +class Widget(models.Model): + name = models.CharField(max_length=255) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + + +class WidgetProxy(Widget): + class Meta: + proxy = True + + +# Check for forward references in FKs and M2Ms with natural keys +class TestManager(models.Manager): + def get_by_natural_key(self, key): + return self.get(name=key) + + +class Store(models.Model): + objects = TestManager() + name = models.CharField(max_length=255) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + + def natural_key(self): + return (self.name,) + + +class Person(models.Model): + objects = TestManager() + name = models.CharField(max_length=255) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + + # Person doesn't actually have a dependency on store, but we need to define + # one to test the behaviour of the dependency resolution algorithm. + def natural_key(self): + return (self.name,) + natural_key.dependencies = ['fixtures_regress.store'] + + +class Book(models.Model): + name = models.CharField(max_length=255) + author = models.ForeignKey(Person) + stores = models.ManyToManyField(Store) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return u'%s by %s (available at %s)' % ( + self.name, + self.author.name, + ', '.join(s.name for s in self.stores.all()) + ) + + +class NKManager(models.Manager): + def get_by_natural_key(self, data): + return self.get(data=data) + + +class NKChild(Parent): + data = models.CharField(max_length=10, unique=True) + objects = NKManager() + + def natural_key(self): + return self.data + + def __unicode__(self): + return u'NKChild %s:%s' % (self.name, self.data) + + +class RefToNKChild(models.Model): + text = models.CharField(max_length=10) + nk_fk = models.ForeignKey(NKChild, related_name='ref_fks') + nk_m2m = models.ManyToManyField(NKChild, related_name='ref_m2ms') + + def __unicode__(self): + return u'%s: Reference to %s [%s]' % ( + self.text, + self.nk_fk, + ', '.join(str(o) for o in self.nk_m2m.all()) + ) + + +# ome models with pathological circular dependencies +class Circle1(models.Model): + name = models.CharField(max_length=255) + + def natural_key(self): + return self.name + natural_key.dependencies = ['fixtures_regress.circle2'] + + +class Circle2(models.Model): + name = models.CharField(max_length=255) + + def natural_key(self): + return self.name + natural_key.dependencies = ['fixtures_regress.circle1'] + + +class Circle3(models.Model): + name = models.CharField(max_length=255) + + def natural_key(self): + return self.name + natural_key.dependencies = ['fixtures_regress.circle3'] + + +class Circle4(models.Model): + name = models.CharField(max_length=255) + + def natural_key(self): + return self.name + natural_key.dependencies = ['fixtures_regress.circle5'] + + +class Circle5(models.Model): + name = models.CharField(max_length=255) + + def natural_key(self): + return self.name + natural_key.dependencies = ['fixtures_regress.circle6'] + + +class Circle6(models.Model): + name = models.CharField(max_length=255) + + def natural_key(self): + return self.name + natural_key.dependencies = ['fixtures_regress.circle4'] + + +class ExternalDependency(models.Model): + name = models.CharField(max_length=255) + + def natural_key(self): + return self.name + natural_key.dependencies = ['fixtures_regress.book'] + + +# Model for regression test of #11101 +class Thingy(models.Model): + name = models.CharField(max_length=255) diff --git a/parts/django/tests/regressiontests/fixtures_regress/tests.py b/parts/django/tests/regressiontests/fixtures_regress/tests.py new file mode 100644 index 0000000..57ee7c0 --- /dev/null +++ b/parts/django/tests/regressiontests/fixtures_regress/tests.py @@ -0,0 +1,611 @@ +# -*- coding: utf-8 -*- +# Unittests for fixtures. +import os +import sys +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +from django.conf import settings +from django.core import management +from django.core.management.commands.dumpdata import sort_dependencies +from django.core.management.base import CommandError +from django.db.models import signals +from django.db import DEFAULT_DB_ALIAS, transaction +from django.test import TestCase, TransactionTestCase + +from models import Animal, Stuff +from models import Absolute, Parent, Child +from models import Article, Widget +from models import Store, Person, Book +from models import NKChild, RefToNKChild +from models import Circle1, Circle2, Circle3 +from models import ExternalDependency +from models import Thingy + + +pre_save_checks = [] +def animal_pre_save_check(signal, sender, instance, **kwargs): + "A signal that is used to check the type of data loaded from fixtures" + pre_save_checks.append( + ( + 'Count = %s (%s)' % (instance.count, type(instance.count)), + 'Weight = %s (%s)' % (instance.weight, type(instance.weight)), + ) + ) + + +class TestFixtures(TestCase): + def test_duplicate_pk(self): + """ + This is a regression test for ticket #3790. + """ + # Load a fixture that uses PK=1 + management.call_command( + 'loaddata', + 'sequence', + verbosity=0, + commit=False + ) + + # Create a new animal. Without a sequence reset, this new object + # will take a PK of 1 (on Postgres), and the save will fail. + + animal = Animal( + name='Platypus', + latin_name='Ornithorhynchus anatinus', + count=2, + weight=2.2 + ) + animal.save() + self.assertTrue(animal.id > 1) + + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.oracle': + def test_pretty_print_xml(self): + """ + Regression test for ticket #4558 -- pretty printing of XML fixtures + doesn't affect parsing of None values. + """ + # Load a pretty-printed XML fixture with Nulls. + management.call_command( + 'loaddata', + 'pretty.xml', + verbosity=0, + commit=False + ) + self.assertEqual(Stuff.objects.all()[0].name, None) + self.assertEqual(Stuff.objects.all()[0].owner, None) + + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.oracle': + def test_pretty_print_xml_empty_strings(self): + """ + Regression test for ticket #4558 -- pretty printing of XML fixtures + doesn't affect parsing of None values. + """ + # Load a pretty-printed XML fixture with Nulls. + management.call_command( + 'loaddata', + 'pretty.xml', + verbosity=0, + commit=False + ) + self.assertEqual(Stuff.objects.all()[0].name, u'') + self.assertEqual(Stuff.objects.all()[0].owner, None) + + def test_absolute_path(self): + """ + Regression test for ticket #6436 -- + os.path.join will throw away the initial parts of a path if it + encounters an absolute path. + This means that if a fixture is specified as an absolute path, + we need to make sure we don't discover the absolute path in every + fixture directory. + """ + load_absolute_path = os.path.join( + os.path.dirname(__file__), + 'fixtures', + 'absolute.json' + ) + management.call_command( + 'loaddata', + load_absolute_path, + verbosity=0, + commit=False + ) + self.assertEqual(Absolute.load_count, 1) + + + def test_unknown_format(self): + """ + Test for ticket #4371 -- Loading data of an unknown format should fail + Validate that error conditions are caught correctly + """ + stderr = StringIO() + management.call_command( + 'loaddata', + 'bad_fixture1.unkn', + verbosity=0, + commit=False, + stderr=stderr, + ) + self.assertEqual( + stderr.getvalue(), + "Problem installing fixture 'bad_fixture1': unkn is not a known serialization format.\n" + ) + + def test_invalid_data(self): + """ + Test for ticket #4371 -- Loading a fixture file with invalid data + using explicit filename. + Validate that error conditions are caught correctly + """ + stderr = StringIO() + management.call_command( + 'loaddata', + 'bad_fixture2.xml', + verbosity=0, + commit=False, + stderr=stderr, + ) + self.assertEqual( + stderr.getvalue(), + "No fixture data found for 'bad_fixture2'. (File format may be invalid.)\n" + ) + + def test_invalid_data_no_ext(self): + """ + Test for ticket #4371 -- Loading a fixture file with invalid data + without file extension. + Validate that error conditions are caught correctly + """ + stderr = StringIO() + management.call_command( + 'loaddata', + 'bad_fixture2', + verbosity=0, + commit=False, + stderr=stderr, + ) + self.assertEqual( + stderr.getvalue(), + "No fixture data found for 'bad_fixture2'. (File format may be invalid.)\n" + ) + + def test_empty(self): + """ + Test for ticket #4371 -- Loading a fixture file with no data returns an error. + Validate that error conditions are caught correctly + """ + stderr = StringIO() + management.call_command( + 'loaddata', + 'empty', + verbosity=0, + commit=False, + stderr=stderr, + ) + self.assertEqual( + stderr.getvalue(), + "No fixture data found for 'empty'. (File format may be invalid.)\n" + ) + + def test_abort_loaddata_on_error(self): + """ + Test for ticket #4371 -- If any of the fixtures contain an error, + loading is aborted. + Validate that error conditions are caught correctly + """ + stderr = StringIO() + management.call_command( + 'loaddata', + 'empty', + verbosity=0, + commit=False, + stderr=stderr, + ) + self.assertEqual( + stderr.getvalue(), + "No fixture data found for 'empty'. (File format may be invalid.)\n" + ) + + def test_error_message(self): + """ + (Regression for #9011 - error message is correct) + """ + stderr = StringIO() + management.call_command( + 'loaddata', + 'bad_fixture2', + 'animal', + verbosity=0, + commit=False, + stderr=stderr, + ) + self.assertEqual( + stderr.getvalue(), + "No fixture data found for 'bad_fixture2'. (File format may be invalid.)\n" + ) + + def test_pg_sequence_resetting_checks(self): + """ + Test for ticket #7565 -- PostgreSQL sequence resetting checks shouldn't + ascend to parent models when inheritance is used + (since they are treated individually). + """ + management.call_command( + 'loaddata', + 'model-inheritance.json', + verbosity=0, + commit=False + ) + self.assertEqual(Parent.objects.all()[0].id, 1) + self.assertEqual(Child.objects.all()[0].id, 1) + + def test_close_connection_after_loaddata(self): + """ + Test for ticket #7572 -- MySQL has a problem if the same connection is + used to create tables, load data, and then query over that data. + To compensate, we close the connection after running loaddata. + This ensures that a new connection is opened when test queries are + issued. + """ + management.call_command( + 'loaddata', + 'big-fixture.json', + verbosity=0, + commit=False + ) + articles = Article.objects.exclude(id=9) + self.assertEqual( + list(articles.values_list('id', flat=True)), + [1, 2, 3, 4, 5, 6, 7, 8] + ) + # Just for good measure, run the same query again. + # Under the influence of ticket #7572, this will + # give a different result to the previous call. + self.assertEqual( + list(articles.values_list('id', flat=True)), + [1, 2, 3, 4, 5, 6, 7, 8] + ) + + def test_field_value_coerce(self): + """ + Test for tickets #8298, #9942 - Field values should be coerced into the + correct type by the deserializer, not as part of the database write. + """ + global pre_save_checks + pre_save_checks = [] + signals.pre_save.connect(animal_pre_save_check) + management.call_command( + 'loaddata', + 'animal.xml', + verbosity=0, + commit=False, + ) + self.assertEqual( + pre_save_checks, + [ + ("Count = 42 (<type 'int'>)", "Weight = 1.2 (<type 'float'>)") + ] + ) + signals.pre_save.disconnect(animal_pre_save_check) + + def test_dumpdata_uses_default_manager(self): + """ + Regression for #11286 + Ensure that dumpdata honors the default manager + Dump the current contents of the database as a JSON fixture + """ + management.call_command( + 'loaddata', + 'animal.xml', + verbosity=0, + commit=False, + ) + management.call_command( + 'loaddata', + 'sequence.json', + verbosity=0, + commit=False, + ) + animal = Animal( + name='Platypus', + latin_name='Ornithorhynchus anatinus', + count=2, + weight=2.2 + ) + animal.save() + + stdout = StringIO() + management.call_command( + 'dumpdata', + 'fixtures_regress.animal', + format='json', + stdout=stdout + ) + + # Output order isn't guaranteed, so check for parts + data = stdout.getvalue() + lion_json = '{"pk": 1, "model": "fixtures_regress.animal", "fields": {"count": 3, "weight": 1.2, "name": "Lion", "latin_name": "Panthera leo"}}' + emu_json = '{"pk": 10, "model": "fixtures_regress.animal", "fields": {"count": 42, "weight": 1.2, "name": "Emu", "latin_name": "Dromaius novaehollandiae"}}' + platypus_json = '{"pk": %d, "model": "fixtures_regress.animal", "fields": {"count": 2, "weight": 2.2000000000000002, "name": "Platypus", "latin_name": "Ornithorhynchus anatinus"}}' + platypus_json = platypus_json % animal.pk + + self.assertEqual(len(data), len('[%s]' % ', '.join([lion_json, emu_json, platypus_json]))) + self.assertTrue(lion_json in data) + self.assertTrue(emu_json in data) + self.assertTrue(platypus_json in data) + + def test_proxy_model_included(self): + """ + Regression for #11428 - Proxy models aren't included when you dumpdata + """ + stdout = StringIO() + # Create an instance of the concrete class + Widget(name='grommet').save() + management.call_command( + 'dumpdata', + 'fixtures_regress.widget', + 'fixtures_regress.widgetproxy', + format='json', + stdout=stdout + ) + self.assertEqual( + stdout.getvalue(), + """[{"pk": 1, "model": "fixtures_regress.widget", "fields": {"name": "grommet"}}]""" + ) + + +class NaturalKeyFixtureTests(TestCase): + def assertRaisesMessage(self, exc, msg, func, *args, **kwargs): + try: + func(*args, **kwargs) + except Exception, e: + self.assertEqual(msg, str(e)) + self.assertTrue(isinstance(e, exc), "Expected %s, got %s" % (exc, type(e))) + + def test_nk_deserialize(self): + """ + Test for ticket #13030 - Python based parser version + natural keys deserialize with fk to inheriting model + """ + management.call_command( + 'loaddata', + 'model-inheritance.json', + verbosity=0, + commit=False + ) + management.call_command( + 'loaddata', + 'nk-inheritance.json', + verbosity=0, + commit=False + ) + self.assertEqual( + NKChild.objects.get(pk=1).data, + 'apple' + ) + + self.assertEqual( + RefToNKChild.objects.get(pk=1).nk_fk.data, + 'apple' + ) + + def test_nk_deserialize_xml(self): + """ + Test for ticket #13030 - XML version + natural keys deserialize with fk to inheriting model + """ + management.call_command( + 'loaddata', + 'model-inheritance.json', + verbosity=0, + commit=False + ) + management.call_command( + 'loaddata', + 'nk-inheritance.json', + verbosity=0, + commit=False + ) + management.call_command( + 'loaddata', + 'nk-inheritance2.xml', + verbosity=0, + commit=False + ) + self.assertEqual( + NKChild.objects.get(pk=2).data, + 'banana' + ) + self.assertEqual( + RefToNKChild.objects.get(pk=2).nk_fk.data, + 'apple' + ) + + def test_nk_on_serialize(self): + """ + Check that natural key requirements are taken into account + when serializing models + """ + management.call_command( + 'loaddata', + 'forward_ref_lookup.json', + verbosity=0, + commit=False + ) + + stdout = StringIO() + management.call_command( + 'dumpdata', + 'fixtures_regress.book', + 'fixtures_regress.person', + 'fixtures_regress.store', + verbosity=0, + format='json', + use_natural_keys=True, + stdout=stdout, + ) + self.assertEqual( + stdout.getvalue(), + """[{"pk": 2, "model": "fixtures_regress.store", "fields": {"name": "Amazon"}}, {"pk": 3, "model": "fixtures_regress.store", "fields": {"name": "Borders"}}, {"pk": 4, "model": "fixtures_regress.person", "fields": {"name": "Neal Stephenson"}}, {"pk": 1, "model": "fixtures_regress.book", "fields": {"stores": [["Amazon"], ["Borders"]], "name": "Cryptonomicon", "author": ["Neal Stephenson"]}}]""" + ) + + def test_dependency_sorting(self): + """ + Now lets check the dependency sorting explicitly + It doesn't matter what order you mention the models + Store *must* be serialized before then Person, and both + must be serialized before Book. + """ + sorted_deps = sort_dependencies( + [('fixtures_regress', [Book, Person, Store])] + ) + self.assertEqual( + sorted_deps, + [Store, Person, Book] + ) + + def test_dependency_sorting_2(self): + sorted_deps = sort_dependencies( + [('fixtures_regress', [Book, Store, Person])] + ) + self.assertEqual( + sorted_deps, + [Store, Person, Book] + ) + + def test_dependency_sorting_3(self): + sorted_deps = sort_dependencies( + [('fixtures_regress', [Store, Book, Person])] + ) + self.assertEqual( + sorted_deps, + [Store, Person, Book] + ) + + def test_dependency_sorting_4(self): + sorted_deps = sort_dependencies( + [('fixtures_regress', [Store, Person, Book])] + ) + self.assertEqual( + sorted_deps, + [Store, Person, Book] + ) + + def test_dependency_sorting_5(self): + sorted_deps = sort_dependencies( + [('fixtures_regress', [Person, Book, Store])] + ) + self.assertEqual( + sorted_deps, + [Store, Person, Book] + ) + + def test_dependency_sorting_6(self): + sorted_deps = sort_dependencies( + [('fixtures_regress', [Person, Store, Book])] + ) + self.assertEqual( + sorted_deps, + [Store, Person, Book] + ) + + def test_dependency_sorting_dangling(self): + sorted_deps = sort_dependencies( + [('fixtures_regress', [Person, Circle1, Store, Book])] + ) + self.assertEqual( + sorted_deps, + [Circle1, Store, Person, Book] + ) + + def test_dependency_sorting_tight_circular(self): + self.assertRaisesMessage( + CommandError, + """Can't resolve dependencies for fixtures_regress.Circle1, fixtures_regress.Circle2 in serialized app list.""", + sort_dependencies, + [('fixtures_regress', [Person, Circle2, Circle1, Store, Book])], + ) + + def test_dependency_sorting_tight_circular_2(self): + self.assertRaisesMessage( + CommandError, + """Can't resolve dependencies for fixtures_regress.Circle1, fixtures_regress.Circle2 in serialized app list.""", + sort_dependencies, + [('fixtures_regress', [Circle1, Book, Circle2])], + ) + + def test_dependency_self_referential(self): + self.assertRaisesMessage( + CommandError, + """Can't resolve dependencies for fixtures_regress.Circle3 in serialized app list.""", + sort_dependencies, + [('fixtures_regress', [Book, Circle3])], + ) + + def test_dependency_sorting_long(self): + self.assertRaisesMessage( + CommandError, + """Can't resolve dependencies for fixtures_regress.Circle1, fixtures_regress.Circle2, fixtures_regress.Circle3 in serialized app list.""", + sort_dependencies, + [('fixtures_regress', [Person, Circle2, Circle1, Circle3, Store, Book])], + ) + + def test_dependency_sorting_normal(self): + sorted_deps = sort_dependencies( + [('fixtures_regress', [Person, ExternalDependency, Book])] + ) + self.assertEqual( + sorted_deps, + [Person, Book, ExternalDependency] + ) + + def test_normal_pk(self): + """ + Check that normal primary keys still work + on a model with natural key capabilities + """ + management.call_command( + 'loaddata', + 'non_natural_1.json', + verbosity=0, + commit=False + ) + management.call_command( + 'loaddata', + 'forward_ref_lookup.json', + verbosity=0, + commit=False + ) + management.call_command( + 'loaddata', + 'non_natural_2.xml', + verbosity=0, + commit=False + ) + books = Book.objects.all() + self.assertEqual( + books.__repr__(), + """[<Book: Cryptonomicon by Neal Stephenson (available at Amazon, Borders)>, <Book: Ender's Game by Orson Scott Card (available at Collins Bookstore)>, <Book: Permutation City by Greg Egan (available at Angus and Robertson)>]""" + ) + + +class TestTicket11101(TransactionTestCase): + + def ticket_11101(self): + management.call_command( + 'loaddata', + 'thingy.json', + verbosity=0, + commit=False + ) + self.assertEqual(Thingy.objects.count(), 1) + transaction.rollback() + self.assertEqual(Thingy.objects.count(), 0) + + def test_ticket_11101(self): + """Test that fixtures can be rolled back (ticket #11101).""" + ticket_11101 = transaction.commit_manually(self.ticket_11101) + ticket_11101() diff --git a/parts/django/tests/regressiontests/forms/__init__.py b/parts/django/tests/regressiontests/forms/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/__init__.py diff --git a/parts/django/tests/regressiontests/forms/localflavor/__init__.py b/parts/django/tests/regressiontests/forms/localflavor/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/__init__.py diff --git a/parts/django/tests/regressiontests/forms/localflavor/ar.py b/parts/django/tests/regressiontests/forms/localflavor/ar.py new file mode 100644 index 0000000..9c67269 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/ar.py @@ -0,0 +1,99 @@ +from django.contrib.localflavor.ar.forms import (ARProvinceSelect, + ARPostalCodeField, ARDNIField, ARCUITField) + +from utils import LocalFlavorTestCase + + +class ARLocalFlavorTests(LocalFlavorTestCase): + def test_ARProvinceSelect(self): + f = ARProvinceSelect() + out = u'''<select name="provincias"> +<option value="B">Buenos Aires</option> +<option value="K">Catamarca</option> +<option value="H">Chaco</option> +<option value="U">Chubut</option> +<option value="C">Ciudad Aut\xf3noma de Buenos Aires</option> +<option value="X">C\xf3rdoba</option> +<option value="W">Corrientes</option> +<option value="E">Entre R\xedos</option> +<option value="P">Formosa</option> +<option value="Y">Jujuy</option> +<option value="L">La Pampa</option> +<option value="F">La Rioja</option> +<option value="M">Mendoza</option> +<option value="N">Misiones</option> +<option value="Q">Neuqu\xe9n</option> +<option value="R">R\xedo Negro</option> +<option value="A" selected="selected">Salta</option> +<option value="J">San Juan</option> +<option value="D">San Luis</option> +<option value="Z">Santa Cruz</option> +<option value="S">Santa Fe</option> +<option value="G">Santiago del Estero</option> +<option value="V">Tierra del Fuego, Ant\xe1rtida e Islas del Atl\xe1ntico Sur</option> +<option value="T">Tucum\xe1n</option> +</select>''' + self.assertEqual(f.render('provincias', 'A'), out) + + def test_ARPostalCodeField(self): + error_format = [u'Enter a postal code in the format NNNN or ANNNNAAA.'] + error_atmost = [u'Ensure this value has at most 8 characters (it has 9).'] + error_atleast = [u'Ensure this value has at least 4 characters (it has 3).'] + valid = { + '5000': '5000', + 'C1064AAB': 'C1064AAB', + 'c1064AAB': 'C1064AAB', + 'C1064aab': 'C1064AAB', + '4400': '4400', + u'C1064AAB': 'C1064AAB', + } + invalid = { + 'C1064AABB': error_atmost + error_format, + 'C1064AA': error_format, + 'C1064AB': error_format, + '106AAB': error_format, + '500': error_atleast + error_format, + '5PPP': error_format, + } + self.assertFieldOutput(ARPostalCodeField, valid, invalid) + + def test_ARDNIField(self): + error_length = [u'This field requires 7 or 8 digits.'] + error_digitsonly = [u'This field requires only numbers.'] + valid = { + '20123456': '20123456', + '20.123.456': '20123456', + u'20123456': '20123456', + u'20.123.456': '20123456', + '20.123456': '20123456', + '9123456': '9123456', + '9.123.456': '9123456', + } + invalid = { + '101234566': error_length, + 'W0123456': error_digitsonly, + '10,123,456': error_digitsonly, + } + self.assertFieldOutput(ARDNIField, valid, invalid) + + def test_ARCUITField(self): + error_format = [u'Enter a valid CUIT in XX-XXXXXXXX-X or XXXXXXXXXXXX format.'] + error_invalid = [u'Invalid CUIT.'] + valid = { + '20-10123456-9': '20-10123456-9', + u'20-10123456-9': '20-10123456-9', + '27-10345678-4': '27-10345678-4', + '20101234569': '20-10123456-9', + '27103456784': '27-10345678-4', + } + invalid = { + '2-10123456-9': error_format, + '210123456-9': error_format, + '20-10123456': error_format, + '20-10123456-': error_format, + '20-10123456-5': error_invalid, + '2-10123456-9': error_format, + '27-10345678-1': error_invalid, + u'27-10345678-1': error_invalid, + } + self.assertFieldOutput(ARCUITField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/at.py b/parts/django/tests/regressiontests/forms/localflavor/at.py new file mode 100644 index 0000000..3fa50ac --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/at.py @@ -0,0 +1,45 @@ +from django.contrib.localflavor.at.forms import (ATZipCodeField, ATStateSelect, + ATSocialSecurityNumberField) + +from utils import LocalFlavorTestCase + + +class ATLocalFlavorTests(LocalFlavorTestCase): + def test_ATStateSelect(self): + f = ATStateSelect() + out = u'''<select name="bundesland"> +<option value="BL">Burgenland</option> +<option value="KA">Carinthia</option> +<option value="NO">Lower Austria</option> +<option value="OO">Upper Austria</option> +<option value="SA">Salzburg</option> +<option value="ST">Styria</option> +<option value="TI">Tyrol</option> +<option value="VO">Vorarlberg</option> +<option value="WI" selected="selected">Vienna</option> +</select>''' + self.assertEqual(f.render('bundesland', 'WI'), out) + + def test_ATZipCodeField(self): + error_format = [u'Enter a zip code in the format XXXX.'] + valid = { + '1150': '1150', + '4020': '4020', + '8020': '8020', + } + invalid = { + '111222': error_format, + 'eeffee': error_format, + } + self.assertFieldOutput(ATZipCodeField, valid, invalid) + + def test_ATSocialSecurityNumberField(self): + error_format = [u'Enter a valid Austrian Social Security Number in XXXX XXXXXX format.'] + valid = { + '1237 010180': '1237 010180', + } + invalid = { + '1237 010181': error_format, + '12370 010180': error_format, + } + self.assertFieldOutput(ATSocialSecurityNumberField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/au.py b/parts/django/tests/regressiontests/forms/localflavor/au.py new file mode 100644 index 0000000..4bd8a76 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/au.py @@ -0,0 +1,50 @@ +from django.contrib.localflavor.au.forms import (AUPostCodeField, + AUPhoneNumberField, AUStateSelect) + +from utils import LocalFlavorTestCase + + +class AULocalFlavorTests(LocalFlavorTestCase): + def test_AUStateSelect(self): + f = AUStateSelect() + out = u'''<select name="state"> +<option value="ACT">Australian Capital Territory</option> +<option value="NSW" selected="selected">New South Wales</option> +<option value="NT">Northern Territory</option> +<option value="QLD">Queensland</option> +<option value="SA">South Australia</option> +<option value="TAS">Tasmania</option> +<option value="VIC">Victoria</option> +<option value="WA">Western Australia</option> +</select>''' + self.assertEqual(f.render('state', 'NSW'), out) + + def test_AUPostCodeField(self): + error_format = [u'Enter a 4 digit post code.'] + valid = { + '1234': '1234', + '2000': '2000', + } + invalid = { + 'abcd': error_format, + '20001': error_format, + } + self.assertFieldOutput(AUPostCodeField, valid, invalid) + + def test_AUPhoneNumberField(self): + error_format = [u'Phone numbers must contain 10 digits.'] + valid = { + '1234567890': '1234567890', + '0213456789': '0213456789', + '02 13 45 67 89': '0213456789', + '(02) 1345 6789': '0213456789', + '(02) 1345-6789': '0213456789', + '(02)1345-6789': '0213456789', + '0408 123 456': '0408123456', + } + invalid = { + '123': error_format, + '1800DJANGO': error_format, + } + self.assertFieldOutput(AUPhoneNumberField, valid, invalid) + diff --git a/parts/django/tests/regressiontests/forms/localflavor/br.py b/parts/django/tests/regressiontests/forms/localflavor/br.py new file mode 100644 index 0000000..6ae7105 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/br.py @@ -0,0 +1,144 @@ +from django.contrib.localflavor.br.forms import (BRZipCodeField, + BRCNPJField, BRCPFField, BRPhoneNumberField, BRStateSelect, + BRStateChoiceField) + +from utils import LocalFlavorTestCase + + +class BRLocalFlavorTests(LocalFlavorTestCase): + def test_BRZipCodeField(self): + error_format = [u'Enter a zip code in the format XXXXX-XXX.'] + valid = { + '12345-123': '12345-123', + } + invalid = { + '12345_123': error_format, + '1234-123': error_format, + 'abcde-abc': error_format, + '12345-': error_format, + '-123': error_format, + } + self.assertFieldOutput(BRZipCodeField, valid, invalid) + + def test_BRCNPJField(self): + error_format = [u'Invalid CNPJ number.'] + error_numbersonly = [u'This field requires only numbers.'] + valid = { + '64.132.916/0001-88': '64.132.916/0001-88', + '64-132-916/0001-88': '64-132-916/0001-88', + '64132916/0001-88': '64132916/0001-88', + } + invalid = { + '12-345-678/9012-10': error_format, + '12.345.678/9012-10': error_format, + '12345678/9012-10': error_format, + '64.132.916/0001-XX': error_numbersonly, + } + self.assertFieldOutput(BRCNPJField, valid, invalid) + + def test_BRCPFField(self): + error_format = [u'Invalid CPF number.'] + error_numbersonly = [u'This field requires only numbers.'] + error_atmost_chars = [u'Ensure this value has at most 14 characters (it has 15).'] + error_atleast_chars = [u'Ensure this value has at least 11 characters (it has 10).'] + error_atmost = [u'This field requires at most 11 digits or 14 characters.'] + valid = { + '663.256.017-26': '663.256.017-26', + '66325601726': '66325601726', + '375.788.573-20': '375.788.573-20', + '84828509895': '84828509895', + } + invalid = { + '489.294.654-54': error_format, + '295.669.575-98': error_format, + '539.315.127-22': error_format, + '375.788.573-XX': error_numbersonly, + '375.788.573-000': error_atmost_chars, + '123.456.78': error_atleast_chars, + '123456789555': error_atmost, + } + self.assertFieldOutput(BRCPFField, valid, invalid) + + def test_BRPhoneNumberField(self): + # TODO: this doesn't test for any invalid inputs. + valid = { + '41-3562-3464': u'41-3562-3464', + '4135623464': u'41-3562-3464', + '41 3562-3464': u'41-3562-3464', + '41 3562 3464': u'41-3562-3464', + '(41) 3562 3464': u'41-3562-3464', + '41.3562.3464': u'41-3562-3464', + '41.3562-3464': u'41-3562-3464', + ' (41) 3562.3464': u'41-3562-3464', + } + invalid = {} + self.assertFieldOutput(BRPhoneNumberField, valid, invalid) + + def test_BRStateSelect(self): + f = BRStateSelect() + out = u'''<select name="states"> +<option value="AC">Acre</option> +<option value="AL">Alagoas</option> +<option value="AP">Amap\xe1</option> +<option value="AM">Amazonas</option> +<option value="BA">Bahia</option> +<option value="CE">Cear\xe1</option> +<option value="DF">Distrito Federal</option> +<option value="ES">Esp\xedrito Santo</option> +<option value="GO">Goi\xe1s</option> +<option value="MA">Maranh\xe3o</option> +<option value="MT">Mato Grosso</option> +<option value="MS">Mato Grosso do Sul</option> +<option value="MG">Minas Gerais</option> +<option value="PA">Par\xe1</option> +<option value="PB">Para\xedba</option> +<option value="PR" selected="selected">Paran\xe1</option> +<option value="PE">Pernambuco</option> +<option value="PI">Piau\xed</option> +<option value="RJ">Rio de Janeiro</option> +<option value="RN">Rio Grande do Norte</option> +<option value="RS">Rio Grande do Sul</option> +<option value="RO">Rond\xf4nia</option> +<option value="RR">Roraima</option> +<option value="SC">Santa Catarina</option> +<option value="SP">S\xe3o Paulo</option> +<option value="SE">Sergipe</option> +<option value="TO">Tocantins</option> +</select>''' + self.assertEqual(f.render('states', 'PR'), out) + + def test_BRStateChoiceField(self): + error_invalid = [u'Select a valid brazilian state. That state is not one of the available states.'] + valid = { + 'AC': 'AC', + 'AL': 'AL', + 'AP': 'AP', + 'AM': 'AM', + 'BA': 'BA', + 'CE': 'CE', + 'DF': 'DF', + 'ES': 'ES', + 'GO': 'GO', + 'MA': 'MA', + 'MT': 'MT', + 'MS': 'MS', + 'MG': 'MG', + 'PA': 'PA', + 'PB': 'PB', + 'PR': 'PR', + 'PE': 'PE', + 'PI': 'PI', + 'RJ': 'RJ', + 'RN': 'RN', + 'RS': 'RS', + 'RO': 'RO', + 'RR': 'RR', + 'SC': 'SC', + 'SP': 'SP', + 'SE': 'SE', + 'TO': 'TO', + } + invalid = { + 'pr': error_invalid, + } + self.assertFieldOutput(BRStateChoiceField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/ca.py b/parts/django/tests/regressiontests/forms/localflavor/ca.py new file mode 100644 index 0000000..575f41c --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/ca.py @@ -0,0 +1,95 @@ +from django.contrib.localflavor.ca.forms import (CAPostalCodeField, + CAPhoneNumberField, CAProvinceField, CAProvinceSelect, + CASocialInsuranceNumberField) + +from utils import LocalFlavorTestCase + + +class CALocalFlavorTests(LocalFlavorTestCase): + def test_CAProvinceSelect(self): + f = CAProvinceSelect() + out = u'''<select name="province"> +<option value="AB" selected="selected">Alberta</option> +<option value="BC">British Columbia</option> +<option value="MB">Manitoba</option> +<option value="NB">New Brunswick</option> +<option value="NF">Newfoundland and Labrador</option> +<option value="NT">Northwest Territories</option> +<option value="NS">Nova Scotia</option> +<option value="NU">Nunavut</option> +<option value="ON">Ontario</option> +<option value="PE">Prince Edward Island</option> +<option value="QC">Quebec</option> +<option value="SK">Saskatchewan</option> +<option value="YK">Yukon</option> +</select>''' + self.assertEqual(f.render('province', 'AB'), out) + + def test_CAPostalCodeField(self): + error_format = [u'Enter a postal code in the format XXX XXX.'] + valid = { + 'T2S 2H7': 'T2S 2H7', + 'T2S 2W7': 'T2S 2W7', + 'T2S 2Z7': 'T2S 2Z7', + 'T2Z 2H7': 'T2Z 2H7', + + } + invalid = { + 'T2S2H7' : error_format, + 'T2S 2H' : error_format, + '2T6 H8I': error_format, + 'T2S2H' : error_format, + 90210 : error_format, + 'W2S 2H3': error_format, + 'Z2S 2H3': error_format, + 'F2S 2H3': error_format, + 'A2S 2D3': error_format, + 'A2I 2R3': error_format, + 'A2Q 2R3': error_format, + 'U2B 2R3': error_format, + 'O2B 2R3': error_format, + } + self.assertFieldOutput(CAPostalCodeField, valid, invalid) + + def test_CAPhoneNumberField(self): + error_format = [u'Phone numbers must be in XXX-XXX-XXXX format.'] + valid = { + '403-555-1212': '403-555-1212', + '4035551212': '403-555-1212', + '403 555-1212': '403-555-1212', + '(403) 555-1212': '403-555-1212', + '403 555 1212': '403-555-1212', + '403.555.1212': '403-555-1212', + '403.555-1212': '403-555-1212', + ' (403) 555.1212 ': '403-555-1212', + } + invalid = { + '555-1212': error_format, + '403-55-1212': error_format, + } + self.assertFieldOutput(CAPhoneNumberField, valid, invalid) + + def test_CAProvinceField(self): + error_format = [u'Enter a Canadian province or territory.'] + valid = { + 'ab': 'AB', + 'BC': 'BC', + 'nova scotia': 'NS', + ' manitoba ': 'MB', + } + invalid = { + 'T2S 2H7': error_format, + } + self.assertFieldOutput(CAProvinceField, valid, invalid) + + def test_CASocialInsuranceField(self): + error_format = [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXX format.'] + valid = { + '046-454-286': '046-454-286', + } + invalid = { + '046-454-287': error_format, + '046 454 286': error_format, + '046-44-286': error_format, + } + self.assertFieldOutput(CASocialInsuranceNumberField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/ch.py b/parts/django/tests/regressiontests/forms/localflavor/ch.py new file mode 100644 index 0000000..c67bfcf --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/ch.py @@ -0,0 +1,75 @@ +from django.contrib.localflavor.ch.forms import (CHZipCodeField, + CHPhoneNumberField, CHIdentityCardNumberField, CHStateSelect) + +from utils import LocalFlavorTestCase + + +class CHLocalFlavorTests(LocalFlavorTestCase): + def test_CHStateSelect(self): + f = CHStateSelect() + out = u'''<select name="state"> +<option value="AG" selected="selected">Aargau</option> +<option value="AI">Appenzell Innerrhoden</option> +<option value="AR">Appenzell Ausserrhoden</option> +<option value="BS">Basel-Stadt</option> +<option value="BL">Basel-Land</option> +<option value="BE">Berne</option> +<option value="FR">Fribourg</option> +<option value="GE">Geneva</option> +<option value="GL">Glarus</option> +<option value="GR">Graubuenden</option> +<option value="JU">Jura</option> +<option value="LU">Lucerne</option> +<option value="NE">Neuchatel</option> +<option value="NW">Nidwalden</option> +<option value="OW">Obwalden</option> +<option value="SH">Schaffhausen</option> +<option value="SZ">Schwyz</option> +<option value="SO">Solothurn</option> +<option value="SG">St. Gallen</option> +<option value="TG">Thurgau</option> +<option value="TI">Ticino</option> +<option value="UR">Uri</option> +<option value="VS">Valais</option> +<option value="VD">Vaud</option> +<option value="ZG">Zug</option> +<option value="ZH">Zurich</option> +</select>''' + self.assertEqual(f.render('state', 'AG'), out) + + def test_CHZipCodeField(self): + error_format = [u'Enter a zip code in the format XXXX.'] + valid = { + '1234': '1234', + '0000': '0000', + } + invalid = { + '800x': error_format, + '80 00': error_format, + } + self.assertFieldOutput(CHZipCodeField, valid, invalid) + + def test_CHPhoneNumberField(self): + error_format = [u'Phone numbers must be in 0XX XXX XX XX format.'] + valid = { + '012 345 67 89': '012 345 67 89', + '0123456789': '012 345 67 89', + } + invalid = { + '01234567890': error_format, + '1234567890': error_format, + } + self.assertFieldOutput(CHPhoneNumberField, valid, invalid) + + def test_CHIdentityCardNumberField(self): + error_format = [u'Enter a valid Swiss identity or passport card number in X1234567<0 or 1234567890 format.'] + valid = { + 'C1234567<0': 'C1234567<0', + '2123456700': '2123456700', + } + invalid = { + 'C1234567<1': error_format, + '2123456701': error_format, + } + self.assertFieldOutput(CHIdentityCardNumberField, valid, invalid) + diff --git a/parts/django/tests/regressiontests/forms/localflavor/cl.py b/parts/django/tests/regressiontests/forms/localflavor/cl.py new file mode 100644 index 0000000..15b8c7b --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/cl.py @@ -0,0 +1,56 @@ +from django.contrib.localflavor.cl.forms import CLRutField, CLRegionSelect +from django.core.exceptions import ValidationError + +from utils import LocalFlavorTestCase + + +class CLLocalFlavorTests(LocalFlavorTestCase): + def test_CLRegionSelect(self): + f = CLRegionSelect() + out = u'''<select name="foo"> +<option value="RM">Regi\xf3n Metropolitana de Santiago</option> +<option value="I">Regi\xf3n de Tarapac\xe1</option> +<option value="II">Regi\xf3n de Antofagasta</option> +<option value="III">Regi\xf3n de Atacama</option> +<option value="IV">Regi\xf3n de Coquimbo</option> +<option value="V">Regi\xf3n de Valpara\xedso</option> +<option value="VI">Regi\xf3n del Libertador Bernardo O'Higgins</option> +<option value="VII">Regi\xf3n del Maule</option> +<option value="VIII">Regi\xf3n del B\xedo B\xedo</option> +<option value="IX">Regi\xf3n de la Araucan\xeda</option> +<option value="X">Regi\xf3n de los Lagos</option> +<option value="XI">Regi\xf3n de Ays\xe9n del General Carlos Ib\xe1\xf1ez del Campo</option> +<option value="XII">Regi\xf3n de Magallanes y la Ant\xe1rtica Chilena</option> +<option value="XIV">Regi\xf3n de Los R\xedos</option> +<option value="XV">Regi\xf3n de Arica-Parinacota</option> +</select>''' + self.assertEqual(f.render('foo', 'bar'), out) + + def test_CLRutField(self): + error_invalid = [u'The Chilean RUT is not valid.'] + error_format = [u'Enter a valid Chilean RUT. The format is XX.XXX.XXX-X.'] + valid = { + '11-6': '11-6', + '116': '11-6', + '767484100': '76.748.410-0', + '78.412.790-7': '78.412.790-7', + '8.334.6043': '8.334.604-3', + '76793310-K': '76.793.310-K', + } + invalid = { + '11.111.111-0': error_invalid, + '111': error_invalid, + } + self.assertFieldOutput(CLRutField, valid, invalid) + + # deal with special "Strict Mode". + invalid = { + '11-6': error_format, + '767484100': error_format, + '8.334.6043': error_format, + '76793310-K': error_format, + '11.111.111-0': error_invalid + } + self.assertFieldOutput(CLRutField, + {}, invalid, field_kwargs={"strict": True} + ) diff --git a/parts/django/tests/regressiontests/forms/localflavor/cz.py b/parts/django/tests/regressiontests/forms/localflavor/cz.py new file mode 100644 index 0000000..4cff172 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/cz.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# Tests for the contrib/localflavor/ CZ Form Fields + +tests = r""" +# CZPostalCodeField ######################################################### + +>>> from django.contrib.localflavor.cz.forms import CZPostalCodeField +>>> f = CZPostalCodeField() +>>> f.clean('84545x') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXXXX or XXX XX.'] +>>> f.clean('91909') +u'91909' +>>> f.clean('917 01') +u'91701' +>>> f.clean('12345') +u'12345' +>>> f.clean('123456') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXXXX or XXX XX.'] +>>> f.clean('1234') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXXXX or XXX XX.'] +>>> f.clean('123 4') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXXXX or XXX XX.'] + +# CZRegionSelect ############################################################ + +>>> from django.contrib.localflavor.cz.forms import CZRegionSelect +>>> w = CZRegionSelect() +>>> w.render('regions', 'TT') +u'<select name="regions">\n<option value="PR">Prague</option>\n<option value="CE">Central Bohemian Region</option>\n<option value="SO">South Bohemian Region</option>\n<option value="PI">Pilsen Region</option>\n<option value="CA">Carlsbad Region</option>\n<option value="US">Usti Region</option>\n<option value="LB">Liberec Region</option>\n<option value="HK">Hradec Region</option>\n<option value="PA">Pardubice Region</option>\n<option value="VY">Vysocina Region</option>\n<option value="SM">South Moravian Region</option>\n<option value="OL">Olomouc Region</option>\n<option value="ZL">Zlin Region</option>\n<option value="MS">Moravian-Silesian Region</option>\n</select>' + +# CZBirthNumberField ######################################################## + +>>> from django.contrib.localflavor.cz.forms import CZBirthNumberField +>>> f = CZBirthNumberField() +>>> f.clean('880523/1237') +u'880523/1237' +>>> f.clean('8805231237') +u'8805231237' +>>> f.clean('880523/000') +u'880523/000' +>>> f.clean('880523000') +u'880523000' +>>> f.clean('882101/0011') +u'882101/0011' +>>> f.clean('880523/1237', 'm') +u'880523/1237' +>>> f.clean('885523/1231', 'f') +u'885523/1231' +>>> f.clean('123456/12') +Traceback (most recent call last): +... +ValidationError: [u'Enter a birth number in the format XXXXXX/XXXX or XXXXXXXXXX.'] +>>> f.clean('123456/12345') +Traceback (most recent call last): +... +ValidationError: [u'Enter a birth number in the format XXXXXX/XXXX or XXXXXXXXXX.'] +>>> f.clean('12345612') +Traceback (most recent call last): +... +ValidationError: [u'Enter a birth number in the format XXXXXX/XXXX or XXXXXXXXXX.'] +>>> f.clean('12345612345') +Traceback (most recent call last): +... +ValidationError: [u'Enter a birth number in the format XXXXXX/XXXX or XXXXXXXXXX.'] +>>> f.clean('881523/0000', 'm') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid birth number.'] +>>> f.clean('885223/0000', 'm') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid birth number.'] +>>> f.clean('881223/0000', 'f') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid birth number.'] +>>> f.clean('886523/0000', 'f') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid birth number.'] +>>> f.clean('880523/1239') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid birth number.'] +>>> f.clean('8805231239') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid birth number.'] +>>> f.clean('990101/0011') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid birth number.'] + +# CZICNumberField ######################################################## + +>>> from django.contrib.localflavor.cz.forms import CZICNumberField +>>> f = CZICNumberField() +>>> f.clean('12345679') +u'12345679' +>>> f.clean('12345601') +u'12345601' +>>> f.clean('12345661') +u'12345661' +>>> f.clean('12345610') +u'12345610' +>>> f.clean('1234567') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IC number.'] +>>> f.clean('12345660') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IC number.'] +>>> f.clean('12345600') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IC number.'] +""" diff --git a/parts/django/tests/regressiontests/forms/localflavor/de.py b/parts/django/tests/regressiontests/forms/localflavor/de.py new file mode 100644 index 0000000..7c68bcc --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/de.py @@ -0,0 +1,49 @@ +from django.contrib.localflavor.de.forms import (DEZipCodeField, DEStateSelect, + DEIdentityCardNumberField) + +from utils import LocalFlavorTestCase + + +class DELocalFlavorTests(LocalFlavorTestCase): + def test_DEStateSelect(self): + f = DEStateSelect() + out = u'''<select name="states"> +<option value="BW">Baden-Wuerttemberg</option> +<option value="BY">Bavaria</option> +<option value="BE">Berlin</option> +<option value="BB">Brandenburg</option> +<option value="HB">Bremen</option> +<option value="HH">Hamburg</option> +<option value="HE">Hessen</option> +<option value="MV">Mecklenburg-Western Pomerania</option> +<option value="NI">Lower Saxony</option> +<option value="NW">North Rhine-Westphalia</option> +<option value="RP">Rhineland-Palatinate</option> +<option value="SL">Saarland</option> +<option value="SN">Saxony</option> +<option value="ST">Saxony-Anhalt</option> +<option value="SH">Schleswig-Holstein</option> +<option value="TH" selected="selected">Thuringia</option> +</select>''' + self.assertEqual(f.render('states', 'TH'), out) + + def test_DEZipCodeField(self): + error_format = [u'Enter a zip code in the format XXXXX.'] + valid = { + '99423': '99423', + } + invalid = { + ' 99423': error_format, + } + self.assertFieldOutput(DEZipCodeField, valid, invalid) + + def test_DEIdentityCardNumberField(self): + error_format = [u'Enter a valid German identity card number in XXXXXXXXXXX-XXXXXXX-XXXXXXX-X format.'] + valid = { + '7549313035D-6004103-0903042-0': '7549313035D-6004103-0903042-0', + '9786324830D 6104243 0910271 2': '9786324830D-6104243-0910271-2', + } + invalid = { + '0434657485D-6407276-0508137-9': error_format, + } + self.assertFieldOutput(DEIdentityCardNumberField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/es.py b/parts/django/tests/regressiontests/forms/localflavor/es.py new file mode 100644 index 0000000..b584075 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/es.py @@ -0,0 +1,172 @@ +from django.contrib.localflavor.es.forms import (ESPostalCodeField, ESPhoneNumberField, + ESIdentityCardNumberField, ESCCCField, ESRegionSelect, ESProvinceSelect) + +from utils import LocalFlavorTestCase + + +class ESLocalFlavorTests(LocalFlavorTestCase): + def test_ESRegionSelect(self): + f = ESRegionSelect() + out = u'''<select name="regions"> +<option value="AN">Andalusia</option> +<option value="AR">Aragon</option> +<option value="O">Principality of Asturias</option> +<option value="IB">Balearic Islands</option> +<option value="PV">Basque Country</option> +<option value="CN">Canary Islands</option> +<option value="S">Cantabria</option> +<option value="CM">Castile-La Mancha</option> +<option value="CL">Castile and Leon</option> +<option value="CT" selected="selected">Catalonia</option> +<option value="EX">Extremadura</option> +<option value="GA">Galicia</option> +<option value="LO">La Rioja</option> +<option value="M">Madrid</option> +<option value="MU">Region of Murcia</option> +<option value="NA">Foral Community of Navarre</option> +<option value="VC">Valencian Community</option> +</select>''' + self.assertEqual(f.render('regions', 'CT'), out) + + def test_ESProvinceSelect(self): + f = ESProvinceSelect() + out = u'''<select name="provinces"> +<option value="01">Arava</option> +<option value="02">Albacete</option> +<option value="03">Alacant</option> +<option value="04">Almeria</option> +<option value="05">Avila</option> +<option value="06">Badajoz</option> +<option value="07">Illes Balears</option> +<option value="08" selected="selected">Barcelona</option> +<option value="09">Burgos</option> +<option value="10">Caceres</option> +<option value="11">Cadiz</option> +<option value="12">Castello</option> +<option value="13">Ciudad Real</option> +<option value="14">Cordoba</option> +<option value="15">A Coruna</option> +<option value="16">Cuenca</option> +<option value="17">Girona</option> +<option value="18">Granada</option> +<option value="19">Guadalajara</option> +<option value="20">Guipuzkoa</option> +<option value="21">Huelva</option> +<option value="22">Huesca</option> +<option value="23">Jaen</option> +<option value="24">Leon</option> +<option value="25">Lleida</option> +<option value="26">La Rioja</option> +<option value="27">Lugo</option> +<option value="28">Madrid</option> +<option value="29">Malaga</option> +<option value="30">Murcia</option> +<option value="31">Navarre</option> +<option value="32">Ourense</option> +<option value="33">Asturias</option> +<option value="34">Palencia</option> +<option value="35">Las Palmas</option> +<option value="36">Pontevedra</option> +<option value="37">Salamanca</option> +<option value="38">Santa Cruz de Tenerife</option> +<option value="39">Cantabria</option> +<option value="40">Segovia</option> +<option value="41">Seville</option> +<option value="42">Soria</option> +<option value="43">Tarragona</option> +<option value="44">Teruel</option> +<option value="45">Toledo</option> +<option value="46">Valencia</option> +<option value="47">Valladolid</option> +<option value="48">Bizkaia</option> +<option value="49">Zamora</option> +<option value="50">Zaragoza</option> +<option value="51">Ceuta</option> +<option value="52">Melilla</option> +</select>''' + self.assertEqual(f.render('provinces', '08'), out) + + def test_ESPostalCodeField(self): + error_invalid = [u'Enter a valid postal code in the range and format 01XXX - 52XXX.'] + valid = { + '08028': '08028', + '28080': '28080', + } + invalid = { + '53001': error_invalid, + '0801': error_invalid, + '080001': error_invalid, + '00999': error_invalid, + '08 01': error_invalid, + '08A01': error_invalid, + } + self.assertFieldOutput(ESPostalCodeField, valid, invalid) + + def test_ESPhoneNumberField(self): + error_invalid = [u'Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX.'] + valid = { + '650010101': '650010101', + '931234567': '931234567', + '800123123': '800123123', + } + invalid = { + '555555555': error_invalid, + '789789789': error_invalid, + '99123123': error_invalid, + '9999123123': error_invalid, + } + self.assertFieldOutput(ESPhoneNumberField, valid, invalid) + + def test_ESIdentityCardNumberField(self): + error_invalid = [u'Please enter a valid NIF, NIE, or CIF.'] + error_checksum_nif = [u'Invalid checksum for NIF.'] + error_checksum_nie = [u'Invalid checksum for NIE.'] + error_checksum_cif = [u'Invalid checksum for CIF.'] + valid = { + '78699688J': '78699688J', + '78699688-J': '78699688J', + '78699688 J': '78699688J', + '78699688 j': '78699688J', + 'X0901797J': 'X0901797J', + 'X-6124387-Q': 'X6124387Q', + 'X 0012953 G': 'X0012953G', + 'x-3287690-r': 'X3287690R', + 't-03287690r': 'T03287690R', + 'P2907500I': 'P2907500I', + 'B38790911': 'B38790911', + 'B31234560': 'B31234560', + 'B-3879091A': 'B3879091A', + 'B 38790911': 'B38790911', + 'P-3900800-H': 'P3900800H', + 'P 39008008': 'P39008008', + 'C-28795565': 'C28795565', + 'C 2879556E': 'C2879556E', + } + invalid = { + '78699688T': error_checksum_nif, + 'X-03287690': error_invalid, + 'X-03287690-T': error_checksum_nie, + 'B 38790917': error_checksum_cif, + 'C28795567': error_checksum_cif, + 'I38790911': error_invalid, + '78699688-2': error_invalid, + } + self.assertFieldOutput(ESIdentityCardNumberField, valid, invalid) + + def test_ESCCCField(self): + error_invalid = [u'Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX.'] + error_checksum = [u'Invalid checksum for bank account number.'] + valid = { + '20770338793100254321': '20770338793100254321', + '2077 0338 79 3100254321': '2077 0338 79 3100254321', + '2077-0338-79-3100254321': '2077-0338-79-3100254321', + } + invalid = { + '2077.0338.79.3100254321': error_invalid, + '2077-0338-78-3100254321': error_checksum, + '2077-0338-89-3100254321': error_checksum, + '2077-03-3879-3100254321': error_invalid, + } + self.assertFieldOutput(ESCCCField, valid, invalid) + + diff --git a/parts/django/tests/regressiontests/forms/localflavor/fi.py b/parts/django/tests/regressiontests/forms/localflavor/fi.py new file mode 100644 index 0000000..161eb17 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/fi.py @@ -0,0 +1,382 @@ +from django.contrib.localflavor.fi.forms import (FIZipCodeField, + FISocialSecurityNumber, FIMunicipalitySelect) + +from utils import LocalFlavorTestCase + + +class FILocalFlavorTests(LocalFlavorTestCase): + def test_FIMunicipalitySelect(self): + f = FIMunicipalitySelect() + out = u'''<select name="municipalities"> +<option value="akaa">Akaa</option> +<option value="alajarvi">Alaj\xe4rvi</option> +<option value="alavieska">Alavieska</option> +<option value="alavus">Alavus</option> +<option value="artjarvi">Artj\xe4rvi</option> +<option value="asikkala">Asikkala</option> +<option value="askola">Askola</option> +<option value="aura">Aura</option> +<option value="brando">Br\xe4nd\xf6</option> +<option value="eckero">Ecker\xf6</option> +<option value="enonkoski">Enonkoski</option> +<option value="enontekio">Enonteki\xf6</option> +<option value="espoo">Espoo</option> +<option value="eura">Eura</option> +<option value="eurajoki">Eurajoki</option> +<option value="evijarvi">Evij\xe4rvi</option> +<option value="finstrom">Finstr\xf6m</option> +<option value="forssa">Forssa</option> +<option value="foglo">F\xf6gl\xf6</option> +<option value="geta">Geta</option> +<option value="haapajarvi">Haapaj\xe4rvi</option> +<option value="haapavesi">Haapavesi</option> +<option value="hailuoto">Hailuoto</option> +<option value="halsua">Halsua</option> +<option value="hamina">Hamina</option> +<option value="hammarland">Hammarland</option> +<option value="hankasalmi">Hankasalmi</option> +<option value="hanko">Hanko</option> +<option value="harjavalta">Harjavalta</option> +<option value="hartola">Hartola</option> +<option value="hattula">Hattula</option> +<option value="haukipudas">Haukipudas</option> +<option value="hausjarvi">Hausj\xe4rvi</option> +<option value="heinola">Heinola</option> +<option value="heinavesi">Hein\xe4vesi</option> +<option value="helsinki">Helsinki</option> +<option value="hirvensalmi">Hirvensalmi</option> +<option value="hollola">Hollola</option> +<option value="honkajoki">Honkajoki</option> +<option value="huittinen">Huittinen</option> +<option value="humppila">Humppila</option> +<option value="hyrynsalmi">Hyrynsalmi</option> +<option value="hyvinkaa">Hyvink\xe4\xe4</option> +<option value="hameenkoski">H\xe4meenkoski</option> +<option value="hameenkyro">H\xe4meenkyr\xf6</option> +<option value="hameenlinna">H\xe4meenlinna</option> +<option value="ii">Ii</option> +<option value="iisalmi">Iisalmi</option> +<option value="iitti">Iitti</option> +<option value="ikaalinen">Ikaalinen</option> +<option value="ilmajoki">Ilmajoki</option> +<option value="ilomantsi">Ilomantsi</option> +<option value="imatra">Imatra</option> +<option value="inari">Inari</option> +<option value="inkoo">Inkoo</option> +<option value="isojoki">Isojoki</option> +<option value="isokyro">Isokyr\xf6</option> +<option value="jalasjarvi">Jalasj\xe4rvi</option> +<option value="janakkala">Janakkala</option> +<option value="joensuu">Joensuu</option> +<option value="jokioinen">Jokioinen</option> +<option value="jomala">Jomala</option> +<option value="joroinen">Joroinen</option> +<option value="joutsa">Joutsa</option> +<option value="juankoski">Juankoski</option> +<option value="juuka">Juuka</option> +<option value="juupajoki">Juupajoki</option> +<option value="juva">Juva</option> +<option value="jyvaskyla">Jyv\xe4skyl\xe4</option> +<option value="jamijarvi">J\xe4mij\xe4rvi</option> +<option value="jamsa">J\xe4ms\xe4</option> +<option value="jarvenpaa">J\xe4rvenp\xe4\xe4</option> +<option value="kaarina">Kaarina</option> +<option value="kaavi">Kaavi</option> +<option value="kajaani">Kajaani</option> +<option value="kalajoki">Kalajoki</option> +<option value="kangasala">Kangasala</option> +<option value="kangasniemi">Kangasniemi</option> +<option value="kankaanpaa">Kankaanp\xe4\xe4</option> +<option value="kannonkoski">Kannonkoski</option> +<option value="kannus">Kannus</option> +<option value="karijoki">Karijoki</option> +<option value="karjalohja">Karjalohja</option> +<option value="karkkila">Karkkila</option> +<option value="karstula">Karstula</option> +<option value="karttula">Karttula</option> +<option value="karvia">Karvia</option> +<option value="kaskinen">Kaskinen</option> +<option value="kauhajoki">Kauhajoki</option> +<option value="kauhava">Kauhava</option> +<option value="kauniainen">Kauniainen</option> +<option value="kaustinen">Kaustinen</option> +<option value="keitele">Keitele</option> +<option value="kemi">Kemi</option> +<option value="kemijarvi">Kemij\xe4rvi</option> +<option value="keminmaa">Keminmaa</option> +<option value="kemionsaari">Kemi\xf6nsaari</option> +<option value="kempele">Kempele</option> +<option value="kerava">Kerava</option> +<option value="kerimaki">Kerim\xe4ki</option> +<option value="kesalahti">Kes\xe4lahti</option> +<option value="keuruu">Keuruu</option> +<option value="kihnio">Kihni\xf6</option> +<option value="kiikoinen">Kiikoinen</option> +<option value="kiiminki">Kiiminki</option> +<option value="kinnula">Kinnula</option> +<option value="kirkkonummi">Kirkkonummi</option> +<option value="kitee">Kitee</option> +<option value="kittila">Kittil\xe4</option> +<option value="kiuruvesi">Kiuruvesi</option> +<option value="kivijarvi">Kivij\xe4rvi</option> +<option value="kokemaki">Kokem\xe4ki</option> +<option value="kokkola">Kokkola</option> +<option value="kolari">Kolari</option> +<option value="konnevesi">Konnevesi</option> +<option value="kontiolahti">Kontiolahti</option> +<option value="korsnas">Korsn\xe4s</option> +<option value="koskitl">Koski Tl</option> +<option value="kotka">Kotka</option> +<option value="kouvola">Kouvola</option> +<option value="kristiinankaupunki">Kristiinankaupunki</option> +<option value="kruunupyy">Kruunupyy</option> +<option value="kuhmalahti">Kuhmalahti</option> +<option value="kuhmo">Kuhmo</option> +<option value="kuhmoinen">Kuhmoinen</option> +<option value="kumlinge">Kumlinge</option> +<option value="kuopio">Kuopio</option> +<option value="kuortane">Kuortane</option> +<option value="kurikka">Kurikka</option> +<option value="kustavi">Kustavi</option> +<option value="kuusamo">Kuusamo</option> +<option value="kylmakoski">Kylm\xe4koski</option> +<option value="kyyjarvi">Kyyj\xe4rvi</option> +<option value="karkola">K\xe4rk\xf6l\xe4</option> +<option value="karsamaki">K\xe4rs\xe4m\xe4ki</option> +<option value="kokar">K\xf6kar</option> +<option value="koylio">K\xf6yli\xf6</option> +<option value="lahti">Lahti</option> +<option value="laihia">Laihia</option> +<option value="laitila">Laitila</option> +<option value="lapinjarvi">Lapinj\xe4rvi</option> +<option value="lapinlahti">Lapinlahti</option> +<option value="lappajarvi">Lappaj\xe4rvi</option> +<option value="lappeenranta">Lappeenranta</option> +<option value="lapua">Lapua</option> +<option value="laukaa">Laukaa</option> +<option value="lavia">Lavia</option> +<option value="lemi">Lemi</option> +<option value="lemland">Lemland</option> +<option value="lempaala">Lemp\xe4\xe4l\xe4</option> +<option value="leppavirta">Lepp\xe4virta</option> +<option value="lestijarvi">Lestij\xe4rvi</option> +<option value="lieksa">Lieksa</option> +<option value="lieto">Lieto</option> +<option value="liminka">Liminka</option> +<option value="liperi">Liperi</option> +<option value="lohja">Lohja</option> +<option value="loimaa">Loimaa</option> +<option value="loppi">Loppi</option> +<option value="loviisa">Loviisa</option> +<option value="luhanka">Luhanka</option> +<option value="lumijoki">Lumijoki</option> +<option value="lumparland">Lumparland</option> +<option value="luoto">Luoto</option> +<option value="luumaki">Luum\xe4ki</option> +<option value="luvia">Luvia</option> +<option value="lansi-turunmaa">L\xe4nsi-Turunmaa</option> +<option value="maalahti">Maalahti</option> +<option value="maaninka">Maaninka</option> +<option value="maarianhamina">Maarianhamina</option> +<option value="marttila">Marttila</option> +<option value="masku">Masku</option> +<option value="merijarvi">Merij\xe4rvi</option> +<option value="merikarvia">Merikarvia</option> +<option value="miehikkala">Miehikk\xe4l\xe4</option> +<option value="mikkeli">Mikkeli</option> +<option value="muhos">Muhos</option> +<option value="multia">Multia</option> +<option value="muonio">Muonio</option> +<option value="mustasaari">Mustasaari</option> +<option value="muurame">Muurame</option> +<option value="mynamaki">Myn\xe4m\xe4ki</option> +<option value="myrskyla">Myrskyl\xe4</option> +<option value="mantsala">M\xe4nts\xe4l\xe4</option> +<option value="mantta-vilppula">M\xe4ntt\xe4-Vilppula</option> +<option value="mantyharju">M\xe4ntyharju</option> +<option value="naantali">Naantali</option> +<option value="nakkila">Nakkila</option> +<option value="nastola">Nastola</option> +<option value="nilsia">Nilsi\xe4</option> +<option value="nivala">Nivala</option> +<option value="nokia">Nokia</option> +<option value="nousiainen">Nousiainen</option> +<option value="nummi-pusula">Nummi-Pusula</option> +<option value="nurmes">Nurmes</option> +<option value="nurmijarvi">Nurmij\xe4rvi</option> +<option value="narpio">N\xe4rpi\xf6</option> +<option value="oravainen">Oravainen</option> +<option value="orimattila">Orimattila</option> +<option value="oripaa">Orip\xe4\xe4</option> +<option value="orivesi">Orivesi</option> +<option value="oulainen">Oulainen</option> +<option value="oulu">Oulu</option> +<option value="oulunsalo">Oulunsalo</option> +<option value="outokumpu">Outokumpu</option> +<option value="padasjoki">Padasjoki</option> +<option value="paimio">Paimio</option> +<option value="paltamo">Paltamo</option> +<option value="parikkala">Parikkala</option> +<option value="parkano">Parkano</option> +<option value="pedersore">Peders\xf6re</option> +<option value="pelkosenniemi">Pelkosenniemi</option> +<option value="pello">Pello</option> +<option value="perho">Perho</option> +<option value="pertunmaa">Pertunmaa</option> +<option value="petajavesi">Pet\xe4j\xe4vesi</option> +<option value="pieksamaki">Pieks\xe4m\xe4ki</option> +<option value="pielavesi">Pielavesi</option> +<option value="pietarsaari">Pietarsaari</option> +<option value="pihtipudas">Pihtipudas</option> +<option value="pirkkala">Pirkkala</option> +<option value="polvijarvi">Polvij\xe4rvi</option> +<option value="pomarkku">Pomarkku</option> +<option value="pori">Pori</option> +<option value="pornainen">Pornainen</option> +<option value="porvoo">Porvoo</option> +<option value="posio">Posio</option> +<option value="pudasjarvi">Pudasj\xe4rvi</option> +<option value="pukkila">Pukkila</option> +<option value="punkaharju">Punkaharju</option> +<option value="punkalaidun">Punkalaidun</option> +<option value="puolanka">Puolanka</option> +<option value="puumala">Puumala</option> +<option value="pyhtaa">Pyht\xe4\xe4</option> +<option value="pyhajoki">Pyh\xe4joki</option> +<option value="pyhajarvi">Pyh\xe4j\xe4rvi</option> +<option value="pyhanta">Pyh\xe4nt\xe4</option> +<option value="pyharanta">Pyh\xe4ranta</option> +<option value="palkane">P\xe4lk\xe4ne</option> +<option value="poytya">P\xf6yty\xe4</option> +<option value="raahe">Raahe</option> +<option value="raasepori">Raasepori</option> +<option value="raisio">Raisio</option> +<option value="rantasalmi">Rantasalmi</option> +<option value="ranua">Ranua</option> +<option value="rauma">Rauma</option> +<option value="rautalampi">Rautalampi</option> +<option value="rautavaara">Rautavaara</option> +<option value="rautjarvi">Rautj\xe4rvi</option> +<option value="reisjarvi">Reisj\xe4rvi</option> +<option value="riihimaki">Riihim\xe4ki</option> +<option value="ristiina">Ristiina</option> +<option value="ristijarvi">Ristij\xe4rvi</option> +<option value="rovaniemi">Rovaniemi</option> +<option value="ruokolahti">Ruokolahti</option> +<option value="ruovesi">Ruovesi</option> +<option value="rusko">Rusko</option> +<option value="raakkyla">R\xe4\xe4kkyl\xe4</option> +<option value="saarijarvi">Saarij\xe4rvi</option> +<option value="salla">Salla</option> +<option value="salo">Salo</option> +<option value="saltvik">Saltvik</option> +<option value="sastamala">Sastamala</option> +<option value="sauvo">Sauvo</option> +<option value="savitaipale">Savitaipale</option> +<option value="savonlinna">Savonlinna</option> +<option value="savukoski">Savukoski</option> +<option value="seinajoki">Sein\xe4joki</option> +<option value="sievi">Sievi</option> +<option value="siikainen">Siikainen</option> +<option value="siikajoki">Siikajoki</option> +<option value="siikalatva">Siikalatva</option> +<option value="siilinjarvi">Siilinj\xe4rvi</option> +<option value="simo">Simo</option> +<option value="sipoo">Sipoo</option> +<option value="siuntio">Siuntio</option> +<option value="sodankyla">Sodankyl\xe4</option> +<option value="soini">Soini</option> +<option value="somero">Somero</option> +<option value="sonkajarvi">Sonkaj\xe4rvi</option> +<option value="sotkamo">Sotkamo</option> +<option value="sottunga">Sottunga</option> +<option value="sulkava">Sulkava</option> +<option value="sund">Sund</option> +<option value="suomenniemi">Suomenniemi</option> +<option value="suomussalmi">Suomussalmi</option> +<option value="suonenjoki">Suonenjoki</option> +<option value="sysma">Sysm\xe4</option> +<option value="sakyla">S\xe4kyl\xe4</option> +<option value="taipalsaari">Taipalsaari</option> +<option value="taivalkoski">Taivalkoski</option> +<option value="taivassalo">Taivassalo</option> +<option value="tammela">Tammela</option> +<option value="tampere">Tampere</option> +<option value="tarvasjoki">Tarvasjoki</option> +<option value="tervo">Tervo</option> +<option value="tervola">Tervola</option> +<option value="teuva">Teuva</option> +<option value="tohmajarvi">Tohmaj\xe4rvi</option> +<option value="toholampi">Toholampi</option> +<option value="toivakka">Toivakka</option> +<option value="tornio">Tornio</option> +<option value="turku" selected="selected">Turku</option> +<option value="tuusniemi">Tuusniemi</option> +<option value="tuusula">Tuusula</option> +<option value="tyrnava">Tyrn\xe4v\xe4</option> +<option value="toysa">T\xf6ys\xe4</option> +<option value="ulvila">Ulvila</option> +<option value="urjala">Urjala</option> +<option value="utajarvi">Utaj\xe4rvi</option> +<option value="utsjoki">Utsjoki</option> +<option value="uurainen">Uurainen</option> +<option value="uusikaarlepyy">Uusikaarlepyy</option> +<option value="uusikaupunki">Uusikaupunki</option> +<option value="vaala">Vaala</option> +<option value="vaasa">Vaasa</option> +<option value="valkeakoski">Valkeakoski</option> +<option value="valtimo">Valtimo</option> +<option value="vantaa">Vantaa</option> +<option value="varkaus">Varkaus</option> +<option value="varpaisjarvi">Varpaisj\xe4rvi</option> +<option value="vehmaa">Vehmaa</option> +<option value="vesanto">Vesanto</option> +<option value="vesilahti">Vesilahti</option> +<option value="veteli">Veteli</option> +<option value="vierema">Vierem\xe4</option> +<option value="vihanti">Vihanti</option> +<option value="vihti">Vihti</option> +<option value="viitasaari">Viitasaari</option> +<option value="vimpeli">Vimpeli</option> +<option value="virolahti">Virolahti</option> +<option value="virrat">Virrat</option> +<option value="vardo">V\xe5rd\xf6</option> +<option value="vahakyro">V\xe4h\xe4kyr\xf6</option> +<option value="voyri-maksamaa">V\xf6yri-Maksamaa</option> +<option value="yli-ii">Yli-Ii</option> +<option value="ylitornio">Ylitornio</option> +<option value="ylivieska">Ylivieska</option> +<option value="ylojarvi">Yl\xf6j\xe4rvi</option> +<option value="ypaja">Yp\xe4j\xe4</option> +<option value="ahtari">\xc4ht\xe4ri</option> +<option value="aanekoski">\xc4\xe4nekoski</option> +</select>''' + self.assertEquals(f.render('municipalities', 'turku'), out) + + def test_FIZipCodeField(self): + error_format = [u'Enter a zip code in the format XXXXX.'] + valid = { + '20540': '20540', + '20101': '20101', + } + invalid = { + '20s40': error_format, + '205401': error_format + } + self.assertFieldOutput(FIZipCodeField, valid, invalid) + + def test_FISocialSecurityNumber(self): + error_invalid = [u'Enter a valid Finnish social security number.'] + valid = { + '010101-0101': '010101-0101', + '010101+0101': '010101+0101', + '010101A0101': '010101A0101', + } + invalid = { + '101010-0102': error_invalid, + '10a010-0101': error_invalid, + '101010-0\xe401': error_invalid, + '101010b0101': error_invalid, + } + self.assertFieldOutput(FISocialSecurityNumber, valid, invalid) + diff --git a/parts/django/tests/regressiontests/forms/localflavor/fr.py b/parts/django/tests/regressiontests/forms/localflavor/fr.py new file mode 100644 index 0000000..96045c3 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/fr.py @@ -0,0 +1,145 @@ +from django.contrib.localflavor.fr.forms import (FRZipCodeField, + FRPhoneNumberField, FRDepartmentSelect) + +from utils import LocalFlavorTestCase + + +class FRLocalFlavorTests(LocalFlavorTestCase): + def test_FRZipCodeField(self): + error_format = [u'Enter a zip code in the format XXXXX.'] + valid = { + '75001': '75001', + '93200': '93200', + } + invalid = { + '2A200': error_format, + '980001': error_format, + } + self.assertFieldOutput(FRZipCodeField, valid, invalid) + + def test_FRPhoneNumberField(self): + error_format = [u'Phone numbers must be in 0X XX XX XX XX format.'] + valid = { + '01 55 44 58 64': '01 55 44 58 64', + '0155445864': '01 55 44 58 64', + '01 5544 5864': '01 55 44 58 64', + '01 55.44.58.64': '01 55 44 58 64', + '01.55.44.58.64': '01 55 44 58 64', + } + invalid = { + '01,55,44,58,64': error_format, + '555 015 544': error_format, + } + self.assertFieldOutput(FRPhoneNumberField, valid, invalid) + + def test_FRDepartmentSelect(self): + f = FRDepartmentSelect() + out = u'''<select name="dep"> +<option value="01">01 - Ain</option> +<option value="02">02 - Aisne</option> +<option value="03">03 - Allier</option> +<option value="04">04 - Alpes-de-Haute-Provence</option> +<option value="05">05 - Hautes-Alpes</option> +<option value="06">06 - Alpes-Maritimes</option> +<option value="07">07 - Ardeche</option> +<option value="08">08 - Ardennes</option> +<option value="09">09 - Ariege</option> +<option value="10">10 - Aube</option> +<option value="11">11 - Aude</option> +<option value="12">12 - Aveyron</option> +<option value="13">13 - Bouches-du-Rhone</option> +<option value="14">14 - Calvados</option> +<option value="15">15 - Cantal</option> +<option value="16">16 - Charente</option> +<option value="17">17 - Charente-Maritime</option> +<option value="18">18 - Cher</option> +<option value="19">19 - Correze</option> +<option value="21">21 - Cote-d'Or</option> +<option value="22">22 - Cotes-d'Armor</option> +<option value="23">23 - Creuse</option> +<option value="24">24 - Dordogne</option> +<option value="25">25 - Doubs</option> +<option value="26">26 - Drome</option> +<option value="27">27 - Eure</option> +<option value="28">28 - Eure-et-Loire</option> +<option value="29">29 - Finistere</option> +<option value="2A">2A - Corse-du-Sud</option> +<option value="2B">2B - Haute-Corse</option> +<option value="30">30 - Gard</option> +<option value="31">31 - Haute-Garonne</option> +<option value="32">32 - Gers</option> +<option value="33">33 - Gironde</option> +<option value="34">34 - Herault</option> +<option value="35">35 - Ille-et-Vilaine</option> +<option value="36">36 - Indre</option> +<option value="37">37 - Indre-et-Loire</option> +<option value="38">38 - Isere</option> +<option value="39">39 - Jura</option> +<option value="40">40 - Landes</option> +<option value="41">41 - Loir-et-Cher</option> +<option value="42">42 - Loire</option> +<option value="43">43 - Haute-Loire</option> +<option value="44">44 - Loire-Atlantique</option> +<option value="45">45 - Loiret</option> +<option value="46">46 - Lot</option> +<option value="47">47 - Lot-et-Garonne</option> +<option value="48">48 - Lozere</option> +<option value="49">49 - Maine-et-Loire</option> +<option value="50">50 - Manche</option> +<option value="51">51 - Marne</option> +<option value="52">52 - Haute-Marne</option> +<option value="53">53 - Mayenne</option> +<option value="54">54 - Meurthe-et-Moselle</option> +<option value="55">55 - Meuse</option> +<option value="56">56 - Morbihan</option> +<option value="57">57 - Moselle</option> +<option value="58">58 - Nievre</option> +<option value="59">59 - Nord</option> +<option value="60">60 - Oise</option> +<option value="61">61 - Orne</option> +<option value="62">62 - Pas-de-Calais</option> +<option value="63">63 - Puy-de-Dome</option> +<option value="64">64 - Pyrenees-Atlantiques</option> +<option value="65">65 - Hautes-Pyrenees</option> +<option value="66">66 - Pyrenees-Orientales</option> +<option value="67">67 - Bas-Rhin</option> +<option value="68">68 - Haut-Rhin</option> +<option value="69">69 - Rhone</option> +<option value="70">70 - Haute-Saone</option> +<option value="71">71 - Saone-et-Loire</option> +<option value="72">72 - Sarthe</option> +<option value="73">73 - Savoie</option> +<option value="74">74 - Haute-Savoie</option> +<option value="75">75 - Paris</option> +<option value="76">76 - Seine-Maritime</option> +<option value="77">77 - Seine-et-Marne</option> +<option value="78">78 - Yvelines</option> +<option value="79">79 - Deux-Sevres</option> +<option value="80">80 - Somme</option> +<option value="81">81 - Tarn</option> +<option value="82">82 - Tarn-et-Garonne</option> +<option value="83">83 - Var</option> +<option value="84">84 - Vaucluse</option> +<option value="85">85 - Vendee</option> +<option value="86">86 - Vienne</option> +<option value="87">87 - Haute-Vienne</option> +<option value="88">88 - Vosges</option> +<option value="89">89 - Yonne</option> +<option value="90">90 - Territoire de Belfort</option> +<option value="91">91 - Essonne</option> +<option value="92">92 - Hauts-de-Seine</option> +<option value="93">93 - Seine-Saint-Denis</option> +<option value="94">94 - Val-de-Marne</option> +<option value="95">95 - Val-d'Oise</option> +<option value="971">971 - Guadeloupe</option> +<option value="972">972 - Martinique</option> +<option value="973">973 - Guyane</option> +<option value="974">974 - La Reunion</option> +<option value="975">975 - Saint-Pierre-et-Miquelon</option> +<option value="976">976 - Mayotte</option> +<option value="984">984 - Terres Australes et Antarctiques</option> +<option value="986">986 - Wallis et Futuna</option> +<option value="987">987 - Polynesie Francaise</option> +<option value="988">988 - Nouvelle-Caledonie</option> +</select>''' + self.assertEqual(f.render('dep', 'Paris'), out) diff --git a/parts/django/tests/regressiontests/forms/localflavor/generic.py b/parts/django/tests/regressiontests/forms/localflavor/generic.py new file mode 100644 index 0000000..f47fc91 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/generic.py @@ -0,0 +1,88 @@ +import datetime + +from django.contrib.localflavor.generic.forms import DateField, DateTimeField + +from utils import LocalFlavorTestCase + + +class GenericLocalFlavorTests(LocalFlavorTestCase): + def test_GenericDateField(self): + error_invalid = [u'Enter a valid date.'] + valid = { + datetime.date(2006, 10, 25): datetime.date(2006, 10, 25), + datetime.datetime(2006, 10, 25, 14, 30): datetime.date(2006, 10, 25), + datetime.datetime(2006, 10, 25, 14, 30, 59): datetime.date(2006, 10, 25), + datetime.datetime(2006, 10, 25, 14, 30, 59, 200): datetime.date(2006, 10, 25), + '2006-10-25': datetime.date(2006, 10, 25), + '25/10/2006': datetime.date(2006, 10, 25), + '25/10/06': datetime.date(2006, 10, 25), + 'Oct 25 2006': datetime.date(2006, 10, 25), + 'October 25 2006': datetime.date(2006, 10, 25), + 'October 25, 2006': datetime.date(2006, 10, 25), + '25 October 2006': datetime.date(2006, 10, 25), + '25 October, 2006': datetime.date(2006, 10, 25), + } + invalid = { + '2006-4-31': error_invalid, + '200a-10-25': error_invalid, + '10/25/06': error_invalid, + } + self.assertFieldOutput(DateField, valid, invalid, empty_value=None) + + # DateField with optional input_formats parameter + valid = { + datetime.date(2006, 10, 25): datetime.date(2006, 10, 25), + datetime.datetime(2006, 10, 25, 14, 30): datetime.date(2006, 10, 25), + '2006 10 25': datetime.date(2006, 10, 25), + } + invalid = { + '2006-10-25': error_invalid, + '25/10/2006': error_invalid, + '25/10/06': error_invalid, + } + kwargs = {'input_formats':['%Y %m %d'],} + self.assertFieldOutput(DateField, + valid, invalid, field_kwargs=kwargs, empty_value=None + ) + + def test_GenericDateTimeField(self): + error_invalid = [u'Enter a valid date/time.'] + valid = { + datetime.date(2006, 10, 25): datetime.datetime(2006, 10, 25, 0, 0), + datetime.datetime(2006, 10, 25, 14, 30): datetime.datetime(2006, 10, 25, 14, 30), + datetime.datetime(2006, 10, 25, 14, 30, 59): datetime.datetime(2006, 10, 25, 14, 30, 59), + datetime.datetime(2006, 10, 25, 14, 30, 59, 200): datetime.datetime(2006, 10, 25, 14, 30, 59, 200), + '2006-10-25 14:30:45': datetime.datetime(2006, 10, 25, 14, 30, 45), + '2006-10-25 14:30:00': datetime.datetime(2006, 10, 25, 14, 30), + '2006-10-25 14:30': datetime.datetime(2006, 10, 25, 14, 30), + '2006-10-25': datetime.datetime(2006, 10, 25, 0, 0), + '25/10/2006 14:30:45': datetime.datetime(2006, 10, 25, 14, 30, 45), + '25/10/2006 14:30:00': datetime.datetime(2006, 10, 25, 14, 30), + '25/10/2006 14:30': datetime.datetime(2006, 10, 25, 14, 30), + '25/10/2006': datetime.datetime(2006, 10, 25, 0, 0), + '25/10/06 14:30:45': datetime.datetime(2006, 10, 25, 14, 30, 45), + '25/10/06 14:30:00': datetime.datetime(2006, 10, 25, 14, 30), + '25/10/06 14:30': datetime.datetime(2006, 10, 25, 14, 30), + '25/10/06': datetime.datetime(2006, 10, 25, 0, 0), + } + invalid = { + 'hello': error_invalid, + '2006-10-25 4:30 p.m.': error_invalid, + } + self.assertFieldOutput(DateTimeField, valid, invalid, empty_value=None) + + # DateTimeField with optional input_formats paramter + valid = { + datetime.date(2006, 10, 25): datetime.datetime(2006, 10, 25, 0, 0), + datetime.datetime(2006, 10, 25, 14, 30): datetime.datetime(2006, 10, 25, 14, 30), + datetime.datetime(2006, 10, 25, 14, 30, 59): datetime.datetime(2006, 10, 25, 14, 30, 59), + datetime.datetime(2006, 10, 25, 14, 30, 59, 200): datetime.datetime(2006, 10, 25, 14, 30, 59, 200), + '2006 10 25 2:30 PM': datetime.datetime(2006, 10, 25, 14, 30), + } + invalid = { + '2006-10-25 14:30:45': error_invalid, + } + kwargs = {'input_formats':['%Y %m %d %I:%M %p'],} + self.assertFieldOutput(DateTimeField, + valid, invalid, field_kwargs=kwargs, empty_value=None + ) diff --git a/parts/django/tests/regressiontests/forms/localflavor/id.py b/parts/django/tests/regressiontests/forms/localflavor/id.py new file mode 100644 index 0000000..cb346ef --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/id.py @@ -0,0 +1,181 @@ +from django.contrib.localflavor.id.forms import (IDPhoneNumberField, + IDPostCodeField, IDNationalIdentityNumberField, IDLicensePlateField, + IDProvinceSelect, IDLicensePlatePrefixSelect) + +from utils import LocalFlavorTestCase + + +class IDLocalFlavorTests(LocalFlavorTestCase): + def test_IDProvinceSelect(self): + f = IDProvinceSelect() + out = u'''<select name="provinces"> +<option value="BLI">Bali</option> +<option value="BTN">Banten</option> +<option value="BKL">Bengkulu</option> +<option value="DIY">Yogyakarta</option> +<option value="JKT">Jakarta</option> +<option value="GOR">Gorontalo</option> +<option value="JMB">Jambi</option> +<option value="JBR">Jawa Barat</option> +<option value="JTG">Jawa Tengah</option> +<option value="JTM">Jawa Timur</option> +<option value="KBR">Kalimantan Barat</option> +<option value="KSL">Kalimantan Selatan</option> +<option value="KTG">Kalimantan Tengah</option> +<option value="KTM">Kalimantan Timur</option> +<option value="BBL">Kepulauan Bangka-Belitung</option> +<option value="KRI">Kepulauan Riau</option> +<option value="LPG" selected="selected">Lampung</option> +<option value="MLK">Maluku</option> +<option value="MUT">Maluku Utara</option> +<option value="NAD">Nanggroe Aceh Darussalam</option> +<option value="NTB">Nusa Tenggara Barat</option> +<option value="NTT">Nusa Tenggara Timur</option> +<option value="PPA">Papua</option> +<option value="PPB">Papua Barat</option> +<option value="RIU">Riau</option> +<option value="SLB">Sulawesi Barat</option> +<option value="SLS">Sulawesi Selatan</option> +<option value="SLT">Sulawesi Tengah</option> +<option value="SLR">Sulawesi Tenggara</option> +<option value="SLU">Sulawesi Utara</option> +<option value="SMB">Sumatera Barat</option> +<option value="SMS">Sumatera Selatan</option> +<option value="SMU">Sumatera Utara</option> +</select>''' + self.assertEqual(f.render('provinces', 'LPG'), out) + + def test_IDLicensePlatePrefixSelect(self): + f = IDLicensePlatePrefixSelect() + out = u'''<select name="codes"> +<option value="A">Banten</option> +<option value="AA">Magelang</option> +<option value="AB">Yogyakarta</option> +<option value="AD">Surakarta - Solo</option> +<option value="AE">Madiun</option> +<option value="AG">Kediri</option> +<option value="B">Jakarta</option> +<option value="BA">Sumatera Barat</option> +<option value="BB">Tapanuli</option> +<option value="BD">Bengkulu</option> +<option value="BE" selected="selected">Lampung</option> +<option value="BG">Sumatera Selatan</option> +<option value="BH">Jambi</option> +<option value="BK">Sumatera Utara</option> +<option value="BL">Nanggroe Aceh Darussalam</option> +<option value="BM">Riau</option> +<option value="BN">Kepulauan Bangka Belitung</option> +<option value="BP">Kepulauan Riau</option> +<option value="CC">Corps Consulate</option> +<option value="CD">Corps Diplomatic</option> +<option value="D">Bandung</option> +<option value="DA">Kalimantan Selatan</option> +<option value="DB">Sulawesi Utara Daratan</option> +<option value="DC">Sulawesi Barat</option> +<option value="DD">Sulawesi Selatan</option> +<option value="DE">Maluku</option> +<option value="DG">Maluku Utara</option> +<option value="DH">NTT - Timor</option> +<option value="DK">Bali</option> +<option value="DL">Sulawesi Utara Kepulauan</option> +<option value="DM">Gorontalo</option> +<option value="DN">Sulawesi Tengah</option> +<option value="DR">NTB - Lombok</option> +<option value="DS">Papua dan Papua Barat</option> +<option value="DT">Sulawesi Tenggara</option> +<option value="E">Cirebon</option> +<option value="EA">NTB - Sumbawa</option> +<option value="EB">NTT - Flores</option> +<option value="ED">NTT - Sumba</option> +<option value="F">Bogor</option> +<option value="G">Pekalongan</option> +<option value="H">Semarang</option> +<option value="K">Pati</option> +<option value="KB">Kalimantan Barat</option> +<option value="KH">Kalimantan Tengah</option> +<option value="KT">Kalimantan Timur</option> +<option value="L">Surabaya</option> +<option value="M">Madura</option> +<option value="N">Malang</option> +<option value="P">Jember</option> +<option value="R">Banyumas</option> +<option value="RI">Federal Government</option> +<option value="S">Bojonegoro</option> +<option value="T">Purwakarta</option> +<option value="W">Sidoarjo</option> +<option value="Z">Garut</option> +</select>''' + self.assertEqual(f.render('codes', 'BE'), out) + + def test_IDPhoneNumberField(self): + error_invalid = [u'Enter a valid phone number'] + valid = { + '0812-3456789': u'0812-3456789', + '081234567890': u'081234567890', + '021 345 6789': u'021 345 6789', + '0213456789': u'0213456789', + '+62-21-3456789': u'+62-21-3456789', + '(021) 345 6789': u'(021) 345 6789', + } + invalid = { + '0123456789': error_invalid, + '+62-021-3456789': error_invalid, + '+62-021-3456789': error_invalid, + '+62-0812-3456789': error_invalid, + '0812345678901': error_invalid, + 'foo': error_invalid, + } + self.assertFieldOutput(IDPhoneNumberField, valid, invalid) + + def test_IDPostCodeField(self): + error_invalid = [u'Enter a valid post code'] + valid = { + '12340': u'12340', + '25412': u'25412', + ' 12340 ': u'12340', + } + invalid = { + '12 3 4 0': error_invalid, + '12345': error_invalid, + '10100': error_invalid, + '123456': error_invalid, + 'foo': error_invalid, + } + self.assertFieldOutput(IDPostCodeField, valid, invalid) + + def test_IDNationalIdentityNumberField(self): + error_invalid = [u'Enter a valid NIK/KTP number'] + valid = { + ' 12.3456.010178 3456 ': u'12.3456.010178.3456', + '1234560101783456': u'12.3456.010178.3456', + '12.3456.010101.3456': u'12.3456.010101.3456', + } + invalid = { + '12.3456.310278.3456': error_invalid, + '00.0000.010101.0000': error_invalid, + '1234567890123456': error_invalid, + 'foo': error_invalid, + } + self.assertFieldOutput(IDNationalIdentityNumberField, valid, invalid) + + def test_IDLicensePlateField(self): + error_invalid = [u'Enter a valid vehicle license plate number'] + valid = { + ' b 1234 ab ': u'B 1234 AB', + 'B 1234 ABC': u'B 1234 ABC', + 'A 12': u'A 12', + 'DK 12345 12': u'DK 12345 12', + 'RI 10': u'RI 10', + 'CD 12 12': u'CD 12 12', + } + invalid = { + 'CD 10 12': error_invalid, + 'CD 1234 12': error_invalid, + 'RI 10 AB': error_invalid, + 'B 12345 01': error_invalid, + 'N 1234 12': error_invalid, + 'A 12 XYZ': error_invalid, + 'Q 1234 AB': error_invalid, + 'foo': error_invalid, + } + self.assertFieldOutput(IDLicensePlateField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/ie.py b/parts/django/tests/regressiontests/forms/localflavor/ie.py new file mode 100644 index 0000000..fab519b --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/ie.py @@ -0,0 +1,43 @@ +from django.contrib.localflavor.ie.forms import IECountySelect + +from utils import LocalFlavorTestCase + + +class IELocalFlavorTests(LocalFlavorTestCase): + def test_IECountySelect(self): + f = IECountySelect() + out = u'''<select name="counties"> +<option value="antrim">Antrim</option> +<option value="armagh">Armagh</option> +<option value="carlow">Carlow</option> +<option value="cavan">Cavan</option> +<option value="clare">Clare</option> +<option value="cork">Cork</option> +<option value="derry">Derry</option> +<option value="donegal">Donegal</option> +<option value="down">Down</option> +<option value="dublin" selected="selected">Dublin</option> +<option value="fermanagh">Fermanagh</option> +<option value="galway">Galway</option> +<option value="kerry">Kerry</option> +<option value="kildare">Kildare</option> +<option value="kilkenny">Kilkenny</option> +<option value="laois">Laois</option> +<option value="leitrim">Leitrim</option> +<option value="limerick">Limerick</option> +<option value="longford">Longford</option> +<option value="louth">Louth</option> +<option value="mayo">Mayo</option> +<option value="meath">Meath</option> +<option value="monaghan">Monaghan</option> +<option value="offaly">Offaly</option> +<option value="roscommon">Roscommon</option> +<option value="sligo">Sligo</option> +<option value="tipperary">Tipperary</option> +<option value="tyrone">Tyrone</option> +<option value="waterford">Waterford</option> +<option value="westmeath">Westmeath</option> +<option value="wexford">Wexford</option> +<option value="wicklow">Wicklow</option> +</select>''' + self.assertEqual(f.render('counties', 'dublin'), out) diff --git a/parts/django/tests/regressiontests/forms/localflavor/is_.py b/parts/django/tests/regressiontests/forms/localflavor/is_.py new file mode 100644 index 0000000..fa6b133 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/is_.py @@ -0,0 +1,199 @@ +from django.contrib.localflavor.is_.forms import (ISIdNumberField, + ISPhoneNumberField, ISPostalCodeSelect) + +from utils import LocalFlavorTestCase + + +class ISLocalFlavorTests(LocalFlavorTestCase): + def test_ISPostalCodeSelect(self): + f = ISPostalCodeSelect() + out = u'''<select name="foo"> +<option value="101">101 Reykjav\xedk</option> +<option value="103">103 Reykjav\xedk</option> +<option value="104">104 Reykjav\xedk</option> +<option value="105">105 Reykjav\xedk</option> +<option value="107">107 Reykjav\xedk</option> +<option value="108">108 Reykjav\xedk</option> +<option value="109">109 Reykjav\xedk</option> +<option value="110">110 Reykjav\xedk</option> +<option value="111">111 Reykjav\xedk</option> +<option value="112">112 Reykjav\xedk</option> +<option value="113">113 Reykjav\xedk</option> +<option value="116">116 Kjalarnes</option> +<option value="121">121 Reykjav\xedk</option> +<option value="123">123 Reykjav\xedk</option> +<option value="124">124 Reykjav\xedk</option> +<option value="125">125 Reykjav\xedk</option> +<option value="127">127 Reykjav\xedk</option> +<option value="128">128 Reykjav\xedk</option> +<option value="129">129 Reykjav\xedk</option> +<option value="130">130 Reykjav\xedk</option> +<option value="132">132 Reykjav\xedk</option> +<option value="150">150 Reykjav\xedk</option> +<option value="155">155 Reykjav\xedk</option> +<option value="170">170 Seltjarnarnes</option> +<option value="172">172 Seltjarnarnes</option> +<option value="190">190 Vogar</option> +<option value="200">200 K\xf3pavogur</option> +<option value="201">201 K\xf3pavogur</option> +<option value="202">202 K\xf3pavogur</option> +<option value="203">203 K\xf3pavogur</option> +<option value="210">210 Gar\xf0ab\xe6r</option> +<option value="212">212 Gar\xf0ab\xe6r</option> +<option value="220">220 Hafnarfj\xf6r\xf0ur</option> +<option value="221">221 Hafnarfj\xf6r\xf0ur</option> +<option value="222">222 Hafnarfj\xf6r\xf0ur</option> +<option value="225">225 \xc1lftanes</option> +<option value="230">230 Reykjanesb\xe6r</option> +<option value="232">232 Reykjanesb\xe6r</option> +<option value="233">233 Reykjanesb\xe6r</option> +<option value="235">235 Keflav\xedkurflugv\xf6llur</option> +<option value="240">240 Grindav\xedk</option> +<option value="245">245 Sandger\xf0i</option> +<option value="250">250 Gar\xf0ur</option> +<option value="260">260 Reykjanesb\xe6r</option> +<option value="270">270 Mosfellsb\xe6r</option> +<option value="300">300 Akranes</option> +<option value="301">301 Akranes</option> +<option value="302">302 Akranes</option> +<option value="310">310 Borgarnes</option> +<option value="311">311 Borgarnes</option> +<option value="320">320 Reykholt \xed Borgarfir\xf0i</option> +<option value="340">340 Stykkish\xf3lmur</option> +<option value="345">345 Flatey \xe1 Brei\xf0afir\xf0i</option> +<option value="350">350 Grundarfj\xf6r\xf0ur</option> +<option value="355">355 \xd3lafsv\xedk</option> +<option value="356">356 Sn\xe6fellsb\xe6r</option> +<option value="360">360 Hellissandur</option> +<option value="370">370 B\xfa\xf0ardalur</option> +<option value="371">371 B\xfa\xf0ardalur</option> +<option value="380">380 Reykh\xf3lahreppur</option> +<option value="400">400 \xcdsafj\xf6r\xf0ur</option> +<option value="401">401 \xcdsafj\xf6r\xf0ur</option> +<option value="410">410 Hn\xedfsdalur</option> +<option value="415">415 Bolungarv\xedk</option> +<option value="420">420 S\xfa\xf0av\xedk</option> +<option value="425">425 Flateyri</option> +<option value="430">430 Su\xf0ureyri</option> +<option value="450">450 Patreksfj\xf6r\xf0ur</option> +<option value="451">451 Patreksfj\xf6r\xf0ur</option> +<option value="460">460 T\xe1lknafj\xf6r\xf0ur</option> +<option value="465">465 B\xedldudalur</option> +<option value="470">470 \xdeingeyri</option> +<option value="471">471 \xdeingeyri</option> +<option value="500">500 Sta\xf0ur</option> +<option value="510">510 H\xf3lmav\xedk</option> +<option value="512">512 H\xf3lmav\xedk</option> +<option value="520">520 Drangsnes</option> +<option value="522">522 Kj\xf6rvogur</option> +<option value="523">523 B\xe6r</option> +<option value="524">524 Nor\xf0urfj\xf6r\xf0ur</option> +<option value="530">530 Hvammstangi</option> +<option value="531">531 Hvammstangi</option> +<option value="540">540 Bl\xf6ndu\xf3s</option> +<option value="541">541 Bl\xf6ndu\xf3s</option> +<option value="545">545 Skagastr\xf6nd</option> +<option value="550">550 Sau\xf0\xe1rkr\xf3kur</option> +<option value="551">551 Sau\xf0\xe1rkr\xf3kur</option> +<option value="560">560 Varmahl\xed\xf0</option> +<option value="565">565 Hofs\xf3s</option> +<option value="566">566 Hofs\xf3s</option> +<option value="570">570 Flj\xf3t</option> +<option value="580">580 Siglufj\xf6r\xf0ur</option> +<option value="600">600 Akureyri</option> +<option value="601">601 Akureyri</option> +<option value="602">602 Akureyri</option> +<option value="603">603 Akureyri</option> +<option value="610">610 Greniv\xedk</option> +<option value="611">611 Gr\xedmsey</option> +<option value="620">620 Dalv\xedk</option> +<option value="621">621 Dalv\xedk</option> +<option value="625">625 \xd3lafsfj\xf6r\xf0ur</option> +<option value="630">630 Hr\xedsey</option> +<option value="640">640 H\xfasav\xedk</option> +<option value="641">641 H\xfasav\xedk</option> +<option value="645">645 Fossh\xf3ll</option> +<option value="650">650 Laugar</option> +<option value="660">660 M\xfdvatn</option> +<option value="670">670 K\xf3pasker</option> +<option value="671">671 K\xf3pasker</option> +<option value="675">675 Raufarh\xf6fn</option> +<option value="680">680 \xde\xf3rsh\xf6fn</option> +<option value="681">681 \xde\xf3rsh\xf6fn</option> +<option value="685">685 Bakkafj\xf6r\xf0ur</option> +<option value="690">690 Vopnafj\xf6r\xf0ur</option> +<option value="700">700 Egilssta\xf0ir</option> +<option value="701">701 Egilssta\xf0ir</option> +<option value="710">710 Sey\xf0isfj\xf6r\xf0ur</option> +<option value="715">715 Mj\xf3ifj\xf6r\xf0ur</option> +<option value="720">720 Borgarfj\xf6r\xf0ur eystri</option> +<option value="730">730 Rey\xf0arfj\xf6r\xf0ur</option> +<option value="735">735 Eskifj\xf6r\xf0ur</option> +<option value="740">740 Neskaupsta\xf0ur</option> +<option value="750">750 F\xe1skr\xfa\xf0sfj\xf6r\xf0ur</option> +<option value="755">755 St\xf6\xf0varfj\xf6r\xf0ur</option> +<option value="760">760 Brei\xf0dalsv\xedk</option> +<option value="765">765 Dj\xfapivogur</option> +<option value="780">780 H\xf6fn \xed Hornafir\xf0i</option> +<option value="781">781 H\xf6fn \xed Hornafir\xf0i</option> +<option value="785">785 \xd6r\xe6fi</option> +<option value="800">800 Selfoss</option> +<option value="801">801 Selfoss</option> +<option value="802">802 Selfoss</option> +<option value="810">810 Hverager\xf0i</option> +<option value="815">815 \xdeorl\xe1ksh\xf6fn</option> +<option value="820">820 Eyrarbakki</option> +<option value="825">825 Stokkseyri</option> +<option value="840">840 Laugarvatn</option> +<option value="845">845 Fl\xfa\xf0ir</option> +<option value="850">850 Hella</option> +<option value="851">851 Hella</option> +<option value="860">860 Hvolsv\xf6llur</option> +<option value="861">861 Hvolsv\xf6llur</option> +<option value="870">870 V\xedk</option> +<option value="871">871 V\xedk</option> +<option value="880">880 Kirkjub\xe6jarklaustur</option> +<option value="900">900 Vestmannaeyjar</option> +<option value="902">902 Vestmannaeyjar</option> +</select>''' + self.assertEqual(f.render('foo', 'bar'), out) + + def test_ISIdNumberField(self): + error_atleast = [u'Ensure this value has at least 10 characters (it has 9).'] + error_invalid = [u'Enter a valid Icelandic identification number. The format is XXXXXX-XXXX.'] + error_atmost = [u'Ensure this value has at most 11 characters (it has 12).'] + error_notvalid = [u'The Icelandic identification number is not valid.'] + valid = { + '2308803449': '230880-3449', + '230880-3449': '230880-3449', + '230880 3449': '230880-3449', + '2308803440': '230880-3440', + } + invalid = { + '230880343': error_atleast + error_invalid, + '230880343234': error_atmost + error_invalid, + 'abcdefghijk': error_invalid, + '2308803439': error_notvalid, + + } + self.assertFieldOutput(ISIdNumberField, valid, invalid) + + def test_ISPhoneNumberField(self): + error_invalid = [u'Enter a valid value.'] + error_atleast = [u'Ensure this value has at least 7 characters (it has 6).'] + error_atmost = [u'Ensure this value has at most 8 characters (it has 9).'] + valid = { + '1234567': '1234567', + '123 4567': '1234567', + '123-4567': '1234567', + } + invalid = { + '123-456': error_invalid, + '123456': error_atleast + error_invalid, + '123456555': error_atmost + error_invalid, + 'abcdefg': error_invalid, + ' 1234567 ': error_atmost + error_invalid, + ' 12367 ': error_invalid + } + self.assertFieldOutput(ISPhoneNumberField, valid, invalid) + diff --git a/parts/django/tests/regressiontests/forms/localflavor/it.py b/parts/django/tests/regressiontests/forms/localflavor/it.py new file mode 100644 index 0000000..7181e25 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/it.py @@ -0,0 +1,70 @@ +from django.contrib.localflavor.it.forms import (ITZipCodeField, ITRegionSelect, + ITSocialSecurityNumberField, ITVatNumberField) + +from utils import LocalFlavorTestCase + + +class ITLocalFlavorTests(LocalFlavorTestCase): + def test_ITRegionSelect(self): + f = ITRegionSelect() + out = u'''<select name="regions"> +<option value="ABR">Abruzzo</option> +<option value="BAS">Basilicata</option> +<option value="CAL">Calabria</option> +<option value="CAM">Campania</option> +<option value="EMR">Emilia-Romagna</option> +<option value="FVG">Friuli-Venezia Giulia</option> +<option value="LAZ">Lazio</option> +<option value="LIG">Liguria</option> +<option value="LOM">Lombardia</option> +<option value="MAR">Marche</option> +<option value="MOL">Molise</option> +<option value="PMN" selected="selected">Piemonte</option> +<option value="PUG">Puglia</option> +<option value="SAR">Sardegna</option> +<option value="SIC">Sicilia</option> +<option value="TOS">Toscana</option> +<option value="TAA">Trentino-Alto Adige</option> +<option value="UMB">Umbria</option> +<option value="VAO">Valle d\u2019Aosta</option> +<option value="VEN">Veneto</option> +</select>''' + self.assertEqual(f.render('regions', 'PMN'), out) + + def test_ITZipCodeField(self): + error_invalid = [u'Enter a valid zip code.'] + valid = { + '00100': '00100', + } + invalid = { + ' 00100': error_invalid, + } + self.assertFieldOutput(ITZipCodeField, valid, invalid) + + def test_ITSocialSecurityNumberField(self): + error_invalid = [u'Enter a valid Social Security number.'] + valid = { + 'LVSGDU99T71H501L': 'LVSGDU99T71H501L', + 'LBRRME11A01L736W': 'LBRRME11A01L736W', + 'lbrrme11a01l736w': 'LBRRME11A01L736W', + 'LBR RME 11A01 L736W': 'LBRRME11A01L736W', + } + invalid = { + 'LBRRME11A01L736A': error_invalid, + '%BRRME11A01L736W': error_invalid, + } + self.assertFieldOutput(ITSocialSecurityNumberField, valid, invalid) + + def test_ITVatNumberField(self): + error_invalid = [u'Enter a valid VAT number.'] + valid = { + '07973780013': '07973780013', + '7973780013': '07973780013', + 7973780013: '07973780013', + } + invalid = { + '07973780014': error_invalid, + 'A7973780013': error_invalid, + } + self.assertFieldOutput(ITVatNumberField, valid, invalid) + diff --git a/parts/django/tests/regressiontests/forms/localflavor/jp.py b/parts/django/tests/regressiontests/forms/localflavor/jp.py new file mode 100644 index 0000000..1f8362a --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/jp.py @@ -0,0 +1,73 @@ +from django.contrib.localflavor.jp.forms import (JPPostalCodeField, + JPPrefectureSelect) + +from utils import LocalFlavorTestCase + + +class JPLocalFlavorTests(LocalFlavorTestCase): + def test_JPPrefectureSelect(self): + f = JPPrefectureSelect() + out = u'''<select name="prefecture"> +<option value="hokkaido">Hokkaido</option> +<option value="aomori">Aomori</option> +<option value="iwate">Iwate</option> +<option value="miyagi">Miyagi</option> +<option value="akita">Akita</option> +<option value="yamagata">Yamagata</option> +<option value="fukushima">Fukushima</option> +<option value="ibaraki">Ibaraki</option> +<option value="tochigi">Tochigi</option> +<option value="gunma">Gunma</option> +<option value="saitama">Saitama</option> +<option value="chiba">Chiba</option> +<option value="tokyo">Tokyo</option> +<option value="kanagawa" selected="selected">Kanagawa</option> +<option value="yamanashi">Yamanashi</option> +<option value="nagano">Nagano</option> +<option value="niigata">Niigata</option> +<option value="toyama">Toyama</option> +<option value="ishikawa">Ishikawa</option> +<option value="fukui">Fukui</option> +<option value="gifu">Gifu</option> +<option value="shizuoka">Shizuoka</option> +<option value="aichi">Aichi</option> +<option value="mie">Mie</option> +<option value="shiga">Shiga</option> +<option value="kyoto">Kyoto</option> +<option value="osaka">Osaka</option> +<option value="hyogo">Hyogo</option> +<option value="nara">Nara</option> +<option value="wakayama">Wakayama</option> +<option value="tottori">Tottori</option> +<option value="shimane">Shimane</option> +<option value="okayama">Okayama</option> +<option value="hiroshima">Hiroshima</option> +<option value="yamaguchi">Yamaguchi</option> +<option value="tokushima">Tokushima</option> +<option value="kagawa">Kagawa</option> +<option value="ehime">Ehime</option> +<option value="kochi">Kochi</option> +<option value="fukuoka">Fukuoka</option> +<option value="saga">Saga</option> +<option value="nagasaki">Nagasaki</option> +<option value="kumamoto">Kumamoto</option> +<option value="oita">Oita</option> +<option value="miyazaki">Miyazaki</option> +<option value="kagoshima">Kagoshima</option> +<option value="okinawa">Okinawa</option> +</select>''' + self.assertEqual(f.render('prefecture', 'kanagawa'), out) + + def test_JPPostalCodeField(self): + error_format = [u'Enter a postal code in the format XXXXXXX or XXX-XXXX.'] + valid = { + '251-0032': '2510032', + '2510032': '2510032', + } + invalid = { + '2510-032': error_format, + '251a0032': error_format, + 'a51-0032': error_format, + '25100321': error_format, + } + self.assertFieldOutput(JPPostalCodeField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/kw.py b/parts/django/tests/regressiontests/forms/localflavor/kw.py new file mode 100644 index 0000000..af998bd --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/kw.py @@ -0,0 +1,16 @@ +from django.contrib.localflavor.kw.forms import KWCivilIDNumberField + +from utils import LocalFlavorTestCase + + +class KWLocalFlavorTests(LocalFlavorTestCase): + def test_KWCivilIDNumberField(self): + error_invalid = [u'Enter a valid Kuwaiti Civil ID number'] + valid = { + '282040701483': '282040701483', + } + invalid = { + '289332013455': error_invalid, + } + self.assertFieldOutput(KWCivilIDNumberField, valid, invalid) + diff --git a/parts/django/tests/regressiontests/forms/localflavor/nl.py b/parts/django/tests/regressiontests/forms/localflavor/nl.py new file mode 100644 index 0000000..8ef0ae9 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/nl.py @@ -0,0 +1,62 @@ +from django.contrib.localflavor.nl.forms import (NLPhoneNumberField, + NLZipCodeField, NLSoFiNumberField, NLProvinceSelect) + +from utils import LocalFlavorTestCase + + +class NLLocalFlavorTests(LocalFlavorTestCase): + def test_NLProvinceSelect(self): + f = NLProvinceSelect() + out = u'''<select name="provinces"> +<option value="DR">Drenthe</option> +<option value="FL">Flevoland</option> +<option value="FR">Friesland</option> +<option value="GL">Gelderland</option> +<option value="GR">Groningen</option> +<option value="LB">Limburg</option> +<option value="NB">Noord-Brabant</option> +<option value="NH">Noord-Holland</option> +<option value="OV" selected="selected">Overijssel</option> +<option value="UT">Utrecht</option> +<option value="ZE">Zeeland</option> +<option value="ZH">Zuid-Holland</option> +</select>''' + self.assertEqual(f.render('provinces', 'OV'), out) + + def test_NLPhoneNumberField(self): + error_invalid = [u'Enter a valid phone number'] + valid = { + '012-3456789': '012-3456789', + '0123456789': '0123456789', + '+31-12-3456789': '+31-12-3456789', + '(0123) 456789': '(0123) 456789', + } + invalid = { + 'foo': error_invalid, + } + self.assertFieldOutput(NLPhoneNumberField, valid, invalid) + + def test_NLZipCodeField(self): + error_invalid = [u'Enter a valid postal code'] + valid = { + '1234ab': '1234 AB', + '1234 ab': '1234 AB', + '1234 AB': '1234 AB', + } + invalid = { + '0123AB': error_invalid, + 'foo': error_invalid, + } + self.assertFieldOutput(NLZipCodeField, valid, invalid) + + def test_NLSoFiNumberField(self): + error_invalid = [u'Enter a valid SoFi number'] + valid = { + '123456782': '123456782', + } + invalid = { + '000000000': error_invalid, + '123456789': error_invalid, + 'foo': error_invalid, + } + self.assertFieldOutput(NLSoFiNumberField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/pl.py b/parts/django/tests/regressiontests/forms/localflavor/pl.py new file mode 100644 index 0000000..51721f8 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/pl.py @@ -0,0 +1,462 @@ +from django.contrib.localflavor.pl.forms import (PLProvinceSelect, + PLCountySelect, PLPostalCodeField, PLNIPField, PLPESELField, PLREGONField) + +from utils import LocalFlavorTestCase + + +class PLLocalFlavorTests(LocalFlavorTestCase): + def test_PLProvinceSelect(self): + f = PLProvinceSelect() + out = u'''<select name="voivodeships"> +<option value="lower_silesia">Lower Silesia</option> +<option value="kuyavia-pomerania">Kuyavia-Pomerania</option> +<option value="lublin">Lublin</option> +<option value="lubusz">Lubusz</option> +<option value="lodz">Lodz</option> +<option value="lesser_poland">Lesser Poland</option> +<option value="masovia">Masovia</option> +<option value="opole">Opole</option> +<option value="subcarpatia">Subcarpatia</option> +<option value="podlasie">Podlasie</option> +<option value="pomerania" selected="selected">Pomerania</option> +<option value="silesia">Silesia</option> +<option value="swietokrzyskie">Swietokrzyskie</option> +<option value="warmia-masuria">Warmia-Masuria</option> +<option value="greater_poland">Greater Poland</option> +<option value="west_pomerania">West Pomerania</option> +</select>''' + self.assertEqual(f.render('voivodeships', 'pomerania'), out) + + def test_PLCountrySelect(self): + f = PLCountySelect() + out = u'''<select name="administrativeunit"> +<option value="wroclaw">Wroc\u0142aw</option> +<option value="jeleniagora">Jelenia G\xf3ra</option> +<option value="legnica">Legnica</option> +<option value="boleslawiecki">boles\u0142awiecki</option> +<option value="dzierzoniowski">dzier\u017coniowski</option> +<option value="glogowski">g\u0142ogowski</option> +<option value="gorowski">g\xf3rowski</option> +<option value="jaworski">jaworski</option> +<option value="jeleniogorski">jeleniog\xf3rski</option> +<option value="kamiennogorski">kamiennog\xf3rski</option> +<option value="klodzki">k\u0142odzki</option> +<option value="legnicki">legnicki</option> +<option value="lubanski">luba\u0144ski</option> +<option value="lubinski">lubi\u0144ski</option> +<option value="lwowecki">lw\xf3wecki</option> +<option value="milicki">milicki</option> +<option value="olesnicki">ole\u015bnicki</option> +<option value="olawski">o\u0142awski</option> +<option value="polkowicki">polkowicki</option> +<option value="strzelinski">strzeli\u0144ski</option> +<option value="sredzki">\u015bredzki</option> +<option value="swidnicki">\u015bwidnicki</option> +<option value="trzebnicki">trzebnicki</option> +<option value="walbrzyski">wa\u0142brzyski</option> +<option value="wolowski">wo\u0142owski</option> +<option value="wroclawski">wroc\u0142awski</option> +<option value="zabkowicki">z\u0105bkowicki</option> +<option value="zgorzelecki">zgorzelecki</option> +<option value="zlotoryjski">z\u0142otoryjski</option> +<option value="bydgoszcz">Bydgoszcz</option> +<option value="torun">Toru\u0144</option> +<option value="wloclawek">W\u0142oc\u0142awek</option> +<option value="grudziadz">Grudzi\u0105dz</option> +<option value="aleksandrowski">aleksandrowski</option> +<option value="brodnicki">brodnicki</option> +<option value="bydgoski">bydgoski</option> +<option value="chelminski">che\u0142mi\u0144ski</option> +<option value="golubsko-dobrzynski">golubsko-dobrzy\u0144ski</option> +<option value="grudziadzki">grudzi\u0105dzki</option> +<option value="inowroclawski">inowroc\u0142awski</option> +<option value="lipnowski">lipnowski</option> +<option value="mogilenski">mogile\u0144ski</option> +<option value="nakielski">nakielski</option> +<option value="radziejowski">radziejowski</option> +<option value="rypinski">rypi\u0144ski</option> +<option value="sepolenski">s\u0119pole\u0144ski</option> +<option value="swiecki">\u015bwiecki</option> +<option value="torunski">toru\u0144ski</option> +<option value="tucholski">tucholski</option> +<option value="wabrzeski">w\u0105brzeski</option> +<option value="wloclawski">wroc\u0142awski</option> +<option value="zninski">\u017ani\u0144ski</option> +<option value="lublin">Lublin</option> +<option value="biala-podlaska">Bia\u0142a Podlaska</option> +<option value="chelm">Che\u0142m</option> +<option value="zamosc">Zamo\u015b\u0107</option> +<option value="bialski">bialski</option> +<option value="bilgorajski">bi\u0142gorajski</option> +<option value="chelmski">che\u0142mski</option> +<option value="hrubieszowski">hrubieszowski</option> +<option value="janowski">janowski</option> +<option value="krasnostawski">krasnostawski</option> +<option value="krasnicki">kra\u015bnicki</option> +<option value="lubartowski">lubartowski</option> +<option value="lubelski">lubelski</option> +<option value="leczynski">\u0142\u0119czy\u0144ski</option> +<option value="lukowski">\u0142ukowski</option> +<option value="opolski">opolski</option> +<option value="parczewski">parczewski</option> +<option value="pulawski">pu\u0142awski</option> +<option value="radzynski">radzy\u0144ski</option> +<option value="rycki">rycki</option> +<option value="swidnicki">\u015bwidnicki</option> +<option value="tomaszowski">tomaszowski</option> +<option value="wlodawski">w\u0142odawski</option> +<option value="zamojski">zamojski</option> +<option value="gorzow-wielkopolski">Gorz\xf3w Wielkopolski</option> +<option value="zielona-gora">Zielona G\xf3ra</option> +<option value="gorzowski">gorzowski</option> +<option value="krosnienski">kro\u015bnie\u0144ski</option> +<option value="miedzyrzecki">mi\u0119dzyrzecki</option> +<option value="nowosolski">nowosolski</option> +<option value="slubicki">s\u0142ubicki</option> +<option value="strzelecko-drezdenecki">strzelecko-drezdenecki</option> +<option value="sulecinski">sule\u0144ci\u0144ski</option> +<option value="swiebodzinski">\u015bwiebodzi\u0144ski</option> +<option value="wschowski">wschowski</option> +<option value="zielonogorski">zielonog\xf3rski</option> +<option value="zaganski">\u017caga\u0144ski</option> +<option value="zarski">\u017carski</option> +<option value="lodz">\u0141\xf3d\u017a</option> +<option value="piotrkow-trybunalski">Piotrk\xf3w Trybunalski</option> +<option value="skierniewice">Skierniewice</option> +<option value="belchatowski">be\u0142chatowski</option> +<option value="brzezinski">brzezi\u0144ski</option> +<option value="kutnowski">kutnowski</option> +<option value="laski">\u0142aski</option> +<option value="leczycki">\u0142\u0119czycki</option> +<option value="lowicki">\u0142owicki</option> +<option value="lodzki wschodni">\u0142\xf3dzki wschodni</option> +<option value="opoczynski">opoczy\u0144ski</option> +<option value="pabianicki">pabianicki</option> +<option value="pajeczanski">paj\u0119cza\u0144ski</option> +<option value="piotrkowski">piotrkowski</option> +<option value="poddebicki">podd\u0119bicki</option> +<option value="radomszczanski">radomszcza\u0144ski</option> +<option value="rawski">rawski</option> +<option value="sieradzki">sieradzki</option> +<option value="skierniewicki">skierniewicki</option> +<option value="tomaszowski">tomaszowski</option> +<option value="wielunski">wielu\u0144ski</option> +<option value="wieruszowski">wieruszowski</option> +<option value="zdunskowolski">zdu\u0144skowolski</option> +<option value="zgierski">zgierski</option> +<option value="krakow">Krak\xf3w</option> +<option value="tarnow">Tarn\xf3w</option> +<option value="nowy-sacz">Nowy S\u0105cz</option> +<option value="bochenski">boche\u0144ski</option> +<option value="brzeski">brzeski</option> +<option value="chrzanowski">chrzanowski</option> +<option value="dabrowski">d\u0105browski</option> +<option value="gorlicki">gorlicki</option> +<option value="krakowski">krakowski</option> +<option value="limanowski">limanowski</option> +<option value="miechowski">miechowski</option> +<option value="myslenicki">my\u015blenicki</option> +<option value="nowosadecki">nowos\u0105decki</option> +<option value="nowotarski">nowotarski</option> +<option value="olkuski">olkuski</option> +<option value="oswiecimski">o\u015bwi\u0119cimski</option> +<option value="proszowicki">proszowicki</option> +<option value="suski">suski</option> +<option value="tarnowski">tarnowski</option> +<option value="tatrzanski">tatrza\u0144ski</option> +<option value="wadowicki">wadowicki</option> +<option value="wielicki">wielicki</option> +<option value="warszawa">Warszawa</option> +<option value="ostroleka">Ostro\u0142\u0119ka</option> +<option value="plock">P\u0142ock</option> +<option value="radom">Radom</option> +<option value="siedlce">Siedlce</option> +<option value="bialobrzeski">bia\u0142obrzeski</option> +<option value="ciechanowski">ciechanowski</option> +<option value="garwolinski">garwoli\u0144ski</option> +<option value="gostyninski">gostyni\u0144ski</option> +<option value="grodziski">grodziski</option> +<option value="grojecki">gr\xf3jecki</option> +<option value="kozienicki">kozenicki</option> +<option value="legionowski">legionowski</option> +<option value="lipski">lipski</option> +<option value="losicki">\u0142osicki</option> +<option value="makowski">makowski</option> +<option value="minski">mi\u0144ski</option> +<option value="mlawski">m\u0142awski</option> +<option value="nowodworski">nowodworski</option> +<option value="ostrolecki">ostro\u0142\u0119cki</option> +<option value="ostrowski">ostrowski</option> +<option value="otwocki">otwocki</option> +<option value="piaseczynski">piaseczy\u0144ski</option> +<option value="plocki">p\u0142ocki</option> +<option value="plonski">p\u0142o\u0144ski</option> +<option value="pruszkowski">pruszkowski</option> +<option value="przasnyski">przasnyski</option> +<option value="przysuski">przysuski</option> +<option value="pultuski">pu\u0142tuski</option> +<option value="radomski">radomski</option> +<option value="siedlecki">siedlecki</option> +<option value="sierpecki">sierpecki</option> +<option value="sochaczewski">sochaczewski</option> +<option value="sokolowski">soko\u0142owski</option> +<option value="szydlowiecki">szyd\u0142owiecki</option> +<option value="warszawski-zachodni">warszawski zachodni</option> +<option value="wegrowski">w\u0119growski</option> +<option value="wolominski">wo\u0142omi\u0144ski</option> +<option value="wyszkowski">wyszkowski</option> +<option value="zwolenski">zwole\u0144ski</option> +<option value="zurominski">\u017curomi\u0144ski</option> +<option value="zyrardowski">\u017cyrardowski</option> +<option value="opole">Opole</option> +<option value="brzeski">brzeski</option> +<option value="glubczycki">g\u0142ubczyski</option> +<option value="kedzierzynsko-kozielski">k\u0119dzierzy\u0144ski-kozielski</option> +<option value="kluczborski">kluczborski</option> +<option value="krapkowicki">krapkowicki</option> +<option value="namyslowski">namys\u0142owski</option> +<option value="nyski">nyski</option> +<option value="oleski">oleski</option> +<option value="opolski">opolski</option> +<option value="prudnicki">prudnicki</option> +<option value="strzelecki">strzelecki</option> +<option value="rzeszow">Rzesz\xf3w</option> +<option value="krosno">Krosno</option> +<option value="przemysl">Przemy\u015bl</option> +<option value="tarnobrzeg">Tarnobrzeg</option> +<option value="bieszczadzki">bieszczadzki</option> +<option value="brzozowski">brzozowski</option> +<option value="debicki">d\u0119bicki</option> +<option value="jaroslawski">jaros\u0142awski</option> +<option value="jasielski">jasielski</option> +<option value="kolbuszowski">kolbuszowski</option> +<option value="krosnienski">kro\u015bnie\u0144ski</option> +<option value="leski">leski</option> +<option value="lezajski">le\u017cajski</option> +<option value="lubaczowski">lubaczowski</option> +<option value="lancucki">\u0142a\u0144cucki</option> +<option value="mielecki">mielecki</option> +<option value="nizanski">ni\u017ca\u0144ski</option> +<option value="przemyski">przemyski</option> +<option value="przeworski">przeworski</option> +<option value="ropczycko-sedziszowski">ropczycko-s\u0119dziszowski</option> +<option value="rzeszowski">rzeszowski</option> +<option value="sanocki">sanocki</option> +<option value="stalowowolski">stalowowolski</option> +<option value="strzyzowski">strzy\u017cowski</option> +<option value="tarnobrzeski">tarnobrzeski</option> +<option value="bialystok">Bia\u0142ystok</option> +<option value="lomza">\u0141om\u017ca</option> +<option value="suwalki">Suwa\u0142ki</option> +<option value="augustowski">augustowski</option> +<option value="bialostocki">bia\u0142ostocki</option> +<option value="bielski">bielski</option> +<option value="grajewski">grajewski</option> +<option value="hajnowski">hajnowski</option> +<option value="kolnenski">kolne\u0144ski</option> +<option value="\u0142omzynski">\u0142om\u017cy\u0144ski</option> +<option value="moniecki">moniecki</option> +<option value="sejnenski">sejne\u0144ski</option> +<option value="siemiatycki">siematycki</option> +<option value="sokolski">sok\xf3lski</option> +<option value="suwalski">suwalski</option> +<option value="wysokomazowiecki">wysokomazowiecki</option> +<option value="zambrowski">zambrowski</option> +<option value="gdansk">Gda\u0144sk</option> +<option value="gdynia">Gdynia</option> +<option value="slupsk">S\u0142upsk</option> +<option value="sopot">Sopot</option> +<option value="bytowski">bytowski</option> +<option value="chojnicki">chojnicki</option> +<option value="czluchowski">cz\u0142uchowski</option> +<option value="kartuski">kartuski</option> +<option value="koscierski">ko\u015bcierski</option> +<option value="kwidzynski">kwidzy\u0144ski</option> +<option value="leborski">l\u0119borski</option> +<option value="malborski">malborski</option> +<option value="nowodworski">nowodworski</option> +<option value="gdanski">gda\u0144ski</option> +<option value="pucki">pucki</option> +<option value="slupski">s\u0142upski</option> +<option value="starogardzki">starogardzki</option> +<option value="sztumski">sztumski</option> +<option value="tczewski">tczewski</option> +<option value="wejherowski">wejcherowski</option> +<option value="katowice" selected="selected">Katowice</option> +<option value="bielsko-biala">Bielsko-Bia\u0142a</option> +<option value="bytom">Bytom</option> +<option value="chorzow">Chorz\xf3w</option> +<option value="czestochowa">Cz\u0119stochowa</option> +<option value="dabrowa-gornicza">D\u0105browa G\xf3rnicza</option> +<option value="gliwice">Gliwice</option> +<option value="jastrzebie-zdroj">Jastrz\u0119bie Zdr\xf3j</option> +<option value="jaworzno">Jaworzno</option> +<option value="myslowice">Mys\u0142owice</option> +<option value="piekary-slaskie">Piekary \u015al\u0105skie</option> +<option value="ruda-slaska">Ruda \u015al\u0105ska</option> +<option value="rybnik">Rybnik</option> +<option value="siemianowice-slaskie">Siemianowice \u015al\u0105skie</option> +<option value="sosnowiec">Sosnowiec</option> +<option value="swietochlowice">\u015awi\u0119toch\u0142owice</option> +<option value="tychy">Tychy</option> +<option value="zabrze">Zabrze</option> +<option value="zory">\u017bory</option> +<option value="bedzinski">b\u0119dzi\u0144ski</option> +<option value="bielski">bielski</option> +<option value="bierunsko-ledzinski">bieru\u0144sko-l\u0119dzi\u0144ski</option> +<option value="cieszynski">cieszy\u0144ski</option> +<option value="czestochowski">cz\u0119stochowski</option> +<option value="gliwicki">gliwicki</option> +<option value="klobucki">k\u0142obucki</option> +<option value="lubliniecki">lubliniecki</option> +<option value="mikolowski">miko\u0142owski</option> +<option value="myszkowski">myszkowski</option> +<option value="pszczynski">pszczy\u0144ski</option> +<option value="raciborski">raciborski</option> +<option value="rybnicki">rybnicki</option> +<option value="tarnogorski">tarnog\xf3rski</option> +<option value="wodzislawski">wodzis\u0142awski</option> +<option value="zawiercianski">zawiercia\u0144ski</option> +<option value="zywiecki">\u017cywiecki</option> +<option value="kielce">Kielce</option> +<option value="buski">buski</option> +<option value="jedrzejowski">j\u0119drzejowski</option> +<option value="kazimierski">kazimierski</option> +<option value="kielecki">kielecki</option> +<option value="konecki">konecki</option> +<option value="opatowski">opatowski</option> +<option value="ostrowiecki">ostrowiecki</option> +<option value="pinczowski">pi\u0144czowski</option> +<option value="sandomierski">sandomierski</option> +<option value="skarzyski">skar\u017cyski</option> +<option value="starachowicki">starachowicki</option> +<option value="staszowski">staszowski</option> +<option value="wloszczowski">w\u0142oszczowski</option> +<option value="olsztyn">Olsztyn</option> +<option value="elblag">Elbl\u0105g</option> +<option value="bartoszycki">bartoszycki</option> +<option value="braniewski">braniewski</option> +<option value="dzialdowski">dzia\u0142dowski</option> +<option value="elblaski">elbl\u0105ski</option> +<option value="elcki">e\u0142cki</option> +<option value="gizycki">gi\u017cycki</option> +<option value="goldapski">go\u0142dapski</option> +<option value="ilawski">i\u0142awski</option> +<option value="ketrzynski">k\u0119trzy\u0144ski</option> +<option value="lidzbarski">lidzbarski</option> +<option value="mragowski">mr\u0105gowski</option> +<option value="nidzicki">nidzicki</option> +<option value="nowomiejski">nowomiejski</option> +<option value="olecki">olecki</option> +<option value="olsztynski">olszty\u0144ski</option> +<option value="ostrodzki">ostr\xf3dzki</option> +<option value="piski">piski</option> +<option value="szczycienski">szczycie\u0144ski</option> +<option value="wegorzewski">w\u0119gorzewski</option> +<option value="poznan">Pozna\u0144</option> +<option value="kalisz">Kalisz</option> +<option value="konin">Konin</option> +<option value="leszno">Leszno</option> +<option value="chodzieski">chodziejski</option> +<option value="czarnkowsko-trzcianecki">czarnkowsko-trzcianecki</option> +<option value="gnieznienski">gnie\u017anie\u0144ski</option> +<option value="gostynski">gosty\u0144ski</option> +<option value="grodziski">grodziski</option> +<option value="jarocinski">jaroci\u0144ski</option> +<option value="kaliski">kaliski</option> +<option value="kepinski">k\u0119pi\u0144ski</option> +<option value="kolski">kolski</option> +<option value="koninski">koni\u0144ski</option> +<option value="koscianski">ko\u015bcia\u0144ski</option> +<option value="krotoszynski">krotoszy\u0144ski</option> +<option value="leszczynski">leszczy\u0144ski</option> +<option value="miedzychodzki">mi\u0119dzychodzki</option> +<option value="nowotomyski">nowotomyski</option> +<option value="obornicki">obornicki</option> +<option value="ostrowski">ostrowski</option> +<option value="ostrzeszowski">ostrzeszowski</option> +<option value="pilski">pilski</option> +<option value="pleszewski">pleszewski</option> +<option value="poznanski">pozna\u0144ski</option> +<option value="rawicki">rawicki</option> +<option value="slupecki">s\u0142upecki</option> +<option value="szamotulski">szamotulski</option> +<option value="sredzki">\u015bredzki</option> +<option value="sremski">\u015bremski</option> +<option value="turecki">turecki</option> +<option value="wagrowiecki">w\u0105growiecki</option> +<option value="wolsztynski">wolszty\u0144ski</option> +<option value="wrzesinski">wrzesi\u0144ski</option> +<option value="zlotowski">z\u0142otowski</option> +<option value="bialogardzki">bia\u0142ogardzki</option> +<option value="choszczenski">choszcze\u0144ski</option> +<option value="drawski">drawski</option> +<option value="goleniowski">goleniowski</option> +<option value="gryficki">gryficki</option> +<option value="gryfinski">gryfi\u0144ski</option> +<option value="kamienski">kamie\u0144ski</option> +<option value="kolobrzeski">ko\u0142obrzeski</option> +<option value="koszalinski">koszali\u0144ski</option> +<option value="lobeski">\u0142obeski</option> +<option value="mysliborski">my\u015bliborski</option> +<option value="policki">policki</option> +<option value="pyrzycki">pyrzycki</option> +<option value="slawienski">s\u0142awie\u0144ski</option> +<option value="stargardzki">stargardzki</option> +<option value="szczecinecki">szczecinecki</option> +<option value="swidwinski">\u015bwidwi\u0144ski</option> +<option value="walecki">wa\u0142ecki</option> +</select>''' + self.assertEqual(f.render('administrativeunit', 'katowice'), out) + + def test_PLPostalCodeField(self): + error_format = [u'Enter a postal code in the format XX-XXX.'] + valid = { + '41-403': '41-403', + } + invalid = { + '43--434': error_format, + } + self.assertFieldOutput(PLPostalCodeField, valid, invalid) + + def test_PLNIPField(self): + error_format = [u'Enter a tax number field (NIP) in the format XXX-XXX-XX-XX or XX-XX-XXX-XXX.'] + error_checksum = [u'Wrong checksum for the Tax Number (NIP).'] + valid = { + '64-62-414-124': '6462414124', + '646-241-41-24': '6462414124', + } + invalid = { + '43-343-234-323': error_format, + '646-241-41-23': error_checksum, + } + self.assertFieldOutput(PLNIPField, valid, invalid) + + def test_PLPESELField(self): + error_checksum = [u'Wrong checksum for the National Identification Number.'] + error_format = [u'National Identification Number consists of 11 digits.'] + valid = { + '80071610614': '80071610614', + } + invalid = { + '80071610610': error_checksum, + '80': error_format, + '800716106AA': error_format, + } + self.assertFieldOutput(PLPESELField, valid, invalid) + + def test_PLREGONField(self): + error_checksum = [u'Wrong checksum for the National Business Register Number (REGON).'] + error_format = [u'National Business Register Number (REGON) consists of 9 or 14 digits.'] + valid = { + '12345678512347': '12345678512347', + '590096454': '590096454', + } + invalid = { + '123456784': error_checksum, + '12345678412342': error_checksum, + '590096453': error_checksum, + '590096': error_format, + } + self.assertFieldOutput(PLREGONField, valid, invalid) + diff --git a/parts/django/tests/regressiontests/forms/localflavor/pt.py b/parts/django/tests/regressiontests/forms/localflavor/pt.py new file mode 100644 index 0000000..b8d784a --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/pt.py @@ -0,0 +1,32 @@ +from django.contrib.localflavor.pt.forms import PTZipCodeField, PTPhoneNumberField + +from utils import LocalFlavorTestCase + + +class PTLocalFlavorTests(LocalFlavorTestCase): + def test_PTZipCodeField(self): + error_format = [u'Enter a zip code in the format XXXX-XXX.'] + valid = { + '3030-034': '3030-034', + '1003456': '1003-456', + } + invalid = { + '2A200': error_format, + '980001': error_format, + } + self.assertFieldOutput(PTZipCodeField, valid, invalid) + + def test_PTPhoneNumberField(self): + error_format = [u'Phone numbers must have 9 digits, or start by + or 00.'] + valid = { + '917845189': '917845189', + '91 784 5189': '917845189', + '91 784 5189': '917845189', + '+351 91 111': '+35191111', + '00351873': '00351873', + } + invalid = { + '91 784 51 8': error_format, + '091 456 987 1': error_format, + } + self.assertFieldOutput(PTPhoneNumberField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/ro.py b/parts/django/tests/regressiontests/forms/localflavor/ro.py new file mode 100644 index 0000000..c3dd403 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/ro.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +from django.contrib.localflavor.ro.forms import (ROCIFField, ROCNPField, + ROCountyField, ROCountySelect, ROIBANField, ROPhoneNumberField, + ROPostalCodeField) + +from utils import LocalFlavorTestCase + + +class ROLocalFlavorTests(LocalFlavorTestCase): + def test_ROCountySelect(self): + f = ROCountySelect() + out = u'''<select name="county"> +<option value="AB">Alba</option> +<option value="AR">Arad</option> +<option value="AG">Arge\u015f</option> +<option value="BC">Bac\u0103u</option> +<option value="BH">Bihor</option> +<option value="BN">Bistri\u0163a-N\u0103s\u0103ud</option> +<option value="BT">Boto\u015fani</option> +<option value="BV">Bra\u015fov</option> +<option value="BR">Br\u0103ila</option> +<option value="B">Bucure\u015fti</option> +<option value="BZ">Buz\u0103u</option> +<option value="CS">Cara\u015f-Severin</option> +<option value="CL">C\u0103l\u0103ra\u015fi</option> +<option value="CJ" selected="selected">Cluj</option> +<option value="CT">Constan\u0163a</option> +<option value="CV">Covasna</option> +<option value="DB">D\xe2mbovi\u0163a</option> +<option value="DJ">Dolj</option> +<option value="GL">Gala\u0163i</option> +<option value="GR">Giurgiu</option> +<option value="GJ">Gorj</option> +<option value="HR">Harghita</option> +<option value="HD">Hunedoara</option> +<option value="IL">Ialomi\u0163a</option> +<option value="IS">Ia\u015fi</option> +<option value="IF">Ilfov</option> +<option value="MM">Maramure\u015f</option> +<option value="MH">Mehedin\u0163i</option> +<option value="MS">Mure\u015f</option> +<option value="NT">Neam\u0163</option> +<option value="OT">Olt</option> +<option value="PH">Prahova</option> +<option value="SM">Satu Mare</option> +<option value="SJ">S\u0103laj</option> +<option value="SB">Sibiu</option> +<option value="SV">Suceava</option> +<option value="TR">Teleorman</option> +<option value="TM">Timi\u015f</option> +<option value="TL">Tulcea</option> +<option value="VS">Vaslui</option> +<option value="VL">V\xe2lcea</option> +<option value="VN">Vrancea</option> +</select>''' + self.assertEqual(f.render('county', 'CJ'), out) + + def test_ROCIFField(self): + error_invalid = [u'Enter a valid CIF.'] + error_atmost = [u'Ensure this value has at most 10 characters (it has 11).'] + error_atleast = [u'Ensure this value has at least 2 characters (it has 1).'] + valid = { + '21694681': u'21694681', + 'RO21694681': u'21694681', + } + invalid = { + '21694680': error_invalid, + '21694680000': error_atmost, + '0': error_atleast + error_invalid, + } + self.assertFieldOutput(ROCIFField, valid, invalid) + + def test_ROCNPField(self): + error_invalid = [u'Enter a valid CNP.'] + error_atleast = [u'Ensure this value has at least 13 characters (it has 10).'] + error_atmost = [u'Ensure this value has at most 13 characters (it has 14).'] + valid = { + '1981211204489': '1981211204489', + } + invalid = { + '1981211204487': error_invalid, + '1981232204489': error_invalid, + '9981211204489': error_invalid, + '9981211209': error_atleast + error_invalid, + '19812112044891': error_atmost, + } + self.assertFieldOutput(ROCNPField, valid, invalid) + + def test_ROCountyField(self): + error_format = [u'Enter a Romanian county code or name.'] + valid = { + 'CJ': 'CJ', + 'cj': 'CJ', + u'Argeş': 'AG', + u'argeş': 'AG', + } + invalid = { + 'Arges': error_format, + } + self.assertFieldOutput(ROCountyField, valid, invalid) + + def test_ROIBANField(self): + error_invalid = [u'Enter a valid IBAN in ROXX-XXXX-XXXX-XXXX-XXXX-XXXX format'] + error_atleast = [u'Ensure this value has at least 24 characters (it has 23).'] + valid = { + 'RO56RZBR0000060003291177': 'RO56RZBR0000060003291177', + 'RO56-RZBR-0000-0600-0329-1177': 'RO56RZBR0000060003291177', + } + invalid = { + 'RO56RZBR0000060003291176': error_invalid, + 'AT61 1904 3002 3457 3201': error_invalid, + 'RO56RZBR000006000329117': error_atleast + error_invalid, + } + self.assertFieldOutput(ROIBANField, valid, invalid) + + def test_ROPhoneNumberField(self): + error_format = [u'Phone numbers must be in XXXX-XXXXXX format.'] + error_atleast = [u'Ensure this value has at least 10 characters (it has 9).'] + valid = { + '0264485936': '0264485936', + '(0264)-485936': '0264485936', + } + invalid = { + '02644859368': error_format, + '026448593': error_atleast + error_format + , + } + self.assertFieldOutput(ROPhoneNumberField, valid, invalid) + + def test_ROPostalCodeField(self): + error_atleast = [u'Ensure this value has at least 6 characters (it has 5).'] + error_atmost = [u'Ensure this value has at most 6 characters (it has 7).'] + error_invalid = [u'Enter a valid postal code in the format XXXXXX'] + + valid = { + '400473': '400473', + } + invalid = { + '40047': error_atleast + error_invalid, + '4004731': error_atmost + error_invalid, + } + self.assertFieldOutput(ROPostalCodeField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/se.py b/parts/django/tests/regressiontests/forms/localflavor/se.py new file mode 100644 index 0000000..316a612 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/se.py @@ -0,0 +1,331 @@ +# -*- coding: utf-8 -*- +# Tests for the contrib/localflavor/se form fields. + +tests = r""" +# Monkey-patch datetime.date +>>> import datetime +>>> class MockDate(datetime.date): +... def today(cls): +... return datetime.date(2008, 5, 14) +... today = classmethod(today) +... +>>> olddate = datetime.date +>>> datetime.date = MockDate +>>> datetime.date.today() == olddate(2008, 5, 14) +True + +# SECountySelect ##################################################### +>>> from django.contrib.localflavor.se.forms import SECountySelect + +>>> w = SECountySelect() +>>> w.render('swedish_county', 'E') +u'<select name="swedish_county">\n<option value="AB">Stockholm</option>\n<option value="AC">V\xe4sterbotten</option>\n<option value="BD">Norrbotten</option>\n<option value="C">Uppsala</option>\n<option value="D">S\xf6dermanland</option>\n<option value="E" selected="selected">\xd6sterg\xf6tland</option>\n<option value="F">J\xf6nk\xf6ping</option>\n<option value="G">Kronoberg</option>\n<option value="H">Kalmar</option>\n<option value="I">Gotland</option>\n<option value="K">Blekinge</option>\n<option value="M">Sk\xe5ne</option>\n<option value="N">Halland</option>\n<option value="O">V\xe4stra G\xf6taland</option>\n<option value="S">V\xe4rmland</option>\n<option value="T">\xd6rebro</option>\n<option value="U">V\xe4stmanland</option>\n<option value="W">Dalarna</option>\n<option value="X">G\xe4vleborg</option>\n<option value="Y">V\xe4sternorrland</option>\n<option value="Z">J\xe4mtland</option>\n</select>' + +# SEOrganisationNumberField ####################################### + +>>> from django.contrib.localflavor.se.forms import SEOrganisationNumberField + +>>> f = SEOrganisationNumberField() + +# Ordinary personal identity numbers for sole proprietors +# The same rules as for SEPersonalIdentityField applies here +>>> f.clean('870512-1989') +u'198705121989' +>>> f.clean('19870512-1989') +u'198705121989' +>>> f.clean('870512-2128') +u'198705122128' +>>> f.clean('081015-6315') +u'190810156315' +>>> f.clean('081015+6315') +u'180810156315' +>>> f.clean('0810156315') +u'190810156315' + +>>> f.clean('081015 6315') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish organisation number.'] +>>> f.clean('950231-4496') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish organisation number.'] +>>> f.clean('6914104499') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish organisation number.'] +>>> f.clean('950d314496') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish organisation number.'] +>>> f.clean('invalid!!!') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish organisation number.'] +>>> f.clean('870514-1111') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish organisation number.'] + + +# Empty values +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +# Co-ordination number checking +# Co-ordination numbers are not valid organisation numbers +>>> f.clean('870574-1315') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish organisation number.'] + +>>> f.clean('870573-1311') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish organisation number.'] + +# Test some different organisation numbers +>>> f.clean('556074-7569') # IKEA Linköping +u'5560747569' + +>>> f.clean('556074-3089') # Volvo Personvagnar +u'5560743089' + +>>> f.clean('822001-5476') # LJS (organisation) +u'8220015476' + +>>> f.clean('8220015476') # LJS (organisation) +u'8220015476' + +>>> f.clean('2120000449') # Katedralskolan Linköping (school) +u'2120000449' + +# Faux organisation number, which tests that the checksum can be 0 +>>> f.clean('232518-5060') +u'2325185060' + +>>> f.clean('556074+3089') # Volvo Personvagnar, bad format +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish organisation number.'] + + +# Invalid checksum +>>> f.clean('2120000441') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish organisation number.'] + +# Valid checksum but invalid organisation type +f.clean('1120000441') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish organisation number.'] + +# Empty values with required=False +>>> f = SEOrganisationNumberField(required=False) + +>>> f.clean(None) +u'' + +>>> f.clean('') +u'' + + +# SEPersonalIdentityNumberField ####################################### + +>>> from django.contrib.localflavor.se.forms import SEPersonalIdentityNumberField + +>>> f = SEPersonalIdentityNumberField() + +# Valid id numbers +>>> f.clean('870512-1989') +u'198705121989' + +>>> f.clean('870512-2128') +u'198705122128' + +>>> f.clean('19870512-1989') +u'198705121989' + +>>> f.clean('198705121989') +u'198705121989' + +>>> f.clean('081015-6315') +u'190810156315' + +>>> f.clean('0810156315') +u'190810156315' + +# This is a "special-case" in the checksum calculation, +# where the sum is divisible by 10 (the checksum digit == 0) +>>> f.clean('8705141060') +u'198705141060' + +# + means that the person is older than 100 years +>>> f.clean('081015+6315') +u'180810156315' + +# Bogus values +>>> f.clean('081015 6315') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish personal identity number.'] + +>>> f.clean('950d314496') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish personal identity number.'] + +>>> f.clean('invalid!!!') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish personal identity number.'] + + +# Invalid dates + +# February 31st does not exist +>>> f.clean('950231-4496') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish personal identity number.'] + +# Month 14 does not exist +>>> f.clean('6914104499') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish personal identity number.'] + +# There are no Swedish personal id numbers where year < 1800 +>>> f.clean('17430309-7135') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish personal identity number.'] + +# Invalid checksum +>>> f.clean('870514-1111') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish personal identity number.'] + +# Empty values +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +# Co-ordination number checking +>>> f.clean('870574-1315') +u'198705741315' + +>>> f.clean('870574+1315') +u'188705741315' + +>>> f.clean('198705741315') +u'198705741315' + +# Co-ordination number with bad checksum +>>> f.clean('870573-1311') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish personal identity number.'] + + +# Check valid co-ordination numbers, that should not be accepted +# because of coordination_number=False +>>> f = SEPersonalIdentityNumberField(coordination_number=False) + +>>> f.clean('870574-1315') +Traceback (most recent call last): +... +ValidationError: [u'Co-ordination numbers are not allowed.'] + +>>> f.clean('870574+1315') +Traceback (most recent call last): +... +ValidationError: [u'Co-ordination numbers are not allowed.'] + +>>> f.clean('8705741315') +Traceback (most recent call last): +... +ValidationError: [u'Co-ordination numbers are not allowed.'] + +# Invalid co-ordination numbers should be treated as invalid, and not +# as co-ordination numbers +>>> f.clean('870573-1311') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Swedish personal identity number.'] + +# Empty values with required=False +>>> f = SEPersonalIdentityNumberField(required=False) + +>>> f.clean(None) +u'' + +>>> f.clean('') +u'' + +# SEPostalCodeField ############################################### +>>> from django.contrib.localflavor.se.forms import SEPostalCodeField +>>> f = SEPostalCodeField() +>>> +Postal codes can have spaces +>>> f.clean('589 37') +u'58937' + +... but the dont have to +>>> f.clean('58937') +u'58937' +>>> f.clean('abcasfassadf') +Traceback (most recent call last): +... +ValidationError: [u'Enter a Swedish postal code in the format XXXXX.'] + +# Only one space is allowed for separation +>>> f.clean('589 37') +Traceback (most recent call last): +... +ValidationError: [u'Enter a Swedish postal code in the format XXXXX.'] + +# The postal code must not start with 0 +>>> f.clean('01234') +Traceback (most recent call last): +... +ValidationError: [u'Enter a Swedish postal code in the format XXXXX.'] + +# Empty values +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +# Empty values, required=False +>>> f = SEPostalCodeField(required=False) +>>> f.clean('') +u'' +>>> f.clean(None) +u'' + +# Revert the monkey patching +>>> datetime.date = olddate + +""" diff --git a/parts/django/tests/regressiontests/forms/localflavor/sk.py b/parts/django/tests/regressiontests/forms/localflavor/sk.py new file mode 100644 index 0000000..a5bed6f --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/sk.py @@ -0,0 +1,116 @@ +from django.contrib.localflavor.sk.forms import (SKRegionSelect, + SKPostalCodeField, SKDistrictSelect) + +from utils import LocalFlavorTestCase + + +class SKLocalFlavorTests(LocalFlavorTestCase): + def test_SKRegionSelect(self): + f = SKRegionSelect() + out = u'''<select name="regions"> +<option value="BB">Banska Bystrica region</option> +<option value="BA">Bratislava region</option> +<option value="KE">Kosice region</option> +<option value="NR">Nitra region</option> +<option value="PO">Presov region</option> +<option value="TN">Trencin region</option> +<option value="TT" selected="selected">Trnava region</option> +<option value="ZA">Zilina region</option> +</select>''' + self.assertEqual(f.render('regions', 'TT'), out) + + def test_SKDistrictSelect(self): + f = SKDistrictSelect() + out = u'''<select name="Districts"> +<option value="BB">Banska Bystrica</option> +<option value="BS">Banska Stiavnica</option> +<option value="BJ">Bardejov</option> +<option value="BN">Banovce nad Bebravou</option> +<option value="BR">Brezno</option> +<option value="BA1">Bratislava I</option> +<option value="BA2">Bratislava II</option> +<option value="BA3">Bratislava III</option> +<option value="BA4">Bratislava IV</option> +<option value="BA5">Bratislava V</option> +<option value="BY">Bytca</option> +<option value="CA">Cadca</option> +<option value="DT">Detva</option> +<option value="DK">Dolny Kubin</option> +<option value="DS">Dunajska Streda</option> +<option value="GA">Galanta</option> +<option value="GL">Gelnica</option> +<option value="HC">Hlohovec</option> +<option value="HE">Humenne</option> +<option value="IL">Ilava</option> +<option value="KK">Kezmarok</option> +<option value="KN">Komarno</option> +<option value="KE1">Kosice I</option> +<option value="KE2">Kosice II</option> +<option value="KE3">Kosice III</option> +<option value="KE4">Kosice IV</option> +<option value="KEO">Kosice - okolie</option> +<option value="KA">Krupina</option> +<option value="KM">Kysucke Nove Mesto</option> +<option value="LV">Levice</option> +<option value="LE">Levoca</option> +<option value="LM">Liptovsky Mikulas</option> +<option value="LC">Lucenec</option> +<option value="MA">Malacky</option> +<option value="MT">Martin</option> +<option value="ML">Medzilaborce</option> +<option value="MI">Michalovce</option> +<option value="MY">Myjava</option> +<option value="NO">Namestovo</option> +<option value="NR">Nitra</option> +<option value="NM">Nove Mesto nad Vahom</option> +<option value="NZ">Nove Zamky</option> +<option value="PE">Partizanske</option> +<option value="PK">Pezinok</option> +<option value="PN">Piestany</option> +<option value="PT">Poltar</option> +<option value="PP">Poprad</option> +<option value="PB">Povazska Bystrica</option> +<option value="PO">Presov</option> +<option value="PD">Prievidza</option> +<option value="PU">Puchov</option> +<option value="RA">Revuca</option> +<option value="RS">Rimavska Sobota</option> +<option value="RV">Roznava</option> +<option value="RK" selected="selected">Ruzomberok</option> +<option value="SB">Sabinov</option> +<option value="SC">Senec</option> +<option value="SE">Senica</option> +<option value="SI">Skalica</option> +<option value="SV">Snina</option> +<option value="SO">Sobrance</option> +<option value="SN">Spisska Nova Ves</option> +<option value="SL">Stara Lubovna</option> +<option value="SP">Stropkov</option> +<option value="SK">Svidnik</option> +<option value="SA">Sala</option> +<option value="TO">Topolcany</option> +<option value="TV">Trebisov</option> +<option value="TN">Trencin</option> +<option value="TT">Trnava</option> +<option value="TR">Turcianske Teplice</option> +<option value="TS">Tvrdosin</option> +<option value="VK">Velky Krtis</option> +<option value="VT">Vranov nad Toplou</option> +<option value="ZM">Zlate Moravce</option> +<option value="ZV">Zvolen</option> +<option value="ZC">Zarnovica</option> +<option value="ZH">Ziar nad Hronom</option> +<option value="ZA">Zilina</option> +</select>''' + self.assertEqual(f.render('Districts', 'RK'), out) + + def test_SKPostalCodeField(self): + error_format = [u'Enter a postal code in the format XXXXX or XXX XX.'] + valid = { + '91909': '91909', + '917 01': '91701', + } + invalid = { + '84545x': error_format, + } + self.assertFieldOutput(SKPostalCodeField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/uk.py b/parts/django/tests/regressiontests/forms/localflavor/uk.py new file mode 100644 index 0000000..6fd536f --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/uk.py @@ -0,0 +1,30 @@ +from django.contrib.localflavor.uk.forms import UKPostcodeField + +from utils import LocalFlavorTestCase + + +class UKLocalFlavorTests(LocalFlavorTestCase): + def test_UKPostcodeField(self): + error_invalid = [u'Enter a valid postcode.'] + valid = { + 'BT32 4PX': 'BT32 4PX', + 'GIR 0AA': 'GIR 0AA', + 'BT324PX': 'BT32 4PX', + ' so11aa ': 'SO1 1AA', + ' so1 1aa ': 'SO1 1AA', + 'G2 3wt': 'G2 3WT', + 'EC1A 1BB': 'EC1A 1BB', + 'Ec1a1BB': 'EC1A 1BB', + } + invalid = { + '1NV 4L1D': error_invalid, + '1NV4L1D': error_invalid, + ' b0gUS': error_invalid, + } + self.assertFieldOutput(UKPostcodeField, valid, invalid) + valid = {} + invalid = { + '1NV 4L1D': [u'Enter a bloody postcode!'], + } + kwargs = {'error_messages': {'invalid': 'Enter a bloody postcode!'}} + self.assertFieldOutput(UKPostcodeField, valid, invalid, field_kwargs=kwargs) diff --git a/parts/django/tests/regressiontests/forms/localflavor/us.py b/parts/django/tests/regressiontests/forms/localflavor/us.py new file mode 100644 index 0000000..65dd1bb --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/us.py @@ -0,0 +1,126 @@ +from django.contrib.localflavor.us.forms import (USZipCodeField, + USPhoneNumberField, USStateField, USStateSelect, USSocialSecurityNumberField) + +from utils import LocalFlavorTestCase + + +class USLocalFlavorTests(LocalFlavorTestCase): + def test_USStateSelect(self): + f = USStateSelect() + out = u'''<select name="state"> +<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">Georgia</option> +<option value="GU">Guam</option> +<option value="HI">Hawaii</option> +<option value="ID">Idaho</option> +<option value="IL" selected="selected">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.assertEquals(f.render('state', 'IL'), out) + + def test_USZipCodeField(self): + error_format = [u'Enter a zip code in the format XXXXX or XXXXX-XXXX.'] + valid = { + '60606': '60606', + 60606: '60606', + '04000': '04000', + '60606-1234': '60606-1234', + } + invalid = { + '4000': error_format, + '6060-1234': error_format, + '60606-': error_format, + } + self.assertFieldOutput(USZipCodeField, valid, invalid) + + def test_USPhoneNumberField(self): + error_format = [u'Phone numbers must be in XXX-XXX-XXXX format.'] + valid = { + '312-555-1212': '312-555-1212', + '3125551212': '312-555-1212', + '312 555-1212': '312-555-1212', + '(312) 555-1212': '312-555-1212', + '312 555 1212': '312-555-1212', + '312.555.1212': '312-555-1212', + '312.555-1212': '312-555-1212', + ' (312) 555.1212 ': '312-555-1212', + } + invalid = { + '555-1212': error_format, + '312-55-1212': error_format, + } + self.assertFieldOutput(USPhoneNumberField, valid, invalid) + + def test_USStateField(self): + error_invalid = [u'Enter a U.S. state or territory.'] + valid = { + 'il': 'IL', + 'IL': 'IL', + 'illinois': 'IL', + ' illinois ': 'IL', + } + invalid = { + 60606: error_invalid, + } + self.assertFieldOutput(USStateField, valid, invalid) + + def test_USSocialSecurityNumberField(self): + error_invalid = [u'Enter a valid U.S. Social Security number in XXX-XX-XXXX format.'] + + valid = { + '987-65-4330': '987-65-4330', + '987654330': '987-65-4330', + } + invalid = { + '078-05-1120': error_invalid, + } + self.assertFieldOutput(USSocialSecurityNumberField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavor/utils.py b/parts/django/tests/regressiontests/forms/localflavor/utils.py new file mode 100644 index 0000000..a10258f --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/utils.py @@ -0,0 +1,51 @@ +from unittest import TestCase + +from django.core.exceptions import ValidationError +from django.core.validators import EMPTY_VALUES + + +class LocalFlavorTestCase(TestCase): + def assertFieldOutput(self, fieldclass, valid, invalid, field_args=[], + field_kwargs={}, empty_value=u''): + """Asserts that a field behaves correctly with various inputs. + + Args: + fieldclass: the class of the field to be tested. + valid: a dictionary mapping valid inputs to their expected + cleaned values. + invalid: a dictionary mapping invalid inputs to one or more + raised error messages. + fieldargs: the args passed to instantiate the field + fieldkwargs: the kwargs passed to instantiate the field + emptyvalue: the expected clean output for inputs in EMPTY_VALUES + """ + required = fieldclass(*field_args, **field_kwargs) + optional = fieldclass(*field_args, **dict(field_kwargs, required=False)) + # test valid inputs + for input, output in valid.items(): + self.assertEqual(required.clean(input), output) + self.assertEqual(optional.clean(input), output) + # test invalid inputs + for input, errors in invalid.items(): + try: + required.clean(input) + except ValidationError, e: + self.assertEqual(errors, e.messages) + else: + self.fail() + try: + optional.clean(input) + except ValidationError, e: + self.assertEqual(errors, e.messages) + else: + self.fail() + # test required inputs + error_required = [u'This field is required.'] + for val in EMPTY_VALUES: + try: + required.clean(val) + except ValidationError, e: + self.assertEqual(error_required, e.messages) + else: + self.fail() + self.assertEqual(optional.clean(val), empty_value) diff --git a/parts/django/tests/regressiontests/forms/localflavor/uy.py b/parts/django/tests/regressiontests/forms/localflavor/uy.py new file mode 100644 index 0000000..2b9e134 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/uy.py @@ -0,0 +1,52 @@ +from django.contrib.localflavor.uy.forms import UYDepartamentSelect, UYCIField +from django.contrib.localflavor.uy.util import get_validation_digit + +from utils import LocalFlavorTestCase + + +class UYLocalFlavorTests(LocalFlavorTestCase): + def test_UYDepartmentSelect(self): + f = UYDepartamentSelect() + out = u'''<select name="departamentos"> +<option value="G">Artigas</option> +<option value="A">Canelones</option> +<option value="E">Cerro Largo</option> +<option value="L">Colonia</option> +<option value="Q">Durazno</option> +<option value="N">Flores</option> +<option value="O">Florida</option> +<option value="P">Lavalleja</option> +<option value="B">Maldonado</option> +<option value="S" selected="selected">Montevideo</option> +<option value="I">Paysand\xfa</option> +<option value="J">R\xedo Negro</option> +<option value="F">Rivera</option> +<option value="C">Rocha</option> +<option value="H">Salto</option> +<option value="M">San Jos\xe9</option> +<option value="K">Soriano</option> +<option value="R">Tacuaremb\xf3</option> +<option value="D">Treinta y Tres</option> +</select>''' + self.assertEqual(f.render('departamentos', 'S'), out) + + def test_UYCIField(self): + error_format = [u'Enter a valid CI number in X.XXX.XXX-X,XXXXXXX-X or XXXXXXXX format.'] + error_invalid = [u'Enter a valid CI number.'] + valid = { + '4098053': '4098053', + '409805-3': '409805-3', + '409.805-3': '409.805-3', + '10054112': '10054112', + '1005411-2': '1005411-2', + '1.005.411-2': '1.005.411-2', + } + invalid = { + 'foo': [u'Enter a valid CI number in X.XXX.XXX-X,XXXXXXX-X or XXXXXXXX format.'], + '409805-2': [u'Enter a valid CI number.'], + '1.005.411-5': [u'Enter a valid CI number.'], + } + self.assertFieldOutput(UYCIField, valid, invalid) + self.assertEqual(get_validation_digit(409805), 3) + self.assertEqual(get_validation_digit(1005411), 2) + diff --git a/parts/django/tests/regressiontests/forms/localflavor/za.py b/parts/django/tests/regressiontests/forms/localflavor/za.py new file mode 100644 index 0000000..c912421 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavor/za.py @@ -0,0 +1,29 @@ +from django.contrib.localflavor.za.forms import ZAIDField, ZAPostCodeField + +from utils import LocalFlavorTestCase + + +class ZALocalFlavorTests(LocalFlavorTestCase): + def test_ZAIDField(self): + error_invalid = [u'Enter a valid South African ID number'] + valid = { + '0002290001003': '0002290001003', + '000229 0001 003': '0002290001003', + } + invalid = { + '0102290001001': error_invalid, + '811208': error_invalid, + '0002290001004': error_invalid, + } + self.assertFieldOutput(ZAIDField, valid, invalid) + + def test_ZAPostCodeField(self): + error_invalid = [u'Enter a valid South African postal code'] + valid = { + '0000': '0000', + } + invalid = { + 'abcd': error_invalid, + ' 7530': error_invalid, + } + self.assertFieldOutput(ZAPostCodeField, valid, invalid) diff --git a/parts/django/tests/regressiontests/forms/localflavortests.py b/parts/django/tests/regressiontests/forms/localflavortests.py new file mode 100644 index 0000000..2582656 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/localflavortests.py @@ -0,0 +1,37 @@ +from localflavor.cz import tests as localflavor_cz_tests +from localflavor.se import tests as localflavor_se_tests + +from localflavor.ar import ARLocalFlavorTests +from localflavor.at import ATLocalFlavorTests +from localflavor.au import AULocalFlavorTests +from localflavor.at import ATLocalFlavorTests +from localflavor.br import BRLocalFlavorTests +from localflavor.ca import CALocalFlavorTests +from localflavor.ch import CHLocalFlavorTests +from localflavor.cl import CLLocalFlavorTests +from localflavor.de import DELocalFlavorTests +from localflavor.es import ESLocalFlavorTests +from localflavor.fi import FILocalFlavorTests +from localflavor.fr import FRLocalFlavorTests +from localflavor.generic import GenericLocalFlavorTests +from localflavor.id import IDLocalFlavorTests +from localflavor.ie import IELocalFlavorTests +from localflavor.is_ import ISLocalFlavorTests +from localflavor.it import ITLocalFlavorTests +from localflavor.jp import JPLocalFlavorTests +from localflavor.kw import KWLocalFlavorTests +from localflavor.nl import NLLocalFlavorTests +from localflavor.pl import PLLocalFlavorTests +from localflavor.pt import PTLocalFlavorTests +from localflavor.ro import ROLocalFlavorTests +from localflavor.sk import SKLocalFlavorTests +from localflavor.uk import UKLocalFlavorTests +from localflavor.us import USLocalFlavorTests +from localflavor.uy import UYLocalFlavorTests +from localflavor.za import ZALocalFlavorTests + + +__test__ = { + 'localflavor_cz_tests': localflavor_cz_tests, + 'localflavor_se_tests': localflavor_se_tests, +} diff --git a/parts/django/tests/regressiontests/forms/models.py b/parts/django/tests/regressiontests/forms/models.py new file mode 100644 index 0000000..203980c --- /dev/null +++ b/parts/django/tests/regressiontests/forms/models.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +import datetime +import tempfile + +from django.db import models +from django.core.files.storage import FileSystemStorage + + +temp_storage_location = tempfile.mkdtemp() +temp_storage = FileSystemStorage(location=temp_storage_location) + + +class BoundaryModel(models.Model): + positive_integer = models.PositiveIntegerField(null=True, blank=True) + + +callable_default_value = 0 +def callable_default(): + global callable_default_value + callable_default_value = callable_default_value + 1 + return callable_default_value + + +class Defaults(models.Model): + name = models.CharField(max_length=255, default='class default value') + def_date = models.DateField(default = datetime.date(1980, 1, 1)) + value = models.IntegerField(default=42) + callable_default = models.IntegerField(default=callable_default) + + +class ChoiceModel(models.Model): + """For ModelChoiceField and ModelMultipleChoiceField tests.""" + name = models.CharField(max_length=10) + + +class ChoiceOptionModel(models.Model): + """Destination for ChoiceFieldModel's ForeignKey. + 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.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 FileModel(models.Model): + file = models.FileField(storage=temp_storage, upload_to='tests') + + +class Group(models.Model): + name = models.CharField(max_length=10) + + def __unicode__(self): + return u'%s' % self.name + + +class Cheese(models.Model): + name = models.CharField(max_length=100) diff --git a/parts/django/tests/regressiontests/forms/tests/__init__.py b/parts/django/tests/regressiontests/forms/tests/__init__.py new file mode 100644 index 0000000..95df6a1 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/__init__.py @@ -0,0 +1,43 @@ +from error_messages import * +from extra import * +from fields import FieldsTests +from forms import * +from formsets import * +from input_formats import * +from media import * +from models import * +from regressions import * +from util import * +from validators import TestFieldWithValidators +from widgets import * + +from regressiontests.forms.localflavortests import ( + __test__, + ARLocalFlavorTests, + ATLocalFlavorTests, + AULocalFlavorTests, + BRLocalFlavorTests, + CALocalFlavorTests, + CHLocalFlavorTests, + CLLocalFlavorTests, + DELocalFlavorTests, + ESLocalFlavorTests, + FILocalFlavorTests, + FRLocalFlavorTests, + GenericLocalFlavorTests, + IDLocalFlavorTests, + IELocalFlavorTests, + ISLocalFlavorTests, + ITLocalFlavorTests, + JPLocalFlavorTests, + KWLocalFlavorTests, + NLLocalFlavorTests, + PLLocalFlavorTests, + PTLocalFlavorTests, + ROLocalFlavorTests, + SKLocalFlavorTests, + UKLocalFlavorTests, + USLocalFlavorTests, + UYLocalFlavorTests, + ZALocalFlavorTests, +) diff --git a/parts/django/tests/regressiontests/forms/tests/error_messages.py b/parts/django/tests/regressiontests/forms/tests/error_messages.py new file mode 100644 index 0000000..39d88a6 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/error_messages.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +import unittest +from django.core.files.uploadedfile import SimpleUploadedFile +from django.forms import * +from django.test import TestCase +from django.utils.safestring import mark_safe + +class AssertFormErrorsMixin(object): + def assertFormErrors(self, expected, the_callable, *args, **kwargs): + try: + the_callable(*args, **kwargs) + self.fail("Testing the 'clean' method on %s failed to raise a ValidationError.") + except ValidationError, e: + self.assertEqual(e.messages, expected) + + +class FormsErrorMessagesTestCase(unittest.TestCase, AssertFormErrorsMixin): + def test_charfield(self): + e = { + 'required': 'REQUIRED', + 'min_length': 'LENGTH %(show_value)s, MIN LENGTH %(limit_value)s', + 'max_length': 'LENGTH %(show_value)s, MAX LENGTH %(limit_value)s', + } + f = CharField(min_length=5, max_length=10, error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'LENGTH 4, MIN LENGTH 5'], f.clean, '1234') + self.assertFormErrors([u'LENGTH 11, MAX LENGTH 10'], f.clean, '12345678901') + + def test_integerfield(self): + e = { + 'required': 'REQUIRED', + 'invalid': 'INVALID', + 'min_value': 'MIN VALUE IS %(limit_value)s', + 'max_value': 'MAX VALUE IS %(limit_value)s', + } + f = IntegerField(min_value=5, max_value=10, error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID'], f.clean, 'abc') + self.assertFormErrors([u'MIN VALUE IS 5'], f.clean, '4') + self.assertFormErrors([u'MAX VALUE IS 10'], f.clean, '11') + + def test_floatfield(self): + e = { + 'required': 'REQUIRED', + 'invalid': 'INVALID', + 'min_value': 'MIN VALUE IS %(limit_value)s', + 'max_value': 'MAX VALUE IS %(limit_value)s', + } + f = FloatField(min_value=5, max_value=10, error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID'], f.clean, 'abc') + self.assertFormErrors([u'MIN VALUE IS 5'], f.clean, '4') + self.assertFormErrors([u'MAX VALUE IS 10'], f.clean, '11') + + def test_decimalfield(self): + e = { + 'required': 'REQUIRED', + 'invalid': 'INVALID', + 'min_value': 'MIN VALUE IS %(limit_value)s', + 'max_value': 'MAX VALUE IS %(limit_value)s', + 'max_digits': 'MAX DIGITS IS %s', + 'max_decimal_places': 'MAX DP IS %s', + 'max_whole_digits': 'MAX DIGITS BEFORE DP IS %s', + } + f = DecimalField(min_value=5, max_value=10, error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID'], f.clean, 'abc') + self.assertFormErrors([u'MIN VALUE IS 5'], f.clean, '4') + self.assertFormErrors([u'MAX VALUE IS 10'], f.clean, '11') + + f2 = DecimalField(max_digits=4, decimal_places=2, error_messages=e) + self.assertFormErrors([u'MAX DIGITS IS 4'], f2.clean, '123.45') + self.assertFormErrors([u'MAX DP IS 2'], f2.clean, '1.234') + self.assertFormErrors([u'MAX DIGITS BEFORE DP IS 2'], f2.clean, '123.4') + + def test_datefield(self): + e = { + 'required': 'REQUIRED', + 'invalid': 'INVALID', + } + f = DateField(error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID'], f.clean, 'abc') + + def test_timefield(self): + e = { + 'required': 'REQUIRED', + 'invalid': 'INVALID', + } + f = TimeField(error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID'], f.clean, 'abc') + + def test_datetimefield(self): + e = { + 'required': 'REQUIRED', + 'invalid': 'INVALID', + } + f = DateTimeField(error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID'], f.clean, 'abc') + + def test_regexfield(self): + e = { + 'required': 'REQUIRED', + 'invalid': 'INVALID', + 'min_length': 'LENGTH %(show_value)s, MIN LENGTH %(limit_value)s', + 'max_length': 'LENGTH %(show_value)s, MAX LENGTH %(limit_value)s', + } + f = RegexField(r'^\d+$', min_length=5, max_length=10, error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID'], f.clean, 'abcde') + self.assertFormErrors([u'LENGTH 4, MIN LENGTH 5'], f.clean, '1234') + self.assertFormErrors([u'LENGTH 11, MAX LENGTH 10'], f.clean, '12345678901') + + def test_emailfield(self): + e = { + 'required': 'REQUIRED', + 'invalid': 'INVALID', + 'min_length': 'LENGTH %(show_value)s, MIN LENGTH %(limit_value)s', + 'max_length': 'LENGTH %(show_value)s, MAX LENGTH %(limit_value)s', + } + f = EmailField(min_length=8, max_length=10, error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID'], f.clean, 'abcdefgh') + self.assertFormErrors([u'LENGTH 7, MIN LENGTH 8'], f.clean, 'a@b.com') + self.assertFormErrors([u'LENGTH 11, MAX LENGTH 10'], f.clean, 'aye@bee.com') + + def test_filefield(self): + e = { + 'required': 'REQUIRED', + 'invalid': 'INVALID', + 'missing': 'MISSING', + 'empty': 'EMPTY FILE', + } + f = FileField(error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID'], f.clean, 'abc') + self.assertFormErrors([u'EMPTY FILE'], f.clean, SimpleUploadedFile('name', None)) + self.assertFormErrors([u'EMPTY FILE'], f.clean, SimpleUploadedFile('name', '')) + + def test_urlfield(self): + e = { + 'required': 'REQUIRED', + 'invalid': 'INVALID', + 'invalid_link': 'INVALID LINK', + } + f = URLField(verify_exists=True, error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID'], f.clean, 'abc.c') + self.assertFormErrors([u'INVALID LINK'], f.clean, 'http://www.broken.djangoproject.com') + + def test_booleanfield(self): + e = { + 'required': 'REQUIRED', + } + f = BooleanField(error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + + def test_choicefield(self): + e = { + 'required': 'REQUIRED', + 'invalid_choice': '%(value)s IS INVALID CHOICE', + } + f = ChoiceField(choices=[('a', 'aye')], error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'b IS INVALID CHOICE'], f.clean, 'b') + + def test_multiplechoicefield(self): + e = { + 'required': 'REQUIRED', + 'invalid_choice': '%(value)s IS INVALID CHOICE', + 'invalid_list': 'NOT A LIST', + } + f = MultipleChoiceField(choices=[('a', 'aye')], error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'NOT A LIST'], f.clean, 'b') + self.assertFormErrors([u'b IS INVALID CHOICE'], f.clean, ['b']) + + def test_splitdatetimefield(self): + e = { + 'required': 'REQUIRED', + 'invalid_date': 'INVALID DATE', + 'invalid_time': 'INVALID TIME', + } + f = SplitDateTimeField(error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID DATE', u'INVALID TIME'], f.clean, ['a', 'b']) + + def test_ipaddressfield(self): + e = { + 'required': 'REQUIRED', + 'invalid': 'INVALID IP ADDRESS', + } + f = IPAddressField(error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID IP ADDRESS'], f.clean, '127.0.0') + + def test_subclassing_errorlist(self): + class TestForm(Form): + first_name = CharField() + last_name = CharField() + birthday = DateField() + + def clean(self): + raise ValidationError("I like to be awkward.") + + class CustomErrorList(util.ErrorList): + def __unicode__(self): + return self.as_divs() + + def as_divs(self): + if not self: return u'' + return mark_safe(u'<div class="error">%s</div>' % ''.join([u'<p>%s</p>' % e for e in self])) + + # This form should print errors the default way. + form1 = TestForm({'first_name': 'John'}) + self.assertEqual(str(form1['last_name'].errors), '<ul class="errorlist"><li>This field is required.</li></ul>') + self.assertEqual(str(form1.errors['__all__']), '<ul class="errorlist"><li>I like to be awkward.</li></ul>') + + # This one should wrap error groups in the customized way. + form2 = TestForm({'first_name': 'John'}, error_class=CustomErrorList) + self.assertEqual(str(form2['last_name'].errors), '<div class="error"><p>This field is required.</p></div>') + self.assertEqual(str(form2.errors['__all__']), '<div class="error"><p>I like to be awkward.</p></div>') + + +class ModelChoiceFieldErrorMessagesTestCase(TestCase, AssertFormErrorsMixin): + def test_modelchoicefield(self): + # Create choices for the model choice field tests below. + from regressiontests.forms.models import ChoiceModel + c1 = ChoiceModel.objects.create(pk=1, name='a') + c2 = ChoiceModel.objects.create(pk=2, name='b') + c3 = ChoiceModel.objects.create(pk=3, name='c') + + # ModelChoiceField + e = { + 'required': 'REQUIRED', + 'invalid_choice': 'INVALID CHOICE', + } + f = ModelChoiceField(queryset=ChoiceModel.objects.all(), error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'INVALID CHOICE'], f.clean, '4') + + # ModelMultipleChoiceField + e = { + 'required': 'REQUIRED', + 'invalid_choice': '%s IS INVALID CHOICE', + 'list': 'NOT A LIST OF VALUES', + } + f = ModelMultipleChoiceField(queryset=ChoiceModel.objects.all(), error_messages=e) + self.assertFormErrors([u'REQUIRED'], f.clean, '') + self.assertFormErrors([u'NOT A LIST OF VALUES'], f.clean, '3') + self.assertFormErrors([u'4 IS INVALID CHOICE'], f.clean, ['4']) diff --git a/parts/django/tests/regressiontests/forms/tests/extra.py b/parts/django/tests/regressiontests/forms/tests/extra.py new file mode 100644 index 0000000..bcdf4dd --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/extra.py @@ -0,0 +1,610 @@ +# -*- coding: utf-8 -*- +import datetime +from decimal import Decimal +import re +import time +import unittest +from django.conf import settings +from django.forms import * +from django.forms.extras import SelectDateWidget +from django.forms.util import ErrorList +from django.test import TestCase +from django.utils import translation +from django.utils.encoding import force_unicode +from django.utils.encoding import smart_unicode +from error_messages import AssertFormErrorsMixin + + +class FormsExtraTestCase(unittest.TestCase, AssertFormErrorsMixin): + ############### + # Extra stuff # + ############### + + # The forms library comes with some extra, higher-level Field and Widget + def test_selectdate(self): + w = SelectDateWidget(years=('2007','2008','2009','2010','2011','2012','2013','2014','2015','2016')) + self.assertEqual(w.render('mydate', ''), """<select name="mydate_month" id="id_mydate_month"> +<option value="0">---</option> +<option value="1">January</option> +<option value="2">February</option> +<option value="3">March</option> +<option value="4">April</option> +<option value="5">May</option> +<option value="6">June</option> +<option value="7">July</option> +<option value="8">August</option> +<option value="9">September</option> +<option value="10">October</option> +<option value="11">November</option> +<option value="12">December</option> +</select> +<select name="mydate_day" id="id_mydate_day"> +<option value="0">---</option> +<option value="1">1</option> +<option value="2">2</option> +<option value="3">3</option> +<option value="4">4</option> +<option value="5">5</option> +<option value="6">6</option> +<option value="7">7</option> +<option value="8">8</option> +<option value="9">9</option> +<option value="10">10</option> +<option value="11">11</option> +<option value="12">12</option> +<option value="13">13</option> +<option value="14">14</option> +<option value="15">15</option> +<option value="16">16</option> +<option value="17">17</option> +<option value="18">18</option> +<option value="19">19</option> +<option value="20">20</option> +<option value="21">21</option> +<option value="22">22</option> +<option value="23">23</option> +<option value="24">24</option> +<option value="25">25</option> +<option value="26">26</option> +<option value="27">27</option> +<option value="28">28</option> +<option value="29">29</option> +<option value="30">30</option> +<option value="31">31</option> +</select> +<select name="mydate_year" id="id_mydate_year"> +<option value="0">---</option> +<option value="2007">2007</option> +<option value="2008">2008</option> +<option value="2009">2009</option> +<option value="2010">2010</option> +<option value="2011">2011</option> +<option value="2012">2012</option> +<option value="2013">2013</option> +<option value="2014">2014</option> +<option value="2015">2015</option> +<option value="2016">2016</option> +</select>""") + self.assertEqual(w.render('mydate', None), w.render('mydate', '')) + + self.assertEqual(w.render('mydate', '2010-04-15'), """<select name="mydate_month" id="id_mydate_month"> +<option value="1">January</option> +<option value="2">February</option> +<option value="3">March</option> +<option value="4" selected="selected">April</option> +<option value="5">May</option> +<option value="6">June</option> +<option value="7">July</option> +<option value="8">August</option> +<option value="9">September</option> +<option value="10">October</option> +<option value="11">November</option> +<option value="12">December</option> +</select> +<select name="mydate_day" id="id_mydate_day"> +<option value="1">1</option> +<option value="2">2</option> +<option value="3">3</option> +<option value="4">4</option> +<option value="5">5</option> +<option value="6">6</option> +<option value="7">7</option> +<option value="8">8</option> +<option value="9">9</option> +<option value="10">10</option> +<option value="11">11</option> +<option value="12">12</option> +<option value="13">13</option> +<option value="14">14</option> +<option value="15" selected="selected">15</option> +<option value="16">16</option> +<option value="17">17</option> +<option value="18">18</option> +<option value="19">19</option> +<option value="20">20</option> +<option value="21">21</option> +<option value="22">22</option> +<option value="23">23</option> +<option value="24">24</option> +<option value="25">25</option> +<option value="26">26</option> +<option value="27">27</option> +<option value="28">28</option> +<option value="29">29</option> +<option value="30">30</option> +<option value="31">31</option> +</select> +<select name="mydate_year" id="id_mydate_year"> +<option value="2007">2007</option> +<option value="2008">2008</option> +<option value="2009">2009</option> +<option value="2010" selected="selected">2010</option> +<option value="2011">2011</option> +<option value="2012">2012</option> +<option value="2013">2013</option> +<option value="2014">2014</option> +<option value="2015">2015</option> +<option value="2016">2016</option> +</select>""") + + # Accepts a datetime or a string: + self.assertEqual(w.render('mydate', datetime.date(2010, 4, 15)), w.render('mydate', '2010-04-15')) + + # Invalid dates still render the failed date: + self.assertEqual(w.render('mydate', '2010-02-31'), """<select name="mydate_month" id="id_mydate_month"> +<option value="1">January</option> +<option value="2" selected="selected">February</option> +<option value="3">March</option> +<option value="4">April</option> +<option value="5">May</option> +<option value="6">June</option> +<option value="7">July</option> +<option value="8">August</option> +<option value="9">September</option> +<option value="10">October</option> +<option value="11">November</option> +<option value="12">December</option> +</select> +<select name="mydate_day" id="id_mydate_day"> +<option value="1">1</option> +<option value="2">2</option> +<option value="3">3</option> +<option value="4">4</option> +<option value="5">5</option> +<option value="6">6</option> +<option value="7">7</option> +<option value="8">8</option> +<option value="9">9</option> +<option value="10">10</option> +<option value="11">11</option> +<option value="12">12</option> +<option value="13">13</option> +<option value="14">14</option> +<option value="15">15</option> +<option value="16">16</option> +<option value="17">17</option> +<option value="18">18</option> +<option value="19">19</option> +<option value="20">20</option> +<option value="21">21</option> +<option value="22">22</option> +<option value="23">23</option> +<option value="24">24</option> +<option value="25">25</option> +<option value="26">26</option> +<option value="27">27</option> +<option value="28">28</option> +<option value="29">29</option> +<option value="30">30</option> +<option value="31" selected="selected">31</option> +</select> +<select name="mydate_year" id="id_mydate_year"> +<option value="2007">2007</option> +<option value="2008">2008</option> +<option value="2009">2009</option> +<option value="2010" selected="selected">2010</option> +<option value="2011">2011</option> +<option value="2012">2012</option> +<option value="2013">2013</option> +<option value="2014">2014</option> +<option value="2015">2015</option> +<option value="2016">2016</option> +</select>""") + + # Using a SelectDateWidget in a form: + w = SelectDateWidget(years=('2007','2008','2009','2010','2011','2012','2013','2014','2015','2016'), required=False) + self.assertEqual(w.render('mydate', ''), """<select name="mydate_month" id="id_mydate_month"> +<option value="0">---</option> +<option value="1">January</option> +<option value="2">February</option> +<option value="3">March</option> +<option value="4">April</option> +<option value="5">May</option> +<option value="6">June</option> +<option value="7">July</option> +<option value="8">August</option> +<option value="9">September</option> +<option value="10">October</option> +<option value="11">November</option> +<option value="12">December</option> +</select> +<select name="mydate_day" id="id_mydate_day"> +<option value="0">---</option> +<option value="1">1</option> +<option value="2">2</option> +<option value="3">3</option> +<option value="4">4</option> +<option value="5">5</option> +<option value="6">6</option> +<option value="7">7</option> +<option value="8">8</option> +<option value="9">9</option> +<option value="10">10</option> +<option value="11">11</option> +<option value="12">12</option> +<option value="13">13</option> +<option value="14">14</option> +<option value="15">15</option> +<option value="16">16</option> +<option value="17">17</option> +<option value="18">18</option> +<option value="19">19</option> +<option value="20">20</option> +<option value="21">21</option> +<option value="22">22</option> +<option value="23">23</option> +<option value="24">24</option> +<option value="25">25</option> +<option value="26">26</option> +<option value="27">27</option> +<option value="28">28</option> +<option value="29">29</option> +<option value="30">30</option> +<option value="31">31</option> +</select> +<select name="mydate_year" id="id_mydate_year"> +<option value="0">---</option> +<option value="2007">2007</option> +<option value="2008">2008</option> +<option value="2009">2009</option> +<option value="2010">2010</option> +<option value="2011">2011</option> +<option value="2012">2012</option> +<option value="2013">2013</option> +<option value="2014">2014</option> +<option value="2015">2015</option> +<option value="2016">2016</option> +</select>""") + self.assertEqual(w.render('mydate', '2010-04-15'), """<select name="mydate_month" id="id_mydate_month"> +<option value="0">---</option> +<option value="1">January</option> +<option value="2">February</option> +<option value="3">March</option> +<option value="4" selected="selected">April</option> +<option value="5">May</option> +<option value="6">June</option> +<option value="7">July</option> +<option value="8">August</option> +<option value="9">September</option> +<option value="10">October</option> +<option value="11">November</option> +<option value="12">December</option> +</select> +<select name="mydate_day" id="id_mydate_day"> +<option value="0">---</option> +<option value="1">1</option> +<option value="2">2</option> +<option value="3">3</option> +<option value="4">4</option> +<option value="5">5</option> +<option value="6">6</option> +<option value="7">7</option> +<option value="8">8</option> +<option value="9">9</option> +<option value="10">10</option> +<option value="11">11</option> +<option value="12">12</option> +<option value="13">13</option> +<option value="14">14</option> +<option value="15" selected="selected">15</option> +<option value="16">16</option> +<option value="17">17</option> +<option value="18">18</option> +<option value="19">19</option> +<option value="20">20</option> +<option value="21">21</option> +<option value="22">22</option> +<option value="23">23</option> +<option value="24">24</option> +<option value="25">25</option> +<option value="26">26</option> +<option value="27">27</option> +<option value="28">28</option> +<option value="29">29</option> +<option value="30">30</option> +<option value="31">31</option> +</select> +<select name="mydate_year" id="id_mydate_year"> +<option value="0">---</option> +<option value="2007">2007</option> +<option value="2008">2008</option> +<option value="2009">2009</option> +<option value="2010" selected="selected">2010</option> +<option value="2011">2011</option> +<option value="2012">2012</option> +<option value="2013">2013</option> +<option value="2014">2014</option> +<option value="2015">2015</option> +<option value="2016">2016</option> +</select>""") + + class GetDate(Form): + mydate = DateField(widget=SelectDateWidget) + + a = GetDate({'mydate_month':'4', 'mydate_day':'1', 'mydate_year':'2008'}) + self.assertTrue(a.is_valid()) + self.assertEqual(a.cleaned_data['mydate'], datetime.date(2008, 4, 1)) + + # As with any widget that implements get_value_from_datadict, + # we must be prepared to accept the input from the "as_hidden" + # rendering as well. + + self.assertEqual(a['mydate'].as_hidden(), '<input type="hidden" name="mydate" value="2008-4-1" id="id_mydate" />') + + b = GetDate({'mydate':'2008-4-1'}) + self.assertTrue(b.is_valid()) + self.assertEqual(b.cleaned_data['mydate'], datetime.date(2008, 4, 1)) + + def test_multiwidget(self): + # MultiWidget and MultiValueField ############################################# + # MultiWidgets are widgets composed of other widgets. They are usually + # combined with MultiValueFields - a field that is composed of other fields. + # MulitWidgets can themselved be composed of other MultiWidgets. + # SplitDateTimeWidget is one example of a MultiWidget. + + class ComplexMultiWidget(MultiWidget): + def __init__(self, attrs=None): + widgets = ( + TextInput(), + SelectMultiple(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), + SplitDateTimeWidget(), + ) + super(ComplexMultiWidget, self).__init__(widgets, attrs) + + def decompress(self, value): + if value: + data = value.split(',') + return [data[0], data[1], datetime.datetime(*time.strptime(data[2], "%Y-%m-%d %H:%M:%S")[0:6])] + return [None, None, None] + + def format_output(self, rendered_widgets): + return u'\n'.join(rendered_widgets) + + w = ComplexMultiWidget() + self.assertEqual(w.render('name', 'some text,JP,2007-04-25 06:24:00'), """<input type="text" name="name_0" value="some text" /> +<select multiple="multiple" name="name_1"> +<option value="J" selected="selected">John</option> +<option value="P" selected="selected">Paul</option> +<option value="G">George</option> +<option value="R">Ringo</option> +</select> +<input type="text" name="name_2_0" value="2007-04-25" /><input type="text" name="name_2_1" value="06:24:00" />""") + + class ComplexField(MultiValueField): + def __init__(self, required=True, widget=None, label=None, initial=None): + fields = ( + CharField(), + MultipleChoiceField(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), + SplitDateTimeField() + ) + super(ComplexField, self).__init__(fields, required, widget, label, initial) + + def compress(self, data_list): + if data_list: + return '%s,%s,%s' % (data_list[0],''.join(data_list[1]),data_list[2]) + return None + + f = ComplexField(widget=w) + self.assertEqual(f.clean(['some text', ['J','P'], ['2007-04-25','6:24:00']]), u'some text,JP,2007-04-25 06:24:00') + self.assertFormErrors([u'Select a valid choice. X is not one of the available choices.'], f.clean, ['some text',['X'], ['2007-04-25','6:24:00']]) + + # If insufficient data is provided, None is substituted + self.assertFormErrors([u'This field is required.'], f.clean, ['some text',['JP']]) + + class ComplexFieldForm(Form): + field1 = ComplexField(widget=w) + + f = ComplexFieldForm() + self.assertEqual(f.as_table(), """<tr><th><label for="id_field1_0">Field1:</label></th><td><input type="text" name="field1_0" id="id_field1_0" /> +<select multiple="multiple" name="field1_1" id="id_field1_1"> +<option value="J">John</option> +<option value="P">Paul</option> +<option value="G">George</option> +<option value="R">Ringo</option> +</select> +<input type="text" name="field1_2_0" id="id_field1_2_0" /><input type="text" name="field1_2_1" id="id_field1_2_1" /></td></tr>""") + + f = ComplexFieldForm({'field1_0':'some text','field1_1':['J','P'], 'field1_2_0':'2007-04-25', 'field1_2_1':'06:24:00'}) + self.assertEqual(f.as_table(), """<tr><th><label for="id_field1_0">Field1:</label></th><td><input type="text" name="field1_0" value="some text" id="id_field1_0" /> +<select multiple="multiple" name="field1_1" id="id_field1_1"> +<option value="J" selected="selected">John</option> +<option value="P" selected="selected">Paul</option> +<option value="G">George</option> +<option value="R">Ringo</option> +</select> +<input type="text" name="field1_2_0" value="2007-04-25" id="id_field1_2_0" /><input type="text" name="field1_2_1" value="06:24:00" id="id_field1_2_1" /></td></tr>""") + + self.assertEqual(f.cleaned_data['field1'], u'some text,JP,2007-04-25 06:24:00') + + def test_ipaddress(self): + f = IPAddressField() + self.assertFormErrors([u'This field is required.'], f.clean, '') + self.assertFormErrors([u'This field is required.'], f.clean, None) + self.assertEqual(f.clean('127.0.0.1'), u'127.0.0.1') + self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, 'foo') + self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '127.0.0.') + self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '1.2.3.4.5') + self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '256.125.1.5') + + f = IPAddressField(required=False) + self.assertEqual(f.clean(''), u'') + self.assertEqual(f.clean(None), u'') + self.assertEqual(f.clean('127.0.0.1'), u'127.0.0.1') + self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, 'foo') + self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '127.0.0.') + self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '1.2.3.4.5') + self.assertFormErrors([u'Enter a valid IPv4 address.'], f.clean, '256.125.1.5') + + def test_smart_unicode(self): + class Test: + def __str__(self): + return 'ŠĐĆŽćžšđ' + + class TestU: + def __str__(self): + return 'Foo' + def __unicode__(self): + return u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111' + + self.assertEqual(smart_unicode(Test()), u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111') + self.assertEqual(smart_unicode(TestU()), u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111') + self.assertEqual(smart_unicode(1), u'1') + self.assertEqual(smart_unicode('foo'), u'foo') + + def test_accessing_clean(self): + class UserForm(Form): + username = CharField(max_length=10) + password = CharField(widget=PasswordInput) + + def clean(self): + data = self.cleaned_data + + if not self.errors: + data['username'] = data['username'].lower() + + return data + + f = UserForm({'username': 'SirRobin', 'password': 'blue'}) + self.assertTrue(f.is_valid()) + self.assertEqual(f.cleaned_data['username'], u'sirrobin') + + def test_overriding_errorlist(self): + class DivErrorList(ErrorList): + def __unicode__(self): + return self.as_divs() + + def as_divs(self): + if not self: return u'' + return u'<div class="errorlist">%s</div>' % ''.join([u'<div class="error">%s</div>' % force_unicode(e) for e in self]) + + class CommentForm(Form): + name = CharField(max_length=50, required=False) + email = EmailField() + comment = CharField() + + data = dict(email='invalid') + f = CommentForm(data, auto_id=False, error_class=DivErrorList) + self.assertEqual(f.as_p(), """<p>Name: <input type="text" name="name" maxlength="50" /></p> +<div class="errorlist"><div class="error">Enter a valid e-mail address.</div></div> +<p>Email: <input type="text" name="email" value="invalid" /></p> +<div class="errorlist"><div class="error">This field is required.</div></div> +<p>Comment: <input type="text" name="comment" /></p>""") + + def test_multipart_encoded_form(self): + class FormWithoutFile(Form): + username = CharField() + + class FormWithFile(Form): + username = CharField() + file = FileField() + + class FormWithImage(Form): + image = ImageField() + + self.assertFalse(FormWithoutFile().is_multipart()) + self.assertTrue(FormWithFile().is_multipart()) + self.assertTrue(FormWithImage().is_multipart()) + + +class FormsExtraL10NTestCase(unittest.TestCase): + def setUp(self): + super(FormsExtraL10NTestCase, self).setUp() + self.old_use_l10n = getattr(settings, 'USE_L10N', False) + settings.USE_L10N = True + translation.activate('nl') + + def tearDown(self): + translation.deactivate() + settings.USE_L10N = self.old_use_l10n + super(FormsExtraL10NTestCase, self).tearDown() + + def test_l10n(self): + w = SelectDateWidget(years=('2007','2008','2009','2010','2011','2012','2013','2014','2015','2016'), required=False) + self.assertEqual(w.value_from_datadict({'date_year': '2010', 'date_month': '8', 'date_day': '13'}, {}, 'date'), '13-08-2010') + + self.assertEqual(w.render('date', '13-08-2010'), """<select name="date_day" id="id_date_day"> +<option value="0">---</option> +<option value="1">1</option> +<option value="2">2</option> +<option value="3">3</option> +<option value="4">4</option> +<option value="5">5</option> +<option value="6">6</option> +<option value="7">7</option> +<option value="8">8</option> +<option value="9">9</option> +<option value="10">10</option> +<option value="11">11</option> +<option value="12">12</option> +<option value="13" selected="selected">13</option> +<option value="14">14</option> +<option value="15">15</option> +<option value="16">16</option> +<option value="17">17</option> +<option value="18">18</option> +<option value="19">19</option> +<option value="20">20</option> +<option value="21">21</option> +<option value="22">22</option> +<option value="23">23</option> +<option value="24">24</option> +<option value="25">25</option> +<option value="26">26</option> +<option value="27">27</option> +<option value="28">28</option> +<option value="29">29</option> +<option value="30">30</option> +<option value="31">31</option> +</select> +<select name="date_month" id="id_date_month"> +<option value="0">---</option> +<option value="1">januari</option> +<option value="2">februari</option> +<option value="3">maart</option> +<option value="4">april</option> +<option value="5">mei</option> +<option value="6">juni</option> +<option value="7">juli</option> +<option value="8" selected="selected">augustus</option> +<option value="9">september</option> +<option value="10">oktober</option> +<option value="11">november</option> +<option value="12">december</option> +</select> +<select name="date_year" id="id_date_year"> +<option value="0">---</option> +<option value="2007">2007</option> +<option value="2008">2008</option> +<option value="2009">2009</option> +<option value="2010" selected="selected">2010</option> +<option value="2011">2011</option> +<option value="2012">2012</option> +<option value="2013">2013</option> +<option value="2014">2014</option> +<option value="2015">2015</option> +<option value="2016">2016</option> +</select>""") + + # Years before 1900 work + w = SelectDateWidget(years=('1899',)) + self.assertEqual(w.value_from_datadict({'date_year': '1899', 'date_month': '8', 'date_day': '13'}, {}, 'date'), '13-08-1899') diff --git a/parts/django/tests/regressiontests/forms/tests/fields.py b/parts/django/tests/regressiontests/forms/tests/fields.py new file mode 100644 index 0000000..133a183 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/fields.py @@ -0,0 +1,862 @@ +# -*- coding: utf-8 -*- +""" +########## +# Fields # +########## + +Each Field class does some sort of validation. Each Field has a clean() method, +which either raises django.forms.ValidationError or returns the "clean" +data -- usually a Unicode object, but, in some rare cases, a list. + +Each Field's __init__() takes at least these parameters: + required -- Boolean that specifies whether the field is required. + True by default. + widget -- A Widget class, or instance of a Widget class, that should be + used for this Field when displaying it. Each Field has a default + Widget that it'll use if you don't specify this. In most cases, + the default widget is TextInput. + label -- A verbose name for this field, for use in displaying this field in + a form. By default, Django will use a "pretty" version of the form + field name, if the Field is part of a Form. + initial -- A value to use in this Field's initial display. This value is + *not* used as a fallback if data isn't given. + +Other than that, the Field subclasses have class-specific options for +__init__(). For example, CharField has a max_length option. +""" +import datetime +import time +import re +import os +from decimal import Decimal + +from unittest import TestCase + +from django.core.files.uploadedfile import SimpleUploadedFile +from django.forms import * +from django.forms.widgets import RadioFieldRenderer + + +def fix_os_paths(x): + if isinstance(x, basestring): + return x.replace('\\', '/') + elif isinstance(x, tuple): + return tuple(fix_os_paths(list(x))) + elif isinstance(x, list): + return [fix_os_paths(y) for y in x] + else: + return x + + +class FieldsTests(TestCase): + + def assertRaisesErrorWithMessage(self, error, message, callable, *args, **kwargs): + self.assertRaises(error, callable, *args, **kwargs) + try: + callable(*args, **kwargs) + except error, e: + self.assertEqual(message, str(e)) + + # CharField ################################################################### + + def test_charfield_1(self): + f = CharField() + self.assertEqual(u'1', f.clean(1)) + self.assertEqual(u'hello', f.clean('hello')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertEqual(u'[1, 2, 3]', f.clean([1, 2, 3])) + + def test_charfield_2(self): + f = CharField(required=False) + self.assertEqual(u'1', f.clean(1)) + self.assertEqual(u'hello', f.clean('hello')) + self.assertEqual(u'', f.clean(None)) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'[1, 2, 3]', f.clean([1, 2, 3])) + + def test_charfield_3(self): + f = CharField(max_length=10, required=False) + self.assertEqual(u'12345', f.clean('12345')) + self.assertEqual(u'1234567890', f.clean('1234567890')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 10 characters (it has 11).']", f.clean, '1234567890a') + + def test_charfield_4(self): + f = CharField(min_length=10, required=False) + self.assertEqual(u'', f.clean('')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 10 characters (it has 5).']", f.clean, '12345') + self.assertEqual(u'1234567890', f.clean('1234567890')) + self.assertEqual(u'1234567890a', f.clean('1234567890a')) + + def test_charfield_5(self): + f = CharField(min_length=10, required=True) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 10 characters (it has 5).']", f.clean, '12345') + self.assertEqual(u'1234567890', f.clean('1234567890')) + self.assertEqual(u'1234567890a', f.clean('1234567890a')) + + # IntegerField ################################################################ + + def test_integerfield_1(self): + f = IntegerField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(1, f.clean('1')) + self.assertEqual(True, isinstance(f.clean('1'), int)) + self.assertEqual(23, f.clean('23')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a whole number.']", f.clean, 'a') + self.assertEqual(42, f.clean(42)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a whole number.']", f.clean, 3.14) + self.assertEqual(1, f.clean('1 ')) + self.assertEqual(1, f.clean(' 1')) + self.assertEqual(1, f.clean(' 1 ')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a whole number.']", f.clean, '1a') + + def test_integerfield_2(self): + f = IntegerField(required=False) + self.assertEqual(None, f.clean('')) + self.assertEqual('None', repr(f.clean(''))) + self.assertEqual(None, f.clean(None)) + self.assertEqual('None', repr(f.clean(None))) + self.assertEqual(1, f.clean('1')) + self.assertEqual(True, isinstance(f.clean('1'), int)) + self.assertEqual(23, f.clean('23')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a whole number.']", f.clean, 'a') + self.assertEqual(1, f.clean('1 ')) + self.assertEqual(1, f.clean(' 1')) + self.assertEqual(1, f.clean(' 1 ')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a whole number.']", f.clean, '1a') + + def test_integerfield_3(self): + f = IntegerField(max_value=10) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(1, f.clean(1)) + self.assertEqual(10, f.clean(10)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is less than or equal to 10.']", f.clean, 11) + self.assertEqual(10, f.clean('10')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is less than or equal to 10.']", f.clean, '11') + + def test_integerfield_4(self): + f = IntegerField(min_value=10) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is greater than or equal to 10.']", f.clean, 1) + self.assertEqual(10, f.clean(10)) + self.assertEqual(11, f.clean(11)) + self.assertEqual(10, f.clean('10')) + self.assertEqual(11, f.clean('11')) + + def test_integerfield_5(self): + f = IntegerField(min_value=10, max_value=20) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is greater than or equal to 10.']", f.clean, 1) + self.assertEqual(10, f.clean(10)) + self.assertEqual(11, f.clean(11)) + self.assertEqual(10, f.clean('10')) + self.assertEqual(11, f.clean('11')) + self.assertEqual(20, f.clean(20)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is less than or equal to 20.']", f.clean, 21) + + # FloatField ################################################################## + + def test_floatfield_1(self): + f = FloatField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(1.0, f.clean('1')) + self.assertEqual(True, isinstance(f.clean('1'), float)) + self.assertEqual(23.0, f.clean('23')) + self.assertEqual(3.1400000000000001, f.clean('3.14')) + self.assertEqual(3.1400000000000001, f.clean(3.14)) + self.assertEqual(42.0, f.clean(42)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, 'a') + self.assertEqual(1.0, f.clean('1.0 ')) + self.assertEqual(1.0, f.clean(' 1.0')) + self.assertEqual(1.0, f.clean(' 1.0 ')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, '1.0a') + + def test_floatfield_2(self): + f = FloatField(required=False) + self.assertEqual(None, f.clean('')) + self.assertEqual(None, f.clean(None)) + self.assertEqual(1.0, f.clean('1')) + + def test_floatfield_3(self): + f = FloatField(max_value=1.5, min_value=0.5) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is less than or equal to 1.5.']", f.clean, '1.6') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is greater than or equal to 0.5.']", f.clean, '0.4') + self.assertEqual(1.5, f.clean('1.5')) + self.assertEqual(0.5, f.clean('0.5')) + + # DecimalField ################################################################ + + def test_decimalfield_1(self): + f = DecimalField(max_digits=4, decimal_places=2) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(f.clean('1'), Decimal("1")) + self.assertEqual(True, isinstance(f.clean('1'), Decimal)) + self.assertEqual(f.clean('23'), Decimal("23")) + self.assertEqual(f.clean('3.14'), Decimal("3.14")) + self.assertEqual(f.clean(3.14), Decimal("3.14")) + self.assertEqual(f.clean(Decimal('3.14')), Decimal("3.14")) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, 'NaN') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, 'Inf') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, '-Inf') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, 'a') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, u'łąść') + self.assertEqual(f.clean('1.0 '), Decimal("1.0")) + self.assertEqual(f.clean(' 1.0'), Decimal("1.0")) + self.assertEqual(f.clean(' 1.0 '), Decimal("1.0")) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, '1.0a') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 4 digits in total.']", f.clean, '123.45') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 2 decimal places.']", f.clean, '1.234') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 2 digits before the decimal point.']", f.clean, '123.4') + self.assertEqual(f.clean('-12.34'), Decimal("-12.34")) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 4 digits in total.']", f.clean, '-123.45') + self.assertEqual(f.clean('-.12'), Decimal("-0.12")) + self.assertEqual(f.clean('-00.12'), Decimal("-0.12")) + self.assertEqual(f.clean('-000.12'), Decimal("-0.12")) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 2 decimal places.']", f.clean, '-000.123') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 4 digits in total.']", f.clean, '-000.12345') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a number.']", f.clean, '--0.12') + + def test_decimalfield_2(self): + f = DecimalField(max_digits=4, decimal_places=2, required=False) + self.assertEqual(None, f.clean('')) + self.assertEqual(None, f.clean(None)) + self.assertEqual(f.clean('1'), Decimal("1")) + + def test_decimalfield_3(self): + f = DecimalField(max_digits=4, decimal_places=2, max_value=Decimal('1.5'), min_value=Decimal('0.5')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is less than or equal to 1.5.']", f.clean, '1.6') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value is greater than or equal to 0.5.']", f.clean, '0.4') + self.assertEqual(f.clean('1.5'), Decimal("1.5")) + self.assertEqual(f.clean('0.5'), Decimal("0.5")) + self.assertEqual(f.clean('.5'), Decimal("0.5")) + self.assertEqual(f.clean('00.50'), Decimal("0.50")) + + def test_decimalfield_4(self): + f = DecimalField(decimal_places=2) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 2 decimal places.']", f.clean, '0.00000001') + + def test_decimalfield_5(self): + f = DecimalField(max_digits=3) + # Leading whole zeros "collapse" to one digit. + self.assertEqual(f.clean('0000000.10'), Decimal("0.1")) + # But a leading 0 before the . doesn't count towards max_digits + self.assertEqual(f.clean('0000000.100'), Decimal("0.100")) + # Only leading whole zeros "collapse" to one digit. + self.assertEqual(f.clean('000000.02'), Decimal('0.02')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 3 digits in total.']", f.clean, '000000.0002') + self.assertEqual(f.clean('.002'), Decimal("0.002")) + + def test_decimalfield_6(self): + f = DecimalField(max_digits=2, decimal_places=2) + self.assertEqual(f.clean('.01'), Decimal(".01")) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure that there are no more than 0 digits before the decimal point.']", f.clean, '1.1') + + # DateField ################################################################### + + def test_datefield_1(self): + f = DateField() + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.date(2006, 10, 25))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.datetime(2006, 10, 25, 14, 30))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('2006-10-25')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('10/25/2006')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('10/25/06')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('Oct 25 2006')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('October 25 2006')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('October 25, 2006')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('25 October 2006')) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('25 October, 2006')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '2006-4-31') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '200a-10-25') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '25/10/06') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + + def test_datefield_2(self): + f = DateField(required=False) + self.assertEqual(None, f.clean(None)) + self.assertEqual('None', repr(f.clean(None))) + self.assertEqual(None, f.clean('')) + self.assertEqual('None', repr(f.clean(''))) + + def test_datefield_3(self): + f = DateField(input_formats=['%Y %m %d']) + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.date(2006, 10, 25))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean(datetime.datetime(2006, 10, 25, 14, 30))) + self.assertEqual(datetime.date(2006, 10, 25), f.clean('2006 10 25')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '2006-10-25') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '10/25/2006') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, '10/25/06') + + # TimeField ################################################################### + + def test_timefield_1(self): + f = TimeField() + self.assertEqual(datetime.time(14, 25), f.clean(datetime.time(14, 25))) + self.assertEqual(datetime.time(14, 25, 59), f.clean(datetime.time(14, 25, 59))) + self.assertEqual(datetime.time(14, 25), f.clean('14:25')) + self.assertEqual(datetime.time(14, 25, 59), f.clean('14:25:59')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, 'hello') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, '1:24 p.m.') + + def test_timefield_2(self): + f = TimeField(input_formats=['%I:%M %p']) + self.assertEqual(datetime.time(14, 25), f.clean(datetime.time(14, 25))) + self.assertEqual(datetime.time(14, 25, 59), f.clean(datetime.time(14, 25, 59))) + self.assertEqual(datetime.time(4, 25), f.clean('4:25 AM')) + self.assertEqual(datetime.time(16, 25), f.clean('4:25 PM')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, '14:30:45') + + # DateTimeField ############################################################### + + def test_datetimefield_1(self): + f = DateTimeField() + self.assertEqual(datetime.datetime(2006, 10, 25, 0, 0), f.clean(datetime.date(2006, 10, 25))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean(datetime.datetime(2006, 10, 25, 14, 30))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 59), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 59, 200), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 45), f.clean('2006-10-25 14:30:45')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('2006-10-25 14:30:00')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('2006-10-25 14:30')) + self.assertEqual(datetime.datetime(2006, 10, 25, 0, 0), f.clean('2006-10-25')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 45), f.clean('10/25/2006 14:30:45')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('10/25/2006 14:30:00')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('10/25/2006 14:30')) + self.assertEqual(datetime.datetime(2006, 10, 25, 0, 0), f.clean('10/25/2006')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 45), f.clean('10/25/06 14:30:45')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('10/25/06 14:30:00')) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('10/25/06 14:30')) + self.assertEqual(datetime.datetime(2006, 10, 25, 0, 0), f.clean('10/25/06')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date/time.']", f.clean, 'hello') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date/time.']", f.clean, '2006-10-25 4:30 p.m.') + + def test_datetimefield_2(self): + f = DateTimeField(input_formats=['%Y %m %d %I:%M %p']) + self.assertEqual(datetime.datetime(2006, 10, 25, 0, 0), f.clean(datetime.date(2006, 10, 25))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean(datetime.datetime(2006, 10, 25, 14, 30))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 59), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 59, 200), f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200))) + self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30), f.clean('2006 10 25 2:30 PM')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date/time.']", f.clean, '2006-10-25 14:30:45') + + def test_datetimefield_3(self): + f = DateTimeField(required=False) + self.assertEqual(None, f.clean(None)) + self.assertEqual('None', repr(f.clean(None))) + self.assertEqual(None, f.clean('')) + self.assertEqual('None', repr(f.clean(''))) + + # RegexField ################################################################## + + def test_regexfield_1(self): + f = RegexField('^\d[A-F]\d$') + self.assertEqual(u'2A2', f.clean('2A2')) + self.assertEqual(u'3F3', f.clean('3F3')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '3G3') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, ' 2A2') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '2A2 ') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + + def test_regexfield_2(self): + f = RegexField('^\d[A-F]\d$', required=False) + self.assertEqual(u'2A2', f.clean('2A2')) + self.assertEqual(u'3F3', f.clean('3F3')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '3G3') + self.assertEqual(u'', f.clean('')) + + def test_regexfield_3(self): + f = RegexField(re.compile('^\d[A-F]\d$')) + self.assertEqual(u'2A2', f.clean('2A2')) + self.assertEqual(u'3F3', f.clean('3F3')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '3G3') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, ' 2A2') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '2A2 ') + + def test_regexfield_4(self): + f = RegexField('^\d\d\d\d$', error_message='Enter a four-digit number.') + self.assertEqual(u'1234', f.clean('1234')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a four-digit number.']", f.clean, '123') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a four-digit number.']", f.clean, 'abcd') + + def test_regexfield_5(self): + f = RegexField('^\d+$', min_length=5, max_length=10) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 5 characters (it has 3).']", f.clean, '123') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 5 characters (it has 3).', u'Enter a valid value.']", f.clean, 'abc') + self.assertEqual(u'12345', f.clean('12345')) + self.assertEqual(u'1234567890', f.clean('1234567890')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 10 characters (it has 11).']", f.clean, '12345678901') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '12345a') + + # EmailField ################################################################## + + def test_emailfield_1(self): + f = EmailField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(u'person@example.com', f.clean('person@example.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo@') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo@bar') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'example@invalid-.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'example@-invalid.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'example@inv-.alid-.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'example@inv-.-alid.com') + self.assertEqual(u'example@valid-----hyphens.com', f.clean('example@valid-----hyphens.com')) + self.assertEqual(u'example@valid-with-hyphens.com', f.clean('example@valid-with-hyphens.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'example@.com') + self.assertEqual(u'local@domain.with.idn.xyz\xe4\xf6\xfc\xdfabc.part.com', f.clean('local@domain.with.idn.xyzäöüßabc.part.com')) + + def test_email_regexp_for_performance(self): + f = EmailField() + # Check for runaway regex security problem. This will take for-freeking-ever + # if the security fix isn't in place. + self.assertRaisesErrorWithMessage( + ValidationError, + "[u'Enter a valid e-mail address.']", + f.clean, + 'viewx3dtextx26qx3d@yahoo.comx26latlngx3d15854521645943074058' + ) + + def test_emailfield_2(self): + f = EmailField(required=False) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'', f.clean(None)) + self.assertEqual(u'person@example.com', f.clean('person@example.com')) + self.assertEqual(u'example@example.com', f.clean(' example@example.com \t \t ')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo@') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'foo@bar') + + def test_emailfield_3(self): + f = EmailField(min_length=10, max_length=15) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 10 characters (it has 9).']", f.clean, 'a@foo.com') + self.assertEqual(u'alf@foo.com', f.clean('alf@foo.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 15 characters (it has 20).']", f.clean, 'alf123456788@foo.com') + + # FileField ################################################################## + + def test_filefield_1(self): + f = FileField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '', '') + self.assertEqual('files/test1.pdf', f.clean('', 'files/test1.pdf')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None, '') + self.assertEqual('files/test2.pdf', f.clean(None, 'files/test2.pdf')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'No file was submitted. Check the encoding type on the form.']", f.clean, SimpleUploadedFile('', '')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'No file was submitted. Check the encoding type on the form.']", f.clean, SimpleUploadedFile('', ''), '') + self.assertEqual('files/test3.pdf', f.clean(None, 'files/test3.pdf')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'No file was submitted. Check the encoding type on the form.']", f.clean, 'some content that is not a file') + self.assertRaisesErrorWithMessage(ValidationError, "[u'The submitted file is empty.']", f.clean, SimpleUploadedFile('name', None)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'The submitted file is empty.']", f.clean, SimpleUploadedFile('name', '')) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', 'Some File Content')))) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह')))) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', 'Some File Content'), 'files/test4.pdf'))) + + def test_filefield_2(self): + f = FileField(max_length = 5) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this filename has at most 5 characters (it has 18).']", f.clean, SimpleUploadedFile('test_maxlength.txt', 'hello world')) + self.assertEqual('files/test1.pdf', f.clean('', 'files/test1.pdf')) + self.assertEqual('files/test2.pdf', f.clean(None, 'files/test2.pdf')) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', 'Some File Content')))) + + # URLField ################################################################## + + def test_urlfield_1(self): + f = URLField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(u'http://localhost/', f.clean('http://localhost')) + self.assertEqual(u'http://example.com/', f.clean('http://example.com')) + self.assertEqual(u'http://example.com./', f.clean('http://example.com.')) + self.assertEqual(u'http://www.example.com/', f.clean('http://www.example.com')) + self.assertEqual(u'http://www.example.com:8000/test', f.clean('http://www.example.com:8000/test')) + self.assertEqual(u'http://valid-with-hyphens.com/', f.clean('valid-with-hyphens.com')) + self.assertEqual(u'http://subdomain.domain.com/', f.clean('subdomain.domain.com')) + self.assertEqual(u'http://200.8.9.10/', f.clean('http://200.8.9.10')) + self.assertEqual(u'http://200.8.9.10:8000/test', f.clean('http://200.8.9.10:8000/test')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'foo') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://example') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://example.') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'com.') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, '.') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://invalid-.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://-invalid.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://inv-.alid-.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://inv-.-alid.com') + self.assertEqual(u'http://valid-----hyphens.com/', f.clean('http://valid-----hyphens.com')) + self.assertEqual(u'http://some.idn.xyz\xe4\xf6\xfc\xdfabc.domain.com:123/blah', f.clean('http://some.idn.xyzäöüßabc.domain.com:123/blah')) + self.assertEqual(u'http://www.example.com/s/http://code.djangoproject.com/ticket/13804', f.clean('www.example.com/s/http://code.djangoproject.com/ticket/13804')) + + def test_url_regex_ticket11198(self): + f = URLField() + # hangs "forever" if catastrophic backtracking in ticket:#11198 not fixed + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://%s' % ("X"*200,)) + + # a second test, to make sure the problem is really addressed, even on + # domains that don't fail the domain label length check in the regex + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://%s' % ("X"*60,)) + + def test_urlfield_2(self): + f = URLField(required=False) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'', f.clean(None)) + self.assertEqual(u'http://example.com/', f.clean('http://example.com')) + self.assertEqual(u'http://www.example.com/', f.clean('http://www.example.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'foo') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://example') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://example.') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://.com') + + def test_urlfield_3(self): + f = URLField(verify_exists=True) + self.assertEqual(u'http://www.google.com/', f.clean('http://www.google.com')) # This will fail if there's no Internet connection + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid URL.']", f.clean, 'http://example') + self.assertRaises(ValidationError, f.clean, 'http://www.broken.djangoproject.com') # bad domain + try: + f.clean('http://www.broken.djangoproject.com') # bad domain + except ValidationError, e: + self.assertEqual("[u'This URL appears to be a broken link.']", str(e)) + self.assertRaises(ValidationError, f.clean, 'http://google.com/we-love-microsoft.html') # good domain, bad page + try: + f.clean('http://google.com/we-love-microsoft.html') # good domain, bad page + except ValidationError, e: + self.assertEqual("[u'This URL appears to be a broken link.']", str(e)) + # Valid and existent IDN + self.assertEqual(u'http://\u05e2\u05d1\u05e8\u05d9\u05ea.idn.icann.org/', f.clean(u'http://עברית.idn.icann.org/')) + # Valid but non-existent IDN + try: + f.clean(u'http://broken.עברית.idn.icann.org/') + except ValidationError, e: + self.assertEqual("[u'This URL appears to be a broken link.']", str(e)) + + def test_urlfield_4(self): + f = URLField(verify_exists=True, required=False) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'http://www.google.com/', f.clean('http://www.google.com')) # This will fail if there's no Internet connection + + def test_urlfield_5(self): + f = URLField(min_length=15, max_length=20) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at least 15 characters (it has 13).']", f.clean, 'http://f.com') + self.assertEqual(u'http://example.com/', f.clean('http://example.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 20 characters (it has 38).']", f.clean, 'http://abcdefghijklmnopqrstuvwxyz.com') + + def test_urlfield_6(self): + f = URLField(required=False) + self.assertEqual(u'http://example.com/', f.clean('example.com')) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'https://example.com/', f.clean('https://example.com')) + + def test_urlfield_7(self): + f = URLField() + self.assertEqual(u'http://example.com/', f.clean('http://example.com')) + self.assertEqual(u'http://example.com/test', f.clean('http://example.com/test')) + + def test_urlfield_ticket11826(self): + f = URLField() + self.assertEqual(u'http://example.com/?some_param=some_value', f.clean('http://example.com?some_param=some_value')) + + # BooleanField ################################################################ + + def test_booleanfield_1(self): + f = BooleanField() + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(True, f.clean(True)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, False) + self.assertEqual(True, f.clean(1)) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, 0) + self.assertEqual(True, f.clean('Django rocks')) + self.assertEqual(True, f.clean('True')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, 'False') + + def test_booleanfield_2(self): + f = BooleanField(required=False) + self.assertEqual(False, f.clean('')) + self.assertEqual(False, f.clean(None)) + self.assertEqual(True, f.clean(True)) + self.assertEqual(False, f.clean(False)) + self.assertEqual(True, f.clean(1)) + self.assertEqual(False, f.clean(0)) + self.assertEqual(True, f.clean('1')) + self.assertEqual(False, f.clean('0')) + self.assertEqual(True, f.clean('Django rocks')) + self.assertEqual(False, f.clean('False')) + + # ChoiceField ################################################################# + + def test_choicefield_1(self): + f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')]) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual(u'1', f.clean(1)) + self.assertEqual(u'1', f.clean('1')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 3 is not one of the available choices.']", f.clean, '3') + + def test_choicefield_2(self): + f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'', f.clean(None)) + self.assertEqual(u'1', f.clean(1)) + self.assertEqual(u'1', f.clean('1')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 3 is not one of the available choices.']", f.clean, '3') + + def test_choicefield_3(self): + f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')]) + self.assertEqual(u'J', f.clean('J')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. John is not one of the available choices.']", f.clean, 'John') + + def test_choicefield_4(self): + f = ChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) + self.assertEqual(u'1', f.clean(1)) + self.assertEqual(u'1', f.clean('1')) + self.assertEqual(u'3', f.clean(3)) + self.assertEqual(u'3', f.clean('3')) + self.assertEqual(u'5', f.clean(5)) + self.assertEqual(u'5', f.clean('5')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, '6') + + # TypedChoiceField ############################################################ + # TypedChoiceField is just like ChoiceField, except that coerced types will + # be returned: + + def test_typedchoicefield_1(self): + f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int) + self.assertEqual(1, f.clean('1')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 2 is not one of the available choices.']", f.clean, '2') + + def test_typedchoicefield_2(self): + # Different coercion, same validation. + f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=float) + self.assertEqual(1.0, f.clean('1')) + + def test_typedchoicefield_3(self): + # This can also cause weirdness: be careful (bool(-1) == True, remember) + f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=bool) + self.assertEqual(True, f.clean('-1')) + + def test_typedchoicefield_4(self): + # Even more weirdness: if you have a valid choice but your coercion function + # can't coerce, you'll still get a validation error. Don't do this! + f = TypedChoiceField(choices=[('A', 'A'), ('B', 'B')], coerce=int) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. B is not one of the available choices.']", f.clean, 'B') + # Required fields require values + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + + def test_typedchoicefield_5(self): + # Non-required fields aren't required + f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False) + self.assertEqual('', f.clean('')) + # If you want cleaning an empty value to return a different type, tell the field + + def test_typedchoicefield_6(self): + f = TypedChoiceField(choices=[(1, "+1"), (-1, "-1")], coerce=int, required=False, empty_value=None) + self.assertEqual(None, f.clean('')) + + # NullBooleanField ############################################################ + + def test_nullbooleanfield_1(self): + f = NullBooleanField() + self.assertEqual(None, f.clean('')) + self.assertEqual(True, f.clean(True)) + self.assertEqual(False, f.clean(False)) + self.assertEqual(None, f.clean(None)) + self.assertEqual(False, f.clean('0')) + self.assertEqual(True, f.clean('1')) + self.assertEqual(None, f.clean('2')) + self.assertEqual(None, f.clean('3')) + self.assertEqual(None, f.clean('hello')) + + + def test_nullbooleanfield_2(self): + # Make sure that the internal value is preserved if using HiddenInput (#7753) + class HiddenNullBooleanForm(Form): + hidden_nullbool1 = NullBooleanField(widget=HiddenInput, initial=True) + hidden_nullbool2 = NullBooleanField(widget=HiddenInput, initial=False) + f = HiddenNullBooleanForm() + self.assertEqual('<input type="hidden" name="hidden_nullbool1" value="True" id="id_hidden_nullbool1" /><input type="hidden" name="hidden_nullbool2" value="False" id="id_hidden_nullbool2" />', str(f)) + + def test_nullbooleanfield_3(self): + class HiddenNullBooleanForm(Form): + hidden_nullbool1 = NullBooleanField(widget=HiddenInput, initial=True) + hidden_nullbool2 = NullBooleanField(widget=HiddenInput, initial=False) + f = HiddenNullBooleanForm({ 'hidden_nullbool1': 'True', 'hidden_nullbool2': 'False' }) + self.assertEqual(None, f.full_clean()) + self.assertEqual(True, f.cleaned_data['hidden_nullbool1']) + self.assertEqual(False, f.cleaned_data['hidden_nullbool2']) + + def test_nullbooleanfield_4(self): + # Make sure we're compatible with MySQL, which uses 0 and 1 for its boolean + # values. (#9609) + NULLBOOL_CHOICES = (('1', 'Yes'), ('0', 'No'), ('', 'Unknown')) + class MySQLNullBooleanForm(Form): + nullbool0 = NullBooleanField(widget=RadioSelect(choices=NULLBOOL_CHOICES)) + nullbool1 = NullBooleanField(widget=RadioSelect(choices=NULLBOOL_CHOICES)) + nullbool2 = NullBooleanField(widget=RadioSelect(choices=NULLBOOL_CHOICES)) + f = MySQLNullBooleanForm({ 'nullbool0': '1', 'nullbool1': '0', 'nullbool2': '' }) + self.assertEqual(None, f.full_clean()) + self.assertEqual(True, f.cleaned_data['nullbool0']) + self.assertEqual(False, f.cleaned_data['nullbool1']) + self.assertEqual(None, f.cleaned_data['nullbool2']) + + # MultipleChoiceField ######################################################### + + def test_multiplechoicefield_1(self): + f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')]) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertEqual([u'1'], f.clean([1])) + self.assertEqual([u'1'], f.clean(['1'])) + self.assertEqual([u'1', u'2'], f.clean(['1', '2'])) + self.assertEqual([u'1', u'2'], f.clean([1, '2'])) + self.assertEqual([u'1', u'2'], f.clean((1, '2'))) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a list of values.']", f.clean, 'hello') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, []) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, ()) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 3 is not one of the available choices.']", f.clean, ['3']) + + def test_multiplechoicefield_2(self): + f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) + self.assertEqual([], f.clean('')) + self.assertEqual([], f.clean(None)) + self.assertEqual([u'1'], f.clean([1])) + self.assertEqual([u'1'], f.clean(['1'])) + self.assertEqual([u'1', u'2'], f.clean(['1', '2'])) + self.assertEqual([u'1', u'2'], f.clean([1, '2'])) + self.assertEqual([u'1', u'2'], f.clean((1, '2'))) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a list of values.']", f.clean, 'hello') + self.assertEqual([], f.clean([])) + self.assertEqual([], f.clean(())) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 3 is not one of the available choices.']", f.clean, ['3']) + + def test_multiplechoicefield_3(self): + f = MultipleChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) + self.assertEqual([u'1'], f.clean([1])) + self.assertEqual([u'1'], f.clean(['1'])) + self.assertEqual([u'1', u'5'], f.clean([1, 5])) + self.assertEqual([u'1', u'5'], f.clean([1, '5'])) + self.assertEqual([u'1', u'5'], f.clean(['1', 5])) + self.assertEqual([u'1', u'5'], f.clean(['1', '5'])) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, ['6']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Select a valid choice. 6 is not one of the available choices.']", f.clean, ['1','6']) + + # ComboField ################################################################## + + def test_combofield_1(self): + f = ComboField(fields=[CharField(max_length=20), EmailField()]) + self.assertEqual(u'test@example.com', f.clean('test@example.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 20 characters (it has 28).']", f.clean, 'longemailaddress@example.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'not an e-mail') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + + def test_combofield_2(self): + f = ComboField(fields=[CharField(max_length=20), EmailField()], required=False) + self.assertEqual(u'test@example.com', f.clean('test@example.com')) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Ensure this value has at most 20 characters (it has 28).']", f.clean, 'longemailaddress@example.com') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid e-mail address.']", f.clean, 'not an e-mail') + self.assertEqual(u'', f.clean('')) + self.assertEqual(u'', f.clean(None)) + + # FilePathField ############################################################### + + def test_filepathfield_1(self): + path = os.path.abspath(forms.__file__) + path = os.path.dirname(path) + '/' + self.assertTrue(fix_os_paths(path).endswith('/django/forms/')) + + def test_filepathfield_2(self): + path = forms.__file__ + 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() + expected = [ + ('/django/forms/__init__.py', '__init__.py'), + ('/django/forms/fields.py', 'fields.py'), + ('/django/forms/forms.py', 'forms.py'), + ('/django/forms/formsets.py', 'formsets.py'), + ('/django/forms/models.py', 'models.py'), + ('/django/forms/util.py', 'util.py'), + ('/django/forms/widgets.py', 'widgets.py') + ] + for exp, got in zip(expected, fix_os_paths(f.choices)): + self.assertEqual(exp[1], got[1]) + 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_3(self): + path = forms.__file__ + path = os.path.dirname(os.path.abspath(path)) + '/' + f = FilePathField(path=path, match='^.*?\.py$') + f.choices.sort() + expected = [ + ('/django/forms/__init__.py', '__init__.py'), + ('/django/forms/fields.py', 'fields.py'), + ('/django/forms/forms.py', 'forms.py'), + ('/django/forms/formsets.py', 'formsets.py'), + ('/django/forms/models.py', 'models.py'), + ('/django/forms/util.py', 'util.py'), + ('/django/forms/widgets.py', 'widgets.py') + ] + for exp, got in zip(expected, fix_os_paths(f.choices)): + self.assertEqual(exp[1], got[1]) + self.assertTrue(got[0].endswith(exp[0])) + + def test_filepathfield_4(self): + path = os.path.abspath(forms.__file__) + path = os.path.dirname(path) + '/' + f = FilePathField(path=path, recursive=True, match='^.*?\.py$') + f.choices.sort() + expected = [ + ('/django/forms/__init__.py', '__init__.py'), + ('/django/forms/extras/__init__.py', 'extras/__init__.py'), + ('/django/forms/extras/widgets.py', 'extras/widgets.py'), + ('/django/forms/fields.py', 'fields.py'), + ('/django/forms/forms.py', 'forms.py'), + ('/django/forms/formsets.py', 'formsets.py'), + ('/django/forms/models.py', 'models.py'), + ('/django/forms/util.py', 'util.py'), + ('/django/forms/widgets.py', 'widgets.py') + ] + for exp, got in zip(expected, fix_os_paths(f.choices)): + self.assertEqual(exp[1], got[1]) + self.assertTrue(got[0].endswith(exp[0])) + + # SplitDateTimeField ########################################################## + + def test_splitdatetimefield_1(self): + from django.forms.widgets import SplitDateTimeWidget + f = SplitDateTimeField() + assert isinstance(f.widget, SplitDateTimeWidget) + self.assertEqual(datetime.datetime(2006, 1, 10, 7, 30), f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)])) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, None) + self.assertRaisesErrorWithMessage(ValidationError, "[u'This field is required.']", f.clean, '') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a list of values.']", f.clean, 'hello') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.', u'Enter a valid time.']", f.clean, ['hello', 'there']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, ['2006-01-10', 'there']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, ['hello', '07:30']) + + def test_splitdatetimefield_2(self): + f = SplitDateTimeField(required=False) + self.assertEqual(datetime.datetime(2006, 1, 10, 7, 30), f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)])) + self.assertEqual(datetime.datetime(2006, 1, 10, 7, 30), f.clean(['2006-01-10', '07:30'])) + self.assertEqual(None, f.clean(None)) + self.assertEqual(None, f.clean('')) + self.assertEqual(None, f.clean([''])) + self.assertEqual(None, f.clean(['', ''])) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a list of values.']", f.clean, 'hello') + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.', u'Enter a valid time.']", f.clean, ['hello', 'there']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, ['2006-01-10', 'there']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, ['hello', '07:30']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, ['2006-01-10', '']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid time.']", f.clean, ['2006-01-10']) + self.assertRaisesErrorWithMessage(ValidationError, "[u'Enter a valid date.']", f.clean, ['', '07:30']) diff --git a/parts/django/tests/regressiontests/forms/tests/forms.py b/parts/django/tests/regressiontests/forms/tests/forms.py new file mode 100644 index 0000000..e555a77 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/forms.py @@ -0,0 +1,1700 @@ +# -*- coding: utf-8 -*- +import datetime +from decimal import Decimal +import re +import time +from unittest import TestCase +from django.core.files.uploadedfile import SimpleUploadedFile +from django.forms import * +from django.http import QueryDict +from django.template import Template, Context +from django.utils.datastructures import MultiValueDict, MergeDict +from django.utils.safestring import mark_safe + + +class Person(Form): + first_name = CharField() + last_name = CharField() + birthday = DateField() + + +class PersonNew(Form): + first_name = CharField(widget=TextInput(attrs={'id': 'first_name_id'})) + last_name = CharField() + birthday = DateField() + + +class FormsTestCase(TestCase): + # A Form is a collection of Fields. It knows how to validate a set of data and it + # knows how to render itself in a couple of default ways (e.g., an HTML table). + # You can pass it data in __init__(), as a dictionary. + + def test_form(self): + # Pass a dictionary to a Form's __init__(). + p = Person({'first_name': u'John', 'last_name': u'Lennon', 'birthday': u'1940-10-9'}) + + self.assertTrue(p.is_bound) + self.assertEqual(p.errors, {}) + self.assertTrue(p.is_valid()) + self.assertEqual(p.errors.as_ul(), u'') + self.assertEqual(p.errors.as_text(), u'') + self.assertEqual(p.cleaned_data["first_name"], u'John') + self.assertEqual(p.cleaned_data["last_name"], u'Lennon') + self.assertEqual(p.cleaned_data["birthday"], datetime.date(1940, 10, 9)) + self.assertEqual(str(p['first_name']), '<input type="text" name="first_name" value="John" id="id_first_name" />') + self.assertEqual(str(p['last_name']), '<input type="text" name="last_name" value="Lennon" id="id_last_name" />') + self.assertEqual(str(p['birthday']), '<input type="text" name="birthday" value="1940-10-9" id="id_birthday" />') + try: + p['nonexistentfield'] + self.fail('Attempts to access non-existent fields should fail.') + except KeyError: + pass + + form_output = [] + + for boundfield in p: + form_output.append(str(boundfield)) + + self.assertEqual('\n'.join(form_output), """<input type="text" name="first_name" value="John" id="id_first_name" /> +<input type="text" name="last_name" value="Lennon" id="id_last_name" /> +<input type="text" name="birthday" value="1940-10-9" id="id_birthday" />""") + + form_output = [] + + for boundfield in p: + form_output.append([boundfield.label, boundfield.data]) + + self.assertEqual(form_output, [ + ['First name', u'John'], + ['Last name', u'Lennon'], + ['Birthday', u'1940-10-9'] + ]) + self.assertEqual(str(p), """<tr><th><label for="id_first_name">First name:</label></th><td><input type="text" name="first_name" value="John" id="id_first_name" /></td></tr> +<tr><th><label for="id_last_name">Last name:</label></th><td><input type="text" name="last_name" value="Lennon" id="id_last_name" /></td></tr> +<tr><th><label for="id_birthday">Birthday:</label></th><td><input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></td></tr>""") + + def test_empty_dict(self): + # Empty dictionaries are valid, too. + p = Person({}) + self.assertTrue(p.is_bound) + self.assertEqual(p.errors['first_name'], [u'This field is required.']) + self.assertEqual(p.errors['last_name'], [u'This field is required.']) + self.assertEqual(p.errors['birthday'], [u'This field is required.']) + self.assertFalse(p.is_valid()) + try: + p.cleaned_data + self.fail('Attempts to access cleaned_data when validation fails should fail.') + except AttributeError: + pass + self.assertEqual(str(p), """<tr><th><label for="id_first_name">First name:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="first_name" id="id_first_name" /></td></tr> +<tr><th><label for="id_last_name">Last name:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="last_name" id="id_last_name" /></td></tr> +<tr><th><label for="id_birthday">Birthday:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="birthday" id="id_birthday" /></td></tr>""") + self.assertEqual(p.as_table(), """<tr><th><label for="id_first_name">First name:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="first_name" id="id_first_name" /></td></tr> +<tr><th><label for="id_last_name">Last name:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="last_name" id="id_last_name" /></td></tr> +<tr><th><label for="id_birthday">Birthday:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="birthday" id="id_birthday" /></td></tr>""") + self.assertEqual(p.as_ul(), """<li><ul class="errorlist"><li>This field is required.</li></ul><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></li>""") + self.assertEqual(p.as_p(), """<ul class="errorlist"><li>This field is required.</li></ul> +<p><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></p> +<ul class="errorlist"><li>This field is required.</li></ul> +<p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></p> +<ul class="errorlist"><li>This field is required.</li></ul> +<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></p>""") + + def test_unbound_form(self): + # If you don't pass any values to the Form's __init__(), or if you pass None, + # the Form will be considered unbound and won't do any validation. Form.errors + # will be an empty dictionary *but* Form.is_valid() will return False. + p = Person() + self.assertFalse(p.is_bound) + self.assertEqual(p.errors, {}) + self.assertFalse(p.is_valid()) + try: + p.cleaned_data + self.fail('Attempts to access cleaned_data when validation fails should fail.') + except AttributeError: + pass + self.assertEqual(str(p), """<tr><th><label for="id_first_name">First name:</label></th><td><input type="text" name="first_name" id="id_first_name" /></td></tr> +<tr><th><label for="id_last_name">Last name:</label></th><td><input type="text" name="last_name" id="id_last_name" /></td></tr> +<tr><th><label for="id_birthday">Birthday:</label></th><td><input type="text" name="birthday" id="id_birthday" /></td></tr>""") + self.assertEqual(p.as_table(), """<tr><th><label for="id_first_name">First name:</label></th><td><input type="text" name="first_name" id="id_first_name" /></td></tr> +<tr><th><label for="id_last_name">Last name:</label></th><td><input type="text" name="last_name" id="id_last_name" /></td></tr> +<tr><th><label for="id_birthday">Birthday:</label></th><td><input type="text" name="birthday" id="id_birthday" /></td></tr>""") + self.assertEqual(p.as_ul(), """<li><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></li> +<li><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></li> +<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></li>""") + self.assertEqual(p.as_p(), """<p><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></p> +<p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></p> +<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /></p>""") + + def test_unicode_values(self): + # Unicode values are handled properly. + p = Person({'first_name': u'John', 'last_name': u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111', 'birthday': '1940-10-9'}) + self.assertEqual(p.as_table(), u'<tr><th><label for="id_first_name">First name:</label></th><td><input type="text" name="first_name" value="John" id="id_first_name" /></td></tr>\n<tr><th><label for="id_last_name">Last name:</label></th><td><input type="text" name="last_name" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" id="id_last_name" /></td></tr>\n<tr><th><label for="id_birthday">Birthday:</label></th><td><input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></td></tr>') + self.assertEqual(p.as_ul(), u'<li><label for="id_first_name">First name:</label> <input type="text" name="first_name" value="John" id="id_first_name" /></li>\n<li><label for="id_last_name">Last name:</label> <input type="text" name="last_name" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" id="id_last_name" /></li>\n<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></li>') + self.assertEqual(p.as_p(), u'<p><label for="id_first_name">First name:</label> <input type="text" name="first_name" value="John" id="id_first_name" /></p>\n<p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" id="id_last_name" /></p>\n<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></p>') + + p = Person({'last_name': u'Lennon'}) + self.assertEqual(p.errors['first_name'], [u'This field is required.']) + self.assertEqual(p.errors['birthday'], [u'This field is required.']) + self.assertFalse(p.is_valid()) + self.assertEqual(p.errors.as_ul(), u'<ul class="errorlist"><li>first_name<ul class="errorlist"><li>This field is required.</li></ul></li><li>birthday<ul class="errorlist"><li>This field is required.</li></ul></li></ul>') + self.assertEqual(p.errors.as_text(), """* first_name + * This field is required. +* birthday + * This field is required.""") + try: + p.cleaned_data + self.fail('Attempts to access cleaned_data when validation fails should fail.') + except AttributeError: + pass + self.assertEqual(p['first_name'].errors, [u'This field is required.']) + self.assertEqual(p['first_name'].errors.as_ul(), u'<ul class="errorlist"><li>This field is required.</li></ul>') + self.assertEqual(p['first_name'].errors.as_text(), u'* This field is required.') + + p = Person() + self.assertEqual(str(p['first_name']), '<input type="text" name="first_name" id="id_first_name" />') + self.assertEqual(str(p['last_name']), '<input type="text" name="last_name" id="id_last_name" />') + self.assertEqual(str(p['birthday']), '<input type="text" name="birthday" id="id_birthday" />') + + def test_cleaned_data_only_fields(self): + # cleaned_data will always *only* contain a key for fields defined in the + # Form, even if you pass extra data when you define the Form. In this + # example, we pass a bunch of extra fields to the form constructor, + # but cleaned_data contains only the form's fields. + data = {'first_name': u'John', 'last_name': u'Lennon', 'birthday': u'1940-10-9', 'extra1': 'hello', 'extra2': 'hello'} + p = Person(data) + self.assertTrue(p.is_valid()) + self.assertEqual(p.cleaned_data['first_name'], u'John') + self.assertEqual(p.cleaned_data['last_name'], u'Lennon') + self.assertEqual(p.cleaned_data['birthday'], datetime.date(1940, 10, 9)) + + def test_optional_data(self): + # cleaned_data will include a key and value for *all* fields defined in the Form, + # even if the Form's data didn't include a value for fields that are not + # required. In this example, the data dictionary doesn't include a value for the + # "nick_name" field, but cleaned_data includes it. For CharFields, it's set to the + # empty string. + class OptionalPersonForm(Form): + first_name = CharField() + last_name = CharField() + nick_name = CharField(required=False) + + data = {'first_name': u'John', 'last_name': u'Lennon'} + f = OptionalPersonForm(data) + self.assertTrue(f.is_valid()) + self.assertEqual(f.cleaned_data['nick_name'], u'') + self.assertEqual(f.cleaned_data['first_name'], u'John') + self.assertEqual(f.cleaned_data['last_name'], u'Lennon') + + # For DateFields, it's set to None. + class OptionalPersonForm(Form): + first_name = CharField() + last_name = CharField() + birth_date = DateField(required=False) + + data = {'first_name': u'John', 'last_name': u'Lennon'} + f = OptionalPersonForm(data) + self.assertTrue(f.is_valid()) + self.assertEqual(f.cleaned_data['birth_date'], None) + self.assertEqual(f.cleaned_data['first_name'], u'John') + self.assertEqual(f.cleaned_data['last_name'], u'Lennon') + + def test_auto_id(self): + # "auto_id" tells the Form to add an "id" attribute to each form element. + # If it's a string that contains '%s', Django will use that as a format string + # into which the field's name will be inserted. It will also put a <label> around + # the human-readable labels for a field. + p = Person(auto_id='%s_id') + self.assertEqual(p.as_table(), """<tr><th><label for="first_name_id">First name:</label></th><td><input type="text" name="first_name" id="first_name_id" /></td></tr> +<tr><th><label for="last_name_id">Last name:</label></th><td><input type="text" name="last_name" id="last_name_id" /></td></tr> +<tr><th><label for="birthday_id">Birthday:</label></th><td><input type="text" name="birthday" id="birthday_id" /></td></tr>""") + self.assertEqual(p.as_ul(), """<li><label for="first_name_id">First name:</label> <input type="text" name="first_name" id="first_name_id" /></li> +<li><label for="last_name_id">Last name:</label> <input type="text" name="last_name" id="last_name_id" /></li> +<li><label for="birthday_id">Birthday:</label> <input type="text" name="birthday" id="birthday_id" /></li>""") + self.assertEqual(p.as_p(), """<p><label for="first_name_id">First name:</label> <input type="text" name="first_name" id="first_name_id" /></p> +<p><label for="last_name_id">Last name:</label> <input type="text" name="last_name" id="last_name_id" /></p> +<p><label for="birthday_id">Birthday:</label> <input type="text" name="birthday" id="birthday_id" /></p>""") + + def test_auto_id_true(self): + # If auto_id is any True value whose str() does not contain '%s', the "id" + # attribute will be the name of the field. + p = Person(auto_id=True) + self.assertEqual(p.as_ul(), """<li><label for="first_name">First name:</label> <input type="text" name="first_name" id="first_name" /></li> +<li><label for="last_name">Last name:</label> <input type="text" name="last_name" id="last_name" /></li> +<li><label for="birthday">Birthday:</label> <input type="text" name="birthday" id="birthday" /></li>""") + + def test_auto_id_false(self): + # If auto_id is any False value, an "id" attribute won't be output unless it + # was manually entered. + p = Person(auto_id=False) + self.assertEqual(p.as_ul(), """<li>First name: <input type="text" name="first_name" /></li> +<li>Last name: <input type="text" name="last_name" /></li> +<li>Birthday: <input type="text" name="birthday" /></li>""") + + def test_id_on_field(self): + # In this example, auto_id is False, but the "id" attribute for the "first_name" + # field is given. Also note that field gets a <label>, while the others don't. + p = PersonNew(auto_id=False) + self.assertEqual(p.as_ul(), """<li><label for="first_name_id">First name:</label> <input type="text" id="first_name_id" name="first_name" /></li> +<li>Last name: <input type="text" name="last_name" /></li> +<li>Birthday: <input type="text" name="birthday" /></li>""") + + def test_auto_id_on_form_and_field(self): + # If the "id" attribute is specified in the Form and auto_id is True, the "id" + # attribute in the Form gets precedence. + p = PersonNew(auto_id=True) + self.assertEqual(p.as_ul(), """<li><label for="first_name_id">First name:</label> <input type="text" id="first_name_id" name="first_name" /></li> +<li><label for="last_name">Last name:</label> <input type="text" name="last_name" id="last_name" /></li> +<li><label for="birthday">Birthday:</label> <input type="text" name="birthday" id="birthday" /></li>""") + + def test_various_boolean_values(self): + class SignupForm(Form): + email = EmailField() + get_spam = BooleanField() + + f = SignupForm(auto_id=False) + self.assertEqual(str(f['email']), '<input type="text" name="email" />') + self.assertEqual(str(f['get_spam']), '<input type="checkbox" name="get_spam" />') + + f = SignupForm({'email': 'test@example.com', 'get_spam': True}, auto_id=False) + self.assertEqual(str(f['email']), '<input type="text" name="email" value="test@example.com" />') + self.assertEqual(str(f['get_spam']), '<input checked="checked" type="checkbox" name="get_spam" />') + + # 'True' or 'true' should be rendered without a value attribute + f = SignupForm({'email': 'test@example.com', 'get_spam': 'True'}, auto_id=False) + self.assertEqual(str(f['get_spam']), '<input checked="checked" type="checkbox" name="get_spam" />') + + f = SignupForm({'email': 'test@example.com', 'get_spam': 'true'}, auto_id=False) + self.assertEqual(str(f['get_spam']), '<input checked="checked" type="checkbox" name="get_spam" />') + + # A value of 'False' or 'false' should be rendered unchecked + f = SignupForm({'email': 'test@example.com', 'get_spam': 'False'}, auto_id=False) + self.assertEqual(str(f['get_spam']), '<input type="checkbox" name="get_spam" />') + + f = SignupForm({'email': 'test@example.com', 'get_spam': 'false'}, auto_id=False) + self.assertEqual(str(f['get_spam']), '<input type="checkbox" name="get_spam" />') + + def test_widget_output(self): + # Any Field can have a Widget class passed to its constructor: + class ContactForm(Form): + subject = CharField() + message = CharField(widget=Textarea) + + f = ContactForm(auto_id=False) + self.assertEqual(str(f['subject']), '<input type="text" name="subject" />') + self.assertEqual(str(f['message']), '<textarea rows="10" cols="40" name="message"></textarea>') + + # as_textarea(), as_text() and as_hidden() are shortcuts for changing the output + # widget type: + self.assertEqual(f['subject'].as_textarea(), u'<textarea rows="10" cols="40" name="subject"></textarea>') + self.assertEqual(f['message'].as_text(), u'<input type="text" name="message" />') + self.assertEqual(f['message'].as_hidden(), u'<input type="hidden" name="message" />') + + # The 'widget' parameter to a Field can also be an instance: + class ContactForm(Form): + subject = CharField() + message = CharField(widget=Textarea(attrs={'rows': 80, 'cols': 20})) + + f = ContactForm(auto_id=False) + self.assertEqual(str(f['message']), '<textarea rows="80" cols="20" name="message"></textarea>') + + # Instance-level attrs are *not* carried over to as_textarea(), as_text() and + # as_hidden(): + self.assertEqual(f['message'].as_text(), u'<input type="text" name="message" />') + f = ContactForm({'subject': 'Hello', 'message': 'I love you.'}, auto_id=False) + self.assertEqual(f['subject'].as_textarea(), u'<textarea rows="10" cols="40" name="subject">Hello</textarea>') + self.assertEqual(f['message'].as_text(), u'<input type="text" name="message" value="I love you." />') + self.assertEqual(f['message'].as_hidden(), u'<input type="hidden" name="message" value="I love you." />') + + def test_forms_with_choices(self): + # For a form with a <select>, use ChoiceField: + class FrameworkForm(Form): + name = CharField() + language = ChoiceField(choices=[('P', 'Python'), ('J', 'Java')]) + + f = FrameworkForm(auto_id=False) + self.assertEqual(str(f['language']), """<select name="language"> +<option value="P">Python</option> +<option value="J">Java</option> +</select>""") + f = FrameworkForm({'name': 'Django', 'language': 'P'}, auto_id=False) + self.assertEqual(str(f['language']), """<select name="language"> +<option value="P" selected="selected">Python</option> +<option value="J">Java</option> +</select>""") + + # A subtlety: If one of the choices' value is the empty string and the form is + # unbound, then the <option> for the empty-string choice will get selected="selected". + class FrameworkForm(Form): + name = CharField() + language = ChoiceField(choices=[('', '------'), ('P', 'Python'), ('J', 'Java')]) + + f = FrameworkForm(auto_id=False) + self.assertEqual(str(f['language']), """<select name="language"> +<option value="" selected="selected">------</option> +<option value="P">Python</option> +<option value="J">Java</option> +</select>""") + + # You can specify widget attributes in the Widget constructor. + class FrameworkForm(Form): + name = CharField() + language = ChoiceField(choices=[('P', 'Python'), ('J', 'Java')], widget=Select(attrs={'class': 'foo'})) + + f = FrameworkForm(auto_id=False) + self.assertEqual(str(f['language']), """<select class="foo" name="language"> +<option value="P">Python</option> +<option value="J">Java</option> +</select>""") + f = FrameworkForm({'name': 'Django', 'language': 'P'}, auto_id=False) + self.assertEqual(str(f['language']), """<select class="foo" name="language"> +<option value="P" selected="selected">Python</option> +<option value="J">Java</option> +</select>""") + + # When passing a custom widget instance to ChoiceField, note that setting + # 'choices' on the widget is meaningless. The widget will use the choices + # defined on the Field, not the ones defined on the Widget. + class FrameworkForm(Form): + name = CharField() + language = ChoiceField(choices=[('P', 'Python'), ('J', 'Java')], widget=Select(choices=[('R', 'Ruby'), ('P', 'Perl')], attrs={'class': 'foo'})) + + f = FrameworkForm(auto_id=False) + self.assertEqual(str(f['language']), """<select class="foo" name="language"> +<option value="P">Python</option> +<option value="J">Java</option> +</select>""") + f = FrameworkForm({'name': 'Django', 'language': 'P'}, auto_id=False) + self.assertEqual(str(f['language']), """<select class="foo" name="language"> +<option value="P" selected="selected">Python</option> +<option value="J">Java</option> +</select>""") + + # You can set a ChoiceField's choices after the fact. + class FrameworkForm(Form): + name = CharField() + language = ChoiceField() + + f = FrameworkForm(auto_id=False) + self.assertEqual(str(f['language']), """<select name="language"> +</select>""") + f.fields['language'].choices = [('P', 'Python'), ('J', 'Java')] + self.assertEqual(str(f['language']), """<select name="language"> +<option value="P">Python</option> +<option value="J">Java</option> +</select>""") + + def test_forms_with_radio(self): + # Add widget=RadioSelect to use that widget with a ChoiceField. + class FrameworkForm(Form): + name = CharField() + language = ChoiceField(choices=[('P', 'Python'), ('J', 'Java')], widget=RadioSelect) + + f = FrameworkForm(auto_id=False) + self.assertEqual(str(f['language']), """<ul> +<li><label><input type="radio" name="language" value="P" /> Python</label></li> +<li><label><input type="radio" name="language" value="J" /> Java</label></li> +</ul>""") + self.assertEqual(f.as_table(), """<tr><th>Name:</th><td><input type="text" name="name" /></td></tr> +<tr><th>Language:</th><td><ul> +<li><label><input type="radio" name="language" value="P" /> Python</label></li> +<li><label><input type="radio" name="language" value="J" /> Java</label></li> +</ul></td></tr>""") + self.assertEqual(f.as_ul(), """<li>Name: <input type="text" name="name" /></li> +<li>Language: <ul> +<li><label><input type="radio" name="language" value="P" /> Python</label></li> +<li><label><input type="radio" name="language" value="J" /> Java</label></li> +</ul></li>""") + + # Regarding auto_id and <label>, RadioSelect is a special case. Each radio button + # gets a distinct ID, formed by appending an underscore plus the button's + # zero-based index. + f = FrameworkForm(auto_id='id_%s') + self.assertEqual(str(f['language']), """<ul> +<li><label for="id_language_0"><input type="radio" id="id_language_0" value="P" name="language" /> Python</label></li> +<li><label for="id_language_1"><input type="radio" id="id_language_1" value="J" name="language" /> Java</label></li> +</ul>""") + + # When RadioSelect is used with auto_id, and the whole form is printed using + # either as_table() or as_ul(), the label for the RadioSelect will point to the + # ID of the *first* radio button. + self.assertEqual(f.as_table(), """<tr><th><label for="id_name">Name:</label></th><td><input type="text" name="name" id="id_name" /></td></tr> +<tr><th><label for="id_language_0">Language:</label></th><td><ul> +<li><label for="id_language_0"><input type="radio" id="id_language_0" value="P" name="language" /> Python</label></li> +<li><label for="id_language_1"><input type="radio" id="id_language_1" value="J" name="language" /> Java</label></li> +</ul></td></tr>""") + self.assertEqual(f.as_ul(), """<li><label for="id_name">Name:</label> <input type="text" name="name" id="id_name" /></li> +<li><label for="id_language_0">Language:</label> <ul> +<li><label for="id_language_0"><input type="radio" id="id_language_0" value="P" name="language" /> Python</label></li> +<li><label for="id_language_1"><input type="radio" id="id_language_1" value="J" name="language" /> Java</label></li> +</ul></li>""") + self.assertEqual(f.as_p(), """<p><label for="id_name">Name:</label> <input type="text" name="name" id="id_name" /></p> +<p><label for="id_language_0">Language:</label> <ul> +<li><label for="id_language_0"><input type="radio" id="id_language_0" value="P" name="language" /> Python</label></li> +<li><label for="id_language_1"><input type="radio" id="id_language_1" value="J" name="language" /> Java</label></li> +</ul></p>""") + + def test_forms_wit_hmultiple_choice(self): + # MultipleChoiceField is a special case, as its data is required to be a list: + class SongForm(Form): + name = CharField() + composers = MultipleChoiceField() + + f = SongForm(auto_id=False) + self.assertEqual(str(f['composers']), """<select multiple="multiple" name="composers"> +</select>""") + + class SongForm(Form): + name = CharField() + composers = MultipleChoiceField(choices=[('J', 'John Lennon'), ('P', 'Paul McCartney')]) + + f = SongForm(auto_id=False) + self.assertEqual(str(f['composers']), """<select multiple="multiple" name="composers"> +<option value="J">John Lennon</option> +<option value="P">Paul McCartney</option> +</select>""") + f = SongForm({'name': 'Yesterday', 'composers': ['P']}, auto_id=False) + self.assertEqual(str(f['name']), '<input type="text" name="name" value="Yesterday" />') + self.assertEqual(str(f['composers']), """<select multiple="multiple" name="composers"> +<option value="J">John Lennon</option> +<option value="P" selected="selected">Paul McCartney</option> +</select>""") + + def test_hidden_data(self): + class SongForm(Form): + name = CharField() + composers = MultipleChoiceField(choices=[('J', 'John Lennon'), ('P', 'Paul McCartney')]) + + # MultipleChoiceField rendered as_hidden() is a special case. Because it can + # have multiple values, its as_hidden() renders multiple <input type="hidden"> + # tags. + f = SongForm({'name': 'Yesterday', 'composers': ['P']}, auto_id=False) + self.assertEqual(f['composers'].as_hidden(), '<input type="hidden" name="composers" value="P" />') + f = SongForm({'name': 'From Me To You', 'composers': ['P', 'J']}, auto_id=False) + self.assertEqual(f['composers'].as_hidden(), """<input type="hidden" name="composers" value="P" /> +<input type="hidden" name="composers" value="J" />""") + + def test_mulitple_choice_checkbox(self): + # MultipleChoiceField can also be used with the CheckboxSelectMultiple widget. + class SongForm(Form): + name = CharField() + composers = MultipleChoiceField(choices=[('J', 'John Lennon'), ('P', 'Paul McCartney')], widget=CheckboxSelectMultiple) + + f = SongForm(auto_id=False) + self.assertEqual(str(f['composers']), """<ul> +<li><label><input type="checkbox" name="composers" value="J" /> John Lennon</label></li> +<li><label><input type="checkbox" name="composers" value="P" /> Paul McCartney</label></li> +</ul>""") + f = SongForm({'composers': ['J']}, auto_id=False) + self.assertEqual(str(f['composers']), """<ul> +<li><label><input checked="checked" type="checkbox" name="composers" value="J" /> John Lennon</label></li> +<li><label><input type="checkbox" name="composers" value="P" /> Paul McCartney</label></li> +</ul>""") + f = SongForm({'composers': ['J', 'P']}, auto_id=False) + self.assertEqual(str(f['composers']), """<ul> +<li><label><input checked="checked" type="checkbox" name="composers" value="J" /> John Lennon</label></li> +<li><label><input checked="checked" type="checkbox" name="composers" value="P" /> Paul McCartney</label></li> +</ul>""") + + def test_checkbox_auto_id(self): + # Regarding auto_id, CheckboxSelectMultiple is a special case. Each checkbox + # gets a distinct ID, formed by appending an underscore plus the checkbox's + # zero-based index. + class SongForm(Form): + name = CharField() + composers = MultipleChoiceField(choices=[('J', 'John Lennon'), ('P', 'Paul McCartney')], widget=CheckboxSelectMultiple) + + f = SongForm(auto_id='%s_id') + self.assertEqual(str(f['composers']), """<ul> +<li><label for="composers_id_0"><input type="checkbox" name="composers" value="J" id="composers_id_0" /> John Lennon</label></li> +<li><label for="composers_id_1"><input type="checkbox" name="composers" value="P" id="composers_id_1" /> Paul McCartney</label></li> +</ul>""") + + def test_multiple_choice_list_data(self): + # Data for a MultipleChoiceField should be a list. QueryDict, MultiValueDict and + # MergeDict (when created as a merge of MultiValueDicts) conveniently work with + # this. + class SongForm(Form): + name = CharField() + composers = MultipleChoiceField(choices=[('J', 'John Lennon'), ('P', 'Paul McCartney')], widget=CheckboxSelectMultiple) + + data = {'name': 'Yesterday', 'composers': ['J', 'P']} + f = SongForm(data) + self.assertEqual(f.errors, {}) + + data = QueryDict('name=Yesterday&composers=J&composers=P') + f = SongForm(data) + self.assertEqual(f.errors, {}) + + data = MultiValueDict(dict(name=['Yesterday'], composers=['J', 'P'])) + f = SongForm(data) + self.assertEqual(f.errors, {}) + + data = MergeDict(MultiValueDict(dict(name=['Yesterday'], composers=['J', 'P']))) + f = SongForm(data) + self.assertEqual(f.errors, {}) + + def test_multiple_hidden(self): + class SongForm(Form): + name = CharField() + composers = MultipleChoiceField(choices=[('J', 'John Lennon'), ('P', 'Paul McCartney')], widget=CheckboxSelectMultiple) + + # The MultipleHiddenInput widget renders multiple values as hidden fields. + class SongFormHidden(Form): + name = CharField() + composers = MultipleChoiceField(choices=[('J', 'John Lennon'), ('P', 'Paul McCartney')], widget=MultipleHiddenInput) + + f = SongFormHidden(MultiValueDict(dict(name=['Yesterday'], composers=['J', 'P'])), auto_id=False) + self.assertEqual(f.as_ul(), """<li>Name: <input type="text" name="name" value="Yesterday" /><input type="hidden" name="composers" value="J" /> +<input type="hidden" name="composers" value="P" /></li>""") + + # When using CheckboxSelectMultiple, the framework expects a list of input and + # returns a list of input. + f = SongForm({'name': 'Yesterday'}, auto_id=False) + self.assertEqual(f.errors['composers'], [u'This field is required.']) + f = SongForm({'name': 'Yesterday', 'composers': ['J']}, auto_id=False) + self.assertEqual(f.errors, {}) + self.assertEqual(f.cleaned_data['composers'], [u'J']) + self.assertEqual(f.cleaned_data['name'], u'Yesterday') + f = SongForm({'name': 'Yesterday', 'composers': ['J', 'P']}, auto_id=False) + self.assertEqual(f.errors, {}) + self.assertEqual(f.cleaned_data['composers'], [u'J', u'P']) + self.assertEqual(f.cleaned_data['name'], u'Yesterday') + + def test_escaping(self): + # Validation errors are HTML-escaped when output as HTML. + class EscapingForm(Form): + special_name = CharField(label="<em>Special</em> Field") + special_safe_name = CharField(label=mark_safe("<em>Special</em> Field")) + + def clean_special_name(self): + raise ValidationError("Something's wrong with '%s'" % self.cleaned_data['special_name']) + + def clean_special_safe_name(self): + raise ValidationError(mark_safe("'<b>%s</b>' is a safe string" % self.cleaned_data['special_safe_name'])) + + f = EscapingForm({'special_name': "Nothing to escape", 'special_safe_name': "Nothing to escape"}, auto_id=False) + self.assertEqual(f.as_table(), """<tr><th><em>Special</em> Field:</th><td><ul class="errorlist"><li>Something's wrong with 'Nothing to escape'</li></ul><input type="text" name="special_name" value="Nothing to escape" /></td></tr> +<tr><th><em>Special</em> Field:</th><td><ul class="errorlist"><li>'<b>Nothing to escape</b>' is a safe string</li></ul><input type="text" name="special_safe_name" value="Nothing to escape" /></td></tr>""") + f = EscapingForm({ + 'special_name': "Should escape < & > and <script>alert('xss')</script>", + 'special_safe_name': "<i>Do not escape</i>" + }, auto_id=False) + self.assertEqual(f.as_table(), """<tr><th><em>Special</em> Field:</th><td><ul class="errorlist"><li>Something's wrong with 'Should escape < & > and <script>alert('xss')</script>'</li></ul><input type="text" name="special_name" value="Should escape < & > and <script>alert('xss')</script>" /></td></tr> +<tr><th><em>Special</em> Field:</th><td><ul class="errorlist"><li>'<b><i>Do not escape</i></b>' is a safe string</li></ul><input type="text" name="special_safe_name" value="<i>Do not escape</i>" /></td></tr>""") + + def test_validating_multiple_fields(self): + # There are a couple of ways to do multiple-field validation. If you want the + # validation message to be associated with a particular field, implement the + # clean_XXX() method on the Form, where XXX is the field name. As in + # Field.clean(), the clean_XXX() method should return the cleaned value. In the + # clean_XXX() method, you have access to self.cleaned_data, which is a dictionary + # of all the data that has been cleaned *so far*, in order by the fields, + # including the current field (e.g., the field XXX if you're in clean_XXX()). + class UserRegistration(Form): + username = CharField(max_length=10) + password1 = CharField(widget=PasswordInput) + password2 = CharField(widget=PasswordInput) + + def clean_password2(self): + if self.cleaned_data.get('password1') and self.cleaned_data.get('password2') and self.cleaned_data['password1'] != self.cleaned_data['password2']: + raise ValidationError(u'Please make sure your passwords match.') + + return self.cleaned_data['password2'] + + f = UserRegistration(auto_id=False) + self.assertEqual(f.errors, {}) + f = UserRegistration({}, auto_id=False) + self.assertEqual(f.errors['username'], [u'This field is required.']) + self.assertEqual(f.errors['password1'], [u'This field is required.']) + self.assertEqual(f.errors['password2'], [u'This field is required.']) + f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'bar'}, auto_id=False) + self.assertEqual(f.errors['password2'], [u'Please make sure your passwords match.']) + f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'foo'}, auto_id=False) + self.assertEqual(f.errors, {}) + self.assertEqual(f.cleaned_data['username'], u'adrian') + self.assertEqual(f.cleaned_data['password1'], u'foo') + self.assertEqual(f.cleaned_data['password2'], u'foo') + + # Another way of doing multiple-field validation is by implementing the + # Form's clean() method. If you do this, any ValidationError raised by that + # method will not be associated with a particular field; it will have a + # special-case association with the field named '__all__'. + # Note that in Form.clean(), you have access to self.cleaned_data, a dictionary of + # all the fields/values that have *not* raised a ValidationError. Also note + # Form.clean() is required to return a dictionary of all clean data. + class UserRegistration(Form): + username = CharField(max_length=10) + password1 = CharField(widget=PasswordInput) + password2 = CharField(widget=PasswordInput) + + def clean(self): + if self.cleaned_data.get('password1') and self.cleaned_data.get('password2') and self.cleaned_data['password1'] != self.cleaned_data['password2']: + raise ValidationError(u'Please make sure your passwords match.') + + return self.cleaned_data + + f = UserRegistration(auto_id=False) + self.assertEqual(f.errors, {}) + f = UserRegistration({}, auto_id=False) + self.assertEqual(f.as_table(), """<tr><th>Username:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="username" maxlength="10" /></td></tr> +<tr><th>Password1:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="password" name="password1" /></td></tr> +<tr><th>Password2:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="password" name="password2" /></td></tr>""") + self.assertEqual(f.errors['username'], [u'This field is required.']) + self.assertEqual(f.errors['password1'], [u'This field is required.']) + self.assertEqual(f.errors['password2'], [u'This field is required.']) + f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'bar'}, auto_id=False) + self.assertEqual(f.errors['__all__'], [u'Please make sure your passwords match.']) + self.assertEqual(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>""") + self.assertEqual(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>""") + f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'foo'}, auto_id=False) + self.assertEqual(f.errors, {}) + self.assertEqual(f.cleaned_data['username'], u'adrian') + self.assertEqual(f.cleaned_data['password1'], u'foo') + self.assertEqual(f.cleaned_data['password2'], u'foo') + + def test_dynamic_construction(self): + # It's possible to construct a Form dynamically by adding to the self.fields + # dictionary in __init__(). Don't forget to call Form.__init__() within the + # subclass' __init__(). + class Person(Form): + first_name = CharField() + last_name = CharField() + + def __init__(self, *args, **kwargs): + super(Person, self).__init__(*args, **kwargs) + self.fields['birthday'] = DateField() + + p = Person(auto_id=False) + self.assertEqual(p.as_table(), """<tr><th>First name:</th><td><input type="text" name="first_name" /></td></tr> +<tr><th>Last name:</th><td><input type="text" name="last_name" /></td></tr> +<tr><th>Birthday:</th><td><input type="text" name="birthday" /></td></tr>""") + + # Instances of a dynamic Form do not persist fields from one Form instance to + # the next. + class MyForm(Form): + def __init__(self, data=None, auto_id=False, field_list=[]): + Form.__init__(self, data, auto_id=auto_id) + + for field in field_list: + self.fields[field[0]] = field[1] + + field_list = [('field1', CharField()), ('field2', CharField())] + my_form = MyForm(field_list=field_list) + self.assertEqual(my_form.as_table(), """<tr><th>Field1:</th><td><input type="text" name="field1" /></td></tr> +<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>""") + field_list = [('field3', CharField()), ('field4', CharField())] + my_form = MyForm(field_list=field_list) + self.assertEqual(my_form.as_table(), """<tr><th>Field3:</th><td><input type="text" name="field3" /></td></tr> +<tr><th>Field4:</th><td><input type="text" name="field4" /></td></tr>""") + + class MyForm(Form): + default_field_1 = CharField() + default_field_2 = CharField() + + def __init__(self, data=None, auto_id=False, field_list=[]): + Form.__init__(self, data, auto_id=auto_id) + + for field in field_list: + self.fields[field[0]] = field[1] + + field_list = [('field1', CharField()), ('field2', CharField())] + my_form = MyForm(field_list=field_list) + self.assertEqual(my_form.as_table(), """<tr><th>Default field 1:</th><td><input type="text" name="default_field_1" /></td></tr> +<tr><th>Default field 2:</th><td><input type="text" name="default_field_2" /></td></tr> +<tr><th>Field1:</th><td><input type="text" name="field1" /></td></tr> +<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr>""") + field_list = [('field3', CharField()), ('field4', CharField())] + my_form = MyForm(field_list=field_list) + self.assertEqual(my_form.as_table(), """<tr><th>Default field 1:</th><td><input type="text" name="default_field_1" /></td></tr> +<tr><th>Default field 2:</th><td><input type="text" name="default_field_2" /></td></tr> +<tr><th>Field3:</th><td><input type="text" name="field3" /></td></tr> +<tr><th>Field4:</th><td><input type="text" name="field4" /></td></tr>""") + + # Similarly, changes to field attributes do not persist from one Form instance + # to the next. + class Person(Form): + first_name = CharField(required=False) + last_name = CharField(required=False) + + def __init__(self, names_required=False, *args, **kwargs): + super(Person, self).__init__(*args, **kwargs) + + if names_required: + self.fields['first_name'].required = True + self.fields['first_name'].widget.attrs['class'] = 'required' + self.fields['last_name'].required = True + self.fields['last_name'].widget.attrs['class'] = 'required' + + f = Person(names_required=False) + self.assertEqual(f['first_name'].field.required, f['last_name'].field.required, (False, False)) + self.assertEqual(f['first_name'].field.widget.attrs, f['last_name'].field.widget.attrs, ({}, {})) + f = Person(names_required=True) + self.assertEqual(f['first_name'].field.required, f['last_name'].field.required, (True, True)) + self.assertEqual(f['first_name'].field.widget.attrs, f['last_name'].field.widget.attrs, ({'class': 'required'}, {'class': 'required'})) + f = Person(names_required=False) + self.assertEqual(f['first_name'].field.required, f['last_name'].field.required, (False, False)) + self.assertEqual(f['first_name'].field.widget.attrs, f['last_name'].field.widget.attrs, ({}, {})) + + class Person(Form): + first_name = CharField(max_length=30) + last_name = CharField(max_length=30) + + def __init__(self, name_max_length=None, *args, **kwargs): + super(Person, self).__init__(*args, **kwargs) + + if name_max_length: + self.fields['first_name'].max_length = name_max_length + self.fields['last_name'].max_length = name_max_length + + f = Person(name_max_length=None) + self.assertEqual(f['first_name'].field.max_length, f['last_name'].field.max_length, (30, 30)) + f = Person(name_max_length=20) + self.assertEqual(f['first_name'].field.max_length, f['last_name'].field.max_length, (20, 20)) + f = Person(name_max_length=None) + self.assertEqual(f['first_name'].field.max_length, f['last_name'].field.max_length, (30, 30)) + + def test_hidden_widget(self): + # HiddenInput widgets are displayed differently in the as_table(), as_ul()) + # and as_p() output of a Form -- their verbose names are not displayed, and a + # separate row is not displayed. They're displayed in the last row of the + # form, directly after that row's form element. + class Person(Form): + first_name = CharField() + last_name = CharField() + hidden_text = CharField(widget=HiddenInput) + birthday = DateField() + + p = Person(auto_id=False) + self.assertEqual(p.as_table(), """<tr><th>First name:</th><td><input type="text" name="first_name" /></td></tr> +<tr><th>Last name:</th><td><input type="text" name="last_name" /></td></tr> +<tr><th>Birthday:</th><td><input type="text" name="birthday" /><input type="hidden" name="hidden_text" /></td></tr>""") + self.assertEqual(p.as_ul(), """<li>First name: <input type="text" name="first_name" /></li> +<li>Last name: <input type="text" name="last_name" /></li> +<li>Birthday: <input type="text" name="birthday" /><input type="hidden" name="hidden_text" /></li>""") + self.assertEqual(p.as_p(), """<p>First name: <input type="text" name="first_name" /></p> +<p>Last name: <input type="text" name="last_name" /></p> +<p>Birthday: <input type="text" name="birthday" /><input type="hidden" name="hidden_text" /></p>""") + + # With auto_id set, a HiddenInput still gets an ID, but it doesn't get a label. + p = Person(auto_id='id_%s') + self.assertEqual(p.as_table(), """<tr><th><label for="id_first_name">First name:</label></th><td><input type="text" name="first_name" id="id_first_name" /></td></tr> +<tr><th><label for="id_last_name">Last name:</label></th><td><input type="text" name="last_name" id="id_last_name" /></td></tr> +<tr><th><label for="id_birthday">Birthday:</label></th><td><input type="text" name="birthday" id="id_birthday" /><input type="hidden" name="hidden_text" id="id_hidden_text" /></td></tr>""") + self.assertEqual(p.as_ul(), """<li><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></li> +<li><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></li> +<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /><input type="hidden" name="hidden_text" id="id_hidden_text" /></li>""") + self.assertEqual(p.as_p(), """<p><label for="id_first_name">First name:</label> <input type="text" name="first_name" id="id_first_name" /></p> +<p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" id="id_last_name" /></p> +<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" id="id_birthday" /><input type="hidden" name="hidden_text" id="id_hidden_text" /></p>""") + + # If a field with a HiddenInput has errors, the as_table() and as_ul() output + # will include the error message(s) with the text "(Hidden field [fieldname]) " + # prepended. This message is displayed at the top of the output, regardless of + # its field's order in the form. + p = Person({'first_name': 'John', 'last_name': 'Lennon', 'birthday': '1940-10-9'}, auto_id=False) + self.assertEqual(p.as_table(), """<tr><td colspan="2"><ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul></td></tr> +<tr><th>First name:</th><td><input type="text" name="first_name" value="John" /></td></tr> +<tr><th>Last name:</th><td><input type="text" name="last_name" value="Lennon" /></td></tr> +<tr><th>Birthday:</th><td><input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></td></tr>""") + self.assertEqual(p.as_ul(), """<li><ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul></li> +<li>First name: <input type="text" name="first_name" value="John" /></li> +<li>Last name: <input type="text" name="last_name" value="Lennon" /></li> +<li>Birthday: <input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></li>""") + self.assertEqual(p.as_p(), """<ul class="errorlist"><li>(Hidden field hidden_text) This field is required.</li></ul> +<p>First name: <input type="text" name="first_name" value="John" /></p> +<p>Last name: <input type="text" name="last_name" value="Lennon" /></p> +<p>Birthday: <input type="text" name="birthday" value="1940-10-9" /><input type="hidden" name="hidden_text" /></p>""") + + # A corner case: It's possible for a form to have only HiddenInputs. + class TestForm(Form): + foo = CharField(widget=HiddenInput) + bar = CharField(widget=HiddenInput) + + p = TestForm(auto_id=False) + self.assertEqual(p.as_table(), '<input type="hidden" name="foo" /><input type="hidden" name="bar" />') + self.assertEqual(p.as_ul(), '<input type="hidden" name="foo" /><input type="hidden" name="bar" />') + self.assertEqual(p.as_p(), '<input type="hidden" name="foo" /><input type="hidden" name="bar" />') + + def test_field_order(self): + # A Form's fields are displayed in the same order in which they were defined. + class TestForm(Form): + field1 = CharField() + field2 = CharField() + field3 = CharField() + field4 = CharField() + field5 = CharField() + field6 = CharField() + field7 = CharField() + field8 = CharField() + field9 = CharField() + field10 = CharField() + field11 = CharField() + field12 = CharField() + field13 = CharField() + field14 = CharField() + + p = TestForm(auto_id=False) + self.assertEqual(p.as_table(), """<tr><th>Field1:</th><td><input type="text" name="field1" /></td></tr> +<tr><th>Field2:</th><td><input type="text" name="field2" /></td></tr> +<tr><th>Field3:</th><td><input type="text" name="field3" /></td></tr> +<tr><th>Field4:</th><td><input type="text" name="field4" /></td></tr> +<tr><th>Field5:</th><td><input type="text" name="field5" /></td></tr> +<tr><th>Field6:</th><td><input type="text" name="field6" /></td></tr> +<tr><th>Field7:</th><td><input type="text" name="field7" /></td></tr> +<tr><th>Field8:</th><td><input type="text" name="field8" /></td></tr> +<tr><th>Field9:</th><td><input type="text" name="field9" /></td></tr> +<tr><th>Field10:</th><td><input type="text" name="field10" /></td></tr> +<tr><th>Field11:</th><td><input type="text" name="field11" /></td></tr> +<tr><th>Field12:</th><td><input type="text" name="field12" /></td></tr> +<tr><th>Field13:</th><td><input type="text" name="field13" /></td></tr> +<tr><th>Field14:</th><td><input type="text" name="field14" /></td></tr>""") + + def test_form_html_attributes(self): + # Some Field classes have an effect on the HTML attributes of their associated + # Widget. If you set max_length in a CharField and its associated widget is + # either a TextInput or PasswordInput, then the widget's rendered HTML will + # include the "maxlength" attribute. + class UserRegistration(Form): + username = CharField(max_length=10) # uses TextInput by default + password = CharField(max_length=10, widget=PasswordInput) + realname = CharField(max_length=10, widget=TextInput) # redundantly define widget, just to test + address = CharField() # no max_length defined here + + p = UserRegistration(auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" maxlength="10" /></li> +<li>Password: <input type="password" name="password" maxlength="10" /></li> +<li>Realname: <input type="text" name="realname" maxlength="10" /></li> +<li>Address: <input type="text" name="address" /></li>""") + + # If you specify a custom "attrs" that includes the "maxlength" attribute, + # the Field's max_length attribute will override whatever "maxlength" you specify + # in "attrs". + class UserRegistration(Form): + username = CharField(max_length=10, widget=TextInput(attrs={'maxlength': 20})) + password = CharField(max_length=10, widget=PasswordInput) + + p = UserRegistration(auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" maxlength="10" /></li> +<li>Password: <input type="password" name="password" maxlength="10" /></li>""") + + def test_specifying_labels(self): + # You can specify the label for a field by using the 'label' argument to a Field + # class. If you don't specify 'label', Django will use the field name with + # underscores converted to spaces, and the initial letter capitalized. + class UserRegistration(Form): + username = CharField(max_length=10, label='Your username') + password1 = CharField(widget=PasswordInput) + password2 = CharField(widget=PasswordInput, label='Password (again)') + + p = UserRegistration(auto_id=False) + self.assertEqual(p.as_ul(), """<li>Your username: <input type="text" name="username" maxlength="10" /></li> +<li>Password1: <input type="password" name="password1" /></li> +<li>Password (again): <input type="password" name="password2" /></li>""") + + # Labels for as_* methods will only end in a colon if they don't end in other + # punctuation already. + class Questions(Form): + q1 = CharField(label='The first question') + q2 = CharField(label='What is your name?') + q3 = CharField(label='The answer to life is:') + q4 = CharField(label='Answer this question!') + q5 = CharField(label='The last question. Period.') + + self.assertEqual(Questions(auto_id=False).as_p(), """<p>The first question: <input type="text" name="q1" /></p> +<p>What is your name? <input type="text" name="q2" /></p> +<p>The answer to life is: <input type="text" name="q3" /></p> +<p>Answer this question! <input type="text" name="q4" /></p> +<p>The last question. Period. <input type="text" name="q5" /></p>""") + self.assertEqual(Questions().as_p(), """<p><label for="id_q1">The first question:</label> <input type="text" name="q1" id="id_q1" /></p> +<p><label for="id_q2">What is your name?</label> <input type="text" name="q2" id="id_q2" /></p> +<p><label for="id_q3">The answer to life is:</label> <input type="text" name="q3" id="id_q3" /></p> +<p><label for="id_q4">Answer this question!</label> <input type="text" name="q4" id="id_q4" /></p> +<p><label for="id_q5">The last question. Period.</label> <input type="text" name="q5" id="id_q5" /></p>""") + + # A label can be a Unicode object or a bytestring with special characters. + class UserRegistration(Form): + username = CharField(max_length=10, label='ŠĐĆŽćžšđ') + password = CharField(widget=PasswordInput, label=u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111') + + p = UserRegistration(auto_id=False) + self.assertEqual(p.as_ul(), u'<li>\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111: <input type="text" name="username" maxlength="10" /></li>\n<li>\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111: <input type="password" name="password" /></li>') + + # If a label is set to the empty string for a field, that field won't get a label. + class UserRegistration(Form): + username = CharField(max_length=10, label='') + password = CharField(widget=PasswordInput) + + p = UserRegistration(auto_id=False) + self.assertEqual(p.as_ul(), """<li> <input type="text" name="username" maxlength="10" /></li> +<li>Password: <input type="password" name="password" /></li>""") + p = UserRegistration(auto_id='id_%s') + self.assertEqual(p.as_ul(), """<li> <input id="id_username" type="text" name="username" maxlength="10" /></li> +<li><label for="id_password">Password:</label> <input type="password" name="password" id="id_password" /></li>""") + + # If label is None, Django will auto-create the label from the field name. This + # is default behavior. + class UserRegistration(Form): + username = CharField(max_length=10, label=None) + password = CharField(widget=PasswordInput) + + p = UserRegistration(auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" maxlength="10" /></li> +<li>Password: <input type="password" name="password" /></li>""") + p = UserRegistration(auto_id='id_%s') + self.assertEqual(p.as_ul(), """<li><label for="id_username">Username:</label> <input id="id_username" type="text" name="username" maxlength="10" /></li> +<li><label for="id_password">Password:</label> <input type="password" name="password" id="id_password" /></li>""") + + def test_label_suffix(self): + # You can specify the 'label_suffix' argument to a Form class to modify the + # punctuation symbol used at the end of a label. By default, the colon (:) is + # used, and is only appended to the label if the label doesn't already end with a + # punctuation symbol: ., !, ? or :. If you specify a different suffix, it will + # be appended regardless of the last character of the label. + class FavoriteForm(Form): + color = CharField(label='Favorite color?') + animal = CharField(label='Favorite animal') + + f = FavoriteForm(auto_id=False) + self.assertEqual(f.as_ul(), """<li>Favorite color? <input type="text" name="color" /></li> +<li>Favorite animal: <input type="text" name="animal" /></li>""") + f = FavoriteForm(auto_id=False, label_suffix='?') + self.assertEqual(f.as_ul(), """<li>Favorite color? <input type="text" name="color" /></li> +<li>Favorite animal? <input type="text" name="animal" /></li>""") + f = FavoriteForm(auto_id=False, label_suffix='') + self.assertEqual(f.as_ul(), """<li>Favorite color? <input type="text" name="color" /></li> +<li>Favorite animal <input type="text" name="animal" /></li>""") + f = FavoriteForm(auto_id=False, label_suffix=u'\u2192') + self.assertEqual(f.as_ul(), u'<li>Favorite color? <input type="text" name="color" /></li>\n<li>Favorite animal\u2192 <input type="text" name="animal" /></li>') + + def test_initial_data(self): + # You can specify initial data for a field by using the 'initial' argument to a + # Field class. This initial data is displayed when a Form is rendered with *no* + # data. It is not displayed when a Form is rendered with any data (including an + # empty dictionary). Also, the initial value is *not* used if data for a + # particular required field isn't provided. + class UserRegistration(Form): + username = CharField(max_length=10, initial='django') + password = CharField(widget=PasswordInput) + + # Here, we're not submitting any data, so the initial value will be displayed.) + p = UserRegistration(auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="django" maxlength="10" /></li> +<li>Password: <input type="password" name="password" /></li>""") + + # Here, we're submitting data, so the initial value will *not* be displayed. + p = UserRegistration({}, auto_id=False) + self.assertEqual(p.as_ul(), """<li><ul class="errorlist"><li>This field is required.</li></ul>Username: <input type="text" name="username" maxlength="10" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li>""") + p = UserRegistration({'username': u''}, auto_id=False) + self.assertEqual(p.as_ul(), """<li><ul class="errorlist"><li>This field is required.</li></ul>Username: <input type="text" name="username" maxlength="10" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li>""") + p = UserRegistration({'username': u'foo'}, auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="foo" maxlength="10" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li>""") + + # An 'initial' value is *not* used as a fallback if data is not provided. In this + # example, we don't provide a value for 'username', and the form raises a + # validation error rather than using the initial value for 'username'. + p = UserRegistration({'password': 'secret'}) + self.assertEqual(p.errors['username'], [u'This field is required.']) + self.assertFalse(p.is_valid()) + + def test_dynamic_initial_data(self): + # The previous technique dealt with "hard-coded" initial data, but it's also + # possible to specify initial data after you've already created the Form class + # (i.e., at runtime). Use the 'initial' parameter to the Form constructor. This + # should be a dictionary containing initial values for one or more fields in the + # form, keyed by field name. + class UserRegistration(Form): + username = CharField(max_length=10) + password = CharField(widget=PasswordInput) + + # Here, we're not submitting any data, so the initial value will be displayed.) + p = UserRegistration(initial={'username': 'django'}, auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="django" maxlength="10" /></li> +<li>Password: <input type="password" name="password" /></li>""") + p = UserRegistration(initial={'username': 'stephane'}, auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="stephane" maxlength="10" /></li> +<li>Password: <input type="password" name="password" /></li>""") + + # The 'initial' parameter is meaningless if you pass data. + p = UserRegistration({}, initial={'username': 'django'}, auto_id=False) + self.assertEqual(p.as_ul(), """<li><ul class="errorlist"><li>This field is required.</li></ul>Username: <input type="text" name="username" maxlength="10" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li>""") + p = UserRegistration({'username': u''}, initial={'username': 'django'}, auto_id=False) + self.assertEqual(p.as_ul(), """<li><ul class="errorlist"><li>This field is required.</li></ul>Username: <input type="text" name="username" maxlength="10" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li>""") + p = UserRegistration({'username': u'foo'}, initial={'username': 'django'}, auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="foo" maxlength="10" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li>""") + + # A dynamic 'initial' value is *not* used as a fallback if data is not provided. + # In this example, we don't provide a value for 'username', and the form raises a + # validation error rather than using the initial value for 'username'. + p = UserRegistration({'password': 'secret'}, initial={'username': 'django'}) + self.assertEqual(p.errors['username'], [u'This field is required.']) + self.assertFalse(p.is_valid()) + + # If a Form defines 'initial' *and* 'initial' is passed as a parameter to Form(), + # then the latter will get precedence. + class UserRegistration(Form): + username = CharField(max_length=10, initial='django') + password = CharField(widget=PasswordInput) + + p = UserRegistration(initial={'username': 'babik'}, auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="babik" maxlength="10" /></li> +<li>Password: <input type="password" name="password" /></li>""") + + def test_callable_initial_data(self): + # The previous technique dealt with raw values as initial data, but it's also + # possible to specify callable data. + class UserRegistration(Form): + username = CharField(max_length=10) + password = CharField(widget=PasswordInput) + options = MultipleChoiceField(choices=[('f','foo'),('b','bar'),('w','whiz')]) + + # We need to define functions that get called later.) + def initial_django(): + return 'django' + + def initial_stephane(): + return 'stephane' + + def initial_options(): + return ['f','b'] + + def initial_other_options(): + return ['b','w'] + + # Here, we're not submitting any data, so the initial value will be displayed.) + p = UserRegistration(initial={'username': initial_django, 'options': initial_options}, auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="django" maxlength="10" /></li> +<li>Password: <input type="password" name="password" /></li> +<li>Options: <select multiple="multiple" name="options"> +<option value="f" selected="selected">foo</option> +<option value="b" selected="selected">bar</option> +<option value="w">whiz</option> +</select></li>""") + + # The 'initial' parameter is meaningless if you pass data. + p = UserRegistration({}, initial={'username': initial_django, 'options': initial_options}, auto_id=False) + self.assertEqual(p.as_ul(), """<li><ul class="errorlist"><li>This field is required.</li></ul>Username: <input type="text" name="username" maxlength="10" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul>Options: <select multiple="multiple" name="options"> +<option value="f">foo</option> +<option value="b">bar</option> +<option value="w">whiz</option> +</select></li>""") + p = UserRegistration({'username': u''}, initial={'username': initial_django}, auto_id=False) + self.assertEqual(p.as_ul(), """<li><ul class="errorlist"><li>This field is required.</li></ul>Username: <input type="text" name="username" maxlength="10" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul>Options: <select multiple="multiple" name="options"> +<option value="f">foo</option> +<option value="b">bar</option> +<option value="w">whiz</option> +</select></li>""") + p = UserRegistration({'username': u'foo', 'options':['f','b']}, initial={'username': initial_django}, auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="foo" maxlength="10" /></li> +<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li> +<li>Options: <select multiple="multiple" name="options"> +<option value="f" selected="selected">foo</option> +<option value="b" selected="selected">bar</option> +<option value="w">whiz</option> +</select></li>""") + + # A callable 'initial' value is *not* used as a fallback if data is not provided. + # In this example, we don't provide a value for 'username', and the form raises a + # validation error rather than using the initial value for 'username'. + p = UserRegistration({'password': 'secret'}, initial={'username': initial_django, 'options': initial_options}) + self.assertEqual(p.errors['username'], [u'This field is required.']) + self.assertFalse(p.is_valid()) + + # If a Form defines 'initial' *and* 'initial' is passed as a parameter to Form(), + # then the latter will get precedence. + class UserRegistration(Form): + username = CharField(max_length=10, initial=initial_django) + password = CharField(widget=PasswordInput) + options = MultipleChoiceField(choices=[('f','foo'),('b','bar'),('w','whiz')], initial=initial_other_options) + + p = UserRegistration(auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="django" maxlength="10" /></li> +<li>Password: <input type="password" name="password" /></li> +<li>Options: <select multiple="multiple" name="options"> +<option value="f">foo</option> +<option value="b" selected="selected">bar</option> +<option value="w" selected="selected">whiz</option> +</select></li>""") + p = UserRegistration(initial={'username': initial_stephane, 'options': initial_options}, auto_id=False) + self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="stephane" maxlength="10" /></li> +<li>Password: <input type="password" name="password" /></li> +<li>Options: <select multiple="multiple" name="options"> +<option value="f" selected="selected">foo</option> +<option value="b" selected="selected">bar</option> +<option value="w">whiz</option> +</select></li>""") + + def test_help_text(self): + # You can specify descriptive text for a field by using the 'help_text' argument) + class UserRegistration(Form): + username = CharField(max_length=10, help_text='e.g., user@example.com') + password = CharField(widget=PasswordInput, help_text='Choose wisely.') + + p = UserRegistration(auto_id=False) + self.assertEqual(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>""") + self.assertEqual(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>""") + self.assertEqual(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>""") + + # The help text is displayed whether or not data is provided for the form. + p = UserRegistration({'username': u'foo'}, auto_id=False) + self.assertEqual(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>""") + + # help_text is not displayed for hidden fields. It can be used for documentation + # purposes, though. + class UserRegistration(Form): + username = CharField(max_length=10, help_text='e.g., user@example.com') + password = CharField(widget=PasswordInput) + next = CharField(widget=HiddenInput, initial='/', help_text='Redirect destination') + + p = UserRegistration(auto_id=False) + self.assertEqual(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" /><input type="hidden" name="next" value="/" /></li>""") + + # Help text can include arbitrary Unicode characters. + class UserRegistration(Form): + username = CharField(max_length=10, help_text='ŠĐĆŽćžšđ') + + p = UserRegistration(auto_id=False) + self.assertEqual(p.as_ul(), u'<li>Username: <input type="text" name="username" maxlength="10" /> \u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111</li>') + + def test_subclassing_forms(self): + # You can subclass a Form to add fields. The resulting form subclass will have + # all of the fields of the parent Form, plus whichever fields you define in the + # subclass. + class Person(Form): + first_name = CharField() + last_name = CharField() + birthday = DateField() + + class Musician(Person): + instrument = CharField() + + p = Person(auto_id=False) + self.assertEqual(p.as_ul(), """<li>First name: <input type="text" name="first_name" /></li> +<li>Last name: <input type="text" name="last_name" /></li> +<li>Birthday: <input type="text" name="birthday" /></li>""") + m = Musician(auto_id=False) + self.assertEqual(m.as_ul(), """<li>First name: <input type="text" name="first_name" /></li> +<li>Last name: <input type="text" name="last_name" /></li> +<li>Birthday: <input type="text" name="birthday" /></li> +<li>Instrument: <input type="text" name="instrument" /></li>""") + + # Yes, you can subclass multiple forms. The fields are added in the order in + # which the parent classes are listed. + class Person(Form): + first_name = CharField() + last_name = CharField() + birthday = DateField() + + class Instrument(Form): + instrument = CharField() + + class Beatle(Person, Instrument): + haircut_type = CharField() + + b = Beatle(auto_id=False) + self.assertEqual(b.as_ul(), """<li>First name: <input type="text" name="first_name" /></li> +<li>Last name: <input type="text" name="last_name" /></li> +<li>Birthday: <input type="text" name="birthday" /></li> +<li>Instrument: <input type="text" name="instrument" /></li> +<li>Haircut type: <input type="text" name="haircut_type" /></li>""") + + def test_forms_with_prefixes(self): + # Sometimes it's necessary to have multiple forms display on the same HTML page, + # or multiple copies of the same form. We can accomplish this with form prefixes. + # Pass the keyword argument 'prefix' to the Form constructor to use this feature. + # This value will be prepended to each HTML form field name. One way to think + # about this is "namespaces for HTML forms". Notice that in the data argument, + # each field's key has the prefix, in this case 'person1', prepended to the + # actual field name. + class Person(Form): + first_name = CharField() + last_name = CharField() + birthday = DateField() + + data = { + 'person1-first_name': u'John', + 'person1-last_name': u'Lennon', + 'person1-birthday': u'1940-10-9' + } + p = Person(data, prefix='person1') + self.assertEqual(p.as_ul(), """<li><label for="id_person1-first_name">First name:</label> <input type="text" name="person1-first_name" value="John" id="id_person1-first_name" /></li> +<li><label for="id_person1-last_name">Last name:</label> <input type="text" name="person1-last_name" value="Lennon" id="id_person1-last_name" /></li> +<li><label for="id_person1-birthday">Birthday:</label> <input type="text" name="person1-birthday" value="1940-10-9" id="id_person1-birthday" /></li>""") + self.assertEqual(str(p['first_name']), '<input type="text" name="person1-first_name" value="John" id="id_person1-first_name" />') + self.assertEqual(str(p['last_name']), '<input type="text" name="person1-last_name" value="Lennon" id="id_person1-last_name" />') + self.assertEqual(str(p['birthday']), '<input type="text" name="person1-birthday" value="1940-10-9" id="id_person1-birthday" />') + self.assertEqual(p.errors, {}) + self.assertTrue(p.is_valid()) + self.assertEqual(p.cleaned_data['first_name'], u'John') + self.assertEqual(p.cleaned_data['last_name'], u'Lennon') + self.assertEqual(p.cleaned_data['birthday'], datetime.date(1940, 10, 9)) + + # Let's try submitting some bad data to make sure form.errors and field.errors + # work as expected. + data = { + 'person1-first_name': u'', + 'person1-last_name': u'', + 'person1-birthday': u'' + } + p = Person(data, prefix='person1') + self.assertEqual(p.errors['first_name'], [u'This field is required.']) + self.assertEqual(p.errors['last_name'], [u'This field is required.']) + self.assertEqual(p.errors['birthday'], [u'This field is required.']) + self.assertEqual(p['first_name'].errors, [u'This field is required.']) + try: + p['person1-first_name'].errors + self.fail('Attempts to access non-existent fields should fail.') + except KeyError: + pass + + # In this example, the data doesn't have a prefix, but the form requires it, so + # the form doesn't "see" the fields. + data = { + 'first_name': u'John', + 'last_name': u'Lennon', + 'birthday': u'1940-10-9' + } + p = Person(data, prefix='person1') + self.assertEqual(p.errors['first_name'], [u'This field is required.']) + self.assertEqual(p.errors['last_name'], [u'This field is required.']) + self.assertEqual(p.errors['birthday'], [u'This field is required.']) + + # With prefixes, a single data dictionary can hold data for multiple instances + # of the same form. + data = { + 'person1-first_name': u'John', + 'person1-last_name': u'Lennon', + 'person1-birthday': u'1940-10-9', + 'person2-first_name': u'Jim', + 'person2-last_name': u'Morrison', + 'person2-birthday': u'1943-12-8' + } + p1 = Person(data, prefix='person1') + self.assertTrue(p1.is_valid()) + self.assertEqual(p1.cleaned_data['first_name'], u'John') + self.assertEqual(p1.cleaned_data['last_name'], u'Lennon') + self.assertEqual(p1.cleaned_data['birthday'], datetime.date(1940, 10, 9)) + p2 = Person(data, prefix='person2') + self.assertTrue(p2.is_valid()) + self.assertEqual(p2.cleaned_data['first_name'], u'Jim') + self.assertEqual(p2.cleaned_data['last_name'], u'Morrison') + self.assertEqual(p2.cleaned_data['birthday'], datetime.date(1943, 12, 8)) + + # By default, forms append a hyphen between the prefix and the field name, but a + # form can alter that behavior by implementing the add_prefix() method. This + # method takes a field name and returns the prefixed field, according to + # self.prefix. + class Person(Form): + first_name = CharField() + last_name = CharField() + birthday = DateField() + + def add_prefix(self, field_name): + return self.prefix and '%s-prefix-%s' % (self.prefix, field_name) or field_name + + p = Person(prefix='foo') + self.assertEqual(p.as_ul(), """<li><label for="id_foo-prefix-first_name">First name:</label> <input type="text" name="foo-prefix-first_name" id="id_foo-prefix-first_name" /></li> +<li><label for="id_foo-prefix-last_name">Last name:</label> <input type="text" name="foo-prefix-last_name" id="id_foo-prefix-last_name" /></li> +<li><label for="id_foo-prefix-birthday">Birthday:</label> <input type="text" name="foo-prefix-birthday" id="id_foo-prefix-birthday" /></li>""") + data = { + 'foo-prefix-first_name': u'John', + 'foo-prefix-last_name': u'Lennon', + 'foo-prefix-birthday': u'1940-10-9' + } + p = Person(data, prefix='foo') + self.assertTrue(p.is_valid()) + self.assertEqual(p.cleaned_data['first_name'], u'John') + self.assertEqual(p.cleaned_data['last_name'], u'Lennon') + self.assertEqual(p.cleaned_data['birthday'], datetime.date(1940, 10, 9)) + + def test_forms_with_null_boolean(self): + # NullBooleanField is a bit of a special case because its presentation (widget) + # is different than its data. This is handled transparently, though. + class Person(Form): + name = CharField() + is_cool = NullBooleanField() + + p = Person({'name': u'Joe'}, auto_id=False) + self.assertEqual(str(p['is_cool']), """<select name="is_cool"> +<option value="1" selected="selected">Unknown</option> +<option value="2">Yes</option> +<option value="3">No</option> +</select>""") + p = Person({'name': u'Joe', 'is_cool': u'1'}, auto_id=False) + self.assertEqual(str(p['is_cool']), """<select name="is_cool"> +<option value="1" selected="selected">Unknown</option> +<option value="2">Yes</option> +<option value="3">No</option> +</select>""") + p = Person({'name': u'Joe', 'is_cool': u'2'}, auto_id=False) + self.assertEqual(str(p['is_cool']), """<select name="is_cool"> +<option value="1">Unknown</option> +<option value="2" selected="selected">Yes</option> +<option value="3">No</option> +</select>""") + p = Person({'name': u'Joe', 'is_cool': u'3'}, auto_id=False) + self.assertEqual(str(p['is_cool']), """<select name="is_cool"> +<option value="1">Unknown</option> +<option value="2">Yes</option> +<option value="3" selected="selected">No</option> +</select>""") + p = Person({'name': u'Joe', 'is_cool': True}, auto_id=False) + self.assertEqual(str(p['is_cool']), """<select name="is_cool"> +<option value="1">Unknown</option> +<option value="2" selected="selected">Yes</option> +<option value="3">No</option> +</select>""") + p = Person({'name': u'Joe', 'is_cool': False}, auto_id=False) + self.assertEqual(str(p['is_cool']), """<select name="is_cool"> +<option value="1">Unknown</option> +<option value="2">Yes</option> +<option value="3" selected="selected">No</option> +</select>""") + + def test_forms_with_file_fields(self): + # FileFields are a special case because they take their data from the request.FILES, + # not request.POST. + class FileForm(Form): + file1 = FileField() + + f = FileForm(auto_id=False) + self.assertEqual(f.as_table(), '<tr><th>File1:</th><td><input type="file" name="file1" /></td></tr>') + + f = FileForm(data={}, files={}, auto_id=False) + self.assertEqual(f.as_table(), '<tr><th>File1:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="file" name="file1" /></td></tr>') + + f = FileForm(data={}, files={'file1': SimpleUploadedFile('name', '')}, auto_id=False) + self.assertEqual(f.as_table(), '<tr><th>File1:</th><td><ul class="errorlist"><li>The submitted file is empty.</li></ul><input type="file" name="file1" /></td></tr>') + + f = FileForm(data={}, files={'file1': 'something that is not a file'}, auto_id=False) + self.assertEqual(f.as_table(), '<tr><th>File1:</th><td><ul class="errorlist"><li>No file was submitted. Check the encoding type on the form.</li></ul><input type="file" name="file1" /></td></tr>') + + f = FileForm(data={}, files={'file1': SimpleUploadedFile('name', 'some content')}, auto_id=False) + self.assertEqual(f.as_table(), '<tr><th>File1:</th><td><input type="file" name="file1" /></td></tr>') + self.assertTrue(f.is_valid()) + + f = FileForm(data={}, files={'file1': SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह')}, auto_id=False) + self.assertEqual(f.as_table(), '<tr><th>File1:</th><td><input type="file" name="file1" /></td></tr>') + + def test_basic_processing_in_view(self): + class UserRegistration(Form): + username = CharField(max_length=10) + password1 = CharField(widget=PasswordInput) + password2 = CharField(widget=PasswordInput) + + def clean(self): + if self.cleaned_data.get('password1') and self.cleaned_data.get('password2') and self.cleaned_data['password1'] != self.cleaned_data['password2']: + raise ValidationError(u'Please make sure your passwords match.') + + return self.cleaned_data + + def my_function(method, post_data): + if method == 'POST': + form = UserRegistration(post_data, auto_id=False) + else: + form = UserRegistration(auto_id=False) + + if form.is_valid(): + return 'VALID: %r' % form.cleaned_data + + t = Template('<form action="" method="post">\n<table>\n{{ form }}\n</table>\n<input type="submit" />\n</form>') + return t.render(Context({'form': form})) + + # Case 1: GET (an empty form, with no errors).) + self.assertEqual(my_function('GET', {}), """<form action="" method="post"> +<table> +<tr><th>Username:</th><td><input type="text" name="username" maxlength="10" /></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>""") + # Case 2: POST with erroneous data (a redisplayed form, with errors).) + self.assertEqual(my_function('POST', {'username': 'this-is-a-long-username', 'password1': 'foo', 'password2': 'bar'}), """<form action="" method="post"> +<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> +</table> +<input type="submit" /> +</form>""") + # Case 3: POST with valid data (the success message).) + self.assertEqual(my_function('POST', {'username': 'adrian', 'password1': 'secret', 'password2': 'secret'}), "VALID: {'username': u'adrian', 'password1': u'secret', 'password2': u'secret'}") + + def test_templates_with_forms(self): + class UserRegistration(Form): + username = CharField(max_length=10, help_text="Good luck picking a username that doesn't already exist.") + password1 = CharField(widget=PasswordInput) + password2 = CharField(widget=PasswordInput) + + def clean(self): + if self.cleaned_data.get('password1') and self.cleaned_data.get('password2') and self.cleaned_data['password1'] != self.cleaned_data['password2']: + raise ValidationError(u'Please make sure your passwords match.') + + return self.cleaned_data + + # You have full flexibility in displaying form fields in a template. Just pass a + # Form instance to the template, and use "dot" access to refer to individual + # fields. Note, however, that this flexibility comes with the responsibility of + # displaying all the errors, including any that might not be associated with a + # particular field. + t = Template('''<form action=""> +{{ form.username.errors.as_ul }}<p><label>Your username: {{ form.username }}</label></p> +{{ form.password1.errors.as_ul }}<p><label>Password: {{ form.password1 }}</label></p> +{{ form.password2.errors.as_ul }}<p><label>Password (again): {{ form.password2 }}</label></p> +<input type="submit" /> +</form>''') + self.assertEqual(t.render(Context({'form': UserRegistration(auto_id=False)})), """<form action=""> +<p><label>Your username: <input type="text" name="username" maxlength="10" /></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>""") + self.assertEqual(t.render(Context({'form': UserRegistration({'username': 'django'}, auto_id=False)})), """<form action=""> +<p><label>Your username: <input type="text" name="username" value="django" maxlength="10" /></label></p> +<ul class="errorlist"><li>This field is required.</li></ul><p><label>Password: <input type="password" name="password1" /></label></p> +<ul class="errorlist"><li>This field is required.</li></ul><p><label>Password (again): <input type="password" name="password2" /></label></p> +<input type="submit" /> +</form>""") + + # Use form.[field].label to output a field's label. You can specify the label for + # a field by using the 'label' argument to a Field class. If you don't specify + # 'label', Django will use the field name with underscores converted to spaces, + # and the initial letter capitalized. + t = Template('''<form action=""> +<p><label>{{ form.username.label }}: {{ form.username }}</label></p> +<p><label>{{ form.password1.label }}: {{ form.password1 }}</label></p> +<p><label>{{ form.password2.label }}: {{ form.password2 }}</label></p> +<input type="submit" /> +</form>''') + self.assertEqual(t.render(Context({'form': UserRegistration(auto_id=False)})), """<form action=""> +<p><label>Username: <input type="text" name="username" maxlength="10" /></label></p> +<p><label>Password1: <input type="password" name="password1" /></label></p> +<p><label>Password2: <input type="password" name="password2" /></label></p> +<input type="submit" /> +</form>""") + + # User form.[field].label_tag to output a field's label with a <label> tag + # wrapped around it, but *only* if the given field has an "id" attribute. + # Recall from above that passing the "auto_id" argument to a Form gives each + # field an "id" attribute. + t = Template('''<form action=""> +<p>{{ form.username.label_tag }}: {{ form.username }}</p> +<p>{{ form.password1.label_tag }}: {{ form.password1 }}</p> +<p>{{ form.password2.label_tag }}: {{ form.password2 }}</p> +<input type="submit" /> +</form>''') + self.assertEqual(t.render(Context({'form': UserRegistration(auto_id=False)})), """<form action=""> +<p>Username: <input type="text" name="username" maxlength="10" /></p> +<p>Password1: <input type="password" name="password1" /></p> +<p>Password2: <input type="password" name="password2" /></p> +<input type="submit" /> +</form>""") + self.assertEqual(t.render(Context({'form': UserRegistration(auto_id='id_%s')})), """<form action=""> +<p><label for="id_username">Username</label>: <input id="id_username" type="text" name="username" maxlength="10" /></p> +<p><label for="id_password1">Password1</label>: <input type="password" name="password1" id="id_password1" /></p> +<p><label for="id_password2">Password2</label>: <input type="password" name="password2" id="id_password2" /></p> +<input type="submit" /> +</form>""") + + # User form.[field].help_text to output a field's help text. If the given field + # does not have help text, nothing will be output. + t = Template('''<form action=""> +<p>{{ form.username.label_tag }}: {{ form.username }}<br />{{ form.username.help_text }}</p> +<p>{{ form.password1.label_tag }}: {{ form.password1 }}</p> +<p>{{ form.password2.label_tag }}: {{ form.password2 }}</p> +<input type="submit" /> +</form>''') + self.assertEqual(t.render(Context({'form': UserRegistration(auto_id=False)})), """<form action=""> +<p>Username: <input type="text" name="username" maxlength="10" /><br />Good luck picking a username that doesn't already exist.</p> +<p>Password1: <input type="password" name="password1" /></p> +<p>Password2: <input type="password" name="password2" /></p> +<input type="submit" /> +</form>""") + self.assertEqual(Template('{{ form.password1.help_text }}').render(Context({'form': UserRegistration(auto_id=False)})), u'') + + # The label_tag() method takes an optional attrs argument: a dictionary of HTML + # attributes to add to the <label> tag. + f = UserRegistration(auto_id='id_%s') + form_output = [] + + for bf in f: + form_output.append(bf.label_tag(attrs={'class': 'pretty'})) + + self.assertEqual(form_output, [ + '<label for="id_username" class="pretty">Username</label>', + '<label for="id_password1" class="pretty">Password1</label>', + '<label for="id_password2" class="pretty">Password2</label>', + ]) + + # To display the errors that aren't associated with a particular field -- e.g., + # the errors caused by Form.clean() -- use {{ form.non_field_errors }} in the + # template. If used on its own, it is displayed as a <ul> (or an empty string, if + # the list of errors is empty). You can also use it in {% if %} statements. + t = Template('''<form action=""> +{{ form.username.errors.as_ul }}<p><label>Your username: {{ form.username }}</label></p> +{{ form.password1.errors.as_ul }}<p><label>Password: {{ form.password1 }}</label></p> +{{ form.password2.errors.as_ul }}<p><label>Password (again): {{ form.password2 }}</label></p> +<input type="submit" /> +</form>''') + self.assertEqual(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> +<input type="submit" /> +</form>""") + t = Template('''<form action=""> +{{ form.non_field_errors }} +{{ form.username.errors.as_ul }}<p><label>Your username: {{ form.username }}</label></p> +{{ form.password1.errors.as_ul }}<p><label>Password: {{ form.password1 }}</label></p> +{{ form.password2.errors.as_ul }}<p><label>Password (again): {{ form.password2 }}</label></p> +<input type="submit" /> +</form>''') + self.assertEqual(t.render(Context({'form': UserRegistration({'username': 'django', 'password1': 'foo', 'password2': 'bar'}, auto_id=False)})), """<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> +<input type="submit" /> +</form>""") + + def test_empty_permitted(self): + # Sometimes (pretty much in formsets) we want to allow a form to pass validation + # if it is completely empty. We can accomplish this by using the empty_permitted + # agrument to a form constructor. + class SongForm(Form): + artist = CharField() + name = CharField() + + # First let's show what happens id empty_permitted=False (the default): + data = {'artist': '', 'song': ''} + form = SongForm(data, empty_permitted=False) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors, {'name': [u'This field is required.'], 'artist': [u'This field is required.']}) + try: + form.cleaned_data + self.fail('Attempts to access cleaned_data when validation fails should fail.') + except AttributeError: + pass + + # Now let's show what happens when empty_permitted=True and the form is empty. + form = SongForm(data, empty_permitted=True) + self.assertTrue(form.is_valid()) + self.assertEqual(form.errors, {}) + self.assertEqual(form.cleaned_data, {}) + + # But if we fill in data for one of the fields, the form is no longer empty and + # the whole thing must pass validation. + data = {'artist': 'The Doors', 'song': ''} + form = SongForm(data, empty_permitted=False) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors, {'name': [u'This field is required.']}) + try: + form.cleaned_data + self.fail('Attempts to access cleaned_data when validation fails should fail.') + except AttributeError: + pass + + # If a field is not given in the data then None is returned for its data. Lets + # make sure that when checking for empty_permitted that None is treated + # accordingly. + data = {'artist': None, 'song': ''} + form = SongForm(data, empty_permitted=True) + self.assertTrue(form.is_valid()) + + # However, we *really* need to be sure we are checking for None as any data in + # initial that returns False on a boolean call needs to be treated literally. + class PriceForm(Form): + amount = FloatField() + qty = IntegerField() + + data = {'amount': '0.0', 'qty': ''} + form = PriceForm(data, initial={'amount': 0.0}, empty_permitted=True) + self.assertTrue(form.is_valid()) + + def test_extracting_hidden_and_visible(self): + class SongForm(Form): + token = CharField(widget=HiddenInput) + artist = CharField() + name = CharField() + + form = SongForm() + self.assertEqual([f.name for f in form.hidden_fields()], ['token']) + self.assertEqual([f.name for f in form.visible_fields()], ['artist', 'name']) + + def test_hidden_initial_gets_id(self): + class MyForm(Form): + field1 = CharField(max_length=50, show_hidden_initial=True) + + self.assertEqual(MyForm().as_table(), '<tr><th><label for="id_field1">Field1:</label></th><td><input id="id_field1" type="text" name="field1" maxlength="50" /><input type="hidden" name="initial-field1" id="initial-id_field1" /></td></tr>') + + def test_error_html_required_html_classes(self): + class Person(Form): + name = CharField() + is_cool = NullBooleanField() + email = EmailField(required=False) + age = IntegerField() + + p = Person({}) + p.error_css_class = 'error' + p.required_css_class = 'required' + + self.assertEqual(p.as_ul(), """<li class="required error"><ul class="errorlist"><li>This field is required.</li></ul><label for="id_name">Name:</label> <input type="text" name="name" id="id_name" /></li> +<li class="required"><label for="id_is_cool">Is cool:</label> <select name="is_cool" id="id_is_cool"> +<option value="1" selected="selected">Unknown</option> +<option value="2">Yes</option> +<option value="3">No</option> +</select></li> +<li><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></li> +<li class="required error"><ul class="errorlist"><li>This field is required.</li></ul><label for="id_age">Age:</label> <input type="text" name="age" id="id_age" /></li>""") + + self.assertEqual(p.as_p(), """<ul class="errorlist"><li>This field is required.</li></ul> +<p class="required error"><label for="id_name">Name:</label> <input type="text" name="name" id="id_name" /></p> +<p class="required"><label for="id_is_cool">Is cool:</label> <select name="is_cool" id="id_is_cool"> +<option value="1" selected="selected">Unknown</option> +<option value="2">Yes</option> +<option value="3">No</option> +</select></p> +<p><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></p> +<ul class="errorlist"><li>This field is required.</li></ul> +<p class="required error"><label for="id_age">Age:</label> <input type="text" name="age" id="id_age" /></p>""") + + self.assertEqual(p.as_table(), """<tr class="required error"><th><label for="id_name">Name:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="name" id="id_name" /></td></tr> +<tr class="required"><th><label for="id_is_cool">Is cool:</label></th><td><select name="is_cool" id="id_is_cool"> +<option value="1" selected="selected">Unknown</option> +<option value="2">Yes</option> +<option value="3">No</option> +</select></td></tr> +<tr><th><label for="id_email">Email:</label></th><td><input type="text" name="email" id="id_email" /></td></tr> +<tr class="required error"><th><label for="id_age">Age:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="age" id="id_age" /></td></tr>""") + + def test_label_split_datetime_not_displayed(self): + class EventForm(Form): + happened_at = SplitDateTimeField(widget=widgets.SplitHiddenDateTimeWidget) + + form = EventForm() + self.assertEqual(form.as_ul(), u'<input type="hidden" name="happened_at_0" id="id_happened_at_0" /><input type="hidden" name="happened_at_1" id="id_happened_at_1" />') diff --git a/parts/django/tests/regressiontests/forms/tests/formsets.py b/parts/django/tests/regressiontests/forms/tests/formsets.py new file mode 100644 index 0000000..db94797 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/formsets.py @@ -0,0 +1,763 @@ +# -*- coding: utf-8 -*- +from unittest import TestCase +from django.forms import Form, CharField, IntegerField, ValidationError +from django.forms.formsets import formset_factory, BaseFormSet + + +class Choice(Form): + choice = CharField() + votes = IntegerField() + + +# FormSet allows us to use multiple instance of the same form on 1 page. For now, +# the best way to create a FormSet is by using the formset_factory function. +ChoiceFormSet = formset_factory(Choice) + + +class FavoriteDrinkForm(Form): + name = CharField() + + +class BaseFavoriteDrinksFormSet(BaseFormSet): + def clean(self): + seen_drinks = [] + + for drink in self.cleaned_data: + if drink['name'] in seen_drinks: + raise ValidationError('You may only specify a drink once.') + + seen_drinks.append(drink['name']) + + +# Let's define a FormSet that takes a list of favorite drinks, but raises an +# error if there are any duplicates. Used in ``test_clean_hook``, +# ``test_regression_6926`` & ``test_regression_12878``. +FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm, + formset=BaseFavoriteDrinksFormSet, extra=3) + + +class FormsFormsetTestCase(TestCase): + def test_basic_formset(self): + # A FormSet constructor takes the same arguments as Form. Let's create a FormSet + # for adding data. By default, it displays 1 blank form. It can display more, + # but we'll look at how to do so later. + formset = ChoiceFormSet(auto_id=False, prefix='choices') + self.assertEqual(str(formset), """<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" /> +<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr> +<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr>""") + + # On thing to note is that there needs to be a special value in the data. This + # value tells the FormSet how many forms were displayed so it can tell how + # many forms it needs to clean and validate. You could use javascript to create + # new forms on the client side, but they won't get validated unless you increment + # the TOTAL_FORMS field appropriately. + + data = { + 'choices-TOTAL_FORMS': '1', # the number of forms rendered + 'choices-INITIAL_FORMS': '0', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + } + # We treat FormSet pretty much like we would treat a normal Form. FormSet has an + # is_valid method, and a cleaned_data or errors attribute depending on whether all + # the forms passed validation. However, unlike a Form instance, cleaned_data and + # errors will be a list of dicts rather than just a single dict. + + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(formset.is_valid()) + self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': u'Calexico'}]) + + # If a FormSet was not passed any data, its is_valid method should return False. + formset = ChoiceFormSet() + self.assertFalse(formset.is_valid()) + + def test_formset_validation(self): + # FormSet instances can also have an error attribute if validation failed for + # any of the forms. + + data = { + 'choices-TOTAL_FORMS': '1', # the number of forms rendered + 'choices-INITIAL_FORMS': '0', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '', + } + + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.errors, [{'votes': [u'This field is required.']}]) + + def test_formset_initial_data(self): + # We can also prefill a FormSet with existing data by providing an ``initial`` + # argument to the constructor. ``initial`` should be a list of dicts. By default, + # an extra blank form is included. + + initial = [{'choice': u'Calexico', 'votes': 100}] + formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') + form_output = [] + + for form in formset.forms: + form_output.append(form.as_ul()) + + self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Choice: <input type="text" name="choices-1-choice" /></li> +<li>Votes: <input type="text" name="choices-1-votes" /></li>""") + + # Let's simulate what would happen if we submitted this form. + + data = { + 'choices-TOTAL_FORMS': '2', # the number of forms rendered + 'choices-INITIAL_FORMS': '1', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + 'choices-1-choice': '', + 'choices-1-votes': '', + } + + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(formset.is_valid()) + self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': u'Calexico'}, {}]) + + def test_second_form_partially_filled(self): + # But the second form was blank! Shouldn't we get some errors? No. If we display + # a form as blank, it's ok for it to be submitted as blank. If we fill out even + # one of the fields of a blank form though, it will be validated. We may want to + # required that at least x number of forms are completed, but we'll show how to + # handle that later. + + data = { + 'choices-TOTAL_FORMS': '2', # the number of forms rendered + 'choices-INITIAL_FORMS': '1', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + 'choices-1-choice': 'The Decemberists', + 'choices-1-votes': '', # missing value + } + + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.errors, [{}, {'votes': [u'This field is required.']}]) + + def test_delete_prefilled_data(self): + # If we delete data that was pre-filled, we should get an error. Simply removing + # data from form fields isn't the proper way to delete it. We'll see how to + # handle that case later. + + data = { + 'choices-TOTAL_FORMS': '2', # the number of forms rendered + 'choices-INITIAL_FORMS': '1', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': '', # deleted value + 'choices-0-votes': '', # deleted value + 'choices-1-choice': '', + 'choices-1-votes': '', + } + + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.errors, [{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}]) + + def test_displaying_more_than_one_blank_form(self): + # Displaying more than 1 blank form ########################################### + # We can also display more than 1 empty form at a time. To do so, pass a + # extra argument to formset_factory. + ChoiceFormSet = formset_factory(Choice, extra=3) + + formset = ChoiceFormSet(auto_id=False, prefix='choices') + form_output = [] + + for form in formset.forms: + form_output.append(form.as_ul()) + + self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" /></li> +<li>Votes: <input type="text" name="choices-0-votes" /></li> +<li>Choice: <input type="text" name="choices-1-choice" /></li> +<li>Votes: <input type="text" name="choices-1-votes" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li>""") + + # Since we displayed every form as blank, we will also accept them back as blank. + # This may seem a little strange, but later we will show how to require a minimum + # number of forms to be completed. + + data = { + 'choices-TOTAL_FORMS': '3', # the number of forms rendered + 'choices-INITIAL_FORMS': '0', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': '', + 'choices-0-votes': '', + 'choices-1-choice': '', + 'choices-1-votes': '', + 'choices-2-choice': '', + 'choices-2-votes': '', + } + + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(formset.is_valid()) + self.assertEqual([form.cleaned_data for form in formset.forms], [{}, {}, {}]) + + def test_single_form_completed(self): + # We can just fill out one of the forms. + + data = { + 'choices-TOTAL_FORMS': '3', # the number of forms rendered + 'choices-INITIAL_FORMS': '0', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + 'choices-1-choice': '', + 'choices-1-votes': '', + 'choices-2-choice': '', + 'choices-2-votes': '', + } + + ChoiceFormSet = formset_factory(Choice, extra=3) + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(formset.is_valid()) + self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': u'Calexico'}, {}, {}]) + + def test_second_form_partially_filled_2(self): + # And once again, if we try to partially complete a form, validation will fail. + + data = { + 'choices-TOTAL_FORMS': '3', # the number of forms rendered + 'choices-INITIAL_FORMS': '0', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + 'choices-1-choice': 'The Decemberists', + 'choices-1-votes': '', # missing value + 'choices-2-choice': '', + 'choices-2-votes': '', + } + + ChoiceFormSet = formset_factory(Choice, extra=3) + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.errors, [{}, {'votes': [u'This field is required.']}, {}]) + + def test_more_initial_data(self): + # The extra argument also works when the formset is pre-filled with initial + # data. + + data = { + 'choices-TOTAL_FORMS': '3', # the number of forms rendered + 'choices-INITIAL_FORMS': '0', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + 'choices-1-choice': '', + 'choices-1-votes': '', # missing value + 'choices-2-choice': '', + 'choices-2-votes': '', + } + + initial = [{'choice': u'Calexico', 'votes': 100}] + ChoiceFormSet = formset_factory(Choice, extra=3) + formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') + form_output = [] + + for form in formset.forms: + form_output.append(form.as_ul()) + + self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Choice: <input type="text" name="choices-1-choice" /></li> +<li>Votes: <input type="text" name="choices-1-votes" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li> +<li>Choice: <input type="text" name="choices-3-choice" /></li> +<li>Votes: <input type="text" name="choices-3-votes" /></li>""") + + # Make sure retrieving an empty form works, and it shows up in the form list + + self.assertTrue(formset.empty_form.empty_permitted) + self.assertEqual(formset.empty_form.as_ul(), """<li>Choice: <input type="text" name="choices-__prefix__-choice" /></li> +<li>Votes: <input type="text" name="choices-__prefix__-votes" /></li>""") + + def test_formset_with_deletion(self): + # FormSets with deletion ###################################################### + # We can easily add deletion ability to a FormSet with an argument to + # formset_factory. This will add a boolean field to each form instance. When + # that boolean field is True, the form will be in formset.deleted_forms + + ChoiceFormSet = formset_factory(Choice, can_delete=True) + + initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] + formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') + form_output = [] + + for form in formset.forms: + form_output.append(form.as_ul()) + + self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li> +<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> +<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> +<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li> +<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>""") + + # To delete something, we just need to set that form's special delete field to + # 'on'. Let's go ahead and delete Fergie. + + data = { + 'choices-TOTAL_FORMS': '3', # the number of forms rendered + 'choices-INITIAL_FORMS': '2', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + 'choices-0-DELETE': '', + 'choices-1-choice': 'Fergie', + 'choices-1-votes': '900', + 'choices-1-DELETE': 'on', + 'choices-2-choice': '', + 'choices-2-votes': '', + 'choices-2-DELETE': '', + } + + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(formset.is_valid()) + self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}, {'votes': 900, 'DELETE': True, 'choice': u'Fergie'}, {}]) + self.assertEqual([form.cleaned_data for form in formset.deleted_forms], [{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}]) + + # If we fill a form with something and then we check the can_delete checkbox for + # that form, that form's errors should not make the entire formset invalid since + # it's going to be deleted. + + class CheckForm(Form): + field = IntegerField(min_value=100) + + data = { + 'check-TOTAL_FORMS': '3', # the number of forms rendered + 'check-INITIAL_FORMS': '2', # the number of forms with initial data + 'check-MAX_NUM_FORMS': '0', # max number of forms + 'check-0-field': '200', + 'check-0-DELETE': '', + 'check-1-field': '50', + 'check-1-DELETE': 'on', + 'check-2-field': '', + 'check-2-DELETE': '', + } + CheckFormSet = formset_factory(CheckForm, can_delete=True) + formset = CheckFormSet(data, prefix='check') + self.assertTrue(formset.is_valid()) + + # If we remove the deletion flag now we will have our validation back. + data['check-1-DELETE'] = '' + formset = CheckFormSet(data, prefix='check') + self.assertFalse(formset.is_valid()) + + # Should be able to get deleted_forms from a valid formset even if a + # deleted form would have been invalid. + + class Person(Form): + name = CharField() + + PeopleForm = formset_factory( + form=Person, + can_delete=True) + + p = PeopleForm( + {'form-0-name': u'', 'form-0-DELETE': u'on', # no name! + 'form-TOTAL_FORMS': 1, 'form-INITIAL_FORMS': 1, + 'form-MAX_NUM_FORMS': 1}) + + self.assertTrue(p.is_valid()) + self.assertEqual(len(p.deleted_forms), 1) + + def test_formsets_with_ordering(self): + # FormSets with ordering ###################################################### + # We can also add ordering ability to a FormSet with an argument to + # formset_factory. This will add a integer field to each form instance. When + # form validation succeeds, [form.cleaned_data for form in formset.forms] will have the data in the correct + # order specified by the ordering fields. If a number is duplicated in the set + # of ordering fields, for instance form 0 and form 3 are both marked as 1, then + # the form index used as a secondary ordering criteria. In order to put + # something at the front of the list, you'd need to set it's order to 0. + + ChoiceFormSet = formset_factory(Choice, can_order=True) + + initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] + formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') + form_output = [] + + for form in formset.forms: + form_output.append(form.as_ul()) + + self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li> +<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> +<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> +<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li> +<li>Order: <input type="text" name="choices-2-ORDER" /></li>""") + + data = { + 'choices-TOTAL_FORMS': '3', # the number of forms rendered + 'choices-INITIAL_FORMS': '2', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + 'choices-0-ORDER': '1', + 'choices-1-choice': 'Fergie', + 'choices-1-votes': '900', + 'choices-1-ORDER': '2', + 'choices-2-choice': 'The Decemberists', + 'choices-2-votes': '500', + 'choices-2-ORDER': '0', + } + + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(formset.is_valid()) + form_output = [] + + for form in formset.ordered_forms: + form_output.append(form.cleaned_data) + + self.assertEqual(form_output, [ + {'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'}, + {'votes': 100, 'ORDER': 1, 'choice': u'Calexico'}, + {'votes': 900, 'ORDER': 2, 'choice': u'Fergie'}, + ]) + + def test_empty_ordered_fields(self): + # Ordering fields are allowed to be left blank, and if they *are* left blank, + # they will be sorted below everything else. + + data = { + 'choices-TOTAL_FORMS': '4', # the number of forms rendered + 'choices-INITIAL_FORMS': '3', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + 'choices-0-ORDER': '1', + 'choices-1-choice': 'Fergie', + 'choices-1-votes': '900', + 'choices-1-ORDER': '2', + 'choices-2-choice': 'The Decemberists', + 'choices-2-votes': '500', + 'choices-2-ORDER': '', + 'choices-3-choice': 'Basia Bulat', + 'choices-3-votes': '50', + 'choices-3-ORDER': '', + } + + ChoiceFormSet = formset_factory(Choice, can_order=True) + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(formset.is_valid()) + form_output = [] + + for form in formset.ordered_forms: + form_output.append(form.cleaned_data) + + self.assertEqual(form_output, [ + {'votes': 100, 'ORDER': 1, 'choice': u'Calexico'}, + {'votes': 900, 'ORDER': 2, 'choice': u'Fergie'}, + {'votes': 500, 'ORDER': None, 'choice': u'The Decemberists'}, + {'votes': 50, 'ORDER': None, 'choice': u'Basia Bulat'}, + ]) + + def test_ordering_blank_fieldsets(self): + # Ordering should work with blank fieldsets. + + data = { + 'choices-TOTAL_FORMS': '3', # the number of forms rendered + 'choices-INITIAL_FORMS': '0', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + } + + ChoiceFormSet = formset_factory(Choice, can_order=True) + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(formset.is_valid()) + form_output = [] + + for form in formset.ordered_forms: + form_output.append(form.cleaned_data) + + self.assertEqual(form_output, []) + + def test_formset_with_ordering_and_deletion(self): + # FormSets with ordering + deletion ########################################### + # Let's try throwing ordering and deletion into the same form. + + ChoiceFormSet = formset_factory(Choice, can_order=True, can_delete=True) + + initial = [ + {'choice': u'Calexico', 'votes': 100}, + {'choice': u'Fergie', 'votes': 900}, + {'choice': u'The Decemberists', 'votes': 500}, + ] + formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') + form_output = [] + + for form in formset.forms: + form_output.append(form.as_ul()) + + self.assertEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li> +<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li> +<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> +<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> +<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li> +<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li> +<li>Choice: <input type="text" name="choices-2-choice" value="The Decemberists" /></li> +<li>Votes: <input type="text" name="choices-2-votes" value="500" /></li> +<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li> +<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li> +<li>Choice: <input type="text" name="choices-3-choice" /></li> +<li>Votes: <input type="text" name="choices-3-votes" /></li> +<li>Order: <input type="text" name="choices-3-ORDER" /></li> +<li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li>""") + + # Let's delete Fergie, and put The Decemberists ahead of Calexico. + + data = { + 'choices-TOTAL_FORMS': '4', # the number of forms rendered + 'choices-INITIAL_FORMS': '3', # the number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + 'choices-0-ORDER': '1', + 'choices-0-DELETE': '', + 'choices-1-choice': 'Fergie', + 'choices-1-votes': '900', + 'choices-1-ORDER': '2', + 'choices-1-DELETE': 'on', + 'choices-2-choice': 'The Decemberists', + 'choices-2-votes': '500', + 'choices-2-ORDER': '0', + 'choices-2-DELETE': '', + 'choices-3-choice': '', + 'choices-3-votes': '', + 'choices-3-ORDER': '', + 'choices-3-DELETE': '', + } + + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(formset.is_valid()) + form_output = [] + + for form in formset.ordered_forms: + form_output.append(form.cleaned_data) + + self.assertEqual(form_output, [ + {'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'}, + {'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': u'Calexico'}, + ]) + self.assertEqual([form.cleaned_data for form in formset.deleted_forms], [{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}]) + + def test_invalid_deleted_form_with_ordering(self): + # Should be able to get ordered forms from a valid formset even if a + # deleted form would have been invalid. + + class Person(Form): + name = CharField() + + PeopleForm = formset_factory(form=Person, can_delete=True, can_order=True) + + p = PeopleForm({ + 'form-0-name': u'', + 'form-0-DELETE': u'on', # no name! + 'form-TOTAL_FORMS': 1, + 'form-INITIAL_FORMS': 1, + 'form-MAX_NUM_FORMS': 1 + }) + + self.assertTrue(p.is_valid()) + self.assertEqual(p.ordered_forms, []) + + def test_clean_hook(self): + # FormSet clean hook ########################################################## + # FormSets have a hook for doing extra validation that shouldn't be tied to any + # particular form. It follows the same pattern as the clean hook on Forms. + + # We start out with a some duplicate data. + + data = { + 'drinks-TOTAL_FORMS': '2', # the number of forms rendered + 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data + 'drinks-MAX_NUM_FORMS': '0', # max number of forms + 'drinks-0-name': 'Gin and Tonic', + 'drinks-1-name': 'Gin and Tonic', + } + + formset = FavoriteDrinksFormSet(data, prefix='drinks') + self.assertFalse(formset.is_valid()) + + # Any errors raised by formset.clean() are available via the + # formset.non_form_errors() method. + + for error in formset.non_form_errors(): + self.assertEqual(str(error), 'You may only specify a drink once.') + + # Make sure we didn't break the valid case. + + data = { + 'drinks-TOTAL_FORMS': '2', # the number of forms rendered + 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data + 'drinks-MAX_NUM_FORMS': '0', # max number of forms + 'drinks-0-name': 'Gin and Tonic', + 'drinks-1-name': 'Bloody Mary', + } + + formset = FavoriteDrinksFormSet(data, prefix='drinks') + self.assertTrue(formset.is_valid()) + self.assertEqual(formset.non_form_errors(), []) + + def test_limiting_max_forms(self): + # Limiting the maximum number of forms ######################################## + # Base case for max_num. + + # When not passed, max_num will take its default value of None, i.e. unlimited + # number of forms, only controlled by the value of the extra parameter. + + LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3) + formset = LimitedFavoriteDrinkFormSet() + form_output = [] + + for form in formset.forms: + form_output.append(str(form)) + + self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr> +<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr> +<tr><th><label for="id_form-2-name">Name:</label></th><td><input type="text" name="form-2-name" id="id_form-2-name" /></td></tr>""") + + # If max_num is 0 then no form is rendered at all. + LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=0) + formset = LimitedFavoriteDrinkFormSet() + form_output = [] + + for form in formset.forms: + form_output.append(str(form)) + + self.assertEqual('\n'.join(form_output), "") + + LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=5, max_num=2) + formset = LimitedFavoriteDrinkFormSet() + form_output = [] + + for form in formset.forms: + form_output.append(str(form)) + + self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr> +<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr>""") + + # Ensure that max_num has no effect when extra is less than max_num. + + LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2) + formset = LimitedFavoriteDrinkFormSet() + form_output = [] + + for form in formset.forms: + form_output.append(str(form)) + + self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr>""") + + def test_max_num_with_initial_data(self): + # max_num with initial data + + # When not passed, max_num will take its default value of None, i.e. unlimited + # number of forms, only controlled by the values of the initial and extra + # parameters. + + initial = [ + {'name': 'Fernet and Coke'}, + ] + LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1) + formset = LimitedFavoriteDrinkFormSet(initial=initial) + form_output = [] + + for form in formset.forms: + form_output.append(str(form)) + + self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Fernet and Coke" id="id_form-0-name" /></td></tr> +<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr>""") + + def test_max_num_zero(self): + # If max_num is 0 then no form is rendered at all, even if extra and initial + # are specified. + + initial = [ + {'name': 'Fernet and Coke'}, + {'name': 'Bloody Mary'}, + ] + LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=0) + formset = LimitedFavoriteDrinkFormSet(initial=initial) + form_output = [] + + for form in formset.forms: + form_output.append(str(form)) + + self.assertEqual('\n'.join(form_output), "") + + def test_more_initial_than_max_num(self): + # More initial forms than max_num will result in only the first max_num of + # them to be displayed with no extra forms. + + initial = [ + {'name': 'Gin Tonic'}, + {'name': 'Bloody Mary'}, + {'name': 'Jack and Coke'}, + ] + LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2) + formset = LimitedFavoriteDrinkFormSet(initial=initial) + form_output = [] + + for form in formset.forms: + form_output.append(str(form)) + + self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr> +<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" value="Bloody Mary" id="id_form-1-name" /></td></tr>""") + + # One form from initial and extra=3 with max_num=2 should result in the one + # initial form and one extra. + initial = [ + {'name': 'Gin Tonic'}, + ] + LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=2) + formset = LimitedFavoriteDrinkFormSet(initial=initial) + form_output = [] + + for form in formset.forms: + form_output.append(str(form)) + + self.assertEqual('\n'.join(form_output), """<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr> +<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr>""") + + def test_regression_6926(self): + # Regression test for #6926 ################################################## + # Make sure the management form has the correct prefix. + + formset = FavoriteDrinksFormSet() + self.assertEqual(formset.management_form.prefix, 'form') + + formset = FavoriteDrinksFormSet(data={}) + self.assertEqual(formset.management_form.prefix, 'form') + + formset = FavoriteDrinksFormSet(initial={}) + self.assertEqual(formset.management_form.prefix, 'form') + + def test_regression_12878(self): + # Regression test for #12878 ################################################# + + data = { + 'drinks-TOTAL_FORMS': '2', # the number of forms rendered + 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data + 'drinks-MAX_NUM_FORMS': '0', # max number of forms + 'drinks-0-name': 'Gin and Tonic', + 'drinks-1-name': 'Gin and Tonic', + } + + formset = FavoriteDrinksFormSet(data, prefix='drinks') + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.non_form_errors(), [u'You may only specify a drink once.']) diff --git a/parts/django/tests/regressiontests/forms/tests/input_formats.py b/parts/django/tests/regressiontests/forms/tests/input_formats.py new file mode 100644 index 0000000..498c6de --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/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/parts/django/tests/regressiontests/forms/tests/media.py b/parts/django/tests/regressiontests/forms/tests/media.py new file mode 100644 index 0000000..eb59d13 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/media.py @@ -0,0 +1,460 @@ +# -*- coding: utf-8 -*- +from unittest import TestCase +from django.conf import settings +from django.forms import TextInput, Media, TextInput, CharField, Form, MultiWidget + + +class FormsMediaTestCase(TestCase): + # Tests for the media handling on widgets and forms + def setUp(self): + super(FormsMediaTestCase, self).setUp() + self.original_media_url = settings.MEDIA_URL + settings.MEDIA_URL = 'http://media.example.com/media/' + + def tearDown(self): + settings.MEDIA_URL = self.original_media_url + super(FormsMediaTestCase, self).tearDown() + + def test_construction(self): + # Check construction of media objects + m = Media(css={'all': ('path/to/css1','/path/to/css2')}, js=('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3')) + self.assertEqual(str(m), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>""") + + class Foo: + css = { + 'all': ('path/to/css1','/path/to/css2') + } + js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + + m3 = Media(Foo) + self.assertEqual(str(m3), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>""") + + # A widget can exist without a media definition + class MyWidget(TextInput): + pass + + w = MyWidget() + self.assertEqual(str(w.media), '') + + def test_media_dsl(self): + ############################################################### + # DSL Class-based media definitions + ############################################################### + + # A widget can define media if it needs to. + # Any absolute path will be preserved; relative paths are combined + # with the value of settings.MEDIA_URL + class MyWidget1(TextInput): + class Media: + css = { + 'all': ('path/to/css1','/path/to/css2') + } + js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + + w1 = MyWidget1() + self.assertEqual(str(w1.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>""") + + # Media objects can be interrogated by media type + self.assertEqual(str(w1.media['css']), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" />""") + + self.assertEqual(str(w1.media['js']), """<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>""") + + def test_combine_media(self): + # Media objects can be combined. Any given media resource will appear only + # once. Duplicated media definitions are ignored. + class MyWidget1(TextInput): + class Media: + css = { + 'all': ('path/to/css1','/path/to/css2') + } + js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + + class MyWidget2(TextInput): + class Media: + css = { + 'all': ('/path/to/css2','/path/to/css3') + } + js = ('/path/to/js1','/path/to/js4') + + class MyWidget3(TextInput): + class Media: + css = { + 'all': ('/path/to/css3','path/to/css1') + } + js = ('/path/to/js1','/path/to/js4') + + w1 = MyWidget1() + w2 = MyWidget2() + w3 = MyWidget3() + self.assertEqual(str(w1.media + w2.media + w3.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script>""") + + # Check that media addition hasn't affected the original objects + self.assertEqual(str(w1.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>""") + + # Regression check for #12879: specifying the same CSS or JS file + # multiple times in a single Media instance should result in that file + # only being included once. + class MyWidget4(TextInput): + class Media: + css = {'all': ('/path/to/css1', '/path/to/css1')} + js = ('/path/to/js1', '/path/to/js1') + + w4 = MyWidget4() + self.assertEqual(str(w4.media), """<link href="/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script>""") + + def test_media_property(self): + ############################################################### + # Property-based media definitions + ############################################################### + + # Widget media can be defined as a property + class MyWidget4(TextInput): + def _media(self): + return Media(css={'all': ('/some/path',)}, js = ('/some/js',)) + media = property(_media) + + w4 = MyWidget4() + self.assertEqual(str(w4.media), """<link href="/some/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/some/js"></script>""") + + # Media properties can reference the media of their parents + class MyWidget5(MyWidget4): + def _media(self): + return super(MyWidget5, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',)) + media = property(_media) + + w5 = MyWidget5() + self.assertEqual(str(w5.media), """<link href="/some/path" type="text/css" media="all" rel="stylesheet" /> +<link href="/other/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/some/js"></script> +<script type="text/javascript" src="/other/js"></script>""") + + def test_media_property_parent_references(self): + # Media properties can reference the media of their parents, + # even if the parent media was defined using a class + class MyWidget1(TextInput): + class Media: + css = { + 'all': ('path/to/css1','/path/to/css2') + } + js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + + class MyWidget6(MyWidget1): + def _media(self): + return super(MyWidget6, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',)) + media = property(_media) + + w6 = MyWidget6() + self.assertEqual(str(w6.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/other/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/other/js"></script>""") + + def test_media_inheritance(self): + ############################################################### + # Inheritance of media + ############################################################### + + # If a widget extends another but provides no media definition, it inherits the parent widget's media + class MyWidget1(TextInput): + class Media: + css = { + 'all': ('path/to/css1','/path/to/css2') + } + js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + + class MyWidget7(MyWidget1): + pass + + w7 = MyWidget7() + self.assertEqual(str(w7.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script>""") + + # If a widget extends another but defines media, it extends the parent widget's media by default + class MyWidget8(MyWidget1): + class Media: + css = { + 'all': ('/path/to/css3','path/to/css1') + } + js = ('/path/to/js1','/path/to/js4') + + w8 = MyWidget8() + self.assertEqual(str(w8.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script>""") + + def test_media_inheritance_from_property(self): + # If a widget extends another but defines media, it extends the parents widget's media, + # even if the parent defined media using a property. + class MyWidget1(TextInput): + class Media: + css = { + 'all': ('path/to/css1','/path/to/css2') + } + js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + + class MyWidget4(TextInput): + def _media(self): + return Media(css={'all': ('/some/path',)}, js = ('/some/js',)) + media = property(_media) + + class MyWidget9(MyWidget4): + class Media: + css = { + 'all': ('/other/path',) + } + js = ('/other/js',) + + w9 = MyWidget9() + self.assertEqual(str(w9.media), """<link href="/some/path" type="text/css" media="all" rel="stylesheet" /> +<link href="/other/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/some/js"></script> +<script type="text/javascript" src="/other/js"></script>""") + + # A widget can disable media inheritance by specifying 'extend=False' + class MyWidget10(MyWidget1): + class Media: + extend = False + css = { + 'all': ('/path/to/css3','path/to/css1') + } + js = ('/path/to/js1','/path/to/js4') + + w10 = MyWidget10() + self.assertEqual(str(w10.media), """<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="/path/to/js4"></script>""") + + def test_media_inheritance_extends(self): + # A widget can explicitly enable full media inheritance by specifying 'extend=True' + class MyWidget1(TextInput): + class Media: + css = { + 'all': ('path/to/css1','/path/to/css2') + } + js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + + class MyWidget11(MyWidget1): + class Media: + extend = True + css = { + 'all': ('/path/to/css3','path/to/css1') + } + js = ('/path/to/js1','/path/to/js4') + + w11 = MyWidget11() + self.assertEqual(str(w11.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script>""") + + def test_media_inheritance_single_type(self): + # A widget can enable inheritance of one media type by specifying extend as a tuple + class MyWidget1(TextInput): + class Media: + css = { + 'all': ('path/to/css1','/path/to/css2') + } + js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + + class MyWidget12(MyWidget1): + class Media: + extend = ('css',) + css = { + 'all': ('/path/to/css3','path/to/css1') + } + js = ('/path/to/js1','/path/to/js4') + + w12 = MyWidget12() + self.assertEqual(str(w12.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="/path/to/js4"></script>""") + + def test_multi_media(self): + ############################################################### + # Multi-media handling for CSS + ############################################################### + + # A widget can define CSS media for multiple output media types + class MultimediaWidget(TextInput): + class Media: + css = { + 'screen, print': ('/file1','/file2'), + 'screen': ('/file3',), + 'print': ('/file4',) + } + js = ('/path/to/js1','/path/to/js4') + + multimedia = MultimediaWidget() + self.assertEqual(str(multimedia.media), """<link href="/file4" type="text/css" media="print" rel="stylesheet" /> +<link href="/file3" type="text/css" media="screen" rel="stylesheet" /> +<link href="/file1" type="text/css" media="screen, print" rel="stylesheet" /> +<link href="/file2" type="text/css" media="screen, print" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="/path/to/js4"></script>""") + + def test_multi_widget(self): + ############################################################### + # Multiwidget media handling + ############################################################### + + class MyWidget1(TextInput): + class Media: + css = { + 'all': ('path/to/css1','/path/to/css2') + } + js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + + class MyWidget2(TextInput): + class Media: + css = { + 'all': ('/path/to/css2','/path/to/css3') + } + js = ('/path/to/js1','/path/to/js4') + + class MyWidget3(TextInput): + class Media: + css = { + 'all': ('/path/to/css3','path/to/css1') + } + js = ('/path/to/js1','/path/to/js4') + + # MultiWidgets have a default media definition that gets all the + # media from the component widgets + class MyMultiWidget(MultiWidget): + def __init__(self, attrs=None): + widgets = [MyWidget1, MyWidget2, MyWidget3] + super(MyMultiWidget, self).__init__(widgets, attrs) + + mymulti = MyMultiWidget() + self.assertEqual(str(mymulti.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script>""") + + def test_form_media(self): + ############################################################### + # Media processing for forms + ############################################################### + + class MyWidget1(TextInput): + class Media: + css = { + 'all': ('path/to/css1','/path/to/css2') + } + js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + + class MyWidget2(TextInput): + class Media: + css = { + 'all': ('/path/to/css2','/path/to/css3') + } + js = ('/path/to/js1','/path/to/js4') + + class MyWidget3(TextInput): + class Media: + css = { + 'all': ('/path/to/css3','path/to/css1') + } + js = ('/path/to/js1','/path/to/js4') + + # You can ask a form for the media required by its widgets. + class MyForm(Form): + field1 = CharField(max_length=20, widget=MyWidget1()) + field2 = CharField(max_length=20, widget=MyWidget2()) + f1 = MyForm() + self.assertEqual(str(f1.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script>""") + + # Form media can be combined to produce a single media definition. + class AnotherForm(Form): + field3 = CharField(max_length=20, widget=MyWidget3()) + f2 = AnotherForm() + self.assertEqual(str(f1.media + f2.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script>""") + + # Forms can also define media, following the same rules as widgets. + class FormWithMedia(Form): + field1 = CharField(max_length=20, widget=MyWidget1()) + field2 = CharField(max_length=20, widget=MyWidget2()) + class Media: + js = ('/some/form/javascript',) + css = { + 'all': ('/some/form/css',) + } + f3 = FormWithMedia() + self.assertEqual(str(f3.media), """<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<link href="/some/form/css" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> +<script type="text/javascript" src="/some/form/javascript"></script>""") + + # Media works in templates + from django.template import Template, Context + self.assertEqual(Template("{{ form.media.js }}{{ form.media.css }}").render(Context({'form': f3})), """<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> +<script type="text/javascript" src="/some/form/javascript"></script><link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<link href="/some/form/css" type="text/css" media="all" rel="stylesheet" />""") diff --git a/parts/django/tests/regressiontests/forms/tests/models.py b/parts/django/tests/regressiontests/forms/tests/models.py new file mode 100644 index 0000000..af7473e --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/models.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +import datetime +from django.core.files.uploadedfile import SimpleUploadedFile +from django.conf import settings +from django.db import connection +from django.forms import Form, ModelForm, FileField, ModelChoiceField +from django.test import TestCase +from regressiontests.forms.models import ChoiceModel, ChoiceOptionModel, ChoiceFieldModel, FileModel, Group, BoundaryModel, Defaults + + +class ChoiceFieldForm(ModelForm): + class Meta: + model = ChoiceFieldModel + + +class FileForm(Form): + file1 = FileField() + + +class TestTicket12510(TestCase): + ''' It is not necessary to generate choices for ModelChoiceField (regression test for #12510). ''' + def setUp(self): + self.groups = [Group.objects.create(name=name) for name in 'abc'] + self.old_debug = settings.DEBUG + # turn debug on to get access to connection.queries + settings.DEBUG = True + + def tearDown(self): + settings.DEBUG = self.old_debug + + def test_choices_not_fetched_when_not_rendering(self): + initial_queries = len(connection.queries) + field = ModelChoiceField(Group.objects.order_by('-name')) + self.assertEqual('a', field.clean(self.groups[0].pk).name) + # 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" /> Hold down "Control", or "Command" on a Mac, to select more than one.</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" /> Hold down "Control", or "Command" on a Mac, to select more than one.</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" /> Hold down "Control", or "Command" on a Mac, to select more than one.</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" /> Hold down "Control", or "Command" on a Mac, to select more than one.</p>""") + + + +class FormsModelTestCase(TestCase): + def test_unicode_filename(self): + # FileModel with unicode filename and data ######################### + f = FileForm(data={}, files={'file1': SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह')}, auto_id=False) + self.assertTrue(f.is_valid()) + self.assertTrue('file1' in f.cleaned_data) + m = FileModel.objects.create(file=f.cleaned_data['file1']) + self.assertEqual(m.file.name, u'tests/\u6211\u96bb\u6c23\u588a\u8239\u88dd\u6eff\u6652\u9c54.txt') + m.delete() + + def test_boundary_conditions(self): + # Boundary conditions on a PostitiveIntegerField ######################### + class BoundaryForm(ModelForm): + class Meta: + model = BoundaryModel + + f = BoundaryForm({'positive_integer': 100}) + self.assertTrue(f.is_valid()) + f = BoundaryForm({'positive_integer': 0}) + self.assertTrue(f.is_valid()) + f = BoundaryForm({'positive_integer': -100}) + self.assertFalse(f.is_valid()) + + def test_formfield_initial(self): + # Formfield initial values ######## + # If the model has default values for some fields, they are used as the formfield + # initial values. + class DefaultsForm(ModelForm): + class Meta: + model = Defaults + + self.assertEqual(DefaultsForm().fields['name'].initial, u'class default value') + self.assertEqual(DefaultsForm().fields['def_date'].initial, datetime.date(1980, 1, 1)) + self.assertEqual(DefaultsForm().fields['value'].initial, 42) + r1 = DefaultsForm()['callable_default'].as_widget() + r2 = DefaultsForm()['callable_default'].as_widget() + self.assertNotEqual(r1, r2) + + # In a ModelForm that is passed an instance, the initial values come from the + # instance's values, not the model's defaults. + foo_instance = Defaults(name=u'instance value', def_date=datetime.date(1969, 4, 4), value=12) + instance_form = DefaultsForm(instance=foo_instance) + self.assertEqual(instance_form.initial['name'], u'instance value') + self.assertEqual(instance_form.initial['def_date'], datetime.date(1969, 4, 4)) + self.assertEqual(instance_form.initial['value'], 12) + + from django.forms import CharField + + class ExcludingForm(ModelForm): + name = CharField(max_length=255) + + class Meta: + model = Defaults + exclude = ['name', 'callable_default'] + + f = ExcludingForm({'name': u'Hello', 'value': 99, 'def_date': datetime.date(1999, 3, 2)}) + self.assertTrue(f.is_valid()) + self.assertEqual(f.cleaned_data['name'], u'Hello') + obj = f.save() + self.assertEqual(obj.name, u'class default value') + self.assertEqual(obj.value, 99) + self.assertEqual(obj.def_date, datetime.date(1999, 3, 2)) diff --git a/parts/django/tests/regressiontests/forms/tests/regressions.py b/parts/django/tests/regressiontests/forms/tests/regressions.py new file mode 100644 index 0000000..ddec740 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/regressions.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +from unittest import TestCase +from django.forms import * +from django.utils.translation import ugettext_lazy, activate, deactivate + +from regressiontests.forms.models import Cheese + + +class FormsRegressionsTestCase(TestCase): + def test_class(self): + # Tests to prevent against recurrences of earlier bugs. + extra_attrs = {'class': 'special'} + + class TestForm(Form): + f1 = CharField(max_length=10, widget=TextInput(attrs=extra_attrs)) + f2 = CharField(widget=TextInput(attrs=extra_attrs)) + + self.assertEqual(TestForm(auto_id=False).as_p(), u'<p>F1: <input type="text" class="special" name="f1" maxlength="10" /></p>\n<p>F2: <input type="text" class="special" name="f2" /></p>') + + def test_regression_3600(self): + # Tests for form i18n # + # There were some problems with form translations in #3600 + + class SomeForm(Form): + username = CharField(max_length=10, label=ugettext_lazy('Username')) + + f = SomeForm() + self.assertEqual(f.as_p(), '<p><label for="id_username">Username:</label> <input id="id_username" type="text" name="username" maxlength="10" /></p>') + + # Translations are done at rendering time, so multi-lingual apps can define forms) + activate('de') + self.assertEqual(f.as_p(), '<p><label for="id_username">Benutzername:</label> <input id="id_username" type="text" name="username" maxlength="10" /></p>') + activate('pl') + self.assertEqual(f.as_p(), u'<p><label for="id_username">Nazwa u\u017cytkownika:</label> <input id="id_username" type="text" name="username" maxlength="10" /></p>') + deactivate() + + def test_regression_5216(self): + # There was some problems with form translations in #5216 + class SomeForm(Form): + field_1 = CharField(max_length=10, label=ugettext_lazy('field_1')) + field_2 = CharField(max_length=10, label=ugettext_lazy('field_2'), widget=TextInput(attrs={'id': 'field_2_id'})) + + f = SomeForm() + self.assertEqual(f['field_1'].label_tag(), '<label for="id_field_1">field_1</label>') + self.assertEqual(f['field_2'].label_tag(), '<label for="field_2_id">field_2</label>') + + # Unicode decoding problems... + GENDERS = ((u'\xc5', u'En tied\xe4'), (u'\xf8', u'Mies'), (u'\xdf', u'Nainen')) + + class SomeForm(Form): + somechoice = ChoiceField(choices=GENDERS, widget=RadioSelect(), label=u'\xc5\xf8\xdf') + + f = SomeForm() + self.assertEqual(f.as_p(), u'<p><label for="id_somechoice_0">\xc5\xf8\xdf:</label> <ul>\n<li><label for="id_somechoice_0"><input type="radio" id="id_somechoice_0" value="\xc5" name="somechoice" /> En tied\xe4</label></li>\n<li><label for="id_somechoice_1"><input type="radio" id="id_somechoice_1" value="\xf8" name="somechoice" /> Mies</label></li>\n<li><label for="id_somechoice_2"><input type="radio" id="id_somechoice_2" value="\xdf" name="somechoice" /> Nainen</label></li>\n</ul></p>') + + # Testing choice validation with UTF-8 bytestrings as input (these are the + # Russian abbreviations "мес." and "шт.". + UNITS = (('\xd0\xbc\xd0\xb5\xd1\x81.', '\xd0\xbc\xd0\xb5\xd1\x81.'), ('\xd1\x88\xd1\x82.', '\xd1\x88\xd1\x82.')) + f = ChoiceField(choices=UNITS) + self.assertEqual(f.clean(u'\u0448\u0442.'), u'\u0448\u0442.') + self.assertEqual(f.clean('\xd1\x88\xd1\x82.'), u'\u0448\u0442.') + + # Translated error messages used to be buggy. + activate('ru') + f = SomeForm({}) + self.assertEqual(f.as_p(), u'<ul class="errorlist"><li>\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u043b\u0435.</li></ul>\n<p><label for="id_somechoice_0">\xc5\xf8\xdf:</label> <ul>\n<li><label for="id_somechoice_0"><input type="radio" id="id_somechoice_0" value="\xc5" name="somechoice" /> En tied\xe4</label></li>\n<li><label for="id_somechoice_1"><input type="radio" id="id_somechoice_1" value="\xf8" name="somechoice" /> Mies</label></li>\n<li><label for="id_somechoice_2"><input type="radio" id="id_somechoice_2" value="\xdf" name="somechoice" /> Nainen</label></li>\n</ul></p>') + deactivate() + + # Deep copying translated text shouldn't raise an error) + from django.utils.translation import gettext_lazy + + class CopyForm(Form): + degree = IntegerField(widget=Select(choices=((1, gettext_lazy('test')),))) + + f = CopyForm() + + def test_misc(self): + # There once was a problem with Form fields called "data". Let's make sure that + # doesn't come back. + class DataForm(Form): + data = CharField(max_length=10) + + f = DataForm({'data': 'xyzzy'}) + self.assertTrue(f.is_valid()) + self.assertEqual(f.cleaned_data, {'data': u'xyzzy'}) + + # A form with *only* hidden fields that has errors is going to be very unusual. + class HiddenForm(Form): + data = IntegerField(widget=HiddenInput) + + f = HiddenForm({}) + self.assertEqual(f.as_p(), u'<ul class="errorlist"><li>(Hidden field data) This field is required.</li></ul>\n<p> <input type="hidden" name="data" id="id_data" /></p>') + self.assertEqual(f.as_table(), u'<tr><td colspan="2"><ul class="errorlist"><li>(Hidden field data) This field is required.</li></ul><input type="hidden" name="data" id="id_data" /></td></tr>') + + def test_xss_error_messages(self): + ################################################### + # Tests for XSS vulnerabilities in error messages # + ################################################### + + # The forms layer doesn't escape input values directly because error messages + # might be presented in non-HTML contexts. Instead, the message is just marked + # for escaping by the template engine. So we'll need to construct a little + # silly template to trigger the escaping. + from django.template import Template, Context + t = Template('{{ form.errors }}') + + class SomeForm(Form): + field = ChoiceField(choices=[('one', 'One')]) + + f = SomeForm({'field': '<script>'}) + self.assertEqual(t.render(Context({'form': f})), u'<ul class="errorlist"><li>field<ul class="errorlist"><li>Select a valid choice. <script> is not one of the available choices.</li></ul></li></ul>') + + class SomeForm(Form): + field = MultipleChoiceField(choices=[('one', 'One')]) + + f = SomeForm({'field': ['<script>']}) + self.assertEqual(t.render(Context({'form': f})), u'<ul class="errorlist"><li>field<ul class="errorlist"><li>Select a valid choice. <script> is not one of the available choices.</li></ul></li></ul>') + + from regressiontests.forms.models import ChoiceModel + + class SomeForm(Form): + field = ModelMultipleChoiceField(ChoiceModel.objects.all()) + + f = SomeForm({'field': ['<script>']}) + self.assertEqual(t.render(Context({'form': f})), u'<ul class="errorlist"><li>field<ul class="errorlist"><li>"<script>" is not a valid value for a primary key.</li></ul></li></ul>') + + def test_regression_14234(self): + """ + Re-cleaning an instance that was added via a ModelForm should not raise + a pk uniqueness error. + + """ + class CheeseForm(ModelForm): + class Meta: + model = Cheese + + form = CheeseForm({ + 'name': 'Brie', + }) + + self.assertTrue(form.is_valid()) + + obj = form.save() + obj.name = 'Camembert' + obj.full_clean() diff --git a/parts/django/tests/regressiontests/forms/tests/util.py b/parts/django/tests/regressiontests/forms/tests/util.py new file mode 100644 index 0000000..8ea3bdd --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/util.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +from unittest import TestCase +from django.core.exceptions import ValidationError +from django.forms.util import * +from django.utils.translation import ugettext_lazy + + +class FormsUtilTestCase(TestCase): + # Tests for forms/util.py module. + + def test_flatatt(self): + ########### + # flatatt # + ########### + + self.assertEqual(flatatt({'id': "header"}), u' id="header"') + self.assertEqual(flatatt({'class': "news", 'title': "Read this"}), u' class="news" title="Read this"') + self.assertEqual(flatatt({}), u'') + + def test_validation_error(self): + ################### + # ValidationError # + ################### + + # Can take a string. + self.assertEqual(str(ErrorList(ValidationError("There was an error.").messages)), + '<ul class="errorlist"><li>There was an error.</li></ul>') + + # Can take a unicode string. + self.assertEqual(str(ErrorList(ValidationError(u"Not \u03C0.").messages)), + '<ul class="errorlist"><li>Not π.</li></ul>') + + # Can take a lazy string. + self.assertEqual(str(ErrorList(ValidationError(ugettext_lazy("Error.")).messages)), + '<ul class="errorlist"><li>Error.</li></ul>') + + # Can take a list. + self.assertEqual(str(ErrorList(ValidationError(["Error one.", "Error two."]).messages)), + '<ul class="errorlist"><li>Error one.</li><li>Error two.</li></ul>') + + # Can take a mixture in a list. + self.assertEqual(str(ErrorList(ValidationError(["First error.", u"Not \u03C0.", ugettext_lazy("Error.")]).messages)), + '<ul class="errorlist"><li>First error.</li><li>Not π.</li><li>Error.</li></ul>') + + class VeryBadError: + def __unicode__(self): return u"A very bad error." + + # Can take a non-string. + self.assertEqual(str(ErrorList(ValidationError(VeryBadError()).messages)), + '<ul class="errorlist"><li>A very bad error.</li></ul>') + + # Escapes non-safe input but not input marked safe. + example = 'Example of link: <a href="http://www.example.com/">example</a>' + self.assertEqual(str(ErrorList([example])), + '<ul class="errorlist"><li>Example of link: <a href="http://www.example.com/">example</a></li></ul>') + self.assertEqual(str(ErrorList([mark_safe(example)])), + '<ul class="errorlist"><li>Example of link: <a href="http://www.example.com/">example</a></li></ul>') diff --git a/parts/django/tests/regressiontests/forms/tests/validators.py b/parts/django/tests/regressiontests/forms/tests/validators.py new file mode 100644 index 0000000..ed8e8fb --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/validators.py @@ -0,0 +1,17 @@ +from unittest import TestCase + +from django import forms +from django.core import validators +from django.core.exceptions import ValidationError + + +class TestFieldWithValidators(TestCase): + def test_all_errors_get_reported(self): + field = forms.CharField( + validators=[validators.validate_integer, validators.validate_email] + ) + self.assertRaises(ValidationError, field.clean, 'not int nor mail') + try: + field.clean('not int nor mail') + except ValidationError, e: + self.assertEqual(2, len(e.messages)) diff --git a/parts/django/tests/regressiontests/forms/tests/widgets.py b/parts/django/tests/regressiontests/forms/tests/widgets.py new file mode 100644 index 0000000..9e7c6f6 --- /dev/null +++ b/parts/django/tests/regressiontests/forms/tests/widgets.py @@ -0,0 +1,1070 @@ +# -*- coding: utf-8 -*- +import datetime +from decimal import Decimal +import re +import time +from unittest import TestCase +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.forms import * +from django.forms.widgets import RadioFieldRenderer +from django.utils import copycompat as copy +from django.utils import formats +from django.utils.safestring import mark_safe +from django.utils.translation import activate, deactivate + + + +class FormsWidgetTestCase(TestCase): + # Each Widget class corresponds to an HTML form widget. A Widget knows how to + # render itself, given a field name and some data. Widgets don't perform + # validation. + def test_textinput(self): + w = TextInput() + self.assertEqual(w.render('email', ''), u'<input type="text" name="email" />') + self.assertEqual(w.render('email', None), u'<input type="text" name="email" />') + self.assertEqual(w.render('email', 'test@example.com'), u'<input type="text" name="email" value="test@example.com" />') + self.assertEqual(w.render('email', 'some "quoted" & ampersanded value'), u'<input type="text" name="email" value="some "quoted" & ampersanded value" />') + self.assertEqual(w.render('email', 'test@example.com', attrs={'class': 'fun'}), u'<input type="text" name="email" value="test@example.com" class="fun" />') + + # Note that doctest in Python 2.4 (and maybe 2.5?) doesn't support non-ascii + # characters in output, so we're displaying the repr() here. + self.assertEqual(w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), u'<input type="text" name="email" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" class="fun" />') + + # You can also pass 'attrs' to the constructor: + w = TextInput(attrs={'class': 'fun'}) + self.assertEqual(w.render('email', ''), u'<input type="text" class="fun" name="email" />') + self.assertEqual(w.render('email', 'foo@example.com'), u'<input type="text" class="fun" value="foo@example.com" name="email" />') + + # 'attrs' passed to render() get precedence over those passed to the constructor: + w = TextInput(attrs={'class': 'pretty'}) + self.assertEqual(w.render('email', '', attrs={'class': 'special'}), u'<input type="text" class="special" name="email" />') + + # 'attrs' can be safe-strings if needed) + w = TextInput(attrs={'onBlur': mark_safe("function('foo')")}) + self.assertEqual(w.render('email', ''), u'<input onBlur="function(\'foo\')" type="text" name="email" />') + + def test_passwordinput(self): + w = PasswordInput() + self.assertEqual(w.render('email', ''), u'<input type="password" name="email" />') + self.assertEqual(w.render('email', None), u'<input type="password" name="email" />') + self.assertEqual(w.render('email', 'test@example.com'), u'<input type="password" name="email" value="test@example.com" />') + self.assertEqual(w.render('email', 'some "quoted" & ampersanded value'), u'<input type="password" name="email" value="some "quoted" & ampersanded value" />') + self.assertEqual(w.render('email', 'test@example.com', attrs={'class': 'fun'}), 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'}) + self.assertEqual(w.render('email', ''), u'<input type="password" class="fun" name="email" />') + self.assertEqual(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'}) + self.assertEqual(w.render('email', '', attrs={'class': 'special'}), u'<input type="password" class="special" name="email" />') + + self.assertEqual(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) + self.assertEqual(w.render('email', 'secret'), u'<input type="password" name="email" value="secret" />') + + w = PasswordInput(render_value=False) + self.assertEqual(w.render('email', ''), u'<input type="password" name="email" />') + self.assertEqual(w.render('email', None), u'<input type="password" name="email" />') + self.assertEqual(w.render('email', 'secret'), u'<input type="password" name="email" />') + + w = PasswordInput(attrs={'class': 'fun'}, render_value=False) + self.assertEqual(w.render('email', 'secret'), u'<input type="password" class="fun" name="email" />') + + def test_hiddeninput(self): + w = HiddenInput() + self.assertEqual(w.render('email', ''), u'<input type="hidden" name="email" />') + self.assertEqual(w.render('email', None), u'<input type="hidden" name="email" />') + self.assertEqual(w.render('email', 'test@example.com'), u'<input type="hidden" name="email" value="test@example.com" />') + self.assertEqual(w.render('email', 'some "quoted" & ampersanded value'), u'<input type="hidden" name="email" value="some "quoted" & ampersanded value" />') + self.assertEqual(w.render('email', 'test@example.com', attrs={'class': 'fun'}), u'<input type="hidden" name="email" value="test@example.com" class="fun" />') + + # You can also pass 'attrs' to the constructor: + w = HiddenInput(attrs={'class': 'fun'}) + self.assertEqual(w.render('email', ''), u'<input type="hidden" class="fun" name="email" />') + self.assertEqual(w.render('email', 'foo@example.com'), u'<input type="hidden" class="fun" value="foo@example.com" name="email" />') + + # 'attrs' passed to render() get precedence over those passed to the constructor: + w = HiddenInput(attrs={'class': 'pretty'}) + self.assertEqual(w.render('email', '', attrs={'class': 'special'}), u'<input type="hidden" class="special" name="email" />') + + self.assertEqual(w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), u'<input type="hidden" class="fun" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" name="email" />') + + # 'attrs' passed to render() get precedence over those passed to the constructor: + w = HiddenInput(attrs={'class': 'pretty'}) + self.assertEqual(w.render('email', '', attrs={'class': 'special'}), u'<input type="hidden" class="special" name="email" />') + + # Boolean values are rendered to their string forms ("True" and "False"). + w = HiddenInput() + self.assertEqual(w.render('get_spam', False), u'<input type="hidden" name="get_spam" value="False" />') + self.assertEqual(w.render('get_spam', True), u'<input type="hidden" name="get_spam" value="True" />') + + def test_multiplehiddeninput(self): + w = MultipleHiddenInput() + self.assertEqual(w.render('email', []), u'') + self.assertEqual(w.render('email', None), u'') + self.assertEqual(w.render('email', ['test@example.com']), u'<input type="hidden" name="email" value="test@example.com" />') + self.assertEqual(w.render('email', ['some "quoted" & ampersanded value']), u'<input type="hidden" name="email" value="some "quoted" & ampersanded value" />') + self.assertEqual(w.render('email', ['test@example.com', 'foo@example.com']), u'<input type="hidden" name="email" value="test@example.com" />\n<input type="hidden" name="email" value="foo@example.com" />') + self.assertEqual(w.render('email', ['test@example.com'], attrs={'class': 'fun'}), u'<input type="hidden" name="email" value="test@example.com" class="fun" />') + self.assertEqual(w.render('email', ['test@example.com', 'foo@example.com'], attrs={'class': 'fun'}), u'<input type="hidden" name="email" value="test@example.com" class="fun" />\n<input type="hidden" name="email" value="foo@example.com" class="fun" />') + + # You can also pass 'attrs' to the constructor: + w = MultipleHiddenInput(attrs={'class': 'fun'}) + self.assertEqual(w.render('email', []), u'') + self.assertEqual(w.render('email', ['foo@example.com']), u'<input type="hidden" class="fun" value="foo@example.com" name="email" />') + self.assertEqual(w.render('email', ['foo@example.com', 'test@example.com']), u'<input type="hidden" class="fun" value="foo@example.com" name="email" />\n<input type="hidden" class="fun" value="test@example.com" name="email" />') + + # 'attrs' passed to render() get precedence over those passed to the constructor: + w = MultipleHiddenInput(attrs={'class': 'pretty'}) + self.assertEqual(w.render('email', ['foo@example.com'], attrs={'class': 'special'}), u'<input type="hidden" class="special" value="foo@example.com" name="email" />') + + self.assertEqual(w.render('email', ['ŠĐĆŽćžšđ'], attrs={'class': 'fun'}), u'<input type="hidden" class="fun" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" name="email" />') + + # 'attrs' passed to render() get precedence over those passed to the constructor: + w = MultipleHiddenInput(attrs={'class': 'pretty'}) + self.assertEqual(w.render('email', ['foo@example.com'], attrs={'class': 'special'}), u'<input type="hidden" class="special" value="foo@example.com" name="email" />') + + # Each input gets a separate ID. + w = MultipleHiddenInput() + self.assertEqual(w.render('letters', list('abc'), attrs={'id': 'hideme'}), u'<input type="hidden" name="letters" value="a" id="hideme_0" />\n<input type="hidden" name="letters" value="b" id="hideme_1" />\n<input type="hidden" name="letters" value="c" id="hideme_2" />') + + def test_fileinput(self): + # FileInput widgets don't ever show the value, because the old value is of no use + # if you are updating the form or if the provided file generated an error. + w = FileInput() + self.assertEqual(w.render('email', ''), u'<input type="file" name="email" />') + self.assertEqual(w.render('email', None), u'<input type="file" name="email" />') + self.assertEqual(w.render('email', 'test@example.com'), u'<input type="file" name="email" />') + self.assertEqual(w.render('email', 'some "quoted" & ampersanded value'), u'<input type="file" name="email" />') + self.assertEqual(w.render('email', 'test@example.com', attrs={'class': 'fun'}), u'<input type="file" name="email" class="fun" />') + + # You can also pass 'attrs' to the constructor: + w = FileInput(attrs={'class': 'fun'}) + self.assertEqual(w.render('email', ''), u'<input type="file" class="fun" name="email" />') + self.assertEqual(w.render('email', 'foo@example.com'), u'<input type="file" class="fun" name="email" />') + + self.assertEqual(w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), u'<input type="file" class="fun" name="email" />') + + # Test for the behavior of _has_changed for FileInput. The value of data will + # more than likely come from request.FILES. The value of initial data will + # likely be a filename stored in the database. Since its value is of no use to + # a FileInput it is ignored. + w = FileInput() + + # No file was uploaded and no initial data. + self.assertFalse(w._has_changed(u'', None)) + + # A file was uploaded and no initial data. + self.assertTrue(w._has_changed(u'', {'filename': 'resume.txt', 'content': 'My resume'})) + + # A file was not uploaded, but there is initial data + self.assertFalse(w._has_changed(u'resume.txt', None)) + + # A file was uploaded and there is initial data (file identity is not dealt + # with here) + self.assertTrue(w._has_changed('resume.txt', {'filename': 'resume.txt', 'content': 'My resume'})) + + def test_textarea(self): + w = Textarea() + self.assertEqual(w.render('msg', ''), u'<textarea rows="10" cols="40" name="msg"></textarea>') + self.assertEqual(w.render('msg', None), u'<textarea rows="10" cols="40" name="msg"></textarea>') + self.assertEqual(w.render('msg', 'value'), u'<textarea rows="10" cols="40" name="msg">value</textarea>') + self.assertEqual(w.render('msg', 'some "quoted" & ampersanded value'), u'<textarea rows="10" cols="40" name="msg">some "quoted" & ampersanded value</textarea>') + self.assertEqual(w.render('msg', mark_safe('pre "quoted" value')), u'<textarea rows="10" cols="40" name="msg">pre "quoted" value</textarea>') + self.assertEqual(w.render('msg', 'value', attrs={'class': 'pretty', 'rows': 20}), u'<textarea class="pretty" rows="20" cols="40" name="msg">value</textarea>') + + # You can also pass 'attrs' to the constructor: + w = Textarea(attrs={'class': 'pretty'}) + self.assertEqual(w.render('msg', ''), u'<textarea rows="10" cols="40" name="msg" class="pretty"></textarea>') + self.assertEqual(w.render('msg', 'example'), u'<textarea rows="10" cols="40" name="msg" class="pretty">example</textarea>') + + # 'attrs' passed to render() get precedence over those passed to the constructor: + w = Textarea(attrs={'class': 'pretty'}) + self.assertEqual(w.render('msg', '', attrs={'class': 'special'}), u'<textarea rows="10" cols="40" name="msg" class="special"></textarea>') + + self.assertEqual(w.render('msg', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), u'<textarea rows="10" cols="40" name="msg" class="fun">\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111</textarea>') + + def test_checkboxinput(self): + w = CheckboxInput() + self.assertEqual(w.render('is_cool', ''), u'<input type="checkbox" name="is_cool" />') + self.assertEqual(w.render('is_cool', None), u'<input type="checkbox" name="is_cool" />') + self.assertEqual(w.render('is_cool', False), u'<input type="checkbox" name="is_cool" />') + self.assertEqual(w.render('is_cool', True), u'<input checked="checked" type="checkbox" name="is_cool" />') + + # Using any value that's not in ('', None, False, True) will check the checkbox + # and set the 'value' attribute. + self.assertEqual(w.render('is_cool', 'foo'), u'<input checked="checked" type="checkbox" name="is_cool" value="foo" />') + + self.assertEqual(w.render('is_cool', False, attrs={'class': 'pretty'}), u'<input type="checkbox" name="is_cool" class="pretty" />') + + # You can also pass 'attrs' to the constructor: + w = CheckboxInput(attrs={'class': 'pretty'}) + self.assertEqual(w.render('is_cool', ''), u'<input type="checkbox" class="pretty" name="is_cool" />') + + # 'attrs' passed to render() get precedence over those passed to the constructor: + w = CheckboxInput(attrs={'class': 'pretty'}) + self.assertEqual(w.render('is_cool', '', attrs={'class': 'special'}), u'<input type="checkbox" class="special" name="is_cool" />') + + # You can pass 'check_test' to the constructor. This is a callable that takes the + # value and returns True if the box should be checked. + w = CheckboxInput(check_test=lambda value: value.startswith('hello')) + self.assertEqual(w.render('greeting', ''), u'<input type="checkbox" name="greeting" />') + self.assertEqual(w.render('greeting', 'hello'), u'<input checked="checked" type="checkbox" name="greeting" value="hello" />') + self.assertEqual(w.render('greeting', 'hello there'), u'<input checked="checked" type="checkbox" name="greeting" value="hello there" />') + self.assertEqual(w.render('greeting', 'hello & goodbye'), u'<input checked="checked" type="checkbox" name="greeting" value="hello & goodbye" />') + + # A subtlety: If the 'check_test' argument cannot handle a value and raises any + # exception during its __call__, then the exception will be swallowed and the box + # will not be checked. In this example, the 'check_test' assumes the value has a + # startswith() method, which fails for the values True, False and None. + self.assertEqual(w.render('greeting', True), u'<input type="checkbox" name="greeting" />') + self.assertEqual(w.render('greeting', False), u'<input type="checkbox" name="greeting" />') + self.assertEqual(w.render('greeting', None), u'<input type="checkbox" name="greeting" />') + + # The CheckboxInput widget will return False if the key is not found in the data + # dictionary (because HTML form submission doesn't send any result for unchecked + # checkboxes). + self.assertFalse(w.value_from_datadict({}, {}, 'testing')) + + self.assertFalse(w._has_changed(None, None)) + self.assertFalse(w._has_changed(None, u'')) + self.assertFalse(w._has_changed(u'', None)) + self.assertFalse(w._has_changed(u'', u'')) + self.assertTrue(w._has_changed(False, u'on')) + self.assertFalse(w._has_changed(True, u'on')) + self.assertTrue(w._has_changed(True, u'')) + + def test_select(self): + w = Select() + self.assertEqual(w.render('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select name="beatle"> +<option value="J" selected="selected">John</option> +<option value="P">Paul</option> +<option value="G">George</option> +<option value="R">Ringo</option> +</select>""") + + # If the value is None, none of the options are selected: + self.assertEqual(w.render('beatle', None, choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select name="beatle"> +<option value="J">John</option> +<option value="P">Paul</option> +<option value="G">George</option> +<option value="R">Ringo</option> +</select>""") + + # If the value corresponds to a label (but not to an option value), none of the options are selected: + self.assertEqual(w.render('beatle', 'John', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select name="beatle"> +<option value="J">John</option> +<option value="P">Paul</option> +<option value="G">George</option> +<option value="R">Ringo</option> +</select>""") + + # The value is compared to its str(): + self.assertEqual(w.render('num', 2, choices=[('1', '1'), ('2', '2'), ('3', '3')]), """<select name="num"> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +</select>""") + self.assertEqual(w.render('num', '2', choices=[(1, 1), (2, 2), (3, 3)]), """<select name="num"> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +</select>""") + self.assertEqual(w.render('num', 2, choices=[(1, 1), (2, 2), (3, 3)]), """<select name="num"> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +</select>""") + + # The 'choices' argument can be any iterable: + from itertools import chain + def get_choices(): + for i in range(5): + yield (i, i) + self.assertEqual(w.render('num', 2, choices=get_choices()), """<select name="num"> +<option value="0">0</option> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +<option value="4">4</option> +</select>""") + things = ({'id': 1, 'name': 'And Boom'}, {'id': 2, 'name': 'One More Thing!'}) + class SomeForm(Form): + somechoice = ChoiceField(choices=chain((('', '-'*9),), [(thing['id'], thing['name']) for thing in things])) + f = SomeForm() + self.assertEqual(f.as_table(), u'<tr><th><label for="id_somechoice">Somechoice:</label></th><td><select name="somechoice" id="id_somechoice">\n<option value="" selected="selected">---------</option>\n<option value="1">And Boom</option>\n<option value="2">One More Thing!</option>\n</select></td></tr>') + self.assertEqual(f.as_table(), u'<tr><th><label for="id_somechoice">Somechoice:</label></th><td><select name="somechoice" id="id_somechoice">\n<option value="" selected="selected">---------</option>\n<option value="1">And Boom</option>\n<option value="2">One More Thing!</option>\n</select></td></tr>') + f = SomeForm({'somechoice': 2}) + self.assertEqual(f.as_table(), u'<tr><th><label for="id_somechoice">Somechoice:</label></th><td><select name="somechoice" id="id_somechoice">\n<option value="">---------</option>\n<option value="1">And Boom</option>\n<option value="2" selected="selected">One More Thing!</option>\n</select></td></tr>') + + # You can also pass 'choices' to the constructor: + w = Select(choices=[(1, 1), (2, 2), (3, 3)]) + self.assertEqual(w.render('num', 2), """<select name="num"> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +</select>""") + + # If 'choices' is passed to both the constructor and render(), then they'll both be in the output: + self.assertEqual(w.render('num', 2, choices=[(4, 4), (5, 5)]), """<select name="num"> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +<option value="4">4</option> +<option value="5">5</option> +</select>""") + + # Choices are escaped correctly + self.assertEqual(w.render('escape', None, choices=(('bad', 'you & me'), ('good', mark_safe('you > me')))), """<select name="escape"> +<option value="1">1</option> +<option value="2">2</option> +<option value="3">3</option> +<option value="bad">you & me</option> +<option value="good">you > me</option> +</select>""") + + # Unicode choices are correctly rendered as HTML + self.assertEqual(w.render('email', 'ŠĐĆŽćžšđ', choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]), u'<select name="email">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" selected="selected">\u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</option>\n<option value="\u0107\u017e\u0161\u0111">abc\u0107\u017e\u0161\u0111</option>\n</select>') + + # If choices is passed to the constructor and is a generator, it can be iterated + # over multiple times without getting consumed: + w = Select(choices=get_choices()) + self.assertEqual(w.render('num', 2), """<select name="num"> +<option value="0">0</option> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +<option value="4">4</option> +</select>""") + self.assertEqual(w.render('num', 3), """<select name="num"> +<option value="0">0</option> +<option value="1">1</option> +<option value="2">2</option> +<option value="3" selected="selected">3</option> +<option value="4">4</option> +</select>""") + + # Choices can be nested one level in order to create HTML optgroups: + w.choices=(('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))) + self.assertEqual(w.render('nestchoice', None), """<select name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select>""") + + self.assertEqual(w.render('nestchoice', 'outer1'), """<select name="nestchoice"> +<option value="outer1" selected="selected">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select>""") + + self.assertEqual(w.render('nestchoice', 'inner1'), """<select name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1" selected="selected">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select>""") + + def test_nullbooleanselect(self): + w = NullBooleanSelect() + self.assertTrue(w.render('is_cool', True), """<select name="is_cool"> +<option value="1">Unknown</option> +<option value="2" selected="selected">Yes</option> +<option value="3">No</option> +</select>""") + self.assertEqual(w.render('is_cool', False), """<select name="is_cool"> +<option value="1">Unknown</option> +<option value="2">Yes</option> +<option value="3" selected="selected">No</option> +</select>""") + self.assertEqual(w.render('is_cool', None), """<select name="is_cool"> +<option value="1" selected="selected">Unknown</option> +<option value="2">Yes</option> +<option value="3">No</option> +</select>""") + self.assertEqual(w.render('is_cool', '2'), """<select name="is_cool"> +<option value="1">Unknown</option> +<option value="2" selected="selected">Yes</option> +<option value="3">No</option> +</select>""") + self.assertEqual(w.render('is_cool', '3'), """<select name="is_cool"> +<option value="1">Unknown</option> +<option value="2">Yes</option> +<option value="3" selected="selected">No</option> +</select>""") + self.assertTrue(w._has_changed(False, None)) + self.assertTrue(w._has_changed(None, False)) + self.assertFalse(w._has_changed(None, None)) + self.assertFalse(w._has_changed(False, False)) + self.assertTrue(w._has_changed(True, False)) + self.assertTrue(w._has_changed(True, None)) + self.assertTrue(w._has_changed(True, False)) + + def test_selectmultiple(self): + w = SelectMultiple() + self.assertEqual(w.render('beatles', ['J'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select multiple="multiple" name="beatles"> +<option value="J" selected="selected">John</option> +<option value="P">Paul</option> +<option value="G">George</option> +<option value="R">Ringo</option> +</select>""") + self.assertEqual(w.render('beatles', ['J', 'P'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select multiple="multiple" name="beatles"> +<option value="J" selected="selected">John</option> +<option value="P" selected="selected">Paul</option> +<option value="G">George</option> +<option value="R">Ringo</option> +</select>""") + self.assertEqual(w.render('beatles', ['J', 'P', 'R'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select multiple="multiple" name="beatles"> +<option value="J" selected="selected">John</option> +<option value="P" selected="selected">Paul</option> +<option value="G">George</option> +<option value="R" selected="selected">Ringo</option> +</select>""") + + # If the value is None, none of the options are selected: + self.assertEqual(w.render('beatles', None, choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select multiple="multiple" name="beatles"> +<option value="J">John</option> +<option value="P">Paul</option> +<option value="G">George</option> +<option value="R">Ringo</option> +</select>""") + + # If the value corresponds to a label (but not to an option value), none of the options are selected: + self.assertEqual(w.render('beatles', ['John'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select multiple="multiple" name="beatles"> +<option value="J">John</option> +<option value="P">Paul</option> +<option value="G">George</option> +<option value="R">Ringo</option> +</select>""") + + # If multiple values are given, but some of them are not valid, the valid ones are selected: + self.assertEqual(w.render('beatles', ['J', 'G', 'foo'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select multiple="multiple" name="beatles"> +<option value="J" selected="selected">John</option> +<option value="P">Paul</option> +<option value="G" selected="selected">George</option> +<option value="R">Ringo</option> +</select>""") + + # The value is compared to its str(): + self.assertEqual(w.render('nums', [2], choices=[('1', '1'), ('2', '2'), ('3', '3')]), """<select multiple="multiple" name="nums"> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +</select>""") + self.assertEqual(w.render('nums', ['2'], choices=[(1, 1), (2, 2), (3, 3)]), """<select multiple="multiple" name="nums"> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +</select>""") + self.assertEqual(w.render('nums', [2], choices=[(1, 1), (2, 2), (3, 3)]), """<select multiple="multiple" name="nums"> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +</select>""") + + # The 'choices' argument can be any iterable: + def get_choices(): + for i in range(5): + yield (i, i) + self.assertEqual(w.render('nums', [2], choices=get_choices()), """<select multiple="multiple" name="nums"> +<option value="0">0</option> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +<option value="4">4</option> +</select>""") + + # You can also pass 'choices' to the constructor: + w = SelectMultiple(choices=[(1, 1), (2, 2), (3, 3)]) + self.assertEqual(w.render('nums', [2]), """<select multiple="multiple" name="nums"> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +</select>""") + + # If 'choices' is passed to both the constructor and render(), then they'll both be in the output: + self.assertEqual(w.render('nums', [2], choices=[(4, 4), (5, 5)]), """<select multiple="multiple" name="nums"> +<option value="1">1</option> +<option value="2" selected="selected">2</option> +<option value="3">3</option> +<option value="4">4</option> +<option value="5">5</option> +</select>""") + + # Choices are escaped correctly + self.assertEqual(w.render('escape', None, choices=(('bad', 'you & me'), ('good', mark_safe('you > me')))), """<select multiple="multiple" name="escape"> +<option value="1">1</option> +<option value="2">2</option> +<option value="3">3</option> +<option value="bad">you & me</option> +<option value="good">you > me</option> +</select>""") + + # Unicode choices are correctly rendered as HTML + self.assertEqual(w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]), u'<select multiple="multiple" name="nums">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" selected="selected">\u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</option>\n<option value="\u0107\u017e\u0161\u0111">abc\u0107\u017e\u0161\u0111</option>\n</select>') + + # Test the usage of _has_changed + self.assertFalse(w._has_changed(None, None)) + self.assertFalse(w._has_changed([], None)) + self.assertTrue(w._has_changed(None, [u'1'])) + self.assertFalse(w._has_changed([1, 2], [u'1', u'2'])) + self.assertTrue(w._has_changed([1, 2], [u'1'])) + self.assertTrue(w._has_changed([1, 2], [u'1', u'3'])) + + # Choices can be nested one level in order to create HTML optgroups: + w.choices = (('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))) + self.assertEqual(w.render('nestchoice', None), """<select multiple="multiple" name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select>""") + + self.assertEqual(w.render('nestchoice', ['outer1']), """<select multiple="multiple" name="nestchoice"> +<option value="outer1" selected="selected">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select>""") + + self.assertEqual(w.render('nestchoice', ['inner1']), """<select multiple="multiple" name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1" selected="selected">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select>""") + + self.assertEqual(w.render('nestchoice', ['outer1', 'inner2']), """<select multiple="multiple" name="nestchoice"> +<option value="outer1" selected="selected">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2" selected="selected">Inner 2</option> +</optgroup> +</select>""") + + def test_radioselect(self): + w = RadioSelect() + self.assertEqual(w.render('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<ul> +<li><label><input checked="checked" type="radio" name="beatle" value="J" /> John</label></li> +<li><label><input type="radio" name="beatle" value="P" /> Paul</label></li> +<li><label><input type="radio" name="beatle" value="G" /> George</label></li> +<li><label><input type="radio" name="beatle" value="R" /> Ringo</label></li> +</ul>""") + + # If the value is None, none of the options are checked: + self.assertEqual(w.render('beatle', None, choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<ul> +<li><label><input type="radio" name="beatle" value="J" /> John</label></li> +<li><label><input type="radio" name="beatle" value="P" /> Paul</label></li> +<li><label><input type="radio" name="beatle" value="G" /> George</label></li> +<li><label><input type="radio" name="beatle" value="R" /> Ringo</label></li> +</ul>""") + + # If the value corresponds to a label (but not to an option value), none of the options are checked: + self.assertEqual(w.render('beatle', 'John', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<ul> +<li><label><input type="radio" name="beatle" value="J" /> John</label></li> +<li><label><input type="radio" name="beatle" value="P" /> Paul</label></li> +<li><label><input type="radio" name="beatle" value="G" /> George</label></li> +<li><label><input type="radio" name="beatle" value="R" /> Ringo</label></li> +</ul>""") + + # The value is compared to its str(): + self.assertEqual(w.render('num', 2, choices=[('1', '1'), ('2', '2'), ('3', '3')]), """<ul> +<li><label><input type="radio" name="num" value="1" /> 1</label></li> +<li><label><input checked="checked" type="radio" name="num" value="2" /> 2</label></li> +<li><label><input type="radio" name="num" value="3" /> 3</label></li> +</ul>""") + self.assertEqual(w.render('num', '2', choices=[(1, 1), (2, 2), (3, 3)]), """<ul> +<li><label><input type="radio" name="num" value="1" /> 1</label></li> +<li><label><input checked="checked" type="radio" name="num" value="2" /> 2</label></li> +<li><label><input type="radio" name="num" value="3" /> 3</label></li> +</ul>""") + self.assertEqual(w.render('num', 2, choices=[(1, 1), (2, 2), (3, 3)]), """<ul> +<li><label><input type="radio" name="num" value="1" /> 1</label></li> +<li><label><input checked="checked" type="radio" name="num" value="2" /> 2</label></li> +<li><label><input type="radio" name="num" value="3" /> 3</label></li> +</ul>""") + + # The 'choices' argument can be any iterable: + def get_choices(): + for i in range(5): + yield (i, i) + self.assertEqual(w.render('num', 2, choices=get_choices()), """<ul> +<li><label><input type="radio" name="num" value="0" /> 0</label></li> +<li><label><input type="radio" name="num" value="1" /> 1</label></li> +<li><label><input checked="checked" type="radio" name="num" value="2" /> 2</label></li> +<li><label><input type="radio" name="num" value="3" /> 3</label></li> +<li><label><input type="radio" name="num" value="4" /> 4</label></li> +</ul>""") + + # You can also pass 'choices' to the constructor: + w = RadioSelect(choices=[(1, 1), (2, 2), (3, 3)]) + self.assertEqual(w.render('num', 2), """<ul> +<li><label><input type="radio" name="num" value="1" /> 1</label></li> +<li><label><input checked="checked" type="radio" name="num" value="2" /> 2</label></li> +<li><label><input type="radio" name="num" value="3" /> 3</label></li> +</ul>""") + + # If 'choices' is passed to both the constructor and render(), then they'll both be in the output: + self.assertEqual(w.render('num', 2, choices=[(4, 4), (5, 5)]), """<ul> +<li><label><input type="radio" name="num" value="1" /> 1</label></li> +<li><label><input checked="checked" type="radio" name="num" value="2" /> 2</label></li> +<li><label><input type="radio" name="num" value="3" /> 3</label></li> +<li><label><input type="radio" name="num" value="4" /> 4</label></li> +<li><label><input type="radio" name="num" value="5" /> 5</label></li> +</ul>""") + + # RadioSelect uses a RadioFieldRenderer to render the individual radio inputs. + # You can manipulate that object directly to customize the way the RadioSelect + # is rendered. + w = RadioSelect() + r = w.get_renderer('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))) + inp_set1 = [] + inp_set2 = [] + inp_set3 = [] + inp_set4 = [] + + for inp in r: + inp_set1.append(str(inp)) + inp_set2.append('%s<br />' % inp) + inp_set3.append('<p>%s %s</p>' % (inp.tag(), inp.choice_label)) + inp_set4.append('%s %s %s %s %s' % (inp.name, inp.value, inp.choice_value, inp.choice_label, inp.is_checked())) + + self.assertEqual('\n'.join(inp_set1), """<label><input checked="checked" type="radio" name="beatle" value="J" /> John</label> +<label><input type="radio" name="beatle" value="P" /> Paul</label> +<label><input type="radio" name="beatle" value="G" /> George</label> +<label><input type="radio" name="beatle" value="R" /> Ringo</label>""") + self.assertEqual('\n'.join(inp_set2), """<label><input checked="checked" type="radio" name="beatle" value="J" /> John</label><br /> +<label><input type="radio" name="beatle" value="P" /> Paul</label><br /> +<label><input type="radio" name="beatle" value="G" /> George</label><br /> +<label><input type="radio" name="beatle" value="R" /> Ringo</label><br />""") + self.assertEqual('\n'.join(inp_set3), """<p><input checked="checked" type="radio" name="beatle" value="J" /> John</p> +<p><input type="radio" name="beatle" value="P" /> Paul</p> +<p><input type="radio" name="beatle" value="G" /> George</p> +<p><input type="radio" name="beatle" value="R" /> Ringo</p>""") + self.assertEqual('\n'.join(inp_set4), """beatle J J John True +beatle J P Paul False +beatle J G George False +beatle J R Ringo False""") + + # You can create your own custom renderers for RadioSelect to use. + class MyRenderer(RadioFieldRenderer): + def render(self): + return u'<br />\n'.join([unicode(choice) for choice in self]) + w = RadioSelect(renderer=MyRenderer) + self.assertEqual(w.render('beatle', 'G', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<label><input type="radio" name="beatle" value="J" /> John</label><br /> +<label><input type="radio" name="beatle" value="P" /> Paul</label><br /> +<label><input checked="checked" type="radio" name="beatle" value="G" /> George</label><br /> +<label><input type="radio" name="beatle" value="R" /> Ringo</label>""") + + # Or you can use custom RadioSelect fields that use your custom renderer. + class CustomRadioSelect(RadioSelect): + renderer = MyRenderer + w = CustomRadioSelect() + self.assertEqual(w.render('beatle', 'G', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<label><input type="radio" name="beatle" value="J" /> John</label><br /> +<label><input type="radio" name="beatle" value="P" /> Paul</label><br /> +<label><input checked="checked" type="radio" name="beatle" value="G" /> George</label><br /> +<label><input type="radio" name="beatle" value="R" /> Ringo</label>""") + + # A RadioFieldRenderer object also allows index access to individual RadioInput + w = RadioSelect() + r = w.get_renderer('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))) + self.assertEqual(str(r[1]), '<label><input type="radio" name="beatle" value="P" /> Paul</label>') + self.assertEqual(str(r[0]), '<label><input checked="checked" type="radio" name="beatle" value="J" /> John</label>') + self.assertTrue(r[0].is_checked()) + self.assertFalse(r[1].is_checked()) + self.assertEqual((r[1].name, r[1].value, r[1].choice_value, r[1].choice_label), ('beatle', u'J', u'P', u'Paul')) + + try: + r[10] + self.fail("This offset should not exist.") + except IndexError: + pass + + # Choices are escaped correctly + w = RadioSelect() + self.assertEqual(w.render('escape', None, choices=(('bad', 'you & me'), ('good', mark_safe('you > me')))), """<ul> +<li><label><input type="radio" name="escape" value="bad" /> you & me</label></li> +<li><label><input type="radio" name="escape" value="good" /> you > me</label></li> +</ul>""") + + # Unicode choices are correctly rendered as HTML + w = RadioSelect() + self.assertEqual(unicode(w.render('email', 'ŠĐĆŽćžšđ', choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')])), u'<ul>\n<li><label><input checked="checked" type="radio" name="email" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /> \u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</label></li>\n<li><label><input type="radio" name="email" value="\u0107\u017e\u0161\u0111" /> abc\u0107\u017e\u0161\u0111</label></li>\n</ul>') + + # Attributes provided at instantiation are passed to the constituent inputs + w = RadioSelect(attrs={'id':'foo'}) + self.assertEqual(w.render('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<ul> +<li><label for="foo_0"><input checked="checked" type="radio" id="foo_0" value="J" name="beatle" /> John</label></li> +<li><label for="foo_1"><input type="radio" id="foo_1" value="P" name="beatle" /> Paul</label></li> +<li><label for="foo_2"><input type="radio" id="foo_2" value="G" name="beatle" /> George</label></li> +<li><label for="foo_3"><input type="radio" id="foo_3" value="R" name="beatle" /> Ringo</label></li> +</ul>""") + + # Attributes provided at render-time are passed to the constituent inputs + w = RadioSelect() + self.assertEqual(w.render('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')), attrs={'id':'bar'}), """<ul> +<li><label for="bar_0"><input checked="checked" type="radio" id="bar_0" value="J" name="beatle" /> John</label></li> +<li><label for="bar_1"><input type="radio" id="bar_1" value="P" name="beatle" /> Paul</label></li> +<li><label for="bar_2"><input type="radio" id="bar_2" value="G" name="beatle" /> George</label></li> +<li><label for="bar_3"><input type="radio" id="bar_3" value="R" name="beatle" /> Ringo</label></li> +</ul>""") + + def test_checkboxselectmultiple(self): + w = CheckboxSelectMultiple() + self.assertEqual(w.render('beatles', ['J'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<ul> +<li><label><input checked="checked" type="checkbox" name="beatles" value="J" /> John</label></li> +<li><label><input type="checkbox" name="beatles" value="P" /> Paul</label></li> +<li><label><input type="checkbox" name="beatles" value="G" /> George</label></li> +<li><label><input type="checkbox" name="beatles" value="R" /> Ringo</label></li> +</ul>""") + self.assertEqual(w.render('beatles', ['J', 'P'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<ul> +<li><label><input checked="checked" type="checkbox" name="beatles" value="J" /> John</label></li> +<li><label><input checked="checked" type="checkbox" name="beatles" value="P" /> Paul</label></li> +<li><label><input type="checkbox" name="beatles" value="G" /> George</label></li> +<li><label><input type="checkbox" name="beatles" value="R" /> Ringo</label></li> +</ul>""") + self.assertEqual(w.render('beatles', ['J', 'P', 'R'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<ul> +<li><label><input checked="checked" type="checkbox" name="beatles" value="J" /> John</label></li> +<li><label><input checked="checked" type="checkbox" name="beatles" value="P" /> Paul</label></li> +<li><label><input type="checkbox" name="beatles" value="G" /> George</label></li> +<li><label><input checked="checked" type="checkbox" name="beatles" value="R" /> Ringo</label></li> +</ul>""") + + # If the value is None, none of the options are selected: + self.assertEqual(w.render('beatles', None, choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<ul> +<li><label><input type="checkbox" name="beatles" value="J" /> John</label></li> +<li><label><input type="checkbox" name="beatles" value="P" /> Paul</label></li> +<li><label><input type="checkbox" name="beatles" value="G" /> George</label></li> +<li><label><input type="checkbox" name="beatles" value="R" /> Ringo</label></li> +</ul>""") + + # If the value corresponds to a label (but not to an option value), none of the options are selected: + self.assertEqual(w.render('beatles', ['John'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<ul> +<li><label><input type="checkbox" name="beatles" value="J" /> John</label></li> +<li><label><input type="checkbox" name="beatles" value="P" /> Paul</label></li> +<li><label><input type="checkbox" name="beatles" value="G" /> George</label></li> +<li><label><input type="checkbox" name="beatles" value="R" /> Ringo</label></li> +</ul>""") + + # If multiple values are given, but some of them are not valid, the valid ones are selected: + self.assertEqual(w.render('beatles', ['J', 'G', 'foo'], choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<ul> +<li><label><input checked="checked" type="checkbox" name="beatles" value="J" /> John</label></li> +<li><label><input type="checkbox" name="beatles" value="P" /> Paul</label></li> +<li><label><input checked="checked" type="checkbox" name="beatles" value="G" /> George</label></li> +<li><label><input type="checkbox" name="beatles" value="R" /> Ringo</label></li> +</ul>""") + + # The value is compared to its str(): + self.assertEqual(w.render('nums', [2], choices=[('1', '1'), ('2', '2'), ('3', '3')]), """<ul> +<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li> +<li><label><input checked="checked" type="checkbox" name="nums" value="2" /> 2</label></li> +<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li> +</ul>""") + self.assertEqual(w.render('nums', ['2'], choices=[(1, 1), (2, 2), (3, 3)]), """<ul> +<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li> +<li><label><input checked="checked" type="checkbox" name="nums" value="2" /> 2</label></li> +<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li> +</ul>""") + self.assertEqual(w.render('nums', [2], choices=[(1, 1), (2, 2), (3, 3)]), """<ul> +<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li> +<li><label><input checked="checked" type="checkbox" name="nums" value="2" /> 2</label></li> +<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li> +</ul>""") + + # The 'choices' argument can be any iterable: + def get_choices(): + for i in range(5): + yield (i, i) + self.assertEqual(w.render('nums', [2], choices=get_choices()), """<ul> +<li><label><input type="checkbox" name="nums" value="0" /> 0</label></li> +<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li> +<li><label><input checked="checked" type="checkbox" name="nums" value="2" /> 2</label></li> +<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li> +<li><label><input type="checkbox" name="nums" value="4" /> 4</label></li> +</ul>""") + + # You can also pass 'choices' to the constructor: + w = CheckboxSelectMultiple(choices=[(1, 1), (2, 2), (3, 3)]) + self.assertEqual(w.render('nums', [2]), """<ul> +<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li> +<li><label><input checked="checked" type="checkbox" name="nums" value="2" /> 2</label></li> +<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li> +</ul>""") + + # If 'choices' is passed to both the constructor and render(), then they'll both be in the output: + self.assertEqual(w.render('nums', [2], choices=[(4, 4), (5, 5)]), """<ul> +<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li> +<li><label><input checked="checked" type="checkbox" name="nums" value="2" /> 2</label></li> +<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li> +<li><label><input type="checkbox" name="nums" value="4" /> 4</label></li> +<li><label><input type="checkbox" name="nums" value="5" /> 5</label></li> +</ul>""") + + # Choices are escaped correctly + self.assertEqual(w.render('escape', None, choices=(('bad', 'you & me'), ('good', mark_safe('you > me')))), """<ul> +<li><label><input type="checkbox" name="escape" value="1" /> 1</label></li> +<li><label><input type="checkbox" name="escape" value="2" /> 2</label></li> +<li><label><input type="checkbox" name="escape" value="3" /> 3</label></li> +<li><label><input type="checkbox" name="escape" value="bad" /> you & me</label></li> +<li><label><input type="checkbox" name="escape" value="good" /> you > me</label></li> +</ul>""") + + # Test the usage of _has_changed + self.assertFalse(w._has_changed(None, None)) + self.assertFalse(w._has_changed([], None)) + self.assertTrue(w._has_changed(None, [u'1'])) + self.assertFalse(w._has_changed([1, 2], [u'1', u'2'])) + self.assertTrue(w._has_changed([1, 2], [u'1'])) + self.assertTrue(w._has_changed([1, 2], [u'1', u'3'])) + self.assertFalse(w._has_changed([2, 1], [u'1', u'2'])) + + # Unicode choices are correctly rendered as HTML + self.assertEqual(w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]), u'<ul>\n<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li>\n<li><label><input type="checkbox" name="nums" value="2" /> 2</label></li>\n<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li>\n<li><label><input checked="checked" type="checkbox" name="nums" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /> \u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</label></li>\n<li><label><input type="checkbox" name="nums" value="\u0107\u017e\u0161\u0111" /> abc\u0107\u017e\u0161\u0111</label></li>\n</ul>') + + # Each input gets a separate ID + self.assertEqual(CheckboxSelectMultiple().render('letters', list('ac'), choices=zip(list('abc'), list('ABC')), attrs={'id': 'abc'}), """<ul> +<li><label for="abc_0"><input checked="checked" type="checkbox" name="letters" value="a" id="abc_0" /> A</label></li> +<li><label for="abc_1"><input type="checkbox" name="letters" value="b" id="abc_1" /> B</label></li> +<li><label for="abc_2"><input checked="checked" type="checkbox" name="letters" value="c" id="abc_2" /> C</label></li> +</ul>""") + + def test_multi(self): + class MyMultiWidget(MultiWidget): + def decompress(self, value): + if value: + return value.split('__') + return ['', ''] + def format_output(self, rendered_widgets): + return u'<br />'.join(rendered_widgets) + + w = MyMultiWidget(widgets=(TextInput(attrs={'class': 'big'}), TextInput(attrs={'class': 'small'}))) + self.assertEqual(w.render('name', ['john', 'lennon']), u'<input type="text" class="big" value="john" name="name_0" /><br /><input type="text" class="small" value="lennon" name="name_1" />') + self.assertEqual(w.render('name', 'john__lennon'), u'<input type="text" class="big" value="john" name="name_0" /><br /><input type="text" class="small" value="lennon" name="name_1" />') + self.assertEqual(w.render('name', 'john__lennon', attrs={'id':'foo'}), u'<input id="foo_0" type="text" class="big" value="john" name="name_0" /><br /><input id="foo_1" type="text" class="small" value="lennon" name="name_1" />') + w = MyMultiWidget(widgets=(TextInput(attrs={'class': 'big'}), TextInput(attrs={'class': 'small'})), attrs={'id': 'bar'}) + self.assertEqual(w.render('name', ['john', 'lennon']), u'<input id="bar_0" type="text" class="big" value="john" name="name_0" /><br /><input id="bar_1" type="text" class="small" value="lennon" name="name_1" />') + + w = MyMultiWidget(widgets=(TextInput(), TextInput())) + + # test with no initial data + self.assertTrue(w._has_changed(None, [u'john', u'lennon'])) + + # test when the data is the same as initial + self.assertFalse(w._has_changed(u'john__lennon', [u'john', u'lennon'])) + + # test when the first widget's data has changed + self.assertTrue(w._has_changed(u'john__lennon', [u'alfred', u'lennon'])) + + # test when the last widget's data has changed. this ensures that it is not + # short circuiting while testing the widgets. + self.assertTrue(w._has_changed(u'john__lennon', [u'john', u'denver'])) + + def test_splitdatetime(self): + w = SplitDateTimeWidget() + self.assertEqual(w.render('date', ''), u'<input type="text" name="date_0" /><input type="text" name="date_1" />') + self.assertEqual(w.render('date', None), u'<input type="text" name="date_0" /><input type="text" name="date_1" />') + self.assertEqual(w.render('date', datetime.datetime(2006, 1, 10, 7, 30)), u'<input type="text" name="date_0" value="2006-01-10" /><input type="text" name="date_1" value="07:30:00" />') + self.assertEqual(w.render('date', [datetime.date(2006, 1, 10), datetime.time(7, 30)]), u'<input type="text" name="date_0" value="2006-01-10" /><input type="text" name="date_1" value="07:30:00" />') + + # You can also pass 'attrs' to the constructor. In this case, the attrs will be + w = SplitDateTimeWidget(attrs={'class': 'pretty'}) + self.assertEqual(w.render('date', datetime.datetime(2006, 1, 10, 7, 30)), u'<input type="text" class="pretty" value="2006-01-10" name="date_0" /><input type="text" class="pretty" value="07:30:00" name="date_1" />') + + # Use 'date_format' and 'time_format' to change the way a value is displayed. + w = SplitDateTimeWidget(date_format='%d/%m/%Y', time_format='%H:%M') + self.assertEqual(w.render('date', datetime.datetime(2006, 1, 10, 7, 30)), u'<input type="text" name="date_0" value="10/01/2006" /><input type="text" name="date_1" value="07:30" />') + + self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), [u'2008-05-06', u'12:40:00'])) + self.assertFalse(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), [u'06/05/2008', u'12:40'])) + self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), [u'06/05/2008', u'12:41'])) + + def test_datetimeinput(self): + w = DateTimeInput() + self.assertEqual(w.render('date', None), u'<input type="text" name="date" />') + d = datetime.datetime(2007, 9, 17, 12, 51, 34, 482548) + self.assertEqual(str(d), '2007-09-17 12:51:34.482548') + + # The microseconds are trimmed on display, by default. + self.assertEqual(w.render('date', d), u'<input type="text" name="date" value="2007-09-17 12:51:34" />') + self.assertEqual(w.render('date', datetime.datetime(2007, 9, 17, 12, 51, 34)), u'<input type="text" name="date" value="2007-09-17 12:51:34" />') + self.assertEqual(w.render('date', datetime.datetime(2007, 9, 17, 12, 51)), u'<input type="text" name="date" value="2007-09-17 12:51:00" />') + + # Use 'format' to change the way a value is displayed. + w = DateTimeInput(format='%d/%m/%Y %H:%M') + self.assertEqual(w.render('date', d), u'<input type="text" name="date" value="17/09/2007 12:51" />') + self.assertFalse(w._has_changed(d, '17/09/2007 12:51')) + + # Make sure a custom format works with _has_changed. The hidden input will use + data = datetime.datetime(2010, 3, 6, 12, 0, 0) + custom_format = '%d.%m.%Y %H:%M' + w = DateTimeInput(format=custom_format) + self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format))) + + def test_dateinput(self): + w = DateInput() + self.assertEqual(w.render('date', None), u'<input type="text" name="date" />') + d = datetime.date(2007, 9, 17) + self.assertEqual(str(d), '2007-09-17') + + self.assertEqual(w.render('date', d), u'<input type="text" name="date" value="2007-09-17" />') + self.assertEqual(w.render('date', datetime.date(2007, 9, 17)), u'<input type="text" name="date" value="2007-09-17" />') + + # We should be able to initialize from a unicode value. + self.assertEqual(w.render('date', u'2007-09-17'), u'<input type="text" name="date" value="2007-09-17" />') + + # Use 'format' to change the way a value is displayed. + w = DateInput(format='%d/%m/%Y') + self.assertEqual(w.render('date', d), u'<input type="text" name="date" value="17/09/2007" />') + self.assertFalse(w._has_changed(d, '17/09/2007')) + + # Make sure a custom format works with _has_changed. The hidden input will use + data = datetime.date(2010, 3, 6) + custom_format = '%d.%m.%Y' + w = DateInput(format=custom_format) + self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format))) + + def test_timeinput(self): + w = TimeInput() + self.assertEqual(w.render('time', None), u'<input type="text" name="time" />') + t = datetime.time(12, 51, 34, 482548) + self.assertEqual(str(t), '12:51:34.482548') + + # The microseconds are trimmed on display, by default. + self.assertEqual(w.render('time', t), u'<input type="text" name="time" value="12:51:34" />') + self.assertEqual(w.render('time', datetime.time(12, 51, 34)), u'<input type="text" name="time" value="12:51:34" />') + self.assertEqual(w.render('time', datetime.time(12, 51)), u'<input type="text" name="time" value="12:51:00" />') + + # We should be able to initialize from a unicode value. + self.assertEqual(w.render('time', u'13:12:11'), u'<input type="text" name="time" value="13:12:11" />') + + # Use 'format' to change the way a value is displayed. + w = TimeInput(format='%H:%M') + self.assertEqual(w.render('time', t), u'<input type="text" name="time" value="12:51" />') + self.assertFalse(w._has_changed(t, '12:51')) + + # Make sure a custom format works with _has_changed. The hidden input will use + data = datetime.time(13, 0) + custom_format = '%I:%M %p' + w = TimeInput(format=custom_format) + self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format))) + + def test_splithiddendatetime(self): + from django.forms.widgets import SplitHiddenDateTimeWidget + + w = SplitHiddenDateTimeWidget() + self.assertEqual(w.render('date', ''), u'<input type="hidden" name="date_0" /><input type="hidden" name="date_1" />') + d = datetime.datetime(2007, 9, 17, 12, 51, 34, 482548) + self.assertEqual(str(d), '2007-09-17 12:51:34.482548') + self.assertEqual(w.render('date', d), u'<input type="hidden" name="date_0" value="2007-09-17" /><input type="hidden" name="date_1" value="12:51:34" />') + self.assertEqual(w.render('date', datetime.datetime(2007, 9, 17, 12, 51, 34)), u'<input type="hidden" name="date_0" value="2007-09-17" /><input type="hidden" name="date_1" value="12:51:34" />') + self.assertEqual(w.render('date', datetime.datetime(2007, 9, 17, 12, 51)), u'<input type="hidden" name="date_0" value="2007-09-17" /><input type="hidden" name="date_1" value="12:51:00" />') + + +class FormsI18NWidgetsTestCase(TestCase): + def setUp(self): + super(FormsI18NWidgetsTestCase, self).setUp() + self.old_use_l10n = getattr(settings, 'USE_L10N', False) + settings.USE_L10N = True + activate('de-at') + + def tearDown(self): + deactivate() + settings.USE_L10N = self.old_use_l10n + super(FormsI18NWidgetsTestCase, self).tearDown() + + def test_splitdatetime(self): + w = SplitDateTimeWidget(date_format='%d/%m/%Y', time_format='%H:%M') + self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), [u'06.05.2008', u'12:41'])) + + def test_datetimeinput(self): + w = DateTimeInput() + d = datetime.datetime(2007, 9, 17, 12, 51, 34, 482548) + w.is_localized = True + self.assertEqual(w.render('date', d), u'<input type="text" name="date" value="17.09.2007 12:51:34" />') + + def test_dateinput(self): + w = DateInput() + d = datetime.date(2007, 9, 17) + w.is_localized = True + self.assertEqual(w.render('date', d), u'<input type="text" name="date" value="17.09.2007" />') + + def test_timeinput(self): + w = TimeInput() + t = datetime.time(12, 51, 34, 482548) + w.is_localized = True + self.assertEqual(w.render('time', t), u'<input type="text" name="time" value="12:51:34" />') + + def test_splithiddendatetime(self): + from django.forms.widgets import SplitHiddenDateTimeWidget + + w = SplitHiddenDateTimeWidget() + w.is_localized = True + self.assertEqual(w.render('date', datetime.datetime(2007, 9, 17, 12, 51)), u'<input type="hidden" name="date_0" value="17.09.2007" /><input type="hidden" name="date_1" value="12:51:00" />') + + +class SelectAndTextWidget(MultiWidget): + """ + MultiWidget subclass + """ + def __init__(self, choices=[]): + widgets = [ + RadioSelect(choices=choices), + 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 + """ + self.widgets[0].choices = choices + def _get_choices(self): + """ + The choices for this widget are the Select widget's choices + """ + return self.widgets[0].choices + choices = property(_get_choices, _set_choices) + + +class WidgetTests(TestCase): + def test_12048(self): + # See ticket #12048. + w1 = SelectAndTextWidget(choices=[1,2,3]) + w2 = copy.deepcopy(w1) + w2.choices = [4,5,6] + # 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(Form): + field = DateTimeField(widget=SplitDateTimeWidget, required=False) + + form = SplitDateForm({'field': ''}) + self.assertTrue(form.is_valid()) + form = SplitDateForm({'field': ['', '']}) + self.assertTrue(form.is_valid()) + + class SplitDateRequiredForm(Form): + field = DateTimeField(widget=SplitDateTimeWidget, required=True) + + form = SplitDateRequiredForm({'field': ''}) + self.assertFalse(form.is_valid()) + form = SplitDateRequiredForm({'field': ['', '']}) + self.assertFalse(form.is_valid()) + diff --git a/parts/django/tests/regressiontests/formwizard/__init__.py b/parts/django/tests/regressiontests/formwizard/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/formwizard/__init__.py diff --git a/parts/django/tests/regressiontests/formwizard/forms.py b/parts/django/tests/regressiontests/formwizard/forms.py new file mode 100644 index 0000000..f458eda --- /dev/null +++ b/parts/django/tests/regressiontests/formwizard/forms.py @@ -0,0 +1,18 @@ +from django import forms +from django.contrib.formtools.wizard import FormWizard +from django.http import HttpResponse + +class Page1(forms.Form): + name = forms.CharField(max_length=100) + thirsty = forms.NullBooleanField() + +class Page2(forms.Form): + address1 = forms.CharField(max_length=100) + address2 = forms.CharField(max_length=100) + +class Page3(forms.Form): + random_crap = forms.CharField(max_length=100) + +class ContactWizard(FormWizard): + def done(self, request, form_list): + return HttpResponse("") diff --git a/parts/django/tests/regressiontests/formwizard/models.py b/parts/django/tests/regressiontests/formwizard/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/formwizard/models.py diff --git a/parts/django/tests/regressiontests/formwizard/templates/forms/wizard.html b/parts/django/tests/regressiontests/formwizard/templates/forms/wizard.html new file mode 100644 index 0000000..a31378f --- /dev/null +++ b/parts/django/tests/regressiontests/formwizard/templates/forms/wizard.html @@ -0,0 +1,13 @@ +<html>
+ <body>
+ <p>Step {{ step }} of {{ step_count }}</p>
+ <form action="." method="post">
+ <table>
+ {{ form }}
+ </table>
+ <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
+ {{ previous_fields|safe }}
+ <input type="submit">
+ </form>
+ </body>
+</html>
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/formwizard/tests.py b/parts/django/tests/regressiontests/formwizard/tests.py new file mode 100644 index 0000000..0c94d2e --- /dev/null +++ b/parts/django/tests/regressiontests/formwizard/tests.py @@ -0,0 +1,59 @@ +import re +from django import forms +from django.test import TestCase + +class FormWizardWithNullBooleanField(TestCase): + urls = 'regressiontests.formwizard.urls' + + input_re = re.compile('name="([^"]+)" value="([^"]+)"') + + wizard_url = '/wiz/' + wizard_step_data = ( + { + '0-name': 'Pony', + '0-thirsty': '2', + }, + { + '1-address1': '123 Main St', + '1-address2': 'Djangoland', + }, + { + '2-random_crap': 'blah blah', + } + ) + + def grabFieldData(self, response): + """ + Pull the appropriate field data from the context to pass to the next wizard step + """ + previous_fields = response.context['previous_fields'] + fields = {'wizard_step': response.context['step0']} + + def grab(m): + fields[m.group(1)] = m.group(2) + return '' + + self.input_re.sub(grab, previous_fields) + return fields + + def checkWizardStep(self, response, step_no): + """ + Helper function to test each step of the wizard + - Make sure the call succeeded + - Make sure response is the proper step number + - return the result from the post for the next step + """ + step_count = len(self.wizard_step_data) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Step %d of %d' % (step_no, step_count)) + + data = self.grabFieldData(response) + data.update(self.wizard_step_data[step_no - 1]) + + return self.client.post(self.wizard_url, data) + + def testWizard(self): + response = self.client.get(self.wizard_url) + for step_no in range(1, len(self.wizard_step_data) + 1): + response = self.checkWizardStep(response, step_no) diff --git a/parts/django/tests/regressiontests/formwizard/urls.py b/parts/django/tests/regressiontests/formwizard/urls.py new file mode 100644 index 0000000..d964bc6 --- /dev/null +++ b/parts/django/tests/regressiontests/formwizard/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls.defaults import * +from forms import ContactWizard, Page1, Page2, Page3 + +urlpatterns = patterns('', + url(r'^wiz/$', ContactWizard([Page1, Page2, Page3])), + ) diff --git a/parts/django/tests/regressiontests/generic_inline_admin/__init__.py b/parts/django/tests/regressiontests/generic_inline_admin/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/generic_inline_admin/__init__.py diff --git a/parts/django/tests/regressiontests/generic_inline_admin/fixtures/users.xml b/parts/django/tests/regressiontests/generic_inline_admin/fixtures/users.xml new file mode 100644 index 0000000..6cf441f --- /dev/null +++ b/parts/django/tests/regressiontests/generic_inline_admin/fixtures/users.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="100" model="auth.user"> + <field type="CharField" name="username">super</field> + <field type="CharField" name="first_name">Super</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">super@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">True</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> +</django-objects>
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/generic_inline_admin/models.py b/parts/django/tests/regressiontests/generic_inline_admin/models.py new file mode 100644 index 0000000..329c487 --- /dev/null +++ b/parts/django/tests/regressiontests/generic_inline_admin/models.py @@ -0,0 +1,108 @@ +from django.db import models +from django.contrib import admin +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType + +class Episode(models.Model): + name = models.CharField(max_length=100) + +class Media(models.Model): + """ + Media that can associated to any object. + """ + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey() + url = models.URLField(verify_exists=False) + + def __unicode__(self): + return self.url + +class MediaInline(generic.GenericTabularInline): + model = Media + +class EpisodeAdmin(admin.ModelAdmin): + inlines = [ + MediaInline, + ] +admin.site.register(Episode, EpisodeAdmin) + +# +# These models let us test the different GenericInline settings at +# different urls in the admin site. +# + +# +# Generic inline with extra = 0 +# + +class EpisodeExtra(Episode): + pass + +class MediaExtraInline(generic.GenericTabularInline): + model = Media + extra = 0 + +admin.site.register(EpisodeExtra, inlines=[MediaExtraInline]) + +# +# Generic inline with extra and max_num +# + +class EpisodeMaxNum(Episode): + pass + +class MediaMaxNumInline(generic.GenericTabularInline): + model = Media + extra = 5 + max_num = 2 + +admin.site.register(EpisodeMaxNum, inlines=[MediaMaxNumInline]) + +# +# Generic inline with exclude +# + +class EpisodeExclude(Episode): + pass + +class MediaExcludeInline(generic.GenericTabularInline): + model = Media + exclude = ['url'] + +admin.site.register(EpisodeExclude, inlines=[MediaExcludeInline]) + +# +# Generic inline with unique_together +# + +class PhoneNumber(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + phone_number = models.CharField(max_length=30) + + class Meta: + unique_together = (('content_type', 'object_id', 'phone_number',),) + +class Contact(models.Model): + name = models.CharField(max_length=50) + phone_numbers = generic.GenericRelation(PhoneNumber) + +class PhoneNumberInline(generic.GenericTabularInline): + model = PhoneNumber + +admin.site.register(Contact, inlines=[PhoneNumberInline]) + +# +# Generic inline with can_delete=False +# + +class EpisodePermanent(Episode): + pass + +class MediaPermanentInline(generic.GenericTabularInline): + model = Media + can_delete = False + +admin.site.register(EpisodePermanent, inlines=[MediaPermanentInline]) diff --git a/parts/django/tests/regressiontests/generic_inline_admin/tests.py b/parts/django/tests/regressiontests/generic_inline_admin/tests.py new file mode 100644 index 0000000..d5531f0 --- /dev/null +++ b/parts/django/tests/regressiontests/generic_inline_admin/tests.py @@ -0,0 +1,214 @@ +# coding: utf-8 + +from django.conf import settings +from django.contrib.contenttypes.generic import generic_inlineformset_factory +from django.test import TestCase + +# local test models +from models import Episode, EpisodeExtra, EpisodeMaxNum, EpisodeExclude, \ + Media, EpisodePermanent, MediaPermanentInline + + +class GenericAdminViewTest(TestCase): + fixtures = ['users.xml'] + + def setUp(self): + # set TEMPLATE_DEBUG to True to ensure {% include %} will raise + # exceptions since that is how inlines are rendered and #9498 will + # bubble up if it is an issue. + self.original_template_debug = settings.TEMPLATE_DEBUG + settings.TEMPLATE_DEBUG = True + self.client.login(username='super', password='secret') + + # Can't load content via a fixture (since the GenericForeignKey + # relies on content type IDs, which will vary depending on what + # other tests have been run), thus we do it here. + e = Episode.objects.create(name='This Week in Django') + self.episode_pk = e.pk + m = Media(content_object=e, url='http://example.com/podcast.mp3') + m.save() + self.mp3_media_pk = m.pk + + m = Media(content_object=e, url='http://example.com/logo.png') + m.save() + self.png_media_pk = m.pk + + def tearDown(self): + self.client.logout() + settings.TEMPLATE_DEBUG = self.original_template_debug + + def testBasicAddGet(self): + """ + A smoke test to ensure GET on the add_view works. + """ + response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/episode/add/') + self.assertEqual(response.status_code, 200) + + def testBasicEditGet(self): + """ + A smoke test to ensure GET on the change_view works. + """ + response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/episode/%d/' % self.episode_pk) + self.assertEqual(response.status_code, 200) + + def testBasicAddPost(self): + """ + A smoke test to ensure POST on add_view works. + """ + post_data = { + "name": u"This Week in Django", + # inline data + "generic_inline_admin-media-content_type-object_id-TOTAL_FORMS": u"1", + "generic_inline_admin-media-content_type-object_id-INITIAL_FORMS": u"0", + "generic_inline_admin-media-content_type-object_id-MAX_NUM_FORMS": u"0", + } + response = self.client.post('/generic_inline_admin/admin/generic_inline_admin/episode/add/', post_data) + self.assertEqual(response.status_code, 302) # redirect somewhere + + def testBasicEditPost(self): + """ + A smoke test to ensure POST on edit_view works. + """ + post_data = { + "name": u"This Week in Django", + # inline data + "generic_inline_admin-media-content_type-object_id-TOTAL_FORMS": u"3", + "generic_inline_admin-media-content_type-object_id-INITIAL_FORMS": u"2", + "generic_inline_admin-media-content_type-object_id-MAX_NUM_FORMS": u"0", + "generic_inline_admin-media-content_type-object_id-0-id": u"%d" % self.mp3_media_pk, + "generic_inline_admin-media-content_type-object_id-0-url": u"http://example.com/podcast.mp3", + "generic_inline_admin-media-content_type-object_id-1-id": u"%d" % self.png_media_pk, + "generic_inline_admin-media-content_type-object_id-1-url": u"http://example.com/logo.png", + "generic_inline_admin-media-content_type-object_id-2-id": u"", + "generic_inline_admin-media-content_type-object_id-2-url": u"", + } + url = '/generic_inline_admin/admin/generic_inline_admin/episode/%d/' % self.episode_pk + response = self.client.post(url, post_data) + self.assertEqual(response.status_code, 302) # redirect somewhere + + def testGenericInlineFormset(self): + EpisodeMediaFormSet = generic_inlineformset_factory(Media, can_delete=False, extra=3) + e = Episode.objects.get(name='This Week in Django') + + # Works with no queryset + formset = EpisodeMediaFormSet(instance=e) + self.assertEquals(len(formset.forms), 5) + self.assertEquals(formset.forms[0].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-0-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-0-url" type="text" name="generic_inline_admin-media-content_type-object_id-0-url" value="http://example.com/podcast.mp3" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-0-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-0-id" /></p>' % self.mp3_media_pk) + self.assertEquals(formset.forms[1].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-1-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-1-url" type="text" name="generic_inline_admin-media-content_type-object_id-1-url" value="http://example.com/logo.png" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-1-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-1-id" /></p>' % self.png_media_pk) + self.assertEquals(formset.forms[2].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-2-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-2-url" type="text" name="generic_inline_admin-media-content_type-object_id-2-url" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-2-id" id="id_generic_inline_admin-media-content_type-object_id-2-id" /></p>') + + # A queryset can be used to alter display ordering + formset = EpisodeMediaFormSet(instance=e, queryset=Media.objects.order_by('url')) + self.assertEquals(len(formset.forms), 5) + self.assertEquals(formset.forms[0].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-0-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-0-url" type="text" name="generic_inline_admin-media-content_type-object_id-0-url" value="http://example.com/logo.png" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-0-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-0-id" /></p>' % self.png_media_pk) + self.assertEquals(formset.forms[1].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-1-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-1-url" type="text" name="generic_inline_admin-media-content_type-object_id-1-url" value="http://example.com/podcast.mp3" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-1-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-1-id" /></p>' % self.mp3_media_pk) + self.assertEquals(formset.forms[2].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-2-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-2-url" type="text" name="generic_inline_admin-media-content_type-object_id-2-url" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-2-id" id="id_generic_inline_admin-media-content_type-object_id-2-id" /></p>') + + + # Works with a queryset that omits items + formset = EpisodeMediaFormSet(instance=e, queryset=Media.objects.filter(url__endswith=".png")) + self.assertEquals(len(formset.forms), 4) + self.assertEquals(formset.forms[0].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-0-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-0-url" type="text" name="generic_inline_admin-media-content_type-object_id-0-url" value="http://example.com/logo.png" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-0-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-0-id" /></p>' % self.png_media_pk) + self.assertEquals(formset.forms[1].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-1-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-1-url" type="text" name="generic_inline_admin-media-content_type-object_id-1-url" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-1-id" id="id_generic_inline_admin-media-content_type-object_id-1-id" /></p>') + + def testGenericInlineFormsetFactory(self): + # Regression test for #10522. + inline_formset = generic_inlineformset_factory(Media, + exclude=('url',)) + + # Regression test for #12340. + e = Episode.objects.get(name='This Week in Django') + formset = inline_formset(instance=e) + self.assertTrue(formset.get_queryset().ordered) + +class GenericInlineAdminParametersTest(TestCase): + fixtures = ['users.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def _create_object(self, model): + """ + Create a model with an attached Media object via GFK. We can't + load content via a fixture (since the GenericForeignKey relies on + content type IDs, which will vary depending on what other tests + have been run), thus we do it here. + """ + e = model.objects.create(name='This Week in Django') + Media.objects.create(content_object=e, url='http://example.com/podcast.mp3') + return e + + def testNoParam(self): + """ + With one initial form, extra (default) at 3, there should be 4 forms. + """ + e = self._create_object(Episode) + response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/episode/%s/' % e.pk) + formset = response.context['inline_admin_formsets'][0].formset + self.assertEqual(formset.total_form_count(), 4) + self.assertEqual(formset.initial_form_count(), 1) + + def testExtraParam(self): + """ + With extra=0, there should be one form. + """ + e = self._create_object(EpisodeExtra) + response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/episodeextra/%s/' % e.pk) + formset = response.context['inline_admin_formsets'][0].formset + self.assertEqual(formset.total_form_count(), 1) + self.assertEqual(formset.initial_form_count(), 1) + + def testMaxNumParam(self): + """ + With extra=5 and max_num=2, there should be only 2 forms. + """ + e = self._create_object(EpisodeMaxNum) + inline_form_data = '<input type="hidden" name="generic_inline_admin-media-content_type-object_id-TOTAL_FORMS" value="2" id="id_generic_inline_admin-media-content_type-object_id-TOTAL_FORMS" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-INITIAL_FORMS" value="1" id="id_generic_inline_admin-media-content_type-object_id-INITIAL_FORMS" />' + response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/episodemaxnum/%s/' % e.pk) + formset = response.context['inline_admin_formsets'][0].formset + self.assertEqual(formset.total_form_count(), 2) + self.assertEqual(formset.initial_form_count(), 1) + + def testExcludeParam(self): + """ + Generic inline formsets should respect include. + """ + e = self._create_object(EpisodeExclude) + response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/episodeexclude/%s/' % e.pk) + formset = response.context['inline_admin_formsets'][0].formset + self.assertFalse('url' in formset.forms[0], 'The formset has excluded "url" field.') + +class GenericInlineAdminWithUniqueTogetherTest(TestCase): + fixtures = ['users.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def testAdd(self): + post_data = { + "name": u"John Doe", + # inline data + "generic_inline_admin-phonenumber-content_type-object_id-TOTAL_FORMS": u"1", + "generic_inline_admin-phonenumber-content_type-object_id-INITIAL_FORMS": u"0", + "generic_inline_admin-phonenumber-content_type-object_id-MAX_NUM_FORMS": u"0", + "generic_inline_admin-phonenumber-content_type-object_id-0-id": "", + "generic_inline_admin-phonenumber-content_type-object_id-0-phone_number": "555-555-5555", + } + response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/contact/add/') + self.assertEqual(response.status_code, 200) + response = self.client.post('/generic_inline_admin/admin/generic_inline_admin/contact/add/', post_data) + self.assertEqual(response.status_code, 302) # redirect somewhere + +class NoInlineDeletionTest(TestCase): + def test_no_deletion(self): + fake_site = object() + inline = MediaPermanentInline(EpisodePermanent, fake_site) + fake_request = object() + formset = inline.get_formset(fake_request) + self.assertFalse(formset.can_delete) diff --git a/parts/django/tests/regressiontests/generic_inline_admin/urls.py b/parts/django/tests/regressiontests/generic_inline_admin/urls.py new file mode 100644 index 0000000..c3e8af8 --- /dev/null +++ b/parts/django/tests/regressiontests/generic_inline_admin/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls.defaults import * +from django.contrib import admin + +urlpatterns = patterns('', + (r'^admin/', include(admin.site.urls)), +) diff --git a/parts/django/tests/regressiontests/generic_relations_regress/__init__.py b/parts/django/tests/regressiontests/generic_relations_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/generic_relations_regress/__init__.py diff --git a/parts/django/tests/regressiontests/generic_relations_regress/models.py b/parts/django/tests/regressiontests/generic_relations_regress/models.py new file mode 100644 index 0000000..d28385d --- /dev/null +++ b/parts/django/tests/regressiontests/generic_relations_regress/models.py @@ -0,0 +1,79 @@ +from django.db import models +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType + +__all__ = ('Link', 'Place', 'Restaurant', 'Person', 'Address', + 'CharLink', 'TextLink', 'OddRelation1', 'OddRelation2', + 'Contact', 'Organization', 'Note') + +class Link(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey() + + def __unicode__(self): + return "Link to %s id=%s" % (self.content_type, self.object_id) + +class Place(models.Model): + name = models.CharField(max_length=100) + links = generic.GenericRelation(Link) + + def __unicode__(self): + return "Place: %s" % self.name + +class Restaurant(Place): + def __unicode__(self): + return "Restaurant: %s" % self.name + +class Address(models.Model): + street = models.CharField(max_length=80) + city = models.CharField(max_length=50) + state = models.CharField(max_length=2) + zipcode = models.CharField(max_length=5) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey() + + def __unicode__(self): + return '%s %s, %s %s' % (self.street, self.city, self.state, self.zipcode) + +class Person(models.Model): + account = models.IntegerField(primary_key=True) + name = models.CharField(max_length=128) + addresses = generic.GenericRelation(Address) + + def __unicode__(self): + return self.name + +class CharLink(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.CharField(max_length=100) + content_object = generic.GenericForeignKey() + +class TextLink(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.TextField() + content_object = generic.GenericForeignKey() + +class OddRelation1(models.Model): + name = models.CharField(max_length=100) + clinks = generic.GenericRelation(CharLink) + +class OddRelation2(models.Model): + name = models.CharField(max_length=100) + tlinks = generic.GenericRelation(TextLink) + +# models for test_q_object_or: +class Note(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey() + note = models.TextField() + +class Contact(models.Model): + notes = generic.GenericRelation(Note) + +class Organization(models.Model): + name = models.CharField(max_length=255) + contacts = models.ManyToManyField(Contact, related_name='organizations') + diff --git a/parts/django/tests/regressiontests/generic_relations_regress/tests.py b/parts/django/tests/regressiontests/generic_relations_regress/tests.py new file mode 100644 index 0000000..45e8674 --- /dev/null +++ b/parts/django/tests/regressiontests/generic_relations_regress/tests.py @@ -0,0 +1,74 @@ +from django.test import TestCase +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q +from models import * + +class GenericRelationTests(TestCase): + + def test_inherited_models_content_type(self): + """ + Test that GenericRelations on inherited classes use the correct content + type. + """ + + p = Place.objects.create(name="South Park") + r = Restaurant.objects.create(name="Chubby's") + l1 = Link.objects.create(content_object=p) + l2 = Link.objects.create(content_object=r) + self.assertEqual(list(p.links.all()), [l1]) + self.assertEqual(list(r.links.all()), [l2]) + + def test_reverse_relation_pk(self): + """ + Test that the correct column name is used for the primary key on the + originating model of a query. See #12664. + """ + p = Person.objects.create(account=23, name='Chef') + a = Address.objects.create(street='123 Anywhere Place', + city='Conifer', state='CO', + zipcode='80433', content_object=p) + + qs = Person.objects.filter(addresses__zipcode='80433') + self.assertEqual(1, qs.count()) + self.assertEqual('Chef', qs[0].name) + + def test_charlink_delete(self): + oddrel = OddRelation1.objects.create(name='clink') + cl = CharLink.objects.create(content_object=oddrel) + oddrel.delete() + + def test_textlink_delete(self): + oddrel = OddRelation2.objects.create(name='tlink') + tl = TextLink.objects.create(content_object=oddrel) + oddrel.delete() + + def test_q_object_or(self): + """ + Tests that SQL query parameters for generic relations are properly + grouped when OR is used. + + Test for bug http://code.djangoproject.com/ticket/11535 + + In this bug the first query (below) works while the second, with the + query parameters the same but in reverse order, does not. + + The issue is that the generic relation conditions do not get properly + grouped in parentheses. + """ + note_contact = Contact.objects.create() + org_contact = Contact.objects.create() + note = Note.objects.create(note='note', content_object=note_contact) + org = Organization.objects.create(name='org name') + org.contacts.add(org_contact) + # search with a non-matching note and a matching org name + qs = Contact.objects.filter(Q(notes__note__icontains=r'other note') | + Q(organizations__name__icontains=r'org name')) + self.assertTrue(org_contact in qs) + # search again, with the same query parameters, in reverse order + qs = Contact.objects.filter( + Q(organizations__name__icontains=r'org name') | + Q(notes__note__icontains=r'other note')) + self.assertTrue(org_contact in qs) + + + diff --git a/parts/django/tests/regressiontests/get_or_create_regress/__init__.py b/parts/django/tests/regressiontests/get_or_create_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/get_or_create_regress/__init__.py diff --git a/parts/django/tests/regressiontests/get_or_create_regress/models.py b/parts/django/tests/regressiontests/get_or_create_regress/models.py new file mode 100644 index 0000000..292d96c --- /dev/null +++ b/parts/django/tests/regressiontests/get_or_create_regress/models.py @@ -0,0 +1,13 @@ +from django.db import models + + +class Publisher(models.Model): + name = models.CharField(max_length=100) + +class Author(models.Model): + name = models.CharField(max_length=100) + +class Book(models.Model): + name = models.CharField(max_length=100) + authors = models.ManyToManyField(Author, related_name='books') + publisher = models.ForeignKey(Publisher, related_name='books') diff --git a/parts/django/tests/regressiontests/get_or_create_regress/tests.py b/parts/django/tests/regressiontests/get_or_create_regress/tests.py new file mode 100644 index 0000000..87057da --- /dev/null +++ b/parts/django/tests/regressiontests/get_or_create_regress/tests.py @@ -0,0 +1,53 @@ +from django.test import TestCase + +from models import Author, Publisher + + +class GetOrCreateTests(TestCase): + def test_related(self): + p = Publisher.objects.create(name="Acme Publishing") + # Create a book through the publisher. + book, created = p.books.get_or_create(name="The Book of Ed & Fred") + self.assertTrue(created) + # The publisher should have one book. + self.assertEqual(p.books.count(), 1) + + # Try get_or_create again, this time nothing should be created. + book, created = p.books.get_or_create(name="The Book of Ed & Fred") + self.assertFalse(created) + # And the publisher should still have one book. + self.assertEqual(p.books.count(), 1) + + # Add an author to the book. + ed, created = book.authors.get_or_create(name="Ed") + self.assertTrue(created) + # Book should have one author. + self.assertEqual(book.authors.count(), 1) + + # Try get_or_create again, this time nothing should be created. + ed, created = book.authors.get_or_create(name="Ed") + self.assertFalse(created) + # And the book should still have one author. + self.assertEqual(book.authors.count(), 1) + + # Add a second author to the book. + fred, created = book.authors.get_or_create(name="Fred") + self.assertTrue(created) + + # The book should have two authors now. + self.assertEqual(book.authors.count(), 2) + + # Create an Author not tied to any books. + Author.objects.create(name="Ted") + + # There should be three Authors in total. The book object should have two. + self.assertEqual(Author.objects.count(), 3) + self.assertEqual(book.authors.count(), 2) + + # Try creating a book through an author. + _, created = ed.books.get_or_create(name="Ed's Recipes", publisher=p) + self.assertTrue(created) + + # Now Ed has two Books, Fred just one. + self.assertEqual(ed.books.count(), 2) + self.assertEqual(fred.books.count(), 1) diff --git a/parts/django/tests/regressiontests/httpwrappers/__init__.py b/parts/django/tests/regressiontests/httpwrappers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/httpwrappers/__init__.py diff --git a/parts/django/tests/regressiontests/httpwrappers/models.py b/parts/django/tests/regressiontests/httpwrappers/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/httpwrappers/models.py diff --git a/parts/django/tests/regressiontests/httpwrappers/tests.py b/parts/django/tests/regressiontests/httpwrappers/tests.py new file mode 100644 index 0000000..4e946a2 --- /dev/null +++ b/parts/django/tests/regressiontests/httpwrappers/tests.py @@ -0,0 +1,266 @@ +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.assertTrue(q.has_key('foo')) + self.assertTrue('foo' in q) + self.assertFalse(q.has_key('bar')) + self.assertFalse('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.assertFalse('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.assertTrue(q.has_key('foo')) + self.assertTrue('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.assertTrue(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 + """ + # Python 2.4 compatibility note: Python 2.4's cookie implementation + # always returns Set-Cookie headers terminating in semi-colons. + # That's not the bug this test is looking for, so ignore it. + c = CompatCookie() + c['test'] = "An,awkward;value" + self.assert_(";" not in c.output().rstrip(';')) # IE compat + self.assert_("," not in c.output().rstrip(';')) # Safari compat + + def test_decode(self): + """ + Test that we can still preserve semi-colons and commas + """ + c = CompatCookie() + c['test'] = "An,awkward;value" + c2 = CompatCookie() + c2.load(c.output()) + self.assertEqual(c['test'].value, c2['test'].value) + + def test_decode_2(self): + """ + Test that we haven't broken normal encoding + """ + c = CompatCookie() + c['test'] = "\xf0" + c2 = CompatCookie() + c2.load(c.output()) + self.assertEqual(c['test'].value, c2['test'].value) diff --git a/parts/django/tests/regressiontests/humanize/__init__.py b/parts/django/tests/regressiontests/humanize/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/humanize/__init__.py diff --git a/parts/django/tests/regressiontests/humanize/models.py b/parts/django/tests/regressiontests/humanize/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/humanize/models.py diff --git a/parts/django/tests/regressiontests/humanize/tests.py b/parts/django/tests/regressiontests/humanize/tests.py new file mode 100644 index 0000000..3536c6b --- /dev/null +++ b/parts/django/tests/regressiontests/humanize/tests.py @@ -0,0 +1,76 @@ +import unittest +from datetime import timedelta, date +from django.template import Template, Context, add_to_builtins +from django.utils.dateformat import DateFormat +from django.utils.translation import ugettext as _ +from django.utils.html import escape + +add_to_builtins('django.contrib.humanize.templatetags.humanize') + +class HumanizeTests(unittest.TestCase): + + def humanize_tester(self, test_list, result_list, method): + # Using max below ensures we go through both lists + # However, if the lists are not equal length, this raises an exception + for index in xrange(max(len(test_list), len(result_list))): + test_content = test_list[index] + t = Template('{{ test_content|%s }}' % method) + rendered = t.render(Context(locals())).strip() + self.assertEqual(rendered, escape(result_list[index]), + msg="%s test failed, produced %s, should've produced %s" % (method, rendered, result_list[index])) + + def test_ordinal(self): + test_list = ('1','2','3','4','11','12', + '13','101','102','103','111', + 'something else', None) + result_list = ('1st', '2nd', '3rd', '4th', '11th', + '12th', '13th', '101st', '102nd', '103rd', + '111th', 'something else', None) + + self.humanize_tester(test_list, result_list, 'ordinal') + + def test_intcomma(self): + test_list = (100, 1000, 10123, 10311, 1000000, 1234567.25, + '100', '1000', '10123', '10311', '1000000', '1234567.1234567', + None) + result_list = ('100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.25', + '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.1234567', + None) + + self.humanize_tester(test_list, result_list, 'intcomma') + + def test_intword(self): + test_list = ('100', '1000000', '1200000', '1290000', + '1000000000','2000000000','6000000000000', + None) + result_list = ('100', '1.0 million', '1.2 million', '1.3 million', + '1.0 billion', '2.0 billion', '6.0 trillion', + None) + + self.humanize_tester(test_list, result_list, 'intword') + + def test_apnumber(self): + test_list = [str(x) for x in range(1, 11)] + test_list.append(None) + result_list = (u'one', u'two', u'three', u'four', u'five', u'six', + u'seven', u'eight', u'nine', u'10', None) + + self.humanize_tester(test_list, result_list, 'apnumber') + + def test_naturalday(self): + from django.template import defaultfilters + today = date.today() + yesterday = today - timedelta(days=1) + tomorrow = today + timedelta(days=1) + someday = today - timedelta(days=10) + notdate = u"I'm not a date value" + + test_list = (today, yesterday, tomorrow, someday, notdate, None) + someday_result = defaultfilters.date(someday) + result_list = (_(u'today'), _(u'yesterday'), _(u'tomorrow'), + someday_result, u"I'm not a date value", None) + self.humanize_tester(test_list, result_list, 'naturalday') + +if __name__ == '__main__': + unittest.main() + diff --git a/parts/django/tests/regressiontests/i18n/__init__.py b/parts/django/tests/regressiontests/i18n/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/__init__.py diff --git a/parts/django/tests/regressiontests/i18n/forms.py b/parts/django/tests/regressiontests/i18n/forms.py new file mode 100644 index 0000000..156441c --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/forms.py @@ -0,0 +1,22 @@ +from django import template, forms +from django.forms.extras import SelectDateWidget +from models import Company + +class I18nForm(forms.Form): + decimal_field = forms.DecimalField(localize=True) + float_field = forms.FloatField(localize=True) + date_field = forms.DateField(localize=True) + datetime_field = forms.DateTimeField(localize=True) + time_field = forms.TimeField(localize=True) + integer_field = forms.IntegerField(localize=True) + +class SelectDateForm(forms.Form): + date_field = forms.DateField(widget=SelectDateWidget) + +class CompanyForm(forms.ModelForm): + cents_payed = forms.DecimalField(max_digits=4, decimal_places=2, localize=True) + products_delivered = forms.IntegerField(localize=True) + date_added = forms.DateTimeField(localize=True) + + class Meta: + model = Company diff --git a/parts/django/tests/regressiontests/i18n/models.py b/parts/django/tests/regressiontests/i18n/models.py new file mode 100644 index 0000000..75cd996 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/models.py @@ -0,0 +1,12 @@ +from datetime import datetime +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +class TestModel(models.Model): + text = models.CharField(max_length=10, default=_('Anything')) + +class Company(models.Model): + name = models.CharField(max_length=50) + 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() diff --git a/parts/django/tests/regressiontests/i18n/other/__init__.py b/parts/django/tests/regressiontests/i18n/other/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/other/__init__.py diff --git a/parts/django/tests/regressiontests/i18n/other/locale/__init__.py b/parts/django/tests/regressiontests/i18n/other/locale/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/other/locale/__init__.py diff --git a/parts/django/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.mo b/parts/django/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 0000000..2bc9343 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.mo diff --git a/parts/django/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po b/parts/django/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..2fdcee5 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-02-14 17:33+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: models.py:3 +msgid "Date/time" +msgstr "Datum/Zeit (LOCALE_PATHS)" diff --git a/parts/django/tests/regressiontests/i18n/other/locale/de/__init__.py b/parts/django/tests/regressiontests/i18n/other/locale/de/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/other/locale/de/__init__.py diff --git a/parts/django/tests/regressiontests/i18n/other/locale/de/formats.py b/parts/django/tests/regressiontests/i18n/other/locale/de/formats.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/other/locale/de/formats.py diff --git a/parts/django/tests/regressiontests/i18n/resolution/__init__.py b/parts/django/tests/regressiontests/i18n/resolution/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/resolution/__init__.py diff --git a/parts/django/tests/regressiontests/i18n/resolution/locale/de/LC_MESSAGES/django.mo b/parts/django/tests/regressiontests/i18n/resolution/locale/de/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 0000000..1c37ac5 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/resolution/locale/de/LC_MESSAGES/django.mo diff --git a/parts/django/tests/regressiontests/i18n/resolution/locale/de/LC_MESSAGES/django.po b/parts/django/tests/regressiontests/i18n/resolution/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..11d2d5d --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/resolution/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-02-14 17:33+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: models.py:3 +msgid "Date/time" +msgstr "Datum/Zeit (APP)" diff --git a/parts/django/tests/regressiontests/i18n/resolution/models.py b/parts/django/tests/regressiontests/i18n/resolution/models.py new file mode 100644 index 0000000..4287ca8 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/resolution/models.py @@ -0,0 +1 @@ +#
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/i18n/tests.py b/parts/django/tests/regressiontests/i18n/tests.py new file mode 100644 index 0000000..99f9fe1 --- /dev/null +++ b/parts/django/tests/regressiontests/i18n/tests.py @@ -0,0 +1,667 @@ +# -*- encoding: utf-8 -*- +import datetime +import decimal +import os +import sys +import pickle + +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, iter_format_modules +from django.utils.numberformat import format as nformat +from django.utils.safestring import mark_safe, SafeString, SafeUnicode +from django.utils.translation import ugettext, ugettext_lazy, activate, deactivate, gettext_lazy, to_locale +from django.utils.importlib import import_module + + +from forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm +from models import Company, TestModel + + +class TranslationTests(TestCase): + + def test_lazy_objects(self): + """ + Format string interpolation should work with *_lazy objects. + """ + s = ugettext_lazy('Add %(name)s') + d = {'name': 'Ringo'} + self.assertEqual(u'Add Ringo', s % d) + activate('de') + try: + self.assertEqual(u'Ringo hinzuf\xfcgen', s % d) + activate('pl') + self.assertEqual(u'Dodaj Ringo', s % d) + finally: + deactivate() + + # It should be possible to compare *_lazy objects. + s1 = ugettext_lazy('Add %(name)s') + self.assertEqual(True, s == s1) + s2 = gettext_lazy('Add %(name)s') + s3 = gettext_lazy('Add %(name)s') + self.assertEqual(True, s2 == s3) + self.assertEqual(True, s == s2) + s4 = ugettext_lazy('Some other string') + self.assertEqual(False, s == s4) + + def test_lazy_pickle(self): + s1 = ugettext_lazy("test") + self.assertEqual(unicode(s1), "test") + s2 = pickle.loads(pickle.dumps(s1)) + self.assertEqual(unicode(s2), "test") + + def test_string_concat(self): + """ + unicode(string_concat(...)) should not raise a TypeError - #4796 + """ + import django.utils.translation + self.assertEqual(u'django', unicode(django.utils.translation.string_concat("dja", "ngo"))) + + def test_safe_status(self): + """ + Translating a string requiring no auto-escaping shouldn't change the "safe" status. + """ + s = mark_safe('Password') + self.assertEqual(SafeString, type(s)) + activate('de') + try: + self.assertEqual(SafeUnicode, type(ugettext(s))) + finally: + deactivate() + self.assertEqual('aPassword', SafeString('a') + s) + self.assertEqual('Passworda', s + SafeString('a')) + self.assertEqual('Passworda', s + mark_safe('a')) + self.assertEqual('aPassword', mark_safe('a') + s) + self.assertEqual('as', mark_safe('a') + mark_safe('s')) + + def test_maclines(self): + """ + Translations on files with mac or dos end of lines will be converted + to unix eof in .po catalogs, and they have to match when retrieved + """ + from django.utils.translation.trans_real import translation + ca_translation = translation('ca') + ca_translation._catalog[u'Mac\nEOF\n'] = u'Catalan Mac\nEOF\n' + ca_translation._catalog[u'Win\nEOF\n'] = u'Catalan Win\nEOF\n' + activate('ca') + try: + self.assertEqual(u'Catalan Mac\nEOF\n', ugettext(u'Mac\rEOF\r')) + self.assertEqual(u'Catalan Win\nEOF\n', ugettext(u'Win\r\nEOF\r\n')) + finally: + deactivate() + + def test_to_locale(self): + """ + Tests the to_locale function and the special case of Serbian Latin + (refs #12230 and r11299) + """ + self.assertEqual(to_locale('en-us'), 'en_US') + self.assertEqual(to_locale('sr-lat'), 'sr_Lat') + + def test_to_language(self): + """ + Test the to_language function + """ + from django.utils.translation.trans_real import to_language + self.assertEqual(to_language('en_US'), 'en-us') + self.assertEqual(to_language('sr_Lat'), 'sr-lat') + + +class FormattingTests(TestCase): + + def setUp(self): + self.use_i18n = settings.USE_I18N + self.use_l10n = settings.USE_L10N + self.use_thousand_separator = settings.USE_THOUSAND_SEPARATOR + self.thousand_separator = settings.THOUSAND_SEPARATOR + self.number_grouping = settings.NUMBER_GROUPING + self.n = decimal.Decimal('66666.666') + self.f = 99999.999 + self.d = datetime.date(2009, 12, 31) + self.dt = datetime.datetime(2009, 12, 31, 20, 50) + self.t = datetime.time(10, 15, 48) + self.l = 10000L + self.ctxt = Context({ + 'n': self.n, + 't': self.t, + 'd': self.d, + 'dt': self.dt, + 'f': self.f, + 'l': self.l, + }) + + def tearDown(self): + # Restore defaults + settings.USE_I18N = self.use_i18n + settings.USE_L10N = self.use_l10n + settings.USE_THOUSAND_SEPARATOR = self.use_thousand_separator + settings.THOUSAND_SEPARATOR = self.thousand_separator + settings.NUMBER_GROUPING = self.number_grouping + + def test_locale_independent(self): + """ + Localization of numbers + """ + settings.USE_L10N = True + settings.USE_THOUSAND_SEPARATOR = False + self.assertEqual(u'66666.66', nformat(self.n, decimal_sep='.', decimal_pos=2, grouping=3, thousand_sep=',')) + self.assertEqual(u'66666A6', nformat(self.n, decimal_sep='A', decimal_pos=1, grouping=1, thousand_sep='B')) + + settings.USE_THOUSAND_SEPARATOR = True + self.assertEqual(u'66,666.66', nformat(self.n, decimal_sep='.', decimal_pos=2, grouping=3, thousand_sep=',')) + self.assertEqual(u'6B6B6B6B6A6', nformat(self.n, decimal_sep='A', decimal_pos=1, grouping=1, thousand_sep='B')) + self.assertEqual(u'-66666.6', nformat(-66666.666, decimal_sep='.', decimal_pos=1)) + self.assertEqual(u'-66666.0', nformat(int('-66666'), decimal_sep='.', decimal_pos=1)) + self.assertEqual(u'10000.0', nformat(self.l, decimal_sep='.', decimal_pos=1)) + + # date filter + self.assertEqual(u'31.12.2009 в 20:50', Template('{{ dt|date:"d.m.Y в H:i" }}').render(self.ctxt)) + self.assertEqual(u'⌚ 10:15', Template('{{ t|time:"⌚ H:i" }}').render(self.ctxt)) + + def test_l10n_disabled(self): + """ + Catalan locale with format i18n disabled translations will be used, + but not formats + """ + settings.USE_L10N = False + activate('ca') + try: + self.assertEqual(u'N j, Y', get_format('DATE_FORMAT')) + self.assertEqual(0, get_format('FIRST_DAY_OF_WEEK')) + self.assertEqual(u'.', get_format('DECIMAL_SEPARATOR')) + self.assertEqual(u'10:15 a.m.', time_format(self.t)) + self.assertEqual(u'des. 31, 2009', date_format(self.d)) + 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(u'No localizable', localize('No localizable')) + self.assertEqual(u'66666.666', localize(self.n)) + self.assertEqual(u'99999.999', localize(self.f)) + self.assertEqual(u'10000', localize(self.l)) + 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'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)) + self.assertEqual(u'12/31/2009', Template('{{ d|date:"SHORT_DATE_FORMAT" }}').render(self.ctxt)) + self.assertEqual(u'12/31/2009 8:50 p.m.', Template('{{ dt|date:"SHORT_DATETIME_FORMAT" }}').render(self.ctxt)) + + form = I18nForm({ + 'decimal_field': u'66666,666', + 'float_field': u'99999,999', + 'date_field': u'31/12/2009', + 'datetime_field': u'31/12/2009 20:50', + 'time_field': u'20:50', + 'integer_field': u'1.234', + }) + self.assertEqual(False, form.is_valid()) + self.assertEqual([u'Introdu\xefu un n\xfamero.'], form.errors['float_field']) + self.assertEqual([u'Introdu\xefu un n\xfamero.'], form.errors['decimal_field']) + self.assertEqual([u'Introdu\xefu una data v\xe0lida.'], form.errors['date_field']) + self.assertEqual([u'Introdu\xefu una data/hora v\xe0lides.'], form.errors['datetime_field']) + self.assertEqual([u'Introdu\xefu un n\xfamero sencer.'], form.errors['integer_field']) + + form2 = SelectDateForm({ + 'date_field_month': u'12', + 'date_field_day': u'31', + 'date_field_year': u'2009' + }) + self.assertEqual(True, form2.is_valid()) + self.assertEqual(datetime.date(2009, 12, 31), form2.cleaned_data['date_field']) + self.assertEqual( + u'<select name="mydate_month" id="id_mydate_month">\n<option value="1">gener</option>\n<option value="2">febrer</option>\n<option value="3">mar\xe7</option>\n<option value="4">abril</option>\n<option value="5">maig</option>\n<option value="6">juny</option>\n<option value="7">juliol</option>\n<option value="8">agost</option>\n<option value="9">setembre</option>\n<option value="10">octubre</option>\n<option value="11">novembre</option>\n<option value="12" selected="selected">desembre</option>\n</select>\n<select name="mydate_day" id="id_mydate_day">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31" selected="selected">31</option>\n</select>\n<select name="mydate_year" id="id_mydate_year">\n<option value="2009" selected="selected">2009</option>\n<option value="2010">2010</option>\n<option value="2011">2011</option>\n<option value="2012">2012</option>\n<option value="2013">2013</option>\n<option value="2014">2014</option>\n<option value="2015">2015</option>\n<option value="2016">2016</option>\n<option value="2017">2017</option>\n<option value="2018">2018</option>\n</select>', + SelectDateWidget(years=range(2009, 2019)).render('mydate', datetime.date(2009, 12, 31)) + ) + + # We shouldn't change the behavior of the floatformat filter re: + # thousand separator and grouping when USE_L10N is False even + # if the USE_THOUSAND_SEPARATOR, NUMBER_GROUPING and + # THOUSAND_SEPARATOR settings are specified + settings.USE_THOUSAND_SEPARATOR = True + settings.NUMBER_GROUPING = 1 + settings.THOUSAND_SEPARATOR = '!' + self.assertEqual(u'66666.67', Template('{{ n|floatformat:2 }}').render(self.ctxt)) + self.assertEqual(u'100000.0', Template('{{ f|floatformat }}').render(self.ctxt)) + finally: + deactivate() + + def test_l10n_enabled(self): + """ + Catalan locale + """ + settings.USE_L10N = True + activate('ca') + try: + self.assertEqual('j \de F \de Y', get_format('DATE_FORMAT')) + self.assertEqual(1, get_format('FIRST_DAY_OF_WEEK')) + self.assertEqual(',', get_format('DECIMAL_SEPARATOR')) + self.assertEqual(u'10:15:48', time_format(self.t)) + self.assertEqual(u'31 de desembre de 2009', date_format(self.d)) + self.assertEqual(u'desembre del 2009', date_format(self.d, 'YEAR_MONTH_FORMAT')) + self.assertEqual(u'31/12/2009 20:50', date_format(self.dt, 'SHORT_DATETIME_FORMAT')) + self.assertEqual('No localizable', localize('No localizable')) + + settings.USE_THOUSAND_SEPARATOR = True + self.assertEqual(u'66.666,666', localize(self.n)) + self.assertEqual(u'99.999,999', localize(self.f)) + self.assertEqual(u'10.000', localize(self.l)) + self.assertEqual(u'True', localize(True)) + + settings.USE_THOUSAND_SEPARATOR = False + self.assertEqual(u'66666,666', localize(self.n)) + self.assertEqual(u'99999,999', localize(self.f)) + self.assertEqual(u'10000', localize(self.l)) + self.assertEqual(u'31 de desembre de 2009', localize(self.d)) + self.assertEqual(u'31 de desembre de 2009 a les 20:50', localize(self.dt)) + + settings.USE_THOUSAND_SEPARATOR = True + self.assertEqual(u'66.666,666', Template('{{ n }}').render(self.ctxt)) + self.assertEqual(u'99.999,999', Template('{{ f }}').render(self.ctxt)) + self.assertEqual(u'10.000', Template('{{ l }}').render(self.ctxt)) + + form3 = I18nForm({ + 'decimal_field': u'66.666,666', + 'float_field': u'99.999,999', + 'date_field': u'31/12/2009', + 'datetime_field': u'31/12/2009 20:50', + 'time_field': u'20:50', + 'integer_field': u'1.234', + }) + self.assertEqual(True, form3.is_valid()) + self.assertEqual(decimal.Decimal('66666.666'), form3.cleaned_data['decimal_field']) + self.assertEqual(99999.999, form3.cleaned_data['float_field']) + self.assertEqual(datetime.date(2009, 12, 31), form3.cleaned_data['date_field']) + self.assertEqual(datetime.datetime(2009, 12, 31, 20, 50), form3.cleaned_data['datetime_field']) + self.assertEqual(datetime.time(20, 50), form3.cleaned_data['time_field']) + self.assertEqual(1234, form3.cleaned_data['integer_field']) + + settings.USE_THOUSAND_SEPARATOR = False + self.assertEqual(u'66666,666', Template('{{ n }}').render(self.ctxt)) + self.assertEqual(u'99999,999', Template('{{ f }}').render(self.ctxt)) + self.assertEqual(u'31 de desembre de 2009', Template('{{ d }}').render(self.ctxt)) + self.assertEqual(u'31 de desembre de 2009 a les 20:50', 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:48', Template('{{ t|time:"TIME_FORMAT" }}').render(self.ctxt)) + self.assertEqual(u'31/12/2009', Template('{{ d|date:"SHORT_DATE_FORMAT" }}').render(self.ctxt)) + self.assertEqual(u'31/12/2009 20:50', Template('{{ dt|date:"SHORT_DATETIME_FORMAT" }}').render(self.ctxt)) + + form4 = I18nForm({ + 'decimal_field': u'66666,666', + 'float_field': u'99999,999', + 'date_field': u'31/12/2009', + 'datetime_field': u'31/12/2009 20:50', + 'time_field': u'20:50', + 'integer_field': u'1234', + }) + self.assertEqual(True, form4.is_valid()) + self.assertEqual(decimal.Decimal('66666.666'), form4.cleaned_data['decimal_field']) + self.assertEqual(99999.999, form4.cleaned_data['float_field']) + self.assertEqual(datetime.date(2009, 12, 31), form4.cleaned_data['date_field']) + self.assertEqual(datetime.datetime(2009, 12, 31, 20, 50), form4.cleaned_data['datetime_field']) + self.assertEqual(datetime.time(20, 50), form4.cleaned_data['time_field']) + self.assertEqual(1234, form4.cleaned_data['integer_field']) + + form5 = SelectDateForm({ + 'date_field_month': u'12', + 'date_field_day': u'31', + 'date_field_year': u'2009' + }) + self.assertEqual(True, form5.is_valid()) + self.assertEqual(datetime.date(2009, 12, 31), form5.cleaned_data['date_field']) + self.assertEqual( + u'<select name="mydate_day" id="id_mydate_day">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31" selected="selected">31</option>\n</select>\n<select name="mydate_month" id="id_mydate_month">\n<option value="1">gener</option>\n<option value="2">febrer</option>\n<option value="3">mar\xe7</option>\n<option value="4">abril</option>\n<option value="5">maig</option>\n<option value="6">juny</option>\n<option value="7">juliol</option>\n<option value="8">agost</option>\n<option value="9">setembre</option>\n<option value="10">octubre</option>\n<option value="11">novembre</option>\n<option value="12" selected="selected">desembre</option>\n</select>\n<select name="mydate_year" id="id_mydate_year">\n<option value="2009" selected="selected">2009</option>\n<option value="2010">2010</option>\n<option value="2011">2011</option>\n<option value="2012">2012</option>\n<option value="2013">2013</option>\n<option value="2014">2014</option>\n<option value="2015">2015</option>\n<option value="2016">2016</option>\n<option value="2017">2017</option>\n<option value="2018">2018</option>\n</select>', + SelectDateWidget(years=range(2009, 2019)).render('mydate', datetime.date(2009, 12, 31)) + ) + finally: + deactivate() + + # English locale + + settings.USE_L10N = True + activate('en') + try: + self.assertEqual('N j, Y', get_format('DATE_FORMAT')) + self.assertEqual(0, get_format('FIRST_DAY_OF_WEEK')) + self.assertEqual('.', get_format('DECIMAL_SEPARATOR')) + self.assertEqual(u'Dec. 31, 2009', date_format(self.d)) + self.assertEqual(u'December 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(u'No localizable', localize('No localizable')) + + settings.USE_THOUSAND_SEPARATOR = True + self.assertEqual(u'66,666.666', localize(self.n)) + self.assertEqual(u'99,999.999', localize(self.f)) + self.assertEqual(u'10,000', localize(self.l)) + + settings.USE_THOUSAND_SEPARATOR = False + self.assertEqual(u'66666.666', localize(self.n)) + self.assertEqual(u'99999.999', localize(self.f)) + self.assertEqual(u'10000', localize(self.l)) + self.assertEqual(u'Dec. 31, 2009', localize(self.d)) + self.assertEqual(u'Dec. 31, 2009, 8:50 p.m.', localize(self.dt)) + + settings.USE_THOUSAND_SEPARATOR = True + self.assertEqual(u'66,666.666', Template('{{ n }}').render(self.ctxt)) + self.assertEqual(u'99,999.999', Template('{{ f }}').render(self.ctxt)) + self.assertEqual(u'10,000', Template('{{ l }}').render(self.ctxt)) + + settings.USE_THOUSAND_SEPARATOR = False + self.assertEqual(u'66666.666', Template('{{ n }}').render(self.ctxt)) + self.assertEqual(u'99999.999', Template('{{ f }}').render(self.ctxt)) + self.assertEqual(u'Dec. 31, 2009', Template('{{ d }}').render(self.ctxt)) + self.assertEqual(u'Dec. 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'12/31/2009', Template('{{ d|date:"SHORT_DATE_FORMAT" }}').render(self.ctxt)) + self.assertEqual(u'12/31/2009 8:50 p.m.', Template('{{ dt|date:"SHORT_DATETIME_FORMAT" }}').render(self.ctxt)) + + form5 = I18nForm({ + 'decimal_field': u'66666.666', + 'float_field': u'99999.999', + 'date_field': u'12/31/2009', + 'datetime_field': u'12/31/2009 20:50', + 'time_field': u'20:50', + 'integer_field': u'1234', + }) + self.assertEqual(True, form5.is_valid()) + self.assertEqual(decimal.Decimal('66666.666'), form5.cleaned_data['decimal_field']) + self.assertEqual(99999.999, form5.cleaned_data['float_field']) + self.assertEqual(datetime.date(2009, 12, 31), form5.cleaned_data['date_field']) + self.assertEqual(datetime.datetime(2009, 12, 31, 20, 50), form5.cleaned_data['datetime_field']) + self.assertEqual(datetime.time(20, 50), form5.cleaned_data['time_field']) + self.assertEqual(1234, form5.cleaned_data['integer_field']) + + form6 = SelectDateForm({ + 'date_field_month': u'12', + 'date_field_day': u'31', + 'date_field_year': u'2009' + }) + self.assertEqual(True, form6.is_valid()) + self.assertEqual(datetime.date(2009, 12, 31), form6.cleaned_data['date_field']) + self.assertEqual( + u'<select name="mydate_month" id="id_mydate_month">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12" selected="selected">December</option>\n</select>\n<select name="mydate_day" id="id_mydate_day">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31" selected="selected">31</option>\n</select>\n<select name="mydate_year" id="id_mydate_year">\n<option value="2009" selected="selected">2009</option>\n<option value="2010">2010</option>\n<option value="2011">2011</option>\n<option value="2012">2012</option>\n<option value="2013">2013</option>\n<option value="2014">2014</option>\n<option value="2015">2015</option>\n<option value="2016">2016</option>\n<option value="2017">2017</option>\n<option value="2018">2018</option>\n</select>', + SelectDateWidget(years=range(2009, 2019)).render('mydate', datetime.date(2009, 12, 31)) + ) + finally: + deactivate() + + def test_sub_locales(self): + """ + Check if sublocales fall back to the main locale + """ + settings.USE_L10N = True + activate('de-at') + settings.USE_THOUSAND_SEPARATOR = True + try: + self.assertEqual(u'66.666,666', Template('{{ n }}').render(self.ctxt)) + finally: + deactivate() + + activate('es-us') + try: + self.assertEqual(u'31 de diciembre de 2009', date_format(self.d)) + finally: + deactivate() + + def test_localized_input(self): + """ + Tests if form input is correctly localized + """ + settings.USE_L10N = True + activate('de-at') + try: + form6 = CompanyForm({ + 'name': u'acme', + 'date_added': datetime.datetime(2009, 12, 31, 6, 0, 0), + 'cents_payed': decimal.Decimal('59.47'), + 'products_delivered': 12000, + }) + self.assertEqual(True, form6.is_valid()) + self.assertEqual( + form6.as_ul(), + u'<li><label for="id_name">Name:</label> <input id="id_name" type="text" name="name" value="acme" maxlength="50" /></li>\n<li><label for="id_date_added">Date added:</label> <input type="text" name="date_added" value="31.12.2009 06:00:00" id="id_date_added" /></li>\n<li><label for="id_cents_payed">Cents payed:</label> <input type="text" name="cents_payed" value="59,47" id="id_cents_payed" /></li>\n<li><label for="id_products_delivered">Products delivered:</label> <input type="text" name="products_delivered" value="12000" id="id_products_delivered" /></li>' + ) + self.assertEqual(localize_input(datetime.datetime(2009, 12, 31, 6, 0, 0)), '31.12.2009 06:00:00') + self.assertEqual(datetime.datetime(2009, 12, 31, 6, 0, 0), form6.cleaned_data['date_added']) + settings.USE_THOUSAND_SEPARATOR = True + # Checking for the localized "products_delivered" field + self.assert_(u'<input type="text" name="products_delivered" value="12.000" id="id_products_delivered" />' in form6.as_ul()) + finally: + deactivate() + + def test_iter_format_modules(self): + """ + Tests the iter_format_modules function. + """ + activate('de-at') + old_format_module_path = settings.FORMAT_MODULE_PATH + try: + settings.USE_L10N = True + de_format_mod = import_module('django.conf.locale.de.formats') + self.assertEqual(list(iter_format_modules('de')), [de_format_mod]) + settings.FORMAT_MODULE_PATH = 'regressiontests.i18n.other.locale' + test_de_format_mod = import_module('regressiontests.i18n.other.locale.de.formats') + self.assertEqual(list(iter_format_modules('de')), [test_de_format_mod, de_format_mod]) + finally: + settings.FORMAT_MODULE_PATH = old_format_module_path + deactivate() + + +class MiscTests(TestCase): + + def test_parse_spec_http_header(self): + """ + Testing HTTP header parsing. First, we test that we can parse the + values according to the spec (and that we extract all the pieces in + the right order). + """ + from django.utils.translation.trans_real import parse_accept_lang_header + p = parse_accept_lang_header + # Good headers. + self.assertEqual([('de', 1.0)], p('de')) + self.assertEqual([('en-AU', 1.0)], p('en-AU')) + self.assertEqual([('*', 1.0)], p('*;q=1.00')) + self.assertEqual([('en-AU', 0.123)], p('en-AU;q=0.123')) + self.assertEqual([('en-au', 0.5)], p('en-au;q=0.5')) + self.assertEqual([('en-au', 1.0)], p('en-au;q=1.0')) + self.assertEqual([('da', 1.0), ('en', 0.5), ('en-gb', 0.25)], p('da, en-gb;q=0.25, en;q=0.5')) + self.assertEqual([('en-au-xx', 1.0)], p('en-au-xx')) + self.assertEqual([('de', 1.0), ('en-au', 0.75), ('en-us', 0.5), ('en', 0.25), ('es', 0.125), ('fa', 0.125)], p('de,en-au;q=0.75,en-us;q=0.5,en;q=0.25,es;q=0.125,fa;q=0.125')) + self.assertEqual([('*', 1.0)], p('*')) + self.assertEqual([('de', 1.0)], p('de;q=0.')) + self.assertEqual([], p('')) + + # Bad headers; should always return []. + self.assertEqual([], p('en-gb;q=1.0000')) + self.assertEqual([], p('en;q=0.1234')) + self.assertEqual([], p('en;q=.2')) + self.assertEqual([], p('abcdefghi-au')) + self.assertEqual([], p('**')) + self.assertEqual([], p('en,,gb')) + self.assertEqual([], p('en-au;q=0.1.0')) + self.assertEqual([], p('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ,en')) + self.assertEqual([], p('da, en-gb;q=0.8, en;q=0.7,#')) + self.assertEqual([], p('de;q=2.0')) + self.assertEqual([], p('de;q=0.a')) + self.assertEqual([], p('')) + + def test_parse_literal_http_header(self): + """ + Now test that we parse a literal HTTP header correctly. + """ + from django.utils.translation.trans_real import get_language_from_request + g = get_language_from_request + from django.http import HttpRequest + r = HttpRequest + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-br'} + self.assertEqual('pt-br', g(r)) + + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt'} + self.assertEqual('pt', g(r)) + + r.META = {'HTTP_ACCEPT_LANGUAGE': 'es,de'} + self.assertEqual('es', g(r)) + + r.META = {'HTTP_ACCEPT_LANGUAGE': 'es-ar,de'} + self.assertEqual('es-ar', g(r)) + + # Python 2.3 and 2.4 return slightly different results for completely + # bogus locales, so we omit this test for that anything below 2.4. + # It's relatively harmless in any cases (GIGO). This also means this + # won't be executed on Jython currently, but life's like that + # sometimes. (On those platforms, passing in a truly bogus locale + # will get you the default locale back.) + if sys.version_info >= (2, 5): + # This test assumes there won't be a Django translation to a US + # variation of the Spanish language, a safe assumption. When the + # user sets it as the preferred language, the main 'es' + # translation should be selected instead. + r.META = {'HTTP_ACCEPT_LANGUAGE': 'es-us'} + self.assertEqual(g(r), 'es') + + # This tests the following scenario: there isn't a main language (zh) + # translation of Django but there is a translation to variation (zh_CN) + # the user sets zh-cn as the preferred language, it should be selected + # by Django without falling back nor ignoring it. + r.META = {'HTTP_ACCEPT_LANGUAGE': 'zh-cn,de'} + self.assertEqual(g(r), 'zh-cn') + + def test_parse_language_cookie(self): + """ + Now test that we parse language preferences stored in a cookie correctly. + """ + from django.utils.translation.trans_real import get_language_from_request + g = get_language_from_request + from django.http import HttpRequest + r = HttpRequest + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt-br'} + r.META = {} + self.assertEqual('pt-br', g(r)) + + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt'} + r.META = {} + self.assertEqual('pt', g(r)) + + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'es'} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'} + self.assertEqual('es', g(r)) + + # Python 2.3 and 2.4 return slightly different results for completely + # bogus locales, so we omit this test for that anything below 2.4. + # It's relatively harmless in any cases (GIGO). This also means this + # won't be executed on Jython currently, but life's like that + # sometimes. (On those platforms, passing in a truly bogus locale + # will get you the default locale back.) + if sys.version_info >= (2, 5): + # This test assumes there won't be a Django translation to a US + # variation of the Spanish language, a safe assumption. When the + # user sets it as the preferred language, the main 'es' + # translation should be selected instead. + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'es-us'} + r.META = {} + self.assertEqual(g(r), 'es') + + # This tests the following scenario: there isn't a main language (zh) + # translation of Django but there is a translation to variation (zh_CN) + # the user sets zh-cn as the preferred language, it should be selected + # by Django without falling back nor ignoring it. + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'zh-cn'} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'} + self.assertEqual(g(r), 'zh-cn') + +class ResolutionOrderI18NTests(TestCase): + + def setUp(self): + from django.utils.translation import trans_real + # Okay, this is brutal, but we have no other choice to fully reset + # the translation framework + trans_real._active = {} + trans_real._translations = {} + activate('de') + + def tearDown(self): + deactivate() + + def assertUgettext(self, msgid, msgstr): + result = ugettext(msgid) + self.assert_(msgstr in result, ("The string '%s' isn't in the " + "translation of '%s'; the actual result is '%s'." % (msgstr, msgid, result))) + +class AppResolutionOrderI18NTests(ResolutionOrderI18NTests): + + def setUp(self): + self.old_installed_apps = settings.INSTALLED_APPS + settings.INSTALLED_APPS = list(settings.INSTALLED_APPS) + ['regressiontests.i18n.resolution'] + super(AppResolutionOrderI18NTests, self).setUp() + + def tearDown(self): + settings.INSTALLED_APPS = self.old_installed_apps + super(AppResolutionOrderI18NTests, self).tearDown() + + def test_app_translation(self): + self.assertUgettext('Date/time', 'APP') + +class LocalePathsResolutionOrderI18NTests(ResolutionOrderI18NTests): + + def setUp(self): + self.old_locale_paths = settings.LOCALE_PATHS + settings.LOCALE_PATHS += (os.path.join(os.path.dirname(os.path.abspath(__file__)), 'other', 'locale'),) + super(LocalePathsResolutionOrderI18NTests, self).setUp() + + def tearDown(self): + settings.LOCALE_PATHS = self.old_locale_paths + super(LocalePathsResolutionOrderI18NTests, self).tearDown() + + def test_locale_paths_translation(self): + self.assertUgettext('Date/time', 'LOCALE_PATHS') + +class ProjectResolutionOrderI18NTests(ResolutionOrderI18NTests): + + def setUp(self): + self.old_settings_module = settings.SETTINGS_MODULE + settings.SETTINGS_MODULE = 'regressiontests' + super(ProjectResolutionOrderI18NTests, self).setUp() + + def tearDown(self): + settings.SETTINGS_MODULE = self.old_settings_module + super(ProjectResolutionOrderI18NTests, self).tearDown() + + def test_project_translation(self): + self.assertUgettext('Date/time', 'PROJECT') + + def test_project_override_app_translation(self): + old_installed_apps = settings.INSTALLED_APPS + settings.INSTALLED_APPS = list(settings.INSTALLED_APPS) + ['regressiontests.i18n.resolution'] + self.assertUgettext('Date/time', 'PROJECT') + settings.INSTALLED_APPS = old_installed_apps + + def test_project_override_locale_paths_translation(self): + old_locale_paths = settings.LOCALE_PATHS + settings.LOCALE_PATHS += (os.path.join(os.path.dirname(os.path.abspath(__file__)), 'other', 'locale'),) + self.assertUgettext('Date/time', 'PROJECT') + settings.LOCALE_PATHS = old_locale_paths + +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/parts/django/tests/regressiontests/initial_sql_regress/__init__.py b/parts/django/tests/regressiontests/initial_sql_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/initial_sql_regress/__init__.py diff --git a/parts/django/tests/regressiontests/initial_sql_regress/models.py b/parts/django/tests/regressiontests/initial_sql_regress/models.py new file mode 100644 index 0000000..9f91802 --- /dev/null +++ b/parts/django/tests/regressiontests/initial_sql_regress/models.py @@ -0,0 +1,11 @@ +""" +Regression tests for initial SQL insertion. +""" + +from django.db import models + +class Simple(models.Model): + name = models.CharField(max_length = 50) + +# NOTE: The format of the included SQL file for this test suite is important. +# It must end with a trailing newline in order to test the fix for #2161. diff --git a/parts/django/tests/regressiontests/initial_sql_regress/sql/simple.sql b/parts/django/tests/regressiontests/initial_sql_regress/sql/simple.sql new file mode 100644 index 0000000..ca9bd40 --- /dev/null +++ b/parts/django/tests/regressiontests/initial_sql_regress/sql/simple.sql @@ -0,0 +1,8 @@ +INSERT INTO initial_sql_regress_simple (name) VALUES ('John'); +INSERT INTO initial_sql_regress_simple (name) VALUES ('Paul'); +INSERT INTO initial_sql_regress_simple (name) VALUES ('Ringo'); +INSERT INTO initial_sql_regress_simple (name) VALUES ('George'); +INSERT INTO initial_sql_regress_simple (name) VALUES ('Miles O''Brien'); +INSERT INTO initial_sql_regress_simple (name) VALUES ('Semicolon;Man'); +INSERT INTO initial_sql_regress_simple (name) VALUES ('This line has a Windows line ending');
+ diff --git a/parts/django/tests/regressiontests/initial_sql_regress/tests.py b/parts/django/tests/regressiontests/initial_sql_regress/tests.py new file mode 100644 index 0000000..2b3ca91 --- /dev/null +++ b/parts/django/tests/regressiontests/initial_sql_regress/tests.py @@ -0,0 +1,8 @@ +from django.test import TestCase + +from models import Simple + + +class InitialSQLTests(TestCase): + def test_initial_sql(self): + self.assertEqual(Simple.objects.count(), 7) diff --git a/parts/django/tests/regressiontests/inline_formsets/__init__.py b/parts/django/tests/regressiontests/inline_formsets/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/inline_formsets/__init__.py diff --git a/parts/django/tests/regressiontests/inline_formsets/models.py b/parts/django/tests/regressiontests/inline_formsets/models.py new file mode 100644 index 0000000..d76eea7 --- /dev/null +++ b/parts/django/tests/regressiontests/inline_formsets/models.py @@ -0,0 +1,28 @@ +# coding: utf-8 +from django.db import models + + +class School(models.Model): + name = models.CharField(max_length=100) + +class Parent(models.Model): + name = models.CharField(max_length=100) + +class Child(models.Model): + mother = models.ForeignKey(Parent, related_name='mothers_children') + father = models.ForeignKey(Parent, related_name='fathers_children') + school = models.ForeignKey(School) + name = models.CharField(max_length=100) + +class Poet(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class Poem(models.Model): + poet = models.ForeignKey(Poet) + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/regressiontests/inline_formsets/tests.py b/parts/django/tests/regressiontests/inline_formsets/tests.py new file mode 100644 index 0000000..dd698ab --- /dev/null +++ b/parts/django/tests/regressiontests/inline_formsets/tests.py @@ -0,0 +1,163 @@ +from django.forms.models import inlineformset_factory +from django.test import TestCase + +from regressiontests.inline_formsets.models import Poet, Poem, School, Parent, Child + + +class DeletionTests(TestCase): + + def test_deletion(self): + PoemFormSet = inlineformset_factory(Poet, Poem, can_delete=True) + poet = Poet.objects.create(name='test') + poem = poet.poem_set.create(name='test poem') + data = { + 'poem_set-TOTAL_FORMS': u'1', + 'poem_set-INITIAL_FORMS': u'1', + 'poem_set-MAX_NUM_FORMS': u'0', + 'poem_set-0-id': str(poem.pk), + 'poem_set-0-poet': str(poet.pk), + 'poem_set-0-name': u'test', + 'poem_set-0-DELETE': u'on', + } + formset = PoemFormSet(data, instance=poet) + formset.save() + self.assertTrue(formset.is_valid()) + self.assertEqual(Poem.objects.count(), 0) + + def test_add_form_deletion_when_invalid(self): + """ + Make sure that an add form that is filled out, but marked for deletion + doesn't cause validation errors. + """ + PoemFormSet = inlineformset_factory(Poet, Poem, can_delete=True) + poet = Poet.objects.create(name='test') + data = { + 'poem_set-TOTAL_FORMS': u'1', + 'poem_set-INITIAL_FORMS': u'0', + 'poem_set-MAX_NUM_FORMS': u'0', + 'poem_set-0-id': u'', + 'poem_set-0-poem': u'1', + 'poem_set-0-name': u'x' * 1000, + } + formset = PoemFormSet(data, instance=poet) + # Make sure this form doesn't pass validation. + self.assertEqual(formset.is_valid(), False) + self.assertEqual(Poem.objects.count(), 0) + + # Then make sure that it *does* pass validation and delete the object, + # even though the data isn't actually valid. + data['poem_set-0-DELETE'] = 'on' + formset = PoemFormSet(data, instance=poet) + self.assertEqual(formset.is_valid(), True) + formset.save() + self.assertEqual(Poem.objects.count(), 0) + + def test_change_form_deletion_when_invalid(self): + """ + Make sure that a change form that is filled out, but marked for deletion + doesn't cause validation errors. + """ + PoemFormSet = inlineformset_factory(Poet, Poem, can_delete=True) + poet = Poet.objects.create(name='test') + poet.poem_set.create(name='test poem') + data = { + 'poem_set-TOTAL_FORMS': u'1', + 'poem_set-INITIAL_FORMS': u'1', + 'poem_set-MAX_NUM_FORMS': u'0', + 'poem_set-0-id': u'1', + 'poem_set-0-poem': u'1', + 'poem_set-0-name': u'x' * 1000, + } + formset = PoemFormSet(data, instance=poet) + # Make sure this form doesn't pass validation. + self.assertEqual(formset.is_valid(), False) + self.assertEqual(Poem.objects.count(), 1) + + # Then make sure that it *does* pass validation and delete the object, + # even though the data isn't actually valid. + data['poem_set-0-DELETE'] = 'on' + formset = PoemFormSet(data, instance=poet) + self.assertEqual(formset.is_valid(), True) + formset.save() + self.assertEqual(Poem.objects.count(), 0) + + def test_save_new(self): + """ + Make sure inlineformsets respect commit=False + regression for #10750 + """ + # exclude some required field from the forms + ChildFormSet = inlineformset_factory(School, Child, exclude=['father', 'mother']) + school = School.objects.create(name=u'test') + mother = Parent.objects.create(name=u'mother') + father = Parent.objects.create(name=u'father') + data = { + 'child_set-TOTAL_FORMS': u'1', + 'child_set-INITIAL_FORMS': u'0', + 'child_set-MAX_NUM_FORMS': u'0', + 'child_set-0-name': u'child', + } + formset = ChildFormSet(data, instance=school) + self.assertEqual(formset.is_valid(), True) + objects = formset.save(commit=False) + for obj in objects: + obj.mother = mother + obj.father = father + obj.save() + self.assertEqual(school.child_set.count(), 1) + + +class InlineFormsetFactoryTest(TestCase): + def assertRaisesErrorWithMessage(self, error, message, callable, *args, **kwargs): + self.assertRaises(error, callable, *args, **kwargs) + try: + callable(*args, **kwargs) + except error, e: + self.assertEqual(message, str(e)) + + def test_inline_formset_factory(self): + """ + These should both work without a problem. + """ + inlineformset_factory(Parent, Child, fk_name='mother') + inlineformset_factory(Parent, Child, fk_name='father') + + def test_exception_on_unspecified_foreign_key(self): + """ + Child has two ForeignKeys to Parent, so if we don't specify which one + to use for the inline formset, we should get an exception. + """ + self.assertRaisesErrorWithMessage(Exception, + "<class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>", + inlineformset_factory, Parent, Child + ) + + def test_fk_name_not_foreign_key_field_from_child(self): + """ + If we specify fk_name, but it isn't a ForeignKey from the child model + to the parent model, we should get an exception. + """ + self.assertRaisesErrorWithMessage(Exception, + "fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>", + inlineformset_factory, Parent, Child, fk_name='school' + ) + + def test_non_foreign_key_field(self): + """ + If the field specified in fk_name is not a ForeignKey, we should get an + exception. + """ + self.assertRaisesErrorWithMessage(Exception, + "<class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'", + inlineformset_factory, Parent, Child, fk_name='test' + ) + + def test_any_iterable_allowed_as_argument_to_exclude(self): + # Regression test for #9171. + inlineformset_factory( + Parent, Child, exclude=['school'], fk_name='mother' + ) + + inlineformset_factory( + Parent, Child, exclude=('school',), fk_name='mother' + ) diff --git a/parts/django/tests/regressiontests/introspection/__init__.py b/parts/django/tests/regressiontests/introspection/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/introspection/__init__.py diff --git a/parts/django/tests/regressiontests/introspection/models.py b/parts/django/tests/regressiontests/introspection/models.py new file mode 100644 index 0000000..ef485e3 --- /dev/null +++ b/parts/django/tests/regressiontests/introspection/models.py @@ -0,0 +1,21 @@ +from django.db import models + +class Reporter(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + email = models.EmailField() + facebook_user_id = models.BigIntegerField() + + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name) + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateField() + reporter = models.ForeignKey(Reporter) + + def __unicode__(self): + return self.headline + + class Meta: + ordering = ('headline',)
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/introspection/tests.py b/parts/django/tests/regressiontests/introspection/tests.py new file mode 100644 index 0000000..2a81aa8 --- /dev/null +++ b/parts/django/tests/regressiontests/introspection/tests.py @@ -0,0 +1,111 @@ +from django.conf import settings +from django.db import connection, DEFAULT_DB_ALIAS +from django.test import TestCase +from django.utils import functional + +from models import Reporter, Article + +# +# The introspection module is optional, so methods tested here might raise +# NotImplementedError. This is perfectly acceptable behavior for the backend +# in question, but the tests need to handle this without failing. Ideally we'd +# skip these tests, but until #4788 is done we'll just ignore them. +# +# The easiest way to accomplish this is to decorate every test case with a +# wrapper that ignores the exception. +# +# The metaclass is just for fun. +# + +def ignore_not_implemented(func): + def _inner(*args, **kwargs): + try: + return func(*args, **kwargs) + except NotImplementedError: + return None + functional.update_wrapper(_inner, func) + return _inner + +class IgnoreNotimplementedError(type): + def __new__(cls, name, bases, attrs): + for k,v in attrs.items(): + if k.startswith('test'): + attrs[k] = ignore_not_implemented(v) + return type.__new__(cls, name, bases, attrs) + +class IntrospectionTests(TestCase): + __metaclass__ = IgnoreNotimplementedError + + def test_table_names(self): + tl = connection.introspection.table_names() + self.assert_(Reporter._meta.db_table in tl, + "'%s' isn't in table_list()." % Reporter._meta.db_table) + self.assert_(Article._meta.db_table in tl, + "'%s' isn't in table_list()." % Article._meta.db_table) + + def test_django_table_names(self): + cursor = connection.cursor() + cursor.execute('CREATE TABLE django_ixn_test_table (id INTEGER);'); + tl = connection.introspection.django_table_names() + cursor.execute("DROP TABLE django_ixn_test_table;") + self.assert_('django_ixn_testcase_table' not in tl, + "django_table_names() returned a non-Django table") + + def test_installed_models(self): + tables = [Article._meta.db_table, Reporter._meta.db_table] + models = connection.introspection.installed_models(tables) + self.assertEqual(models, set([Article, Reporter])) + + def test_sequence_list(self): + sequences = connection.introspection.sequence_list() + expected = {'table': Reporter._meta.db_table, 'column': 'id'} + self.assert_(expected in sequences, + 'Reporter sequence not found in sequence_list()') + + def test_get_table_description_names(self): + cursor = connection.cursor() + desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table) + self.assertEqual([r[0] for r in desc], + [f.column for f in Reporter._meta.fields]) + + def test_get_table_description_types(self): + cursor = connection.cursor() + desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table) + self.assertEqual( + [datatype(r[1], r) for r in desc], + ['IntegerField', 'CharField', 'CharField', 'CharField', 'BigIntegerField'] + ) + + # Regression test for #9991 - 'real' types in postgres + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'].startswith('django.db.backends.postgresql'): + def test_postgresql_real_type(self): + cursor = connection.cursor() + cursor.execute("CREATE TABLE django_ixn_real_test_table (number REAL);") + desc = connection.introspection.get_table_description(cursor, 'django_ixn_real_test_table') + cursor.execute('DROP TABLE django_ixn_real_test_table;') + self.assertEqual(datatype(desc[0][1], desc[0]), 'FloatField') + + def test_get_relations(self): + cursor = connection.cursor() + relations = connection.introspection.get_relations(cursor, Article._meta.db_table) + + # Older versions of MySQL don't have the chops to report on this stuff, + # so just skip it if no relations come back. If they do, though, we + # should test that the response is correct. + if relations: + # That's {field_index: (field_index_other_table, other_table)} + self.assertEqual(relations, {3: (0, Reporter._meta.db_table)}) + + def test_get_indexes(self): + cursor = connection.cursor() + indexes = connection.introspection.get_indexes(cursor, Article._meta.db_table) + self.assertEqual(indexes['reporter_id'], {'unique': False, 'primary_key': False}) + + +def datatype(dbtype, description): + """Helper to convert a data type into a string.""" + dt = connection.introspection.get_field_type(dbtype, description) + if type(dt) is tuple: + return dt[0] + else: + return dt diff --git a/parts/django/tests/regressiontests/locale/de/LC_MESSAGES/django.mo b/parts/django/tests/regressiontests/locale/de/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 0000000..2ee860a --- /dev/null +++ b/parts/django/tests/regressiontests/locale/de/LC_MESSAGES/django.mo diff --git a/parts/django/tests/regressiontests/locale/de/LC_MESSAGES/django.po b/parts/django/tests/regressiontests/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..356e3d3 --- /dev/null +++ b/parts/django/tests/regressiontests/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-02-14 17:33+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: models.py:3 +msgid "Date/time" +msgstr "Datum/Zeit (PROJECT)" diff --git a/parts/django/tests/regressiontests/localflavor/__init__.py b/parts/django/tests/regressiontests/localflavor/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/localflavor/__init__.py diff --git a/parts/django/tests/regressiontests/localflavor/models.py b/parts/django/tests/regressiontests/localflavor/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/localflavor/models.py diff --git a/parts/django/tests/regressiontests/localflavor/tests.py b/parts/django/tests/regressiontests/localflavor/tests.py new file mode 100644 index 0000000..6968236 --- /dev/null +++ b/parts/django/tests/regressiontests/localflavor/tests.py @@ -0,0 +1,5 @@ +import unittest +from django.test import TestCase + +# just import your tests here +from us.tests import * diff --git a/parts/django/tests/regressiontests/localflavor/us/__init__.py b/parts/django/tests/regressiontests/localflavor/us/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/localflavor/us/__init__.py diff --git a/parts/django/tests/regressiontests/localflavor/us/forms.py b/parts/django/tests/regressiontests/localflavor/us/forms.py new file mode 100644 index 0000000..9b77e10 --- /dev/null +++ b/parts/django/tests/regressiontests/localflavor/us/forms.py @@ -0,0 +1,7 @@ +from django.forms import ModelForm +from models import USPlace + +class USPlaceForm(ModelForm): + """docstring for PlaceForm""" + class Meta: + model = USPlace diff --git a/parts/django/tests/regressiontests/localflavor/us/models.py b/parts/django/tests/regressiontests/localflavor/us/models.py new file mode 100644 index 0000000..a8a4cf0 --- /dev/null +++ b/parts/django/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/parts/django/tests/regressiontests/localflavor/us/tests.py b/parts/django/tests/regressiontests/localflavor/us/tests.py new file mode 100644 index 0000000..07fe057 --- /dev/null +++ b/parts/django/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/parts/django/tests/regressiontests/m2m_regress/__init__.py b/parts/django/tests/regressiontests/m2m_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/m2m_regress/__init__.py diff --git a/parts/django/tests/regressiontests/m2m_regress/models.py b/parts/django/tests/regressiontests/m2m_regress/models.py new file mode 100644 index 0000000..1c2126d --- /dev/null +++ b/parts/django/tests/regressiontests/m2m_regress/models.py @@ -0,0 +1,58 @@ +from django.db import models +from django.contrib.auth import models as auth + +# No related name is needed here, since symmetrical relations are not +# explicitly reversible. +class SelfRefer(models.Model): + name = models.CharField(max_length=10) + references = models.ManyToManyField('self') + related = models.ManyToManyField('self') + + def __unicode__(self): + return self.name + +class Tag(models.Model): + name = models.CharField(max_length=10) + + def __unicode__(self): + return self.name + +# Regression for #11956 -- a many to many to the base class +class TagCollection(Tag): + tags = models.ManyToManyField(Tag, related_name='tag_collections') + + def __unicode__(self): + return self.name + +# A related_name is required on one of the ManyToManyField entries here because +# they are both addressable as reverse relations from Tag. +class Entry(models.Model): + name = models.CharField(max_length=10) + topics = models.ManyToManyField(Tag) + related = models.ManyToManyField(Tag, related_name="similar") + + def __unicode__(self): + return self.name + +# Two models both inheriting from a base model with a self-referential m2m field +class SelfReferChild(SelfRefer): + pass + +class SelfReferChildSibling(SelfRefer): + pass + +# Many-to-Many relation between models, where one of the PK's isn't an Autofield +class Line(models.Model): + name = models.CharField(max_length=100) + +class Worksheet(models.Model): + id = models.CharField(primary_key=True, max_length=100) + lines = models.ManyToManyField(Line, blank=True, null=True) + +# Regression for #11226 -- A model with the same name that another one to +# which it has a m2m relation. This shouldn't cause a name clash between +# the automatically created m2m intermediary table FK field names when +# running syncdb +class User(models.Model): + name = models.CharField(max_length=30) + friends = models.ManyToManyField(auth.User) diff --git a/parts/django/tests/regressiontests/m2m_regress/tests.py b/parts/django/tests/regressiontests/m2m_regress/tests.py new file mode 100644 index 0000000..7e5e5c3 --- /dev/null +++ b/parts/django/tests/regressiontests/m2m_regress/tests.py @@ -0,0 +1,82 @@ +from django.core.exceptions import FieldError +from django.test import TestCase + +from models import (SelfRefer, Tag, TagCollection, Entry, SelfReferChild, + SelfReferChildSibling, Worksheet) + + +class M2MRegressionTests(TestCase): + def assertRaisesErrorWithMessage(self, error, message, callable, *args, **kwargs): + self.assertRaises(error, callable, *args, **kwargs) + try: + callable(*args, **kwargs) + except error, e: + self.assertEqual(message, str(e)) + + def test_multiple_m2m(self): + # Multiple m2m references to model must be distinguished when + # accessing the relations through an instance attribute. + + s1 = SelfRefer.objects.create(name='s1') + s2 = SelfRefer.objects.create(name='s2') + s3 = SelfRefer.objects.create(name='s3') + s1.references.add(s2) + s1.related.add(s3) + + e1 = Entry.objects.create(name='e1') + t1 = Tag.objects.create(name='t1') + t2 = Tag.objects.create(name='t2') + + e1.topics.add(t1) + e1.related.add(t2) + + self.assertQuerysetEqual(s1.references.all(), ["<SelfRefer: s2>"]) + self.assertQuerysetEqual(s1.related.all(), ["<SelfRefer: s3>"]) + + self.assertQuerysetEqual(e1.topics.all(), ["<Tag: t1>"]) + self.assertQuerysetEqual(e1.related.all(), ["<Tag: t2>"]) + + def test_internal_related_name_not_in_error_msg(self): + # The secret internal related names for self-referential many-to-many + # fields shouldn't appear in the list when an error is made. + + self.assertRaisesErrorWithMessage(FieldError, + "Cannot resolve keyword 'porcupine' into field. Choices are: id, name, references, related, selfreferchild, selfreferchildsibling", + lambda: SelfRefer.objects.filter(porcupine='fred') + ) + + def test_m2m_inheritance_symmetry(self): + # Test to ensure that the relationship between two inherited models + # with a self-referential m2m field maintains symmetry + + sr_child = SelfReferChild(name="Hanna") + sr_child.save() + + sr_sibling = SelfReferChildSibling(name="Beth") + sr_sibling.save() + sr_child.related.add(sr_sibling) + + self.assertQuerysetEqual(sr_child.related.all(), ["<SelfRefer: Beth>"]) + self.assertQuerysetEqual(sr_sibling.related.all(), ["<SelfRefer: Hanna>"]) + + def test_m2m_pk_field_type(self): + # Regression for #11311 - The primary key for models in a m2m relation + # doesn't have to be an AutoField + + w = Worksheet(id='abc') + w.save() + w.delete() + + def test_add_m2m_with_base_class(self): + # Regression for #11956 -- You can add an object to a m2m with the + # base class without causing integrity errors + + t1 = Tag.objects.create(name='t1') + t2 = Tag.objects.create(name='t2') + + c1 = TagCollection.objects.create(name='c1') + c1.tags = [t1,t2] + c1 = TagCollection.objects.get(name='c1') + + self.assertQuerysetEqual(c1.tags.all(), ["<Tag: t1>", "<Tag: t2>"]) + self.assertQuerysetEqual(t1.tag_collections.all(), ["<TagCollection: c1>"]) diff --git a/parts/django/tests/regressiontests/m2m_through_regress/__init__.py b/parts/django/tests/regressiontests/m2m_through_regress/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/parts/django/tests/regressiontests/m2m_through_regress/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/parts/django/tests/regressiontests/m2m_through_regress/fixtures/m2m_through.json b/parts/django/tests/regressiontests/m2m_through_regress/fixtures/m2m_through.json new file mode 100644 index 0000000..6f24886 --- /dev/null +++ b/parts/django/tests/regressiontests/m2m_through_regress/fixtures/m2m_through.json @@ -0,0 +1,34 @@ +[ + { + "pk": "1", + "model": "m2m_through_regress.person", + "fields": { + "name": "Guido" + } + }, + { + "pk": "1", + "model": "auth.user", + "fields": { + "username": "Guido", + "email": "bdfl@python.org", + "password": "abcde" + } + }, + { + "pk": "1", + "model": "m2m_through_regress.group", + "fields": { + "name": "Python Core Group" + } + }, + { + "pk": "1", + "model": "m2m_through_regress.usermembership", + "fields": { + "user": "1", + "group": "1", + "price": "100" + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/m2m_through_regress/models.py b/parts/django/tests/regressiontests/m2m_through_regress/models.py new file mode 100644 index 0000000..ec87985 --- /dev/null +++ b/parts/django/tests/regressiontests/m2m_through_regress/models.py @@ -0,0 +1,55 @@ +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') + group = models.ForeignKey('Group') + price = models.IntegerField(default=100) + + def __unicode__(self): + return "%s is a member of %s" % (self.person.name, self.group.name) + +# using custom id column to test ticket #11107 +class UserMembership(models.Model): + id = models.AutoField(db_column='usermembership_id', primary_key=True) + user = models.ForeignKey(User) + group = models.ForeignKey('Group') + price = models.IntegerField(default=100) + + def __unicode__(self): + return "%s is a user and member of %s" % (self.user.username, self.group.name) + +class Person(models.Model): + name = models.CharField(max_length=128) + + def __unicode__(self): + return self.name + +class Group(models.Model): + name = models.CharField(max_length=128) + # Membership object defined as a class + members = models.ManyToManyField(Person, through=Membership) + user_members = models.ManyToManyField(User, through='UserMembership') + + def __unicode__(self): + return self.name + +# A set of models that use an non-abstract inherited model as the 'through' model. +class A(models.Model): + a_text = models.CharField(max_length=20) + +class ThroughBase(models.Model): + a = models.ForeignKey(A) + b = models.ForeignKey('B') + +class Through(ThroughBase): + extra = models.CharField(max_length=20) + +class B(models.Model): + b_text = models.CharField(max_length=20) + a_list = models.ManyToManyField(A, through=Through) diff --git a/parts/django/tests/regressiontests/m2m_through_regress/tests.py b/parts/django/tests/regressiontests/m2m_through_regress/tests.py new file mode 100644 index 0000000..2eaf886 --- /dev/null +++ b/parts/django/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/parts/django/tests/regressiontests/mail/__init__.py b/parts/django/tests/regressiontests/mail/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/parts/django/tests/regressiontests/mail/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/parts/django/tests/regressiontests/mail/custombackend.py b/parts/django/tests/regressiontests/mail/custombackend.py new file mode 100644 index 0000000..6b0e15a --- /dev/null +++ b/parts/django/tests/regressiontests/mail/custombackend.py @@ -0,0 +1,15 @@ +"""A custom backend for testing.""" + +from django.core.mail.backends.base import BaseEmailBackend + + +class EmailBackend(BaseEmailBackend): + + def __init__(self, *args, **kwargs): + super(EmailBackend, self).__init__(*args, **kwargs) + self.test_outbox = [] + + def send_messages(self, email_messages): + # Messages are stored in a instance variable for testing. + self.test_outbox.extend(email_messages) + return len(email_messages) diff --git a/parts/django/tests/regressiontests/mail/models.py b/parts/django/tests/regressiontests/mail/models.py new file mode 100644 index 0000000..7ff128f --- /dev/null +++ b/parts/django/tests/regressiontests/mail/models.py @@ -0,0 +1 @@ +# This file intentionally left blank
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/mail/tests.py b/parts/django/tests/regressiontests/mail/tests.py new file mode 100644 index 0000000..877df1c --- /dev/null +++ b/parts/django/tests/regressiontests/mail/tests.py @@ -0,0 +1,375 @@ +# coding: utf-8 +import email +import os +import shutil +import sys +import tempfile +from StringIO import StringIO +from django.conf import settings +from django.core import mail +from django.core.mail import EmailMessage, mail_admins, mail_managers, EmailMultiAlternatives +from django.core.mail import send_mail, send_mass_mail +from django.core.mail.backends.base import BaseEmailBackend +from django.core.mail.backends import console, dummy, locmem, filebased, smtp +from django.core.mail.message import BadHeaderError +from django.test import TestCase +from django.utils.translation import ugettext_lazy + +class MailTests(TestCase): + + def test_ascii(self): + email = EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com']) + message = email.message() + self.assertEqual(message['Subject'].encode(), 'Subject') + self.assertEqual(message.get_payload(), 'Content') + self.assertEqual(message['From'], 'from@example.com') + self.assertEqual(message['To'], 'to@example.com') + + def test_multiple_recipients(self): + email = EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com','other@example.com']) + message = email.message() + self.assertEqual(message['Subject'].encode(), 'Subject') + self.assertEqual(message.get_payload(), 'Content') + self.assertEqual(message['From'], 'from@example.com') + self.assertEqual(message['To'], 'to@example.com, other@example.com') + + def test_header_injection(self): + email = EmailMessage('Subject\nInjection Test', 'Content', 'from@example.com', ['to@example.com']) + self.assertRaises(BadHeaderError, email.message) + email = EmailMessage(ugettext_lazy('Subject\nInjection Test'), 'Content', 'from@example.com', ['to@example.com']) + self.assertRaises(BadHeaderError, email.message) + + def test_space_continuation(self): + """ + Test for space continuation character in long (ascii) subject headers (#7747) + """ + email = EmailMessage('Long subject lines that get wrapped should use a space continuation character to get expected behaviour in Outlook and Thunderbird', 'Content', 'from@example.com', ['to@example.com']) + message = email.message() + self.assertEqual(message['Subject'], 'Long subject lines that get wrapped should use a space continuation\n character to get expected behaviour in Outlook and Thunderbird') + + def test_message_header_overrides(self): + """ + Specifying dates or message-ids in the extra headers overrides the + default values (#9233) + """ + headers = {"date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"} + email = EmailMessage('subject', 'content', 'from@example.com', ['to@example.com'], headers=headers) + self.assertEqual(email.message().as_string(), 'Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\nSubject: subject\nFrom: from@example.com\nTo: to@example.com\ndate: Fri, 09 Nov 2001 01:08:47 -0000\nMessage-ID: foo\n\ncontent') + + def test_empty_admins(self): + """ + Test that mail_admins/mail_managers doesn't connect to the mail server + if there are no recipients (#9383) + """ + old_admins = settings.ADMINS + old_managers = settings.MANAGERS + + settings.ADMINS = settings.MANAGERS = [('nobody','nobody@example.com')] + mail.outbox = [] + mail_admins('hi', 'there') + self.assertEqual(len(mail.outbox), 1) + mail.outbox = [] + mail_managers('hi', 'there') + self.assertEqual(len(mail.outbox), 1) + + settings.ADMINS = settings.MANAGERS = [] + mail.outbox = [] + mail_admins('hi', 'there') + self.assertEqual(len(mail.outbox), 0) + mail.outbox = [] + mail_managers('hi', 'there') + self.assertEqual(len(mail.outbox), 0) + + settings.ADMINS = old_admins + settings.MANAGERS = old_managers + + def test_from_header(self): + """ + Make sure we can manually set the From header (#9214) + """ + email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) + message = email.message() + self.assertEqual(message['From'], 'from@example.com') + + def test_multiple_message_call(self): + """ + Regression for #13259 - Make sure that headers are not changed when + calling EmailMessage.message() + """ + email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) + message = email.message() + self.assertEqual(message['From'], 'from@example.com') + message = email.message() + self.assertEqual(message['From'], 'from@example.com') + + def test_unicode_header(self): + """ + Regression for #11144 - When a to/from/cc header contains unicode, + make sure the email addresses are parsed correctly (especially with + regards to commas) + """ + email = EmailMessage('Subject', 'Content', 'from@example.com', ['"Firstname Sürname" <to@example.com>','other@example.com']) + self.assertEqual(email.message()['To'], '=?utf-8?q?Firstname_S=C3=BCrname?= <to@example.com>, other@example.com') + email = EmailMessage('Subject', 'Content', 'from@example.com', ['"Sürname, Firstname" <to@example.com>','other@example.com']) + self.assertEqual(email.message()['To'], '=?utf-8?q?S=C3=BCrname=2C_Firstname?= <to@example.com>, other@example.com') + + def test_safe_mime_multipart(self): + """ + Make sure headers can be set with a different encoding than utf-8 in + SafeMIMEMultipart as well + """ + headers = {"Date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"} + subject, from_email, to = 'hello', 'from@example.com', '"Sürname, Firstname" <to@example.com>' + text_content = 'This is an important message.' + html_content = '<p>This is an <strong>important</strong> message.</p>' + msg = EmailMultiAlternatives('Message from Firstname Sürname', text_content, from_email, [to], headers=headers) + msg.attach_alternative(html_content, "text/html") + msg.encoding = 'iso-8859-1' + self.assertEqual(msg.message()['To'], '=?iso-8859-1?q?S=FCrname=2C_Firstname?= <to@example.com>') + self.assertEqual(msg.message()['Subject'].encode(), u'=?iso-8859-1?q?Message_from_Firstname_S=FCrname?=') + + def test_encoding(self): + """ + Regression for #12791 - Encode body correctly with other encodings + than utf-8 + """ + email = EmailMessage('Subject', 'Firstname Sürname is a great guy.', 'from@example.com', ['other@example.com']) + email.encoding = 'iso-8859-1' + message = email.message() + self.assertTrue(message.as_string().startswith('Content-Type: text/plain; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\nSubject: Subject\nFrom: from@example.com\nTo: other@example.com')) + self.assertEqual(message.get_payload(), 'Firstname S=FCrname is a great guy.') + + # Make sure MIME attachments also works correctly with other encodings than utf-8 + text_content = 'Firstname Sürname is a great guy.' + html_content = '<p>Firstname Sürname is a <strong>great</strong> guy.</p>' + msg = EmailMultiAlternatives('Subject', text_content, 'from@example.com', ['to@example.com']) + msg.encoding = 'iso-8859-1' + msg.attach_alternative(html_content, "text/html") + self.assertEqual(msg.message().get_payload(0).as_string(), 'Content-Type: text/plain; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\n\nFirstname S=FCrname is a great guy.') + self.assertEqual(msg.message().get_payload(1).as_string(), 'Content-Type: text/html; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\n\n<p>Firstname S=FCrname is a <strong>great</strong> guy.</p>') + + def test_attachments(self): + """Regression test for #9367""" + headers = {"Date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"} + subject, from_email, to = 'hello', 'from@example.com', 'to@example.com' + text_content = 'This is an important message.' + html_content = '<p>This is an <strong>important</strong> message.</p>' + msg = EmailMultiAlternatives(subject, text_content, from_email, [to], headers=headers) + msg.attach_alternative(html_content, "text/html") + msg.attach("an attachment.pdf", "%PDF-1.4.%...", mimetype="application/pdf") + msg_str = msg.message().as_string() + message = email.message_from_string(msg_str) + self.assertTrue(message.is_multipart()) + self.assertEqual(message.get_content_type(), 'multipart/mixed') + self.assertEqual(message.get_default_type(), 'text/plain') + payload = message.get_payload() + self.assertEqual(payload[0].get_content_type(), 'multipart/alternative') + self.assertEqual(payload[1].get_content_type(), 'application/pdf') + + def test_arbitrary_stream(self): + """ + Test that the console backend can be pointed at an arbitrary stream. + """ + s = StringIO() + connection = mail.get_connection('django.core.mail.backends.console.EmailBackend', stream=s) + send_mail('Subject', 'Content', 'from@example.com', ['to@example.com'], connection=connection) + self.assertTrue(s.getvalue().startswith('Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\nSubject: Subject\nFrom: from@example.com\nTo: to@example.com\nDate: ')) + + def test_stdout(self): + """Make sure that the console backend writes to stdout by default""" + old_stdout = sys.stdout + sys.stdout = StringIO() + connection = console.EmailBackend() + email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) + connection.send_messages([email]) + self.assertTrue(sys.stdout.getvalue().startswith('Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\nSubject: Subject\nFrom: from@example.com\nTo: to@example.com\nDate: ')) + sys.stdout = old_stdout + + def test_dummy(self): + """ + Make sure that dummy backends returns correct number of sent messages + """ + connection = dummy.EmailBackend() + email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) + self.assertEqual(connection.send_messages([email, email, email]), 3) + + def test_locmem(self): + """ + Make sure that the locmen backend populates the outbox. + """ + mail.outbox = [] + connection = locmem.EmailBackend() + email1 = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) + email2 = EmailMessage('Subject 2', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) + connection.send_messages([email1, email2]) + self.assertEqual(len(mail.outbox), 2) + self.assertEqual(mail.outbox[0].subject, 'Subject') + self.assertEqual(mail.outbox[1].subject, 'Subject 2') + + # Make sure that multiple locmem connections share mail.outbox + mail.outbox = [] + connection2 = locmem.EmailBackend() + email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) + connection.send_messages([email]) + connection2.send_messages([email]) + self.assertEqual(len(mail.outbox), 2) + + def test_file_backend(self): + tmp_dir = tempfile.mkdtemp() + connection = filebased.EmailBackend(file_path=tmp_dir) + email1 = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) + connection.send_messages([email1]) + self.assertEqual(len(os.listdir(tmp_dir)), 1) + message = email.message_from_file(open(os.path.join(tmp_dir, os.listdir(tmp_dir)[0]))) + self.assertEqual(message.get_content_type(), 'text/plain') + self.assertEqual(message.get('subject'), 'Subject') + self.assertEqual(message.get('from'), 'from@example.com') + self.assertEqual(message.get('to'), 'to@example.com') + connection2 = filebased.EmailBackend(file_path=tmp_dir) + connection2.send_messages([email1]) + self.assertEqual(len(os.listdir(tmp_dir)), 2) + connection.send_messages([email1]) + self.assertEqual(len(os.listdir(tmp_dir)), 2) + email1.connection = filebased.EmailBackend(file_path=tmp_dir) + connection_created = connection.open() + email1.send() + self.assertEqual(len(os.listdir(tmp_dir)), 3) + email1.send() + self.assertEqual(len(os.listdir(tmp_dir)), 3) + connection.close() + shutil.rmtree(tmp_dir) + + def test_arbitrary_keyword(self): + """ + Make sure that get_connection() accepts arbitrary keyword that might be + used with custom backends. + """ + c = mail.get_connection(fail_silently=True, foo='bar') + self.assertTrue(c.fail_silently) + + def test_custom_backend(self): + """Test custom backend defined in this suite.""" + conn = mail.get_connection('regressiontests.mail.custombackend.EmailBackend') + self.assertTrue(hasattr(conn, 'test_outbox')) + email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) + conn.send_messages([email]) + self.assertEqual(len(conn.test_outbox), 1) + + def test_backend_arg(self): + """Test backend argument of mail.get_connection()""" + self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.smtp.EmailBackend'), smtp.EmailBackend)) + self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.locmem.EmailBackend'), locmem.EmailBackend)) + self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.dummy.EmailBackend'), dummy.EmailBackend)) + self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.console.EmailBackend'), console.EmailBackend)) + tmp_dir = tempfile.mkdtemp() + self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.filebased.EmailBackend', file_path=tmp_dir), filebased.EmailBackend)) + shutil.rmtree(tmp_dir) + self.assertTrue(isinstance(mail.get_connection(), locmem.EmailBackend)) + + def test_connection_arg(self): + """Test connection argument to send_mail(), et. al.""" + connection = mail.get_connection('django.core.mail.backends.locmem.EmailBackend') + + mail.outbox = [] + send_mail('Subject', 'Content', 'from@example.com', ['to@example.com'], connection=connection) + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0] + self.assertEqual(message.subject, 'Subject') + self.assertEqual(message.from_email, 'from@example.com') + self.assertEqual(message.to, ['to@example.com']) + + mail.outbox = [] + send_mass_mail([ + ('Subject1', 'Content1', 'from1@example.com', ['to1@example.com']), + ('Subject2', 'Content2', 'from2@example.com', ['to2@example.com']) + ], connection=connection) + self.assertEqual(len(mail.outbox), 2) + message = mail.outbox[0] + self.assertEqual(message.subject, 'Subject1') + self.assertEqual(message.from_email, 'from1@example.com') + self.assertEqual(message.to, ['to1@example.com']) + message = mail.outbox[1] + self.assertEqual(message.subject, 'Subject2') + self.assertEqual(message.from_email, 'from2@example.com') + self.assertEqual(message.to, ['to2@example.com']) + + old_admins = settings.ADMINS + old_managers = settings.MANAGERS + settings.ADMINS = settings.MANAGERS = [('nobody','nobody@example.com')] + + mail.outbox = [] + mail_admins('Subject', 'Content', connection=connection) + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0] + self.assertEqual(message.subject, '[Django] Subject') + self.assertEqual(message.from_email, 'root@localhost') + self.assertEqual(message.to, ['nobody@example.com']) + + mail.outbox = [] + mail_managers('Subject', 'Content', connection=connection) + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0] + self.assertEqual(message.subject, '[Django] Subject') + self.assertEqual(message.from_email, 'root@localhost') + self.assertEqual(message.to, ['nobody@example.com']) + + settings.ADMINS = old_admins + settings.MANAGERS = old_managers + + def test_mail_prefix(self): + """Test prefix argument in manager/admin mail.""" + # Regression for #13494. + old_admins = settings.ADMINS + old_managers = settings.MANAGERS + settings.ADMINS = settings.MANAGERS = [('nobody','nobody@example.com')] + + mail_managers(ugettext_lazy('Subject'), 'Content') + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0] + self.assertEqual(message.subject, '[Django] Subject') + + mail.outbox = [] + mail_admins(ugettext_lazy('Subject'), 'Content') + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0] + self.assertEqual(message.subject, '[Django] Subject') + + settings.ADMINS = old_admins + settings.MANAGERS = old_managers + + def test_idn_validation(self): + """Test internationalized email adresses""" + # Regression for #14301. + mail.outbox = [] + from_email = u'fröm@öäü.com' + to_email = u'tö@öäü.com' + connection = mail.get_connection('django.core.mail.backends.locmem.EmailBackend') + send_mail('Subject', 'Content', from_email, [to_email], connection=connection) + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0] + self.assertEqual(message.subject, 'Subject') + self.assertEqual(message.from_email, from_email) + self.assertEqual(message.to, [to_email]) + self.assertTrue(message.message().as_string().startswith('Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\nSubject: Subject\nFrom: =?utf-8?b?ZnLDtm1Aw7bDpMO8LmNvbQ==?=\nTo: =?utf-8?b?dMO2QMO2w6TDvC5jb20=?=')) + + def test_idn_smtp_send(self): + import smtplib + smtplib.SMTP = MockSMTP + from_email = u'fröm@öäü.com' + to_email = u'tö@öäü.com' + connection = mail.get_connection('django.core.mail.backends.smtp.EmailBackend') + self.assertTrue(send_mail('Subject', 'Content', from_email, [to_email], connection=connection)) + +class MockSMTP(object): + def __init__(self, host='', port=0, local_hostname=None, + timeout=1): + pass + + def sendmail(self, from_addr, to_addrs, msg, mail_options=[], + rcpt_options=[]): + for addr in to_addrs: + str(addr.split('@', 1)[-1]) + return {} + + def quit(self): + return 0 diff --git a/parts/django/tests/regressiontests/makemessages/__init__.py b/parts/django/tests/regressiontests/makemessages/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/makemessages/__init__.py diff --git a/parts/django/tests/regressiontests/makemessages/extraction.py b/parts/django/tests/regressiontests/makemessages/extraction.py new file mode 100644 index 0000000..6df6e90 --- /dev/null +++ b/parts/django/tests/regressiontests/makemessages/extraction.py @@ -0,0 +1,109 @@ +import os +import re +import shutil +from django.test import TestCase +from django.core import management + +LOCALE='de' + +class ExtractorTests(TestCase): + + PO_FILE='locale/%s/LC_MESSAGES/django.po' % LOCALE + + def setUp(self): + self._cwd = os.getcwd() + self.test_dir = os.path.abspath(os.path.dirname(__file__)) + + def _rmrf(self, dname): + if os.path.commonprefix([self.test_dir, os.path.abspath(dname)]) != self.test_dir: + return + shutil.rmtree(dname) + + def tearDown(self): + os.chdir(self.test_dir) + try: + self._rmrf('locale/%s' % LOCALE) + except OSError: + pass + os.chdir(self._cwd) + + def assertMsgId(self, msgid, s): + return self.assert_(re.search('^msgid "%s"' % msgid, s, re.MULTILINE)) + + def assertNotMsgId(self, msgid, s): + return self.assert_(not re.search('^msgid "%s"' % msgid, s, re.MULTILINE)) + + +class TemplateExtractorTests(ExtractorTests): + + def test_templatize(self): + os.chdir(self.test_dir) + management.call_command('makemessages', locale=LOCALE, verbosity=0) + self.assert_(os.path.exists(self.PO_FILE)) + po_contents = open(self.PO_FILE, 'r').read() + self.assertMsgId('I think that 100%% is more that 50%% of anything.', po_contents) + self.assertMsgId('I think that 100%% is more that 50%% of %\(obj\)s.', po_contents) + + +class JavascriptExtractorTests(ExtractorTests): + + PO_FILE='locale/%s/LC_MESSAGES/djangojs.po' % LOCALE + + def test_javascript_literals(self): + os.chdir(self.test_dir) + management.call_command('makemessages', domain='djangojs', locale=LOCALE, verbosity=0) + self.assert_(os.path.exists(self.PO_FILE)) + po_contents = open(self.PO_FILE, 'r').read() + self.assertMsgId('This literal should be included.', po_contents) + self.assertMsgId('This one as well.', po_contents) + + +class IgnoredExtractorTests(ExtractorTests): + + def test_ignore_option(self): + os.chdir(self.test_dir) + management.call_command('makemessages', locale=LOCALE, verbosity=0, ignore_patterns=['ignore_dir/*']) + self.assert_(os.path.exists(self.PO_FILE)) + po_contents = open(self.PO_FILE, 'r').read() + self.assertMsgId('This literal should be included.', po_contents) + self.assertNotMsgId('This should be ignored.', po_contents) + + +class SymlinkExtractorTests(ExtractorTests): + + def setUp(self): + self._cwd = os.getcwd() + self.test_dir = os.path.abspath(os.path.dirname(__file__)) + self.symlinked_dir = os.path.join(self.test_dir, 'templates_symlinked') + + def tearDown(self): + super(SymlinkExtractorTests, self).tearDown() + os.chdir(self.test_dir) + try: + os.remove(self.symlinked_dir) + except OSError: + pass + os.chdir(self._cwd) + + def test_symlink(self): + if hasattr(os, 'symlink'): + if os.path.exists(self.symlinked_dir): + self.assert_(os.path.islink(self.symlinked_dir)) + else: + os.symlink(os.path.join(self.test_dir, 'templates'), self.symlinked_dir) + os.chdir(self.test_dir) + management.call_command('makemessages', locale=LOCALE, verbosity=0, symlinks=True) + self.assert_(os.path.exists(self.PO_FILE)) + po_contents = open(self.PO_FILE, 'r').read() + self.assertMsgId('This literal should be included.', po_contents) + self.assert_('templates_symlinked/test.html' in po_contents) + + +class CopyPluralFormsExtractorTests(ExtractorTests): + + def test_copy_plural_forms(self): + os.chdir(self.test_dir) + management.call_command('makemessages', locale=LOCALE, verbosity=0) + self.assert_(os.path.exists(self.PO_FILE)) + po_contents = open(self.PO_FILE, 'r').read() + self.assert_('Plural-Forms: nplurals=2; plural=(n != 1)' in po_contents) diff --git a/parts/django/tests/regressiontests/makemessages/ignore_dir/ignored.html b/parts/django/tests/regressiontests/makemessages/ignore_dir/ignored.html new file mode 100644 index 0000000..6a29678 --- /dev/null +++ b/parts/django/tests/regressiontests/makemessages/ignore_dir/ignored.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% trans "This should be ignored." %} diff --git a/parts/django/tests/regressiontests/makemessages/javascript.js b/parts/django/tests/regressiontests/makemessages/javascript.js new file mode 100644 index 0000000..bc5ec87 --- /dev/null +++ b/parts/django/tests/regressiontests/makemessages/javascript.js @@ -0,0 +1,4 @@ +// ' +gettext('This literal should be included.') +// ' +gettext('This one as well.') diff --git a/parts/django/tests/regressiontests/makemessages/locale/dummy b/parts/django/tests/regressiontests/makemessages/locale/dummy new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/makemessages/locale/dummy diff --git a/parts/django/tests/regressiontests/makemessages/models.py b/parts/django/tests/regressiontests/makemessages/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/makemessages/models.py diff --git a/parts/django/tests/regressiontests/makemessages/templates/test.html b/parts/django/tests/regressiontests/makemessages/templates/test.html new file mode 100644 index 0000000..96438e1 --- /dev/null +++ b/parts/django/tests/regressiontests/makemessages/templates/test.html @@ -0,0 +1,4 @@ +{% load i18n %} +{% trans "This literal should be included." %} +{% blocktrans %}I think that 100% is more that 50% of anything.{% endblocktrans %} +{% blocktrans with 'txt' as obj %}I think that 100% is more that 50% of {{ obj }}.{% endblocktrans %}
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/makemessages/tests.py b/parts/django/tests/regressiontests/makemessages/tests.py new file mode 100644 index 0000000..5798e67 --- /dev/null +++ b/parts/django/tests/regressiontests/makemessages/tests.py @@ -0,0 +1,40 @@ +import os +import re +from subprocess import Popen, PIPE + +def find_command(cmd, path=None, pathext=None): + if path is None: + path = os.environ.get('PATH', []).split(os.pathsep) + if isinstance(path, basestring): + path = [path] + # check if there are funny path extensions for executables, e.g. Windows + if pathext is None: + pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD').split(os.pathsep) + # don't use extensions if the command ends with one of them + for ext in pathext: + if cmd.endswith(ext): + pathext = [''] + break + # check if we find the command on PATH + for p in path: + f = os.path.join(p, cmd) + if os.path.isfile(f): + return f + for ext in pathext: + fext = f + ext + if os.path.isfile(fext): + return fext + return None + +# checks if it can find xgettext on the PATH and +# imports the extraction tests if yes +xgettext_cmd = find_command('xgettext') +if xgettext_cmd: + p = Popen('%s --version' % xgettext_cmd, shell=True, stdout=PIPE, stderr=PIPE, close_fds=os.name != 'nt', universal_newlines=True) + output = p.communicate()[0] + match = re.search(r'(?P<major>\d+)\.(?P<minor>\d+)', output) + if match: + xversion = (int(match.group('major')), int(match.group('minor'))) + if xversion >= (0, 15): + from extraction import * + del p diff --git a/parts/django/tests/regressiontests/managers_regress/__init__.py b/parts/django/tests/regressiontests/managers_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/managers_regress/__init__.py diff --git a/parts/django/tests/regressiontests/managers_regress/models.py b/parts/django/tests/regressiontests/managers_regress/models.py new file mode 100644 index 0000000..1e1b1c9 --- /dev/null +++ b/parts/django/tests/regressiontests/managers_regress/models.py @@ -0,0 +1,100 @@ +""" +Various edge-cases for model managers. +""" + +from django.db import models + +class OnlyFred(models.Manager): + def get_query_set(self): + return super(OnlyFred, self).get_query_set().filter(name='fred') + +class OnlyBarney(models.Manager): + def get_query_set(self): + return super(OnlyBarney, self).get_query_set().filter(name='barney') + +class Value42(models.Manager): + def get_query_set(self): + return super(Value42, self).get_query_set().filter(value=42) + +class AbstractBase1(models.Model): + name = models.CharField(max_length=50) + + class Meta: + abstract = True + + # Custom managers + manager1 = OnlyFred() + manager2 = OnlyBarney() + objects = models.Manager() + +class AbstractBase2(models.Model): + value = models.IntegerField() + + class Meta: + abstract = True + + # Custom manager + restricted = Value42() + +# No custom manager on this class to make sure the default case doesn't break. +class AbstractBase3(models.Model): + comment = models.CharField(max_length=50) + + class Meta: + abstract = True + +class Parent(models.Model): + name = models.CharField(max_length=50) + + manager = OnlyFred() + + def __unicode__(self): + return self.name + +# Managers from base classes are inherited and, if no manager is specified +# *and* the parent has a manager specified, the first one (in the MRO) will +# become the default. +class Child1(AbstractBase1): + data = models.CharField(max_length=25) + + def __unicode__(self): + return self.data + +class Child2(AbstractBase1, AbstractBase2): + data = models.CharField(max_length=25) + + def __unicode__(self): + return self.data + +class Child3(AbstractBase1, AbstractBase3): + data = models.CharField(max_length=25) + + def __unicode__(self): + return self.data + +class Child4(AbstractBase1): + data = models.CharField(max_length=25) + + # Should be the default manager, although the parent managers are + # inherited. + default = models.Manager() + + def __unicode__(self): + return self.data + +class Child5(AbstractBase3): + name = models.CharField(max_length=25) + + default = OnlyFred() + objects = models.Manager() + + def __unicode__(self): + return self.name + +# Will inherit managers from AbstractBase1, but not Child4. +class Child6(Child4): + value = models.IntegerField() + +# Will not inherit default manager from parent. +class Child7(Parent): + pass diff --git a/parts/django/tests/regressiontests/managers_regress/tests.py b/parts/django/tests/regressiontests/managers_regress/tests.py new file mode 100644 index 0000000..9a6db61 --- /dev/null +++ b/parts/django/tests/regressiontests/managers_regress/tests.py @@ -0,0 +1,54 @@ +from django.test import TestCase + +from models import Child1, Child2, Child3, Child4, Child5, Child6, Child7 + + +class ManagersRegressionTests(TestCase): + def test_managers(self): + a1 = Child1.objects.create(name='fred', data='a1') + a2 = Child1.objects.create(name='barney', data='a2') + b1 = Child2.objects.create(name='fred', data='b1', value=1) + b2 = Child2.objects.create(name='barney', data='b2', value=42) + c1 = Child3.objects.create(name='fred', data='c1', comment='yes') + c2 = Child3.objects.create(name='barney', data='c2', comment='no') + d1 = Child4.objects.create(name='fred', data='d1') + d2 = Child4.objects.create(name='barney', data='d2') + e1 = Child5.objects.create(name='fred', comment='yes') + e2 = Child5.objects.create(name='barney', comment='no') + f1 = Child6.objects.create(name='fred', data='f1', value=42) + f2 = Child6.objects.create(name='barney', data='f2', value=42) + g1 = Child7.objects.create(name='fred') + g2 = Child7.objects.create(name='barney') + + self.assertQuerysetEqual(Child1.manager1.all(), ["<Child1: a1>"]) + self.assertQuerysetEqual(Child1.manager2.all(), ["<Child1: a2>"]) + self.assertQuerysetEqual(Child1._default_manager.all(), ["<Child1: a1>"]) + + self.assertQuerysetEqual(Child2._default_manager.all(), ["<Child2: b1>"]) + self.assertQuerysetEqual(Child2.restricted.all(), ["<Child2: b2>"]) + + self.assertQuerysetEqual(Child3._default_manager.all(), ["<Child3: c1>"]) + self.assertQuerysetEqual(Child3.manager1.all(), ["<Child3: c1>"]) + self.assertQuerysetEqual(Child3.manager2.all(), ["<Child3: c2>"]) + + # Since Child6 inherits from Child4, the corresponding rows from f1 and + # f2 also appear here. This is the expected result. + self.assertQuerysetEqual(Child4._default_manager.order_by('data'), [ + "<Child4: d1>", + "<Child4: d2>", + "<Child4: f1>", + "<Child4: f2>" + ] + ) + self.assertQuerysetEqual(Child4.manager1.all(), [ + "<Child4: d1>", + "<Child4: f1>" + ] + ) + self.assertQuerysetEqual(Child5._default_manager.all(), ["<Child5: fred>"]) + self.assertQuerysetEqual(Child6._default_manager.all(), ["<Child6: f1>"]) + self.assertQuerysetEqual(Child7._default_manager.order_by('name'), [ + "<Child7: barney>", + "<Child7: fred>" + ] + ) diff --git a/parts/django/tests/regressiontests/many_to_one_regress/__init__.py b/parts/django/tests/regressiontests/many_to_one_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/many_to_one_regress/__init__.py diff --git a/parts/django/tests/regressiontests/many_to_one_regress/models.py b/parts/django/tests/regressiontests/many_to_one_regress/models.py new file mode 100644 index 0000000..53348a7 --- /dev/null +++ b/parts/django/tests/regressiontests/many_to_one_regress/models.py @@ -0,0 +1,46 @@ +""" +Regression tests for a few ForeignKey bugs. +""" + +from django.db import models + +# If ticket #1578 ever slips back in, these models will not be able to be +# created (the field names being lower-cased versions of their opposite +# classes is important here). + +class First(models.Model): + second = models.IntegerField() + +class Second(models.Model): + first = models.ForeignKey(First, related_name = 'the_first') + +# Protect against repetition of #1839, #2415 and #2536. +class Third(models.Model): + name = models.CharField(max_length=20) + third = models.ForeignKey('self', null=True, related_name='child_set') + +class Parent(models.Model): + name = models.CharField(max_length=20) + bestchild = models.ForeignKey('Child', null=True, related_name='favored_by') + +class Child(models.Model): + name = models.CharField(max_length=20) + parent = models.ForeignKey(Parent) + + +# Multiple paths to the same model (#7110, #7125) +class Category(models.Model): + name = models.CharField(max_length=20) + + def __unicode__(self): + return self.name + +class Record(models.Model): + category = models.ForeignKey(Category) + +class Relation(models.Model): + left = models.ForeignKey(Record, related_name='left_set') + right = models.ForeignKey(Record, related_name='right_set') + + def __unicode__(self): + return u"%s - %s" % (self.left.category.name, self.right.category.name) diff --git a/parts/django/tests/regressiontests/many_to_one_regress/tests.py b/parts/django/tests/regressiontests/many_to_one_regress/tests.py new file mode 100644 index 0000000..7d2a49c --- /dev/null +++ b/parts/django/tests/regressiontests/many_to_one_regress/tests.py @@ -0,0 +1,105 @@ +from django.db import models +from django.test import TestCase + +from models import First, Second, Third, Parent, Child, Category, Record, Relation + +class ManyToOneRegressionTests(TestCase): + def test_object_creation(self): + Third.objects.create(id='3', name='An example') + parent = Parent(name='fred') + parent.save() + Child.objects.create(name='bam-bam', parent=parent) + + def test_fk_assignment_and_related_object_cache(self): + # Tests of ForeignKey assignment and the related-object cache (see #6886). + + p = Parent.objects.create(name="Parent") + c = Child.objects.create(name="Child", parent=p) + + # Look up the object again so that we get a "fresh" object. + c = Child.objects.get(name="Child") + p = c.parent + + # Accessing the related object again returns the exactly same object. + self.assertTrue(c.parent is p) + + # But if we kill the cache, we get a new object. + del c._parent_cache + self.assertFalse(c.parent is p) + + # Assigning a new object results in that object getting cached immediately. + p2 = Parent.objects.create(name="Parent 2") + c.parent = p2 + self.assertTrue(c.parent is p2) + + # Assigning None succeeds if field is null=True. + p.bestchild = None + self.assertTrue(p.bestchild is None) + + # bestchild should still be None after saving. + p.save() + self.assertTrue(p.bestchild is None) + + # bestchild should still be None after fetching the object again. + p = Parent.objects.get(name="Parent") + self.assertTrue(p.bestchild is None) + + # Assigning None fails: Child.parent is null=False. + self.assertRaises(ValueError, setattr, c, "parent", None) + + # You also can't assign an object of the wrong type here + self.assertRaises(ValueError, setattr, c, "parent", First(id=1, second=1)) + + # Nor can you explicitly assign None to Child.parent during object + # creation (regression for #9649). + self.assertRaises(ValueError, Child, name='xyzzy', parent=None) + self.assertRaises(ValueError, Child.objects.create, name='xyzzy', parent=None) + + # Creation using keyword argument should cache the related object. + p = Parent.objects.get(name="Parent") + c = Child(parent=p) + self.assertTrue(c.parent is p) + + # Creation using keyword argument and unsaved related instance (#8070). + p = Parent() + c = Child(parent=p) + self.assertTrue(c.parent is p) + + # Creation using attname keyword argument and an id will cause the + # related object to be fetched. + p = Parent.objects.get(name="Parent") + c = Child(parent_id=p.id) + self.assertFalse(c.parent is p) + self.assertEqual(c.parent, p) + + def test_multiple_foreignkeys(self): + # Test of multiple ForeignKeys to the same model (bug #7125). + c1 = Category.objects.create(name='First') + c2 = Category.objects.create(name='Second') + c3 = Category.objects.create(name='Third') + r1 = Record.objects.create(category=c1) + r2 = Record.objects.create(category=c1) + r3 = Record.objects.create(category=c2) + r4 = Record.objects.create(category=c2) + r5 = Record.objects.create(category=c3) + r = Relation.objects.create(left=r1, right=r2) + r = Relation.objects.create(left=r3, right=r4) + r = Relation.objects.create(left=r1, right=r3) + r = Relation.objects.create(left=r5, right=r2) + r = Relation.objects.create(left=r3, right=r2) + + q1 = Relation.objects.filter(left__category__name__in=['First'], right__category__name__in=['Second']) + self.assertQuerysetEqual(q1, ["<Relation: First - Second>"]) + + q2 = Category.objects.filter(record__left_set__right__category__name='Second').order_by('name') + self.assertQuerysetEqual(q2, ["<Category: First>", "<Category: Second>"]) + + p = Parent.objects.create(name="Parent") + c = Child.objects.create(name="Child", parent=p) + self.assertRaises(ValueError, Child.objects.create, name="Grandchild", parent=c) + + def test_fk_instantiation_outside_model(self): + # Regression for #12190 -- Should be able to instantiate a FK outside + # of a model, and interrogate its related field. + cat = models.ForeignKey(Category) + self.assertEqual('id', cat.rel.get_related_field().name) diff --git a/parts/django/tests/regressiontests/max_lengths/__init__.py b/parts/django/tests/regressiontests/max_lengths/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/parts/django/tests/regressiontests/max_lengths/__init__.py @@ -0,0 +1 @@ + diff --git a/parts/django/tests/regressiontests/max_lengths/models.py b/parts/django/tests/regressiontests/max_lengths/models.py new file mode 100644 index 0000000..78eb30c --- /dev/null +++ b/parts/django/tests/regressiontests/max_lengths/models.py @@ -0,0 +1,13 @@ +from django.db import models + +class PersonWithDefaultMaxLengths(models.Model): + email = models.EmailField() + vcard = models.FileField(upload_to='/tmp') + homepage = models.URLField() + avatar = models.FilePathField() + +class PersonWithCustomMaxLengths(models.Model): + email = models.EmailField(max_length=250) + vcard = models.FileField(upload_to='/tmp', max_length=250) + homepage = models.URLField(max_length=250) + avatar = models.FilePathField(max_length=250) diff --git a/parts/django/tests/regressiontests/max_lengths/tests.py b/parts/django/tests/regressiontests/max_lengths/tests.py new file mode 100644 index 0000000..0fb2f30 --- /dev/null +++ b/parts/django/tests/regressiontests/max_lengths/tests.py @@ -0,0 +1,36 @@ +from unittest import TestCase +from django.db import DatabaseError +from regressiontests.max_lengths.models import PersonWithDefaultMaxLengths, PersonWithCustomMaxLengths + +class MaxLengthArgumentsTests(TestCase): + + def verify_max_length(self, model,field,length): + self.assertEquals(model._meta.get_field(field).max_length,length) + + def test_default_max_lengths(self): + self.verify_max_length(PersonWithDefaultMaxLengths, 'email', 75) + self.verify_max_length(PersonWithDefaultMaxLengths, 'vcard', 100) + self.verify_max_length(PersonWithDefaultMaxLengths, 'homepage', 200) + self.verify_max_length(PersonWithDefaultMaxLengths, 'avatar', 100) + + def test_custom_max_lengths(self): + self.verify_max_length(PersonWithCustomMaxLengths, 'email', 250) + self.verify_max_length(PersonWithCustomMaxLengths, 'vcard', 250) + self.verify_max_length(PersonWithCustomMaxLengths, 'homepage', 250) + self.verify_max_length(PersonWithCustomMaxLengths, 'avatar', 250) + +class MaxLengthORMTests(TestCase): + + def test_custom_max_lengths(self): + args = { + "email": "someone@example.com", + "vcard": "vcard", + "homepage": "http://example.com/", + "avatar": "me.jpg" + } + + for field in ("email", "vcard", "homepage", "avatar"): + new_args = args.copy() + new_args[field] = "X" * 250 # a value longer than any of the default fields could hold. + p = PersonWithCustomMaxLengths.objects.create(**new_args) + self.assertEqual(getattr(p, field), ("X" * 250))
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/middleware/__init__.py b/parts/django/tests/regressiontests/middleware/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/middleware/__init__.py diff --git a/parts/django/tests/regressiontests/middleware/extra_urls.py b/parts/django/tests/regressiontests/middleware/extra_urls.py new file mode 100644 index 0000000..b2a8902 --- /dev/null +++ b/parts/django/tests/regressiontests/middleware/extra_urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import patterns + +urlpatterns = patterns('', + (r'^middleware/customurlconf/noslash$', 'view'), + (r'^middleware/customurlconf/slash/$', 'view'), + (r'^middleware/customurlconf/needsquoting#/$', 'view'), +) diff --git a/parts/django/tests/regressiontests/middleware/models.py b/parts/django/tests/regressiontests/middleware/models.py new file mode 100644 index 0000000..71abcc5 --- /dev/null +++ b/parts/django/tests/regressiontests/middleware/models.py @@ -0,0 +1 @@ +# models.py file for tests to run. diff --git a/parts/django/tests/regressiontests/middleware/tests.py b/parts/django/tests/regressiontests/middleware/tests.py new file mode 100644 index 0000000..b77a2a3 --- /dev/null +++ b/parts/django/tests/regressiontests/middleware/tests.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- + +from django.conf import settings +from django.http import HttpRequest +from django.middleware.common import CommonMiddleware +from django.test import TestCase + + +class CommonMiddlewareTest(TestCase): + def setUp(self): + self.slash = settings.APPEND_SLASH + self.www = settings.PREPEND_WWW + + def tearDown(self): + settings.APPEND_SLASH = self.slash + settings.PREPEND_WWW = self.www + + def _get_request(self, path): + request = HttpRequest() + request.META = { + 'SERVER_NAME': 'testserver', + 'SERVER_PORT': 80, + } + request.path = request.path_info = "/middleware/%s" % path + return request + + def test_append_slash_have_slash(self): + """ + Tests that URLs with slashes go unmolested. + """ + settings.APPEND_SLASH = True + request = self._get_request('slash/') + self.assertEquals(CommonMiddleware().process_request(request), None) + + def test_append_slash_slashless_resource(self): + """ + Tests that matches to explicit slashless URLs go unmolested. + """ + settings.APPEND_SLASH = True + request = self._get_request('noslash') + self.assertEquals(CommonMiddleware().process_request(request), None) + + def test_append_slash_slashless_unknown(self): + """ + Tests that APPEND_SLASH doesn't redirect to unknown resources. + """ + settings.APPEND_SLASH = True + request = self._get_request('unknown') + self.assertEquals(CommonMiddleware().process_request(request), None) + + def test_append_slash_redirect(self): + """ + Tests that APPEND_SLASH redirects slashless URLs to a valid pattern. + """ + settings.APPEND_SLASH = True + request = self._get_request('slash') + r = CommonMiddleware().process_request(request) + self.assertEquals(r.status_code, 301) + self.assertEquals(r['Location'], 'http://testserver/middleware/slash/') + + def test_append_slash_no_redirect_on_POST_in_DEBUG(self): + """ + Tests that while in debug mode, an exception is raised with a warning + when a failed attempt is made to POST to an URL which would normally be + redirected to a slashed version. + """ + settings.APPEND_SLASH = True + settings.DEBUG = True + request = self._get_request('slash') + request.method = 'POST' + self.assertRaises( + RuntimeError, + CommonMiddleware().process_request, + request) + try: + CommonMiddleware().process_request(request) + except RuntimeError, e: + self.assertTrue('end in a slash' in str(e)) + settings.DEBUG = False + + def test_append_slash_disabled(self): + """ + Tests disabling append slash functionality. + """ + settings.APPEND_SLASH = False + request = self._get_request('slash') + self.assertEquals(CommonMiddleware().process_request(request), None) + + def test_append_slash_quoted(self): + """ + Tests that URLs which require quoting are redirected to their slash + version ok. + """ + settings.APPEND_SLASH = True + request = self._get_request('needsquoting#') + r = CommonMiddleware().process_request(request) + self.assertEquals(r.status_code, 301) + self.assertEquals( + r['Location'], + 'http://testserver/middleware/needsquoting%23/') + + def test_prepend_www(self): + settings.PREPEND_WWW = True + settings.APPEND_SLASH = False + request = self._get_request('path/') + r = CommonMiddleware().process_request(request) + self.assertEquals(r.status_code, 301) + self.assertEquals( + r['Location'], + 'http://www.testserver/middleware/path/') + + def test_prepend_www_append_slash_have_slash(self): + settings.PREPEND_WWW = True + settings.APPEND_SLASH = True + request = self._get_request('slash/') + r = CommonMiddleware().process_request(request) + self.assertEquals(r.status_code, 301) + self.assertEquals(r['Location'], + 'http://www.testserver/middleware/slash/') + + def test_prepend_www_append_slash_slashless(self): + settings.PREPEND_WWW = True + settings.APPEND_SLASH = True + request = self._get_request('slash') + r = CommonMiddleware().process_request(request) + self.assertEquals(r.status_code, 301) + self.assertEquals(r['Location'], + 'http://www.testserver/middleware/slash/') + + + # The following tests examine expected behavior given a custom urlconf that + # overrides the default one through the request object. + + def test_append_slash_have_slash_custom_urlconf(self): + """ + Tests that URLs with slashes go unmolested. + """ + settings.APPEND_SLASH = True + request = self._get_request('customurlconf/slash/') + request.urlconf = 'regressiontests.middleware.extra_urls' + self.assertEquals(CommonMiddleware().process_request(request), None) + + def test_append_slash_slashless_resource_custom_urlconf(self): + """ + Tests that matches to explicit slashless URLs go unmolested. + """ + settings.APPEND_SLASH = True + request = self._get_request('customurlconf/noslash') + request.urlconf = 'regressiontests.middleware.extra_urls' + self.assertEquals(CommonMiddleware().process_request(request), None) + + def test_append_slash_slashless_unknown_custom_urlconf(self): + """ + Tests that APPEND_SLASH doesn't redirect to unknown resources. + """ + settings.APPEND_SLASH = True + request = self._get_request('customurlconf/unknown') + request.urlconf = 'regressiontests.middleware.extra_urls' + self.assertEquals(CommonMiddleware().process_request(request), None) + + def test_append_slash_redirect_custom_urlconf(self): + """ + Tests that APPEND_SLASH redirects slashless URLs to a valid pattern. + """ + settings.APPEND_SLASH = True + request = self._get_request('customurlconf/slash') + request.urlconf = 'regressiontests.middleware.extra_urls' + r = CommonMiddleware().process_request(request) + self.assertFalse(r is None, + "CommonMiddlware failed to return APPEND_SLASH redirect using request.urlconf") + self.assertEquals(r.status_code, 301) + self.assertEquals(r['Location'], 'http://testserver/middleware/customurlconf/slash/') + + def test_append_slash_no_redirect_on_POST_in_DEBUG_custom_urlconf(self): + """ + Tests that while in debug mode, an exception is raised with a warning + when a failed attempt is made to POST to an URL which would normally be + redirected to a slashed version. + """ + settings.APPEND_SLASH = True + settings.DEBUG = True + request = self._get_request('customurlconf/slash') + request.urlconf = 'regressiontests.middleware.extra_urls' + request.method = 'POST' + self.assertRaises( + RuntimeError, + CommonMiddleware().process_request, + request) + try: + CommonMiddleware().process_request(request) + except RuntimeError, e: + self.assertTrue('end in a slash' in str(e)) + settings.DEBUG = False + + def test_append_slash_disabled_custom_urlconf(self): + """ + Tests disabling append slash functionality. + """ + settings.APPEND_SLASH = False + request = self._get_request('customurlconf/slash') + request.urlconf = 'regressiontests.middleware.extra_urls' + self.assertEquals(CommonMiddleware().process_request(request), None) + + def test_append_slash_quoted_custom_urlconf(self): + """ + Tests that URLs which require quoting are redirected to their slash + version ok. + """ + settings.APPEND_SLASH = True + request = self._get_request('customurlconf/needsquoting#') + request.urlconf = 'regressiontests.middleware.extra_urls' + r = CommonMiddleware().process_request(request) + self.assertFalse(r is None, + "CommonMiddlware failed to return APPEND_SLASH redirect using request.urlconf") + self.assertEquals(r.status_code, 301) + self.assertEquals( + r['Location'], + 'http://testserver/middleware/customurlconf/needsquoting%23/') + + def test_prepend_www_custom_urlconf(self): + settings.PREPEND_WWW = True + settings.APPEND_SLASH = False + request = self._get_request('customurlconf/path/') + request.urlconf = 'regressiontests.middleware.extra_urls' + r = CommonMiddleware().process_request(request) + self.assertEquals(r.status_code, 301) + self.assertEquals( + r['Location'], + 'http://www.testserver/middleware/customurlconf/path/') + + def test_prepend_www_append_slash_have_slash_custom_urlconf(self): + settings.PREPEND_WWW = True + settings.APPEND_SLASH = True + request = self._get_request('customurlconf/slash/') + request.urlconf = 'regressiontests.middleware.extra_urls' + r = CommonMiddleware().process_request(request) + self.assertEquals(r.status_code, 301) + self.assertEquals(r['Location'], + 'http://www.testserver/middleware/customurlconf/slash/') + + def test_prepend_www_append_slash_slashless_custom_urlconf(self): + settings.PREPEND_WWW = True + settings.APPEND_SLASH = True + request = self._get_request('customurlconf/slash') + request.urlconf = 'regressiontests.middleware.extra_urls' + r = CommonMiddleware().process_request(request) + self.assertEquals(r.status_code, 301) + self.assertEquals(r['Location'], + 'http://www.testserver/middleware/customurlconf/slash/') diff --git a/parts/django/tests/regressiontests/middleware/urls.py b/parts/django/tests/regressiontests/middleware/urls.py new file mode 100644 index 0000000..88a4b37 --- /dev/null +++ b/parts/django/tests/regressiontests/middleware/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import patterns + +urlpatterns = patterns('', + (r'^noslash$', 'view'), + (r'^slash/$', 'view'), + (r'^needsquoting#/$', 'view'), +) diff --git a/parts/django/tests/regressiontests/middleware_exceptions/__init__.py b/parts/django/tests/regressiontests/middleware_exceptions/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/middleware_exceptions/__init__.py diff --git a/parts/django/tests/regressiontests/middleware_exceptions/models.py b/parts/django/tests/regressiontests/middleware_exceptions/models.py new file mode 100644 index 0000000..137941f --- /dev/null +++ b/parts/django/tests/regressiontests/middleware_exceptions/models.py @@ -0,0 +1 @@ +from django.db import models diff --git a/parts/django/tests/regressiontests/middleware_exceptions/tests.py b/parts/django/tests/regressiontests/middleware_exceptions/tests.py new file mode 100644 index 0000000..3d9c5f6 --- /dev/null +++ b/parts/django/tests/regressiontests/middleware_exceptions/tests.py @@ -0,0 +1,40 @@ +import sys + +from django.test import TestCase +from django.core.signals import got_request_exception + +class TestException(Exception): + pass + +class TestMiddleware(object): + def process_request(self, request): + raise TestException('Test Exception') + +class MiddlewareExceptionTest(TestCase): + def setUp(self): + self.exceptions = [] + got_request_exception.connect(self._on_request_exception) + self.client.handler.load_middleware() + + def tearDown(self): + got_request_exception.disconnect(self._on_request_exception) + self.exceptions = [] + + def _on_request_exception(self, sender, request, **kwargs): + self.exceptions.append(sys.exc_info()) + + def test_process_request(self): + self.client.handler._request_middleware.insert(0, TestMiddleware().process_request) + try: + response = self.client.get('/') + except TestException, e: + # Test client indefinitely re-raises any exceptions being raised + # during request handling. Hence actual testing that exception was + # properly handled is done by relying on got_request_exception + # signal being sent. + pass + except Exception, e: + self.fail("Unexpected exception: %s" % e) + self.assertEquals(len(self.exceptions), 1) + exception, value, tb = self.exceptions[0] + self.assertEquals(value.args, ('Test Exception', )) diff --git a/parts/django/tests/regressiontests/middleware_exceptions/urls.py b/parts/django/tests/regressiontests/middleware_exceptions/urls.py new file mode 100644 index 0000000..63f3ba1 --- /dev/null +++ b/parts/django/tests/regressiontests/middleware_exceptions/urls.py @@ -0,0 +1,8 @@ +# coding: utf-8 +from django.conf.urls.defaults import * + +import views + +urlpatterns = patterns('', + (r'^$', views.index), +) diff --git a/parts/django/tests/regressiontests/middleware_exceptions/views.py b/parts/django/tests/regressiontests/middleware_exceptions/views.py new file mode 100644 index 0000000..e2d597d --- /dev/null +++ b/parts/django/tests/regressiontests/middleware_exceptions/views.py @@ -0,0 +1,4 @@ +from django import http + +def index(request): + return http.HttpResponse('') diff --git a/parts/django/tests/regressiontests/model_fields/4x8.png b/parts/django/tests/regressiontests/model_fields/4x8.png Binary files differnew file mode 100644 index 0000000..ffce444 --- /dev/null +++ b/parts/django/tests/regressiontests/model_fields/4x8.png diff --git a/parts/django/tests/regressiontests/model_fields/8x4.png b/parts/django/tests/regressiontests/model_fields/8x4.png Binary files differnew file mode 100644 index 0000000..60e6d69 --- /dev/null +++ b/parts/django/tests/regressiontests/model_fields/8x4.png diff --git a/parts/django/tests/regressiontests/model_fields/__init__.py b/parts/django/tests/regressiontests/model_fields/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/model_fields/__init__.py diff --git a/parts/django/tests/regressiontests/model_fields/imagefield.py b/parts/django/tests/regressiontests/model_fields/imagefield.py new file mode 100644 index 0000000..dd79e7a --- /dev/null +++ b/parts/django/tests/regressiontests/model_fields/imagefield.py @@ -0,0 +1,420 @@ +import os +import shutil + +from django.core.files import File +from django.core.files.base import ContentFile +from django.core.files.images import ImageFile +from django.test import TestCase + +from models import Image, Person, PersonWithHeight, PersonWithHeightAndWidth, \ + PersonDimensionsFirst, PersonTwoImages, TestImageFieldFile + + +# If PIL available, do these tests. +if Image: + + from models import temp_storage_dir + + + class ImageFieldTestMixin(object): + """ + Mixin class to provide common functionality to ImageField test classes. + """ + + # Person model to use for tests. + PersonModel = PersonWithHeightAndWidth + # File class to use for file instances. + File = ImageFile + + def setUp(self): + """ + Creates a pristine temp directory (or deletes and recreates if it + already exists) that the model uses as its storage directory. + + Sets up two ImageFile instances for use in tests. + """ + if os.path.exists(temp_storage_dir): + shutil.rmtree(temp_storage_dir) + os.mkdir(temp_storage_dir) + + file_path1 = os.path.join(os.path.dirname(__file__), "4x8.png") + self.file1 = self.File(open(file_path1, 'rb')) + + file_path2 = os.path.join(os.path.dirname(__file__), "8x4.png") + self.file2 = self.File(open(file_path2, 'rb')) + + def tearDown(self): + """ + Removes temp directory and all its contents. + """ + shutil.rmtree(temp_storage_dir) + + def check_dimensions(self, instance, width, height, + field_name='mugshot'): + """ + Asserts that the given width and height values match both the + field's height and width attributes and the height and width fields + (if defined) the image field is caching to. + + Note, this method will check for dimension fields named by adding + "_width" or "_height" to the name of the ImageField. So, the + models used in these tests must have their fields named + accordingly. + + By default, we check the field named "mugshot", but this can be + specified by passing the field_name parameter. + """ + field = getattr(instance, field_name) + # Check height/width attributes of field. + if width is None and height is None: + self.assertRaises(ValueError, getattr, field, 'width') + self.assertRaises(ValueError, getattr, field, 'height') + else: + self.assertEqual(field.width, width) + self.assertEqual(field.height, height) + + # Check height/width fields of model, if defined. + width_field_name = field_name + '_width' + if hasattr(instance, width_field_name): + self.assertEqual(getattr(instance, width_field_name), width) + height_field_name = field_name + '_height' + if hasattr(instance, height_field_name): + self.assertEqual(getattr(instance, height_field_name), height) + + + class ImageFieldTests(ImageFieldTestMixin, TestCase): + """ + Tests for ImageField that don't need to be run with each of the + different test model classes. + """ + + def test_equal_notequal_hash(self): + """ + Bug #9786: Ensure '==' and '!=' work correctly. + Bug #9508: make sure hash() works as expected (equal items must + hash to the same value). + """ + # Create two Persons with different mugshots. + p1 = self.PersonModel(name="Joe") + p1.mugshot.save("mug", self.file1) + p2 = self.PersonModel(name="Bob") + p2.mugshot.save("mug", self.file2) + self.assertEqual(p1.mugshot == p2.mugshot, False) + self.assertEqual(p1.mugshot != p2.mugshot, True) + + # Test again with an instance fetched from the db. + p1_db = self.PersonModel.objects.get(name="Joe") + self.assertEqual(p1_db.mugshot == p2.mugshot, False) + self.assertEqual(p1_db.mugshot != p2.mugshot, True) + + # Instance from db should match the local instance. + self.assertEqual(p1_db.mugshot == p1.mugshot, True) + self.assertEqual(hash(p1_db.mugshot), hash(p1.mugshot)) + self.assertEqual(p1_db.mugshot != p1.mugshot, False) + + def test_instantiate_missing(self): + """ + If the underlying file is unavailable, still create instantiate the + object without error. + """ + p = self.PersonModel(name="Joan") + p.mugshot.save("shot", self.file1) + p = self.PersonModel.objects.get(name="Joan") + path = p.mugshot.path + shutil.move(path, path + '.moved') + p2 = self.PersonModel.objects.get(name="Joan") + + def test_delete_when_missing(self): + """ + Bug #8175: correctly delete an object where the file no longer + exists on the file system. + """ + p = self.PersonModel(name="Fred") + p.mugshot.save("shot", self.file1) + os.remove(p.mugshot.path) + p.delete() + + def test_size_method(self): + """ + Bug #8534: FileField.size should not leave the file open. + """ + p = self.PersonModel(name="Joan") + p.mugshot.save("shot", self.file1) + + # Get a "clean" model instance + p = self.PersonModel.objects.get(name="Joan") + # It won't have an opened file. + self.assertEqual(p.mugshot.closed, True) + + # After asking for the size, the file should still be closed. + _ = p.mugshot.size + self.assertEqual(p.mugshot.closed, True) + + def test_pickle(self): + """ + Tests that ImageField can be pickled, unpickled, and that the + image of the unpickled version is the same as the original. + """ + import pickle + + p = Person(name="Joe") + p.mugshot.save("mug", self.file1) + dump = pickle.dumps(p) + + p2 = Person(name="Bob") + p2.mugshot = self.file1 + + loaded_p = pickle.loads(dump) + self.assertEqual(p.mugshot, loaded_p.mugshot) + + + class ImageFieldTwoDimensionsTests(ImageFieldTestMixin, TestCase): + """ + Tests behavior of an ImageField and its dimensions fields. + """ + + def test_constructor(self): + """ + Tests assigning an image field through the model's constructor. + """ + p = self.PersonModel(name='Joe', mugshot=self.file1) + self.check_dimensions(p, 4, 8) + p.save() + self.check_dimensions(p, 4, 8) + + def test_image_after_constructor(self): + """ + Tests behavior when image is not passed in constructor. + """ + p = self.PersonModel(name='Joe') + # TestImageField value will default to being an instance of its + # attr_class, a TestImageFieldFile, with name == None, which will + # cause it to evaluate as False. + self.assertEqual(isinstance(p.mugshot, TestImageFieldFile), True) + self.assertEqual(bool(p.mugshot), False) + + # Test setting a fresh created model instance. + p = self.PersonModel(name='Joe') + p.mugshot = self.file1 + self.check_dimensions(p, 4, 8) + + def test_create(self): + """ + Tests assigning an image in Manager.create(). + """ + p = self.PersonModel.objects.create(name='Joe', mugshot=self.file1) + self.check_dimensions(p, 4, 8) + + def test_default_value(self): + """ + Tests that the default value for an ImageField is an instance of + the field's attr_class (TestImageFieldFile in this case) with no + name (name set to None). + """ + p = self.PersonModel() + self.assertEqual(isinstance(p.mugshot, TestImageFieldFile), True) + self.assertEqual(bool(p.mugshot), False) + + def test_assignment_to_None(self): + """ + Tests that assigning ImageField to None clears dimensions. + """ + p = self.PersonModel(name='Joe', mugshot=self.file1) + self.check_dimensions(p, 4, 8) + + # If image assigned to None, dimension fields should be cleared. + p.mugshot = None + self.check_dimensions(p, None, None) + + p.mugshot = self.file2 + self.check_dimensions(p, 8, 4) + + def test_field_save_and_delete_methods(self): + """ + Tests assignment using the field's save method and deletion using + the field's delete method. + """ + p = self.PersonModel(name='Joe') + p.mugshot.save("mug", self.file1) + self.check_dimensions(p, 4, 8) + + # A new file should update dimensions. + p.mugshot.save("mug", self.file2) + self.check_dimensions(p, 8, 4) + + # Field and dimensions should be cleared after a delete. + p.mugshot.delete(save=False) + self.assertEqual(p.mugshot, None) + self.check_dimensions(p, None, None) + + def test_dimensions(self): + """ + Checks that dimensions are updated correctly in various situations. + """ + p = self.PersonModel(name='Joe') + + # Dimensions should get set if file is saved. + p.mugshot.save("mug", self.file1) + self.check_dimensions(p, 4, 8) + + # Test dimensions after fetching from database. + p = self.PersonModel.objects.get(name='Joe') + # Bug 11084: Dimensions should not get recalculated if file is + # coming from the database. We test this by checking if the file + # was opened. + self.assertEqual(p.mugshot.was_opened, False) + self.check_dimensions(p, 4, 8) + # After checking dimensions on the image field, the file will have + # opened. + self.assertEqual(p.mugshot.was_opened, True) + # Dimensions should now be cached, and if we reset was_opened and + # check dimensions again, the file should not have opened. + p.mugshot.was_opened = False + self.check_dimensions(p, 4, 8) + self.assertEqual(p.mugshot.was_opened, False) + + # If we assign a new image to the instance, the dimensions should + # update. + p.mugshot = self.file2 + self.check_dimensions(p, 8, 4) + # Dimensions were recalculated, and hence file should have opened. + self.assertEqual(p.mugshot.was_opened, True) + + + class ImageFieldNoDimensionsTests(ImageFieldTwoDimensionsTests): + """ + Tests behavior of an ImageField with no dimension fields. + """ + + PersonModel = Person + + + class ImageFieldOneDimensionTests(ImageFieldTwoDimensionsTests): + """ + Tests behavior of an ImageField with one dimensions field. + """ + + PersonModel = PersonWithHeight + + + class ImageFieldDimensionsFirstTests(ImageFieldTwoDimensionsTests): + """ + Tests behavior of an ImageField where the dimensions fields are + defined before the ImageField. + """ + + PersonModel = PersonDimensionsFirst + + + class ImageFieldUsingFileTests(ImageFieldTwoDimensionsTests): + """ + Tests behavior of an ImageField when assigning it a File instance + rather than an ImageFile instance. + """ + + PersonModel = PersonDimensionsFirst + File = File + + + class TwoImageFieldTests(ImageFieldTestMixin, TestCase): + """ + Tests a model with two ImageFields. + """ + + PersonModel = PersonTwoImages + + def test_constructor(self): + p = self.PersonModel(mugshot=self.file1, headshot=self.file2) + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + p.save() + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + + def test_create(self): + p = self.PersonModel.objects.create(mugshot=self.file1, + headshot=self.file2) + self.check_dimensions(p, 4, 8) + self.check_dimensions(p, 8, 4, 'headshot') + + def test_assignment(self): + p = self.PersonModel() + self.check_dimensions(p, None, None, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + + p.mugshot = self.file1 + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + p.headshot = self.file2 + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + + # Clear the ImageFields one at a time. + p.mugshot = None + self.check_dimensions(p, None, None, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + p.headshot = None + self.check_dimensions(p, None, None, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + + def test_field_save_and_delete_methods(self): + p = self.PersonModel(name='Joe') + p.mugshot.save("mug", self.file1) + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + p.headshot.save("head", self.file2) + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + + # We can use save=True when deleting the image field with null=True + # dimension fields and the other field has an image. + p.headshot.delete(save=True) + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + p.mugshot.delete(save=False) + self.check_dimensions(p, None, None, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + + def test_dimensions(self): + """ + Checks that dimensions are updated correctly in various situations. + """ + p = self.PersonModel(name='Joe') + + # Dimensions should get set for the saved file. + p.mugshot.save("mug", self.file1) + p.headshot.save("head", self.file2) + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + + # Test dimensions after fetching from database. + p = self.PersonModel.objects.get(name='Joe') + # Bug 11084: Dimensions should not get recalculated if file is + # coming from the database. We test this by checking if the file + # was opened. + self.assertEqual(p.mugshot.was_opened, False) + self.assertEqual(p.headshot.was_opened, False) + self.check_dimensions(p, 4, 8,'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + # After checking dimensions on the image fields, the files will + # have been opened. + self.assertEqual(p.mugshot.was_opened, True) + self.assertEqual(p.headshot.was_opened, True) + # Dimensions should now be cached, and if we reset was_opened and + # check dimensions again, the file should not have opened. + p.mugshot.was_opened = False + p.headshot.was_opened = False + self.check_dimensions(p, 4, 8,'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + self.assertEqual(p.mugshot.was_opened, False) + self.assertEqual(p.headshot.was_opened, False) + + # If we assign a new image to the instance, the dimensions should + # update. + p.mugshot = self.file2 + p.headshot = self.file1 + self.check_dimensions(p, 8, 4, 'mugshot') + self.check_dimensions(p, 4, 8, 'headshot') + # Dimensions were recalculated, and hence file should have opened. + self.assertEqual(p.mugshot.was_opened, True) + self.assertEqual(p.headshot.was_opened, True) diff --git a/parts/django/tests/regressiontests/model_fields/models.py b/parts/django/tests/regressiontests/model_fields/models.py new file mode 100644 index 0000000..45cd223 --- /dev/null +++ b/parts/django/tests/regressiontests/model_fields/models.py @@ -0,0 +1,154 @@ +import os +import tempfile + +# Try to import PIL in either of the two ways it can end up installed. +# Checking for the existence of Image is enough for CPython, but for PyPy, +# you need to check for the underlying modules. + +try: + from PIL import Image, _imaging +except ImportError: + try: + import Image, _imaging + except ImportError: + Image = None + +from django.core.files.storage import FileSystemStorage +from django.db import models +from django.db.models.fields.files import ImageFieldFile, ImageField + + +class Foo(models.Model): + a = models.CharField(max_length=10) + d = models.DecimalField(max_digits=5, decimal_places=3) + +def get_foo(): + return Foo.objects.get(id=1) + +class Bar(models.Model): + b = models.CharField(max_length=10) + a = models.ForeignKey(Foo, default=get_foo) + +class Whiz(models.Model): + CHOICES = ( + ('Group 1', ( + (1,'First'), + (2,'Second'), + ) + ), + ('Group 2', ( + (3,'Third'), + (4,'Fourth'), + ) + ), + (0,'Other'), + ) + c = models.IntegerField(choices=CHOICES, null=True) + +class BigD(models.Model): + d = models.DecimalField(max_digits=38, decimal_places=30) + +class BigS(models.Model): + s = models.SlugField(max_length=255) + +class BigInt(models.Model): + value = models.BigIntegerField() + null_value = models.BigIntegerField(null = True, blank = True) + +class Post(models.Model): + title = models.CharField(max_length=100) + body = models.TextField() + +class NullBooleanModel(models.Model): + nbfield = models.NullBooleanField() + +class BooleanModel(models.Model): + bfield = models.BooleanField() + string = models.CharField(max_length=10, default='abc') + +############################################################################### +# ImageField + +# If PIL available, do these tests. +if Image: + class TestImageFieldFile(ImageFieldFile): + """ + Custom Field File class that records whether or not the underlying file + was opened. + """ + def __init__(self, *args, **kwargs): + self.was_opened = False + super(TestImageFieldFile, self).__init__(*args,**kwargs) + def open(self): + self.was_opened = True + super(TestImageFieldFile, self).open() + + class TestImageField(ImageField): + attr_class = TestImageFieldFile + + # Set up a temp directory for file storage. + temp_storage_dir = tempfile.mkdtemp() + temp_storage = FileSystemStorage(temp_storage_dir) + temp_upload_to_dir = os.path.join(temp_storage.location, 'tests') + + class Person(models.Model): + """ + Model that defines an ImageField with no dimension fields. + """ + name = models.CharField(max_length=50) + mugshot = TestImageField(storage=temp_storage, upload_to='tests') + + class PersonWithHeight(models.Model): + """ + Model that defines an ImageField with only one dimension field. + """ + name = models.CharField(max_length=50) + mugshot = TestImageField(storage=temp_storage, upload_to='tests', + height_field='mugshot_height') + mugshot_height = models.PositiveSmallIntegerField() + + class PersonWithHeightAndWidth(models.Model): + """ + Model that defines height and width fields after the ImageField. + """ + name = models.CharField(max_length=50) + mugshot = TestImageField(storage=temp_storage, upload_to='tests', + height_field='mugshot_height', + width_field='mugshot_width') + mugshot_height = models.PositiveSmallIntegerField() + mugshot_width = models.PositiveSmallIntegerField() + + class PersonDimensionsFirst(models.Model): + """ + Model that defines height and width fields before the ImageField. + """ + name = models.CharField(max_length=50) + mugshot_height = models.PositiveSmallIntegerField() + mugshot_width = models.PositiveSmallIntegerField() + mugshot = TestImageField(storage=temp_storage, upload_to='tests', + height_field='mugshot_height', + width_field='mugshot_width') + + class PersonTwoImages(models.Model): + """ + Model that: + * Defines two ImageFields + * Defines the height/width fields before the ImageFields + * Has a nullalble ImageField + """ + name = models.CharField(max_length=50) + mugshot_height = models.PositiveSmallIntegerField() + mugshot_width = models.PositiveSmallIntegerField() + mugshot = TestImageField(storage=temp_storage, upload_to='tests', + height_field='mugshot_height', + width_field='mugshot_width') + headshot_height = models.PositiveSmallIntegerField( + blank=True, null=True) + headshot_width = models.PositiveSmallIntegerField( + blank=True, null=True) + headshot = TestImageField(blank=True, null=True, + storage=temp_storage, upload_to='tests', + height_field='headshot_height', + width_field='headshot_width') + +############################################################################### diff --git a/parts/django/tests/regressiontests/model_fields/tests.py b/parts/django/tests/regressiontests/model_fields/tests.py new file mode 100644 index 0000000..72a7d4d --- /dev/null +++ b/parts/django/tests/regressiontests/model_fields/tests.py @@ -0,0 +1,313 @@ +import datetime +import unittest +from decimal import Decimal + +import django.test +from django import forms +from django.db import models +from django.core.exceptions import ValidationError + +from models import Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post, NullBooleanModel, BooleanModel + +# If PIL available, do these tests. +if Image: + from imagefield import \ + ImageFieldTests, \ + ImageFieldTwoDimensionsTests, \ + ImageFieldNoDimensionsTests, \ + ImageFieldOneDimensionTests, \ + ImageFieldDimensionsFirstTests, \ + ImageFieldUsingFileTests, \ + TwoImageFieldTests + + +class BasicFieldTests(django.test.TestCase): + def test_show_hidden_initial(self): + """ + Regression test for #12913. Make sure fields with choices respect + show_hidden_initial as a kwarg to models.Field.formfield() + """ + choices = [(0, 0), (1, 1)] + model_field = models.Field(choices=choices) + form_field = model_field.formfield(show_hidden_initial=True) + self.assertTrue(form_field.show_hidden_initial) + + form_field = model_field.formfield(show_hidden_initial=False) + self.assertFalse(form_field.show_hidden_initial) + + def test_nullbooleanfield_blank(self): + """ + Regression test for #13071: NullBooleanField should not throw + a validation error when given a value of None. + + """ + nullboolean = NullBooleanModel(nbfield=None) + try: + nullboolean.full_clean() + except ValidationError, e: + self.fail("NullBooleanField failed validation with value of None: %s" % e.messages) + +class DecimalFieldTests(django.test.TestCase): + def test_to_python(self): + f = models.DecimalField(max_digits=4, decimal_places=2) + self.assertEqual(f.to_python(3), Decimal("3")) + self.assertEqual(f.to_python("3.14"), Decimal("3.14")) + self.assertRaises(ValidationError, f.to_python, "abc") + + def test_default(self): + f = models.DecimalField(default=Decimal("0.00")) + self.assertEqual(f.get_default(), Decimal("0.00")) + + def test_format(self): + f = models.DecimalField(max_digits=5, decimal_places=1) + self.assertEqual(f._format(f.to_python(2)), u'2.0') + self.assertEqual(f._format(f.to_python('2.6')), u'2.6') + self.assertEqual(f._format(None), None) + + def test_get_db_prep_lookup(self): + from django.db import connection + f = models.DecimalField(max_digits=5, decimal_places=1) + self.assertEqual(f.get_db_prep_lookup('exact', None, connection=connection), [None]) + + def test_filter_with_strings(self): + """ + We should be able to filter decimal fields using strings (#8023) + """ + Foo.objects.create(id=1, a='abc', d=Decimal("12.34")) + self.assertEqual(list(Foo.objects.filter(d=u'1.23')), []) + + def test_save_without_float_conversion(self): + """ + Ensure decimals don't go through a corrupting float conversion during + save (#5079). + """ + bd = BigD(d="12.9") + bd.save() + bd = BigD.objects.get(pk=bd.pk) + self.assertEqual(bd.d, Decimal("12.9")) + + def test_lookup_really_big_value(self): + """ + Ensure that really big values can be used in a filter statement, even + with older Python versions. + """ + # This should not crash. That counts as a win for our purposes. + Foo.objects.filter(d__gte=100000000000) + +class ForeignKeyTests(django.test.TestCase): + def test_callable_default(self): + """Test the use of a lazy callable for ForeignKey.default""" + a = Foo.objects.create(id=1, a='abc', d=Decimal("12.34")) + b = Bar.objects.create(b="bcd") + self.assertEqual(b.a, a) + +class DateTimeFieldTests(unittest.TestCase): + def test_datetimefield_to_python_usecs(self): + """DateTimeField.to_python should support usecs""" + f = models.DateTimeField() + self.assertEqual(f.to_python('2001-01-02 03:04:05.000006'), + datetime.datetime(2001, 1, 2, 3, 4, 5, 6)) + self.assertEqual(f.to_python('2001-01-02 03:04:05.999999'), + datetime.datetime(2001, 1, 2, 3, 4, 5, 999999)) + + def test_timefield_to_python_usecs(self): + """TimeField.to_python should support usecs""" + f = models.TimeField() + self.assertEqual(f.to_python('01:02:03.000004'), + datetime.time(1, 2, 3, 4)) + self.assertEqual(f.to_python('01:02:03.999999'), + datetime.time(1, 2, 3, 999999)) + +class BooleanFieldTests(unittest.TestCase): + def _test_get_db_prep_lookup(self, f): + from django.db import connection + self.assertEqual(f.get_db_prep_lookup('exact', True, connection=connection), [True]) + self.assertEqual(f.get_db_prep_lookup('exact', '1', connection=connection), [True]) + self.assertEqual(f.get_db_prep_lookup('exact', 1, connection=connection), [True]) + self.assertEqual(f.get_db_prep_lookup('exact', False, connection=connection), [False]) + self.assertEqual(f.get_db_prep_lookup('exact', '0', connection=connection), [False]) + self.assertEqual(f.get_db_prep_lookup('exact', 0, connection=connection), [False]) + self.assertEqual(f.get_db_prep_lookup('exact', None, connection=connection), [None]) + + def _test_to_python(self, f): + self.assertTrue(f.to_python(1) is True) + self.assertTrue(f.to_python(0) is False) + + def test_booleanfield_get_db_prep_lookup(self): + self._test_get_db_prep_lookup(models.BooleanField()) + + def test_nullbooleanfield_get_db_prep_lookup(self): + self._test_get_db_prep_lookup(models.NullBooleanField()) + + def test_booleanfield_to_python(self): + self._test_to_python(models.BooleanField()) + + def test_nullbooleanfield_to_python(self): + self._test_to_python(models.NullBooleanField()) + + def test_booleanfield_choices_blank(self): + """ + Test that BooleanField with choices and defaults doesn't generate a + formfield with the blank option (#9640, #10549). + """ + choices = [(1, u'Si'), (2, 'No')] + f = models.BooleanField(choices=choices, default=1, null=True) + self.assertEqual(f.formfield().choices, [('', '---------')] + choices) + + f = models.BooleanField(choices=choices, default=1, null=False) + self.assertEqual(f.formfield().choices, choices) + + def test_return_type(self): + b = BooleanModel() + b.bfield = True + b.save() + b2 = BooleanModel.objects.get(pk=b.pk) + self.assertTrue(isinstance(b2.bfield, bool)) + self.assertEqual(b2.bfield, True) + + b3 = BooleanModel() + b3.bfield = False + b3.save() + b4 = BooleanModel.objects.get(pk=b3.pk) + self.assertTrue(isinstance(b4.bfield, bool)) + self.assertEqual(b4.bfield, False) + + b = NullBooleanModel() + b.nbfield = True + b.save() + b2 = NullBooleanModel.objects.get(pk=b.pk) + self.assertTrue(isinstance(b2.nbfield, bool)) + self.assertEqual(b2.nbfield, True) + + b3 = NullBooleanModel() + b3.nbfield = False + b3.save() + b4 = NullBooleanModel.objects.get(pk=b3.pk) + self.assertTrue(isinstance(b4.nbfield, bool)) + self.assertEqual(b4.nbfield, False) + + # http://code.djangoproject.com/ticket/13293 + # Verify that when an extra clause exists, the boolean + # conversions are applied with an offset + b5 = BooleanModel.objects.all().extra( + select={'string_length': 'LENGTH(string)'})[0] + self.assertFalse(isinstance(b5.pk, bool)) + +class ChoicesTests(django.test.TestCase): + def test_choices_and_field_display(self): + """ + Check that get_choices and get_flatchoices interact with + get_FIELD_display to return the expected values (#7913). + """ + self.assertEqual(Whiz(c=1).get_c_display(), 'First') # A nested value + self.assertEqual(Whiz(c=0).get_c_display(), 'Other') # A top level value + self.assertEqual(Whiz(c=9).get_c_display(), 9) # Invalid value + self.assertEqual(Whiz(c=None).get_c_display(), None) # Blank value + self.assertEqual(Whiz(c='').get_c_display(), '') # Empty value + +class SlugFieldTests(django.test.TestCase): + def test_slugfield_max_length(self): + """ + Make sure SlugField honors max_length (#9706) + """ + bs = BigS.objects.create(s = 'slug'*50) + bs = BigS.objects.get(pk=bs.pk) + self.assertEqual(bs.s, 'slug'*50) + + +class ValidationTest(django.test.TestCase): + def test_charfield_raises_error_on_empty_string(self): + f = models.CharField() + self.assertRaises(ValidationError, f.clean, "", None) + + def test_charfield_cleans_empty_string_when_blank_true(self): + f = models.CharField(blank=True) + self.assertEqual('', f.clean('', None)) + + def test_integerfield_cleans_valid_string(self): + f = models.IntegerField() + self.assertEqual(2, f.clean('2', None)) + + def test_integerfield_raises_error_on_invalid_intput(self): + f = models.IntegerField() + self.assertRaises(ValidationError, f.clean, "a", None) + + def test_charfield_with_choices_cleans_valid_choice(self): + f = models.CharField(max_length=1, choices=[('a','A'), ('b','B')]) + self.assertEqual('a', f.clean('a', None)) + + def test_charfield_with_choices_raises_error_on_invalid_choice(self): + f = models.CharField(choices=[('a','A'), ('b','B')]) + self.assertRaises(ValidationError, f.clean, "not a", None) + + def test_choices_validation_supports_named_groups(self): + f = models.IntegerField(choices=(('group',((10,'A'),(20,'B'))),(30,'C'))) + self.assertEqual(10, f.clean(10, None)) + + def test_nullable_integerfield_raises_error_with_blank_false(self): + f = models.IntegerField(null=True, blank=False) + self.assertRaises(ValidationError, f.clean, None, None) + + def test_nullable_integerfield_cleans_none_on_null_and_blank_true(self): + f = models.IntegerField(null=True, blank=True) + self.assertEqual(None, f.clean(None, None)) + + def test_integerfield_raises_error_on_empty_input(self): + f = models.IntegerField(null=False) + self.assertRaises(ValidationError, f.clean, None, None) + self.assertRaises(ValidationError, f.clean, '', None) + + def test_charfield_raises_error_on_empty_input(self): + f = models.CharField(null=False) + self.assertRaises(ValidationError, f.clean, None, None) + + def test_datefield_cleans_date(self): + f = models.DateField() + self.assertEqual(datetime.date(2008, 10, 10), f.clean('2008-10-10', None)) + + def test_boolean_field_doesnt_accept_empty_input(self): + f = models.BooleanField() + self.assertRaises(ValidationError, f.clean, None, None) + + +class BigIntegerFieldTests(django.test.TestCase): + def test_limits(self): + # Ensure that values that are right at the limits can be saved + # and then retrieved without corruption. + maxval = 9223372036854775807 + minval = -maxval - 1 + BigInt.objects.create(value=maxval) + qs = BigInt.objects.filter(value__gte=maxval) + self.assertEqual(qs.count(), 1) + self.assertEqual(qs[0].value, maxval) + BigInt.objects.create(value=minval) + qs = BigInt.objects.filter(value__lte=minval) + self.assertEqual(qs.count(), 1) + self.assertEqual(qs[0].value, minval) + + def test_types(self): + b = BigInt(value = 0) + self.assertTrue(isinstance(b.value, (int, long))) + b.save() + self.assertTrue(isinstance(b.value, (int, long))) + b = BigInt.objects.all()[0] + self.assertTrue(isinstance(b.value, (int, long))) + + def test_coercing(self): + BigInt.objects.create(value ='10') + b = BigInt.objects.get(value = '10') + self.assertEqual(b.value, 10) + +class TypeCoercionTests(django.test.TestCase): + """ + Test that database lookups can accept the wrong types and convert + them with no error: especially on Postgres 8.3+ which does not do + automatic casting at the DB level. See #10015. + + """ + def test_lookup_integer_in_charfield(self): + self.assertEquals(Post.objects.filter(title=9).count(), 0) + + def test_lookup_integer_in_textfield(self): + self.assertEquals(Post.objects.filter(body=24).count(), 0) + diff --git a/parts/django/tests/regressiontests/model_forms_regress/__init__.py b/parts/django/tests/regressiontests/model_forms_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/model_forms_regress/__init__.py diff --git a/parts/django/tests/regressiontests/model_forms_regress/models.py b/parts/django/tests/regressiontests/model_forms_regress/models.py new file mode 100644 index 0000000..4f9811a --- /dev/null +++ b/parts/django/tests/regressiontests/model_forms_regress/models.py @@ -0,0 +1,59 @@ +import os +from django.db import models +from django.core.exceptions import ValidationError + + +class Person(models.Model): + name = models.CharField(max_length=100) + +class Triple(models.Model): + left = models.IntegerField() + middle = models.IntegerField() + right = models.IntegerField() + + class Meta: + unique_together = (('left', 'middle'), (u'middle', u'right')) + +class FilePathModel(models.Model): + path = models.FilePathField(path=os.path.dirname(__file__), match=".*\.py$", blank=True) + +class Publication(models.Model): + title = models.CharField(max_length=30) + date_published = models.DateField() + + def __unicode__(self): + return self.title + +class Article(models.Model): + headline = models.CharField(max_length=100) + publications = models.ManyToManyField(Publication) + + def __unicode__(self): + return self.headline + +class CustomFileField(models.FileField): + def save_form_data(self, instance, data): + been_here = getattr(self, 'been_saved', False) + assert not been_here, "save_form_data called more than once" + setattr(self, 'been_saved', True) + +class CustomFF(models.Model): + f = CustomFileField(upload_to='unused', blank=True) + +class RealPerson(models.Model): + name = models.CharField(max_length=100) + + def clean(self): + if self.name.lower() == 'anonymous': + raise ValidationError("Please specify a real name.") + +class Author(models.Model): + publication = models.OneToOneField(Publication, null=True, blank=True) + full_name = models.CharField(max_length=255) + +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/parts/django/tests/regressiontests/model_forms_regress/tests.py b/parts/django/tests/regressiontests/model_forms_regress/tests.py new file mode 100644 index 0000000..1d0d6ed --- /dev/null +++ b/parts/django/tests/regressiontests/model_forms_regress/tests.py @@ -0,0 +1,311 @@ +from datetime import date + +from django import db, forms +from django.conf import settings +from django.forms.models import modelform_factory, ModelChoiceField +from django.test import TestCase + +from models import Person, RealPerson, Triple, FilePathModel, Article, \ + Publication, CustomFF, Author, Author1, Homepage + + +class ModelMultipleChoiceFieldTests(TestCase): + + def setUp(self): + self.old_debug = settings.DEBUG + settings.DEBUG = True + + def tearDown(self): + settings.DEBUG = self.old_debug + + def test_model_multiple_choice_number_of_queries(self): + """ + Test that ModelMultipleChoiceField does O(1) queries instead of + O(n) (#10156). + """ + for i in range(30): + Person.objects.create(name="Person %s" % i) + + db.reset_queries() + f = forms.ModelMultipleChoiceField(queryset=Person.objects.all()) + selected = f.clean([1, 3, 5, 7, 9]) + self.assertEquals(len(db.connection.queries), 1) + + def test_model_multiple_choice_run_validators(self): + """ + Test that ModelMultipleChoiceField run given validators (#14144). + """ + for i in range(30): + Person.objects.create(name="Person %s" % i) + + self._validator_run = False + def my_validator(value): + self._validator_run = True + + f = forms.ModelMultipleChoiceField(queryset=Person.objects.all(), + validators=[my_validator]) + + f.clean([p.pk for p in Person.objects.all()[8:9]]) + self.assertTrue(self._validator_run) + +class TripleForm(forms.ModelForm): + class Meta: + model = Triple + +class UniqueTogetherTests(TestCase): + def test_multiple_field_unique_together(self): + """ + When the same field is involved in multiple unique_together + constraints, we need to make sure we don't remove the data for it + before doing all the validation checking (not just failing after + the first one). + """ + Triple.objects.create(left=1, middle=2, right=3) + + form = TripleForm({'left': '1', 'middle': '2', 'right': '3'}) + self.assertFalse(form.is_valid()) + + form = TripleForm({'left': '1', 'middle': '3', 'right': '1'}) + self.assertTrue(form.is_valid()) + +class TripleFormWithCleanOverride(forms.ModelForm): + class Meta: + model = Triple + + def clean(self): + if not self.cleaned_data['left'] == self.cleaned_data['right']: + raise forms.ValidationError('Left and right should be equal') + return self.cleaned_data + +class OverrideCleanTests(TestCase): + def test_override_clean(self): + """ + Regression for #12596: Calling super from ModelForm.clean() should be + optional. + """ + form = TripleFormWithCleanOverride({'left': 1, 'middle': 2, 'right': 1}) + self.assertTrue(form.is_valid()) + # form.instance.left will be None if the instance was not constructed + # by form.full_clean(). + self.assertEquals(form.instance.left, 1) + +# Regression test for #12960. +# Make sure the cleaned_data returned from ModelForm.clean() is applied to the +# model instance. + +class PublicationForm(forms.ModelForm): + def clean(self): + self.cleaned_data['title'] = self.cleaned_data['title'].upper() + return self.cleaned_data + + class Meta: + model = Publication + +class ModelFormCleanTest(TestCase): + def test_model_form_clean_applies_to_model(self): + data = {'title': 'test', 'date_published': '2010-2-25'} + form = PublicationForm(data) + publication = form.save() + self.assertEqual(publication.title, 'TEST') + +class FPForm(forms.ModelForm): + class Meta: + model = FilePathModel + +class FilePathFieldTests(TestCase): + def test_file_path_field_blank(self): + """ + Regression test for #8842: FilePathField(blank=True) + """ + form = FPForm() + names = [p[1] for p in form['path'].field.choices] + names.sort() + self.assertEqual(names, ['---------', '__init__.py', 'models.py', 'tests.py']) + +class ManyToManyCallableInitialTests(TestCase): + def test_callable(self): + "Regression for #10349: A callable can be provided as the initial value for an m2m field" + + # Set up a callable initial value + def formfield_for_dbfield(db_field, **kwargs): + if db_field.name == 'publications': + kwargs['initial'] = lambda: Publication.objects.all().order_by('date_published')[:2] + return db_field.formfield(**kwargs) + + # Set up some Publications to use as data + Publication(title="First Book", date_published=date(2007,1,1)).save() + Publication(title="Second Book", date_published=date(2008,1,1)).save() + Publication(title="Third Book", date_published=date(2009,1,1)).save() + + # Create a ModelForm, instantiate it, and check that the output is as expected + ModelForm = modelform_factory(Article, formfield_callback=formfield_for_dbfield) + form = ModelForm() + self.assertEquals(form.as_ul(), u"""<li><label for="id_headline">Headline:</label> <input id="id_headline" type="text" name="headline" maxlength="100" /></li> +<li><label for="id_publications">Publications:</label> <select multiple="multiple" name="publications" id="id_publications"> +<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>""") + +class CFFForm(forms.ModelForm): + class Meta: + model = CustomFF + +class CustomFieldSaveTests(TestCase): + def test_save(self): + "Regression for #11149: save_form_data should be called only once" + + # It's enough that the form saves without error -- the custom save routine will + # generate an AssertionError if it is called more than once during save. + form = CFFForm(data = {'f': None}) + form.save() + +class ModelChoiceIteratorTests(TestCase): + def test_len(self): + class Form(forms.ModelForm): + class Meta: + model = Article + fields = ["publications"] + + Publication.objects.create(title="Pravda", + date_published=date(1991, 8, 22)) + f = Form() + self.assertEqual(len(f.fields["publications"].choices), 1) + +class RealPersonForm(forms.ModelForm): + class Meta: + model = RealPerson + +class CustomModelFormSaveMethod(TestCase): + def test_string_message(self): + data = {'name': 'anonymous'} + form = RealPersonForm(data) + self.assertEqual(form.is_valid(), False) + self.assertEqual(form.errors['__all__'], ['Please specify a real name.']) + +class ModelClassTests(TestCase): + def test_no_model_class(self): + class NoModelModelForm(forms.ModelForm): + pass + self.assertRaises(ValueError, NoModelModelForm) + +class OneToOneFieldTests(TestCase): + def test_assignment_of_none(self): + class AuthorForm(forms.ModelForm): + class Meta: + model = Author + fields = ['publication', 'full_name'] + + publication = Publication.objects.create(title="Pravda", + date_published=date(1991, 8, 22)) + author = Author.objects.create(publication=publication, full_name='John Doe') + form = AuthorForm({'publication':u'', 'full_name':'John Doe'}, instance=author) + self.assert_(form.is_valid()) + self.assertEqual(form.cleaned_data['publication'], None) + author = form.save() + # author object returned from form still retains original publication object + # that's why we need to retreive it from database again + new_author = Author.objects.get(pk=author.pk) + self.assertEqual(new_author.publication, None) + + def test_assignment_of_none_null_false(self): + class AuthorForm(forms.ModelForm): + class Meta: + model = Author1 + fields = ['publication', 'full_name'] + + publication = Publication.objects.create(title="Pravda", + date_published=date(1991, 8, 22)) + author = Author1.objects.create(publication=publication, full_name='John Doe') + form = AuthorForm({'publication':u'', 'full_name':'John Doe'}, instance=author) + self.assert_(not form.is_valid()) + + +class ModelChoiceForm(forms.Form): + person = ModelChoiceField(Person.objects.all()) + + +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, + # 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') diff --git a/parts/django/tests/regressiontests/model_formsets_regress/__init__.py b/parts/django/tests/regressiontests/model_formsets_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/model_formsets_regress/__init__.py diff --git a/parts/django/tests/regressiontests/model_formsets_regress/models.py b/parts/django/tests/regressiontests/model_formsets_regress/models.py new file mode 100644 index 0000000..bd12dd6 --- /dev/null +++ b/parts/django/tests/regressiontests/model_formsets_regress/models.py @@ -0,0 +1,19 @@ +from django.db import models + +class User(models.Model): + username = models.CharField(max_length=12, unique=True) + serial = models.IntegerField() + +class UserSite(models.Model): + user = models.ForeignKey(User, to_field="username") + data = models.IntegerField() + +class Place(models.Model): + name = models.CharField(max_length=50) + +class Restaurant(Place): + pass + +class Manager(models.Model): + retaurant = models.ForeignKey(Restaurant) + name = models.CharField(max_length=50) diff --git a/parts/django/tests/regressiontests/model_formsets_regress/tests.py b/parts/django/tests/regressiontests/model_formsets_regress/tests.py new file mode 100644 index 0000000..ee2a26f --- /dev/null +++ b/parts/django/tests/regressiontests/model_formsets_regress/tests.py @@ -0,0 +1,218 @@ +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" + Form = modelform_factory(User) + FormSet = inlineformset_factory(User, UserSite) + + # Instantiate the Form and FormSet to prove + # you can create a form with no data + form = Form() + form_set = FormSet(instance=User()) + + # Now create a new User and UserSite instance + data = { + 'serial': u'1', + 'username': u'apollo13', + 'usersite_set-TOTAL_FORMS': u'1', + 'usersite_set-INITIAL_FORMS': u'0', + 'usersite_set-MAX_NUM_FORMS': u'0', + 'usersite_set-0-data': u'10', + 'usersite_set-0-user': u'apollo13' + } + user = User() + form = Form(data) + if form.is_valid(): + user = form.save() + else: + self.fail('Errors found on form:%s' % form_set) + + form_set = FormSet(data, instance=user) + if form_set.is_valid(): + form_set.save() + usersite = UserSite.objects.all().values() + self.assertEqual(usersite[0]['data'], 10) + self.assertEqual(usersite[0]['user_id'], u'apollo13') + else: + self.fail('Errors found on formset:%s' % form_set.errors) + + # Now update the UserSite instance + data = { + 'usersite_set-TOTAL_FORMS': u'1', + 'usersite_set-INITIAL_FORMS': u'1', + 'usersite_set-MAX_NUM_FORMS': u'0', + 'usersite_set-0-id': unicode(usersite[0]['id']), + 'usersite_set-0-data': u'11', + 'usersite_set-0-user': u'apollo13' + } + form_set = FormSet(data, instance=user) + if form_set.is_valid(): + form_set.save() + usersite = UserSite.objects.all().values() + self.assertEqual(usersite[0]['data'], 11) + self.assertEqual(usersite[0]['user_id'], u'apollo13') + else: + self.fail('Errors found on formset:%s' % form_set.errors) + + # Now add a new UserSite instance + data = { + 'usersite_set-TOTAL_FORMS': u'2', + 'usersite_set-INITIAL_FORMS': u'1', + 'usersite_set-MAX_NUM_FORMS': u'0', + 'usersite_set-0-id': unicode(usersite[0]['id']), + 'usersite_set-0-data': u'11', + 'usersite_set-0-user': u'apollo13', + 'usersite_set-1-data': u'42', + 'usersite_set-1-user': u'apollo13' + } + form_set = FormSet(data, instance=user) + if form_set.is_valid(): + form_set.save() + usersite = UserSite.objects.all().values().order_by('data') + self.assertEqual(usersite[0]['data'], 11) + self.assertEqual(usersite[0]['user_id'], u'apollo13') + self.assertEqual(usersite[1]['data'], 42) + self.assertEqual(usersite[1]['user_id'], u'apollo13') + else: + self.fail('Errors found on formset:%s' % form_set.errors) + + def test_formset_over_inherited_model(self): + "A formset over a ForeignKey with a to_field can be saved. Regression for #11120" + Form = modelform_factory(Restaurant) + FormSet = inlineformset_factory(Restaurant, Manager) + + # Instantiate the Form and FormSet to prove + # you can create a form with no data + form = Form() + form_set = FormSet(instance=Restaurant()) + + # Now create a new Restaurant and Manager instance + data = { + 'name': u"Guido's House of Pasta", + 'manager_set-TOTAL_FORMS': u'1', + 'manager_set-INITIAL_FORMS': u'0', + 'manager_set-MAX_NUM_FORMS': u'0', + 'manager_set-0-name': u'Guido Van Rossum' + } + restaurant = User() + form = Form(data) + if form.is_valid(): + restaurant = form.save() + else: + self.fail('Errors found on form:%s' % form_set) + + form_set = FormSet(data, instance=restaurant) + if form_set.is_valid(): + form_set.save() + manager = Manager.objects.all().values() + self.assertEqual(manager[0]['name'], 'Guido Van Rossum') + else: + self.fail('Errors found on formset:%s' % form_set.errors) + + # Now update the Manager instance + data = { + 'manager_set-TOTAL_FORMS': u'1', + 'manager_set-INITIAL_FORMS': u'1', + 'manager_set-MAX_NUM_FORMS': u'0', + 'manager_set-0-id': unicode(manager[0]['id']), + 'manager_set-0-name': u'Terry Gilliam' + } + form_set = FormSet(data, instance=restaurant) + if form_set.is_valid(): + form_set.save() + manager = Manager.objects.all().values() + self.assertEqual(manager[0]['name'], 'Terry Gilliam') + else: + self.fail('Errors found on formset:%s' % form_set.errors) + + # Now add a new Manager instance + data = { + 'manager_set-TOTAL_FORMS': u'2', + 'manager_set-INITIAL_FORMS': u'1', + 'manager_set-MAX_NUM_FORMS': u'0', + 'manager_set-0-id': unicode(manager[0]['id']), + 'manager_set-0-name': u'Terry Gilliam', + 'manager_set-1-name': u'John Cleese' + } + form_set = FormSet(data, instance=restaurant) + if form_set.is_valid(): + form_set.save() + manager = Manager.objects.all().values().order_by('name') + self.assertEqual(manager[0]['name'], 'John Cleese') + self.assertEqual(manager[1]['name'], 'Terry Gilliam') + else: + self.fail('Errors found on formset:%s' % form_set.errors) + + def test_formset_with_none_instance(self): + "A formset with instance=None can be created. Regression for #11872" + Form = modelform_factory(User) + FormSet = inlineformset_factory(User, UserSite) + + # Instantiate the Form and FormSet to prove + # 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/parts/django/tests/regressiontests/model_inheritance_regress/__init__.py b/parts/django/tests/regressiontests/model_inheritance_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/model_inheritance_regress/__init__.py diff --git a/parts/django/tests/regressiontests/model_inheritance_regress/models.py b/parts/django/tests/regressiontests/model_inheritance_regress/models.py new file mode 100644 index 0000000..787f163 --- /dev/null +++ b/parts/django/tests/regressiontests/model_inheritance_regress/models.py @@ -0,0 +1,148 @@ +import datetime + +from django.db import models + +class Place(models.Model): + name = models.CharField(max_length=50) + address = models.CharField(max_length=80) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return u"%s the place" % self.name + +class Restaurant(Place): + serves_hot_dogs = models.BooleanField() + serves_pizza = models.BooleanField() + + def __unicode__(self): + return u"%s the restaurant" % self.name + +class ItalianRestaurant(Restaurant): + serves_gnocchi = models.BooleanField() + + def __unicode__(self): + return u"%s the italian restaurant" % self.name + +class ParkingLot(Place): + # An explicit link to the parent (we can control the attribute name). + parent = models.OneToOneField(Place, primary_key=True, parent_link=True) + capacity = models.IntegerField() + + def __unicode__(self): + return u"%s the parking lot" % self.name + +class ParkingLot2(Place): + # In lieu of any other connector, an existing OneToOneField will be + # promoted to the primary key. + parent = models.OneToOneField(Place) + +class ParkingLot3(Place): + # The parent_link connector need not be the pk on the model. + primary_key = models.AutoField(primary_key=True) + parent = models.OneToOneField(Place, parent_link=True) + +class Supplier(models.Model): + restaurant = models.ForeignKey(Restaurant) + +class Wholesaler(Supplier): + retailer = models.ForeignKey(Supplier,related_name='wholesale_supplier') + +class Parent(models.Model): + created = models.DateTimeField(default=datetime.datetime.now) + +class Child(Parent): + name = models.CharField(max_length=10) + +class SelfRefParent(models.Model): + parent_data = models.IntegerField() + self_data = models.ForeignKey('self', null=True) + +class SelfRefChild(SelfRefParent): + child_data = models.IntegerField() + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateTimeField() + class Meta: + ordering = ('-pub_date', 'headline') + + def __unicode__(self): + return self.headline + +class ArticleWithAuthor(Article): + author = models.CharField(max_length=100) + +class M2MBase(models.Model): + articles = models.ManyToManyField(Article) + +class M2MChild(M2MBase): + name = models.CharField(max_length=50) + +class Evaluation(Article): + quality = models.IntegerField() + + class Meta: + abstract = True + +class QualityControl(Evaluation): + assignee = models.CharField(max_length=50) + +class BaseM(models.Model): + base_name = models.CharField(max_length=100) + + def __unicode__(self): + return self.base_name + +class DerivedM(BaseM): + customPK = models.IntegerField(primary_key=True) + derived_name = models.CharField(max_length=100) + + def __unicode__(self): + return "PK = %d, base_name = %s, derived_name = %s" \ + % (self.customPK, self.base_name, self.derived_name) + +class AuditBase(models.Model): + planned_date = models.DateField() + + class Meta: + abstract = True + verbose_name_plural = u'Audits' + +class CertificationAudit(AuditBase): + class Meta(AuditBase.Meta): + abstract = True + +class InternalCertificationAudit(CertificationAudit): + auditing_dept = models.CharField(max_length=20) + +# Check that abstract classes don't get m2m tables autocreated. +class Person(models.Model): + name = models.CharField(max_length=100) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + +class AbstractEvent(models.Model): + name = models.CharField(max_length=100) + attendees = models.ManyToManyField(Person, related_name="%(class)s_set") + + class Meta: + abstract = True + ordering = ('name',) + + def __unicode__(self): + return self.name + +class BirthdayParty(AbstractEvent): + pass + +class BachelorParty(AbstractEvent): + pass + +class MessyBachelorParty(BachelorParty): + pass diff --git a/parts/django/tests/regressiontests/model_inheritance_regress/tests.py b/parts/django/tests/regressiontests/model_inheritance_regress/tests.py new file mode 100644 index 0000000..dac2cb5 --- /dev/null +++ b/parts/django/tests/regressiontests/model_inheritance_regress/tests.py @@ -0,0 +1,388 @@ +""" +Regression tests for Model inheritance behaviour. +""" + +import datetime +from operator import attrgetter + +from django.test import TestCase + +from models import (Place, Restaurant, ItalianRestaurant, ParkingLot, + ParkingLot2, ParkingLot3, Supplier, Wholesaler, Child, SelfRefParent, + SelfRefChild, ArticleWithAuthor, M2MChild, QualityControl, DerivedM, + Person, BirthdayParty, BachelorParty, MessyBachelorParty, + InternalCertificationAudit) + + +class ModelInheritanceTest(TestCase): + def test_model_inheritance(self): + # Regression for #7350, #7202 + # Check that when you create a Parent object with a specific reference + # to an existent child instance, saving the Parent doesn't duplicate + # the child. This behaviour is only activated during a raw save - it + # is mostly relevant to deserialization, but any sort of CORBA style + # 'narrow()' API would require a similar approach. + + # Create a child-parent-grandparent chain + place1 = Place( + name="Guido's House of Pasta", + address='944 W. Fullerton') + place1.save_base(raw=True) + restaurant = Restaurant( + place_ptr=place1, + serves_hot_dogs=True, + serves_pizza=False) + restaurant.save_base(raw=True) + italian_restaurant = ItalianRestaurant( + restaurant_ptr=restaurant, + serves_gnocchi=True) + italian_restaurant.save_base(raw=True) + + # Create a child-parent chain with an explicit parent link + place2 = Place(name='Main St', address='111 Main St') + place2.save_base(raw=True) + park = ParkingLot(parent=place2, capacity=100) + park.save_base(raw=True) + + # Check that no extra parent objects have been created. + places = list(Place.objects.all()) + self.assertEqual(places, [place1, place2]) + + dicts = list(Restaurant.objects.values('name','serves_hot_dogs')) + self.assertEqual(dicts, [{ + 'name': u"Guido's House of Pasta", + 'serves_hot_dogs': True + }]) + + dicts = list(ItalianRestaurant.objects.values( + 'name','serves_hot_dogs','serves_gnocchi')) + self.assertEqual(dicts, [{ + 'name': u"Guido's House of Pasta", + 'serves_gnocchi': True, + 'serves_hot_dogs': True, + }]) + + dicts = list(ParkingLot.objects.values('name','capacity')) + self.assertEqual(dicts, [{ + 'capacity': 100, + 'name': u'Main St', + }]) + + # You can also update objects when using a raw save. + place1.name = "Guido's All New House of Pasta" + place1.save_base(raw=True) + + restaurant.serves_hot_dogs = False + restaurant.save_base(raw=True) + + italian_restaurant.serves_gnocchi = False + italian_restaurant.save_base(raw=True) + + place2.name='Derelict lot' + place2.save_base(raw=True) + + park.capacity = 50 + park.save_base(raw=True) + + # No extra parent objects after an update, either. + places = list(Place.objects.all()) + self.assertEqual(places, [place2, place1]) + self.assertEqual(places[0].name, 'Derelict lot') + self.assertEqual(places[1].name, "Guido's All New House of Pasta") + + dicts = list(Restaurant.objects.values('name','serves_hot_dogs')) + self.assertEqual(dicts, [{ + 'name': u"Guido's All New House of Pasta", + 'serves_hot_dogs': False, + }]) + + dicts = list(ItalianRestaurant.objects.values( + 'name', 'serves_hot_dogs', 'serves_gnocchi')) + self.assertEqual(dicts, [{ + 'name': u"Guido's All New House of Pasta", + 'serves_gnocchi': False, + 'serves_hot_dogs': False, + }]) + + dicts = list(ParkingLot.objects.values('name','capacity')) + self.assertEqual(dicts, [{ + 'capacity': 50, + 'name': u'Derelict lot', + }]) + + # If you try to raw_save a parent attribute onto a child object, + # the attribute will be ignored. + + italian_restaurant.name = "Lorenzo's Pasta Hut" + italian_restaurant.save_base(raw=True) + + # Note that the name has not changed + # - name is an attribute of Place, not ItalianRestaurant + dicts = list(ItalianRestaurant.objects.values( + 'name','serves_hot_dogs','serves_gnocchi')) + self.assertEqual(dicts, [{ + 'name': u"Guido's All New House of Pasta", + 'serves_gnocchi': False, + 'serves_hot_dogs': False, + }]) + + def test_issue_7105(self): + # Regressions tests for #7105: dates() queries should be able to use + # fields from the parent model as easily as the child. + obj = Child.objects.create( + name='child', + created=datetime.datetime(2008, 6, 26, 17, 0, 0)) + dates = list(Child.objects.dates('created', 'month')) + self.assertEqual(dates, [datetime.datetime(2008, 6, 1, 0, 0)]) + + def test_issue_7276(self): + # Regression test for #7276: calling delete() on a model with + # multi-table inheritance should delete the associated rows from any + # ancestor tables, as well as any descendent objects. + place1 = Place( + name="Guido's House of Pasta", + address='944 W. Fullerton') + place1.save_base(raw=True) + restaurant = Restaurant( + place_ptr=place1, + serves_hot_dogs=True, + serves_pizza=False) + restaurant.save_base(raw=True) + italian_restaurant = ItalianRestaurant( + restaurant_ptr=restaurant, + serves_gnocchi=True) + italian_restaurant.save_base(raw=True) + + ident = ItalianRestaurant.objects.all()[0].id + self.assertEqual(Place.objects.get(pk=ident), place1) + xx = Restaurant.objects.create( + name='a', + address='xx', + serves_hot_dogs=True, + serves_pizza=False) + + # This should delete both Restuarants, plus the related places, plus + # the ItalianRestaurant. + Restaurant.objects.all().delete() + + self.assertRaises( + Place.DoesNotExist, + Place.objects.get, + pk=ident) + self.assertRaises( + ItalianRestaurant.DoesNotExist, + ItalianRestaurant.objects.get, + pk=ident) + + def test_issue_6755(self): + """ + Regression test for #6755 + """ + r = Restaurant(serves_pizza=False) + r.save() + self.assertEqual(r.id, r.place_ptr_id) + orig_id = r.id + r = Restaurant(place_ptr_id=orig_id, serves_pizza=True) + r.save() + self.assertEqual(r.id, orig_id) + self.assertEqual(r.id, r.place_ptr_id) + + def test_issue_7488(self): + # Regression test for #7488. This looks a little crazy, but it's the + # equivalent of what the admin interface has to do for the edit-inline + # case. + suppliers = Supplier.objects.filter( + restaurant=Restaurant(name='xx', address='yy')) + suppliers = list(suppliers) + self.assertEqual(suppliers, []) + + def test_issue_11764(self): + """ + Regression test for #11764 + """ + wholesalers = list(Wholesaler.objects.all().select_related()) + self.assertEqual(wholesalers, []) + + def test_issue_7853(self): + """ + Regression test for #7853 + If the parent class has a self-referential link, make sure that any + updates to that link via the child update the right table. + """ + obj = SelfRefChild.objects.create(child_data=37, parent_data=42) + obj.delete() + + def test_get_next_previous_by_date(self): + """ + Regression tests for #8076 + get_(next/previous)_by_date should work + """ + c1 = ArticleWithAuthor( + headline='ArticleWithAuthor 1', + author="Person 1", + pub_date=datetime.datetime(2005, 8, 1, 3, 0)) + c1.save() + c2 = ArticleWithAuthor( + headline='ArticleWithAuthor 2', + author="Person 2", + pub_date=datetime.datetime(2005, 8, 1, 10, 0)) + c2.save() + c3 = ArticleWithAuthor( + headline='ArticleWithAuthor 3', + author="Person 3", + pub_date=datetime.datetime(2005, 8, 2)) + c3.save() + + self.assertEqual(c1.get_next_by_pub_date(), c2) + self.assertEqual(c2.get_next_by_pub_date(), c3) + self.assertRaises( + ArticleWithAuthor.DoesNotExist, + c3.get_next_by_pub_date) + self.assertEqual(c3.get_previous_by_pub_date(), c2) + self.assertEqual(c2.get_previous_by_pub_date(), c1) + self.assertRaises( + ArticleWithAuthor.DoesNotExist, + c1.get_previous_by_pub_date) + + def test_inherited_fields(self): + """ + Regression test for #8825 and #9390 + Make sure all inherited fields (esp. m2m fields, in this case) appear + on the child class. + """ + m2mchildren = list(M2MChild.objects.filter(articles__isnull=False)) + self.assertEqual(m2mchildren, []) + + # Ordering should not include any database column more than once (this + # is most likely to ocurr naturally with model inheritance, so we + # check it here). Regression test for #9390. This necessarily pokes at + # the SQL string for the query, since the duplicate problems are only + # apparent at that late stage. + qs = ArticleWithAuthor.objects.order_by('pub_date', 'pk') + sql = qs.query.get_compiler(qs.db).as_sql()[0] + fragment = sql[sql.find('ORDER BY'):] + pos = fragment.find('pub_date') + self.assertEqual(fragment.find('pub_date', pos + 1), -1) + + def test_queryset_update_on_parent_model(self): + """ + Regression test for #10362 + It is possible to call update() and only change a field in + an ancestor model. + """ + article = ArticleWithAuthor.objects.create( + author="fred", + headline="Hey there!", + pub_date=datetime.datetime(2009, 3, 1, 8, 0, 0)) + update = ArticleWithAuthor.objects.filter( + author="fred").update(headline="Oh, no!") + self.assertEqual(update, 1) + update = ArticleWithAuthor.objects.filter( + pk=article.pk).update(headline="Oh, no!") + self.assertEqual(update, 1) + + derivedm1 = DerivedM.objects.create( + customPK=44, + base_name="b1", + derived_name="d1") + self.assertEqual(derivedm1.customPK, 44) + self.assertEqual(derivedm1.base_name, 'b1') + self.assertEqual(derivedm1.derived_name, 'd1') + derivedms = list(DerivedM.objects.all()) + self.assertEqual(derivedms, [derivedm1]) + + def test_use_explicit_o2o_to_parent_as_pk(self): + """ + Regression tests for #10406 + If there's a one-to-one link between a child model and the parent and + no explicit pk declared, we can use the one-to-one link as the pk on + the child. + """ + self.assertEqual(ParkingLot2._meta.pk.name, "parent") + + # However, the connector from child to parent need not be the pk on + # the child at all. + self.assertEqual(ParkingLot3._meta.pk.name, "primary_key") + # the child->parent link + self.assertEqual( + ParkingLot3._meta.get_ancestor_link(Place).name, + "parent") + + def test_all_fields_from_abstract_base_class(self): + """ + Regression tests for #7588 + """ + # All fields from an ABC, including those inherited non-abstractly + # should be available on child classes (#7588). Creating this instance + # should work without error. + QualityControl.objects.create( + headline="Problems in Django", + pub_date=datetime.datetime.now(), + quality=10, + assignee="adrian") + + def test_abstract_base_class_m2m_relation_inheritance(self): + # Check that many-to-many relations defined on an abstract base class + # are correctly inherited (and created) on the child class. + p1 = Person.objects.create(name='Alice') + p2 = Person.objects.create(name='Bob') + p3 = Person.objects.create(name='Carol') + p4 = Person.objects.create(name='Dave') + + birthday = BirthdayParty.objects.create( + name='Birthday party for Alice') + birthday.attendees = [p1, p3] + + bachelor = BachelorParty.objects.create(name='Bachelor party for Bob') + bachelor.attendees = [p2, p4] + + parties = list(p1.birthdayparty_set.all()) + self.assertEqual(parties, [birthday]) + + parties = list(p1.bachelorparty_set.all()) + self.assertEqual(parties, []) + + parties = list(p2.bachelorparty_set.all()) + self.assertEqual(parties, [bachelor]) + + # Check that a subclass of a subclass of an abstract model doesn't get + # it's own accessor. + self.assertFalse(hasattr(p2, 'messybachelorparty_set')) + + # ... but it does inherit the m2m from it's parent + messy = MessyBachelorParty.objects.create( + name='Bachelor party for Dave') + messy.attendees = [p4] + messy_parent = messy.bachelorparty_ptr + + parties = list(p4.bachelorparty_set.all()) + self.assertEqual(parties, [bachelor, messy_parent]) + + def test_11369(self): + """ + verbose_name_plural correctly inherited from ABC if inheritance chain + includes an abstract model. + """ + # Regression test for #11369: verbose_name_plural should be inherited + # from an ABC even when there are one or more intermediate + # abstract models in the inheritance chain, for consistency with + # verbose_name. + self.assertEquals( + InternalCertificationAudit._meta.verbose_name_plural, + u'Audits' + ) + + def test_inherited_nullable_exclude(self): + obj = SelfRefChild.objects.create(child_data=37, parent_data=42) + self.assertQuerysetEqual( + SelfRefParent.objects.exclude(self_data=72), [ + obj.pk + ], + attrgetter("pk") + ) + self.assertQuerysetEqual( + SelfRefChild.objects.exclude(self_data=72), [ + obj.pk + ], + attrgetter("pk") + ) diff --git a/parts/django/tests/regressiontests/model_inheritance_select_related/__init__.py b/parts/django/tests/regressiontests/model_inheritance_select_related/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/model_inheritance_select_related/__init__.py diff --git a/parts/django/tests/regressiontests/model_inheritance_select_related/models.py b/parts/django/tests/regressiontests/model_inheritance_select_related/models.py new file mode 100644 index 0000000..5851e46 --- /dev/null +++ b/parts/django/tests/regressiontests/model_inheritance_select_related/models.py @@ -0,0 +1,29 @@ +""" +Regression tests for the interaction between model inheritance and +select_related(). +""" + +from django.db import models + +class Place(models.Model): + name = models.CharField(max_length=50) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return u"%s the place" % self.name + +class Restaurant(Place): + serves_sushi = models.BooleanField() + serves_steak = models.BooleanField() + + def __unicode__(self): + return u"%s the restaurant" % self.name + +class Person(models.Model): + name = models.CharField(max_length=50) + favorite_restaurant = models.ForeignKey(Restaurant) + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/regressiontests/model_inheritance_select_related/tests.py b/parts/django/tests/regressiontests/model_inheritance_select_related/tests.py new file mode 100644 index 0000000..e1ed609 --- /dev/null +++ b/parts/django/tests/regressiontests/model_inheritance_select_related/tests.py @@ -0,0 +1,29 @@ +from operator import attrgetter + +from django.test import TestCase + +from models import Restaurant, Person + + +class ModelInheritanceSelectRelatedTests(TestCase): + def test_inherited_select_related(self): + # Regression test for #7246 + r1 = Restaurant.objects.create( + name="Nobu", serves_sushi=True, serves_steak=False + ) + r2 = Restaurant.objects.create( + name="Craft", serves_sushi=False, serves_steak=True + ) + p1 = Person.objects.create(name="John", favorite_restaurant=r1) + p2 = Person.objects.create(name="Jane", favorite_restaurant=r2) + + self.assertQuerysetEqual( + Person.objects.order_by("name").select_related(), [ + "Jane", + "John", + ], + attrgetter("name") + ) + + jane = Person.objects.order_by("name").select_related("favorite_restaurant")[0] + self.assertEqual(jane.favorite_restaurant.name, "Craft") diff --git a/parts/django/tests/regressiontests/model_regress/__init__.py b/parts/django/tests/regressiontests/model_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/model_regress/__init__.py diff --git a/parts/django/tests/regressiontests/model_regress/models.py b/parts/django/tests/regressiontests/model_regress/models.py new file mode 100644 index 0000000..f30b3ee --- /dev/null +++ b/parts/django/tests/regressiontests/model_regress/models.py @@ -0,0 +1,59 @@ +# coding: utf-8 +from django.db import models + + +CHOICES = ( + (1, 'first'), + (2, 'second'), +) + +class Article(models.Model): + headline = models.CharField(max_length=100, default='Default headline') + pub_date = models.DateTimeField() + status = models.IntegerField(blank=True, null=True, choices=CHOICES) + misc_data = models.CharField(max_length=100, blank=True) + article_text = models.TextField() + + class Meta: + ordering = ('pub_date','headline') + # A utf-8 verbose name (Ångström's Articles) to test they are valid. + verbose_name = "\xc3\x85ngstr\xc3\xb6m's Articles" + + def __unicode__(self): + return self.headline + +class Movie(models.Model): + #5218: Test models with non-default primary keys / AutoFields + movie_id = models.AutoField(primary_key=True) + name = models.CharField(max_length=60) + +class Party(models.Model): + when = models.DateField(null=True) + +class Event(models.Model): + when = models.DateTimeField() + +class Department(models.Model): + id = models.PositiveIntegerField(primary_key=True) + name = models.CharField(max_length=200) + + def __unicode__(self): + return self.name + +class Worker(models.Model): + department = models.ForeignKey(Department) + name = models.CharField(max_length=200) + + def __unicode__(self): + return self.name + +class BrokenUnicodeMethod(models.Model): + name = models.CharField(max_length=7) + + def __unicode__(self): + # Intentionally broken (trying to insert a unicode value into a str + # object). + return 'Názov: %s' % self.name + +class NonAutoPK(models.Model): + name = models.CharField(max_length=10, primary_key=True) diff --git a/parts/django/tests/regressiontests/model_regress/tests.py b/parts/django/tests/regressiontests/model_regress/tests.py new file mode 100644 index 0000000..8009839 --- /dev/null +++ b/parts/django/tests/regressiontests/model_regress/tests.py @@ -0,0 +1,175 @@ +import datetime +from operator import attrgetter + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.db import DEFAULT_DB_ALIAS +from django.test import TestCase +from django.utils import tzinfo + +from models import (Worker, Article, Party, Event, Department, + BrokenUnicodeMethod, NonAutoPK) + + + +class ModelTests(TestCase): + # The bug is that the following queries would raise: + # "TypeError: Related Field has invalid lookup: gte" + def test_related_gte_lookup(self): + """ + Regression test for #10153: foreign key __gte lookups. + """ + Worker.objects.filter(department__gte=0) + + def test_related_lte_lookup(self): + """ + Regression test for #10153: foreign key __lte lookups. + """ + Worker.objects.filter(department__lte=0) + + def test_empty_choice(self): + # NOTE: Part of the regression test here is merely parsing the model + # declaration. The verbose_name, in particular, did not always work. + a = Article.objects.create( + headline="Look at me!", pub_date=datetime.datetime.now() + ) + # An empty choice field should return None for the display name. + self.assertEqual(a.get_status_display(), None) + + # Empty strings should be returned as Unicode + a = Article.objects.get(pk=a.pk) + self.assertEqual(a.misc_data, u'') + self.assertEqual(type(a.misc_data), unicode) + + def test_long_textfield(self): + # TextFields can hold more than 4000 characters (this was broken in + # Oracle). + a = Article.objects.create( + headline="Really, really big", + pub_date=datetime.datetime.now(), + article_text = "ABCDE" * 1000 + ) + a = Article.objects.get(pk=a.pk) + self.assertEqual + (len(a.article_text), 5000) + + def test_date_lookup(self): + # Regression test for #659 + Party.objects.create(when=datetime.datetime(1999, 12, 31)) + Party.objects.create(when=datetime.datetime(1998, 12, 31)) + Party.objects.create(when=datetime.datetime(1999, 1, 1)) + self.assertQuerysetEqual( + Party.objects.filter(when__month=2), [] + ) + self.assertQuerysetEqual( + Party.objects.filter(when__month=1), [ + datetime.date(1999, 1, 1) + ], + attrgetter("when") + ) + self.assertQuerysetEqual( + Party.objects.filter(when__month=12), [ + datetime.date(1999, 12, 31), + datetime.date(1998, 12, 31), + ], + attrgetter("when") + ) + self.assertQuerysetEqual( + Party.objects.filter(when__year=1998), [ + datetime.date(1998, 12, 31), + ], + attrgetter("when") + ) + # Regression test for #8510 + self.assertQuerysetEqual( + Party.objects.filter(when__day="31"), [ + datetime.date(1999, 12, 31), + datetime.date(1998, 12, 31), + ], + attrgetter("when") + ) + self.assertQuerysetEqual( + Party.objects.filter(when__month="12"), [ + datetime.date(1999, 12, 31), + datetime.date(1998, 12, 31), + ], + attrgetter("when") + ) + self.assertQuerysetEqual( + Party.objects.filter(when__year="1998"), [ + datetime.date(1998, 12, 31), + ], + attrgetter("when") + ) + + def test_date_filter_null(self): + # Date filtering was failing with NULL date values in SQLite + # (regression test for #3501, amongst other things). + Party.objects.create(when=datetime.datetime(1999, 1, 1)) + Party.objects.create() + p = Party.objects.filter(when__month=1)[0] + self.assertEqual(p.when, datetime.date(1999, 1, 1)) + self.assertQuerysetEqual( + Party.objects.filter(pk=p.pk).dates("when", "month"), [ + 1 + ], + attrgetter("month") + ) + + def test_get_next_prev_by_field(self): + # Check that get_next_by_FIELD and get_previous_by_FIELD don't crash + # when we have usecs values stored on the database + # + # It crashed after the Field.get_db_prep_* refactor, because on most + # backends DateTimeFields supports usecs, but DateTimeField.to_python + # didn't recognize them. (Note that + # Model._get_next_or_previous_by_FIELD coerces values to strings) + Event.objects.create(when=datetime.datetime(2000, 1, 1, 16, 0, 0)) + Event.objects.create(when=datetime.datetime(2000, 1, 1, 6, 1, 1)) + Event.objects.create(when=datetime.datetime(2000, 1, 1, 13, 1, 1)) + e = Event.objects.create(when=datetime.datetime(2000, 1, 1, 12, 0, 20, 24)) + + self.assertEqual( + e.get_next_by_when().when, datetime.datetime(2000, 1, 1, 13, 1, 1) + ) + self.assertEqual( + e.get_previous_by_when().when, datetime.datetime(2000, 1, 1, 6, 1, 1) + ) + + def test_primary_key_foreign_key_types(self): + # Check Department and Worker (non-default PK type) + d = Department.objects.create(id=10, name="IT") + w = Worker.objects.create(department=d, name="Full-time") + self.assertEqual(unicode(w), "Full-time") + + def test_broken_unicode(self): + # Models with broken unicode methods should still have a printable repr + b = BrokenUnicodeMethod.objects.create(name="Jerry") + self.assertEqual(repr(b), "<BrokenUnicodeMethod: [Bad Unicode data]>") + + if settings.DATABASES[DEFAULT_DB_ALIAS]["ENGINE"] not in [ + "django.db.backends.mysql", + "django.db.backends.oracle" + ]: + def test_timezones(self): + # Saving an updating with timezone-aware datetime Python objects. + # Regression test for #10443. + # The idea is that all these creations and saving should work + # without crashing. It's not rocket science. + dt1 = datetime.datetime(2008, 8, 31, 16, 20, tzinfo=tzinfo.FixedOffset(600)) + dt2 = datetime.datetime(2008, 8, 31, 17, 20, tzinfo=tzinfo.FixedOffset(600)) + obj = Article.objects.create( + headline="A headline", pub_date=dt1, article_text="foo" + ) + obj.pub_date = dt2 + obj.save() + self.assertEqual( + Article.objects.filter(headline="A headline").update(pub_date=dt1), + 1 + ) + +class ModelValidationTest(TestCase): + def test_pk_validation(self): + one = NonAutoPK.objects.create(name="one") + again = NonAutoPK(name="one") + self.assertRaises(ValidationError, again.validate_unique) diff --git a/parts/django/tests/regressiontests/modeladmin/__init__.py b/parts/django/tests/regressiontests/modeladmin/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/modeladmin/__init__.py diff --git a/parts/django/tests/regressiontests/modeladmin/models.py b/parts/django/tests/regressiontests/modeladmin/models.py new file mode 100644 index 0000000..20dfe2c --- /dev/null +++ b/parts/django/tests/regressiontests/modeladmin/models.py @@ -0,0 +1,36 @@ +# coding: utf-8 +from datetime import date + +from django.db import models +from django.contrib.auth.models import User + +class Band(models.Model): + name = models.CharField(max_length=100) + bio = models.TextField() + sign_date = models.DateField() + + def __unicode__(self): + return self.name + +class Concert(models.Model): + main_band = models.ForeignKey(Band, related_name='main_concerts') + opening_band = models.ForeignKey(Band, related_name='opening_concerts', + blank=True) + day = models.CharField(max_length=3, choices=((1, 'Fri'), (2, 'Sat'))) + transport = models.CharField(max_length=100, choices=( + (1, 'Plane'), + (2, 'Train'), + (3, 'Bus') + ), blank=True) + +class ValidationTestModel(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField() + users = models.ManyToManyField(User) + state = models.CharField(max_length=2, choices=(("CO", "Colorado"), ("WA", "Washington"))) + is_active = models.BooleanField() + pub_date = models.DateTimeField() + band = models.ForeignKey(Band) + +class ValidationTestInlineModel(models.Model): + parent = models.ForeignKey(ValidationTestModel) diff --git a/parts/django/tests/regressiontests/modeladmin/tests.py b/parts/django/tests/regressiontests/modeladmin/tests.py new file mode 100644 index 0000000..b13a0ec --- /dev/null +++ b/parts/django/tests/regressiontests/modeladmin/tests.py @@ -0,0 +1,1226 @@ +from datetime import date +import unittest + +from django import forms +from django.conf import settings +from django.contrib.admin.options import ModelAdmin, TabularInline, \ + HORIZONTAL, VERTICAL +from django.contrib.admin.sites import AdminSite +from django.contrib.admin.validation import validate +from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect +from django.core.exceptions import ImproperlyConfigured +from django.forms.models import BaseModelFormSet +from django.forms.widgets import Select +from django.test import TestCase + +from models import Band, Concert, ValidationTestModel, \ + ValidationTestInlineModel + + +# None of the following tests really depend on the content of the request, +# so we'll just pass in None. +request = None + + +class ModelAdminTests(TestCase): + + def setUp(self): + self.band = Band.objects.create( + name='The Doors', + bio='', + sign_date=date(1965, 1, 1), + ) + self.site = AdminSite() + + # form/fields/fieldsets interaction ############################## + + def test_default_fields(self): + ma = ModelAdmin(Band, self.site) + + self.assertEquals(ma.get_form(request).base_fields.keys(), + ['name', 'bio', 'sign_date']) + + def test_default_fieldsets(self): + # fieldsets_add and fieldsets_change should return a special data structure that + # is used in the templates. They should generate the "right thing" whether we + # have specified a custom form, the fields argument, or nothing at all. + # + # Here's the default case. There are no custom form_add/form_change methods, + # no fields argument, and no fieldsets argument. + ma = ModelAdmin(Band, self.site) + self.assertEqual(ma.get_fieldsets(request), + [(None, {'fields': ['name', 'bio', 'sign_date']})]) + + self.assertEqual(ma.get_fieldsets(request, self.band), + [(None, {'fields': ['name', 'bio', 'sign_date']})]) + + def test_field_arguments(self): + # If we specify the fields argument, fieldsets_add and fielsets_change should + # just stick the fields into a formsets structure and return it. + class BandAdmin(ModelAdmin): + fields = ['name'] + + ma = BandAdmin(Band, self.site) + + self.assertEqual( ma.get_fieldsets(request), + [(None, {'fields': ['name']})]) + + self.assertEqual(ma.get_fieldsets(request, self.band), + [(None, {'fields': ['name']})]) + + def test_field_arguments_restricted_on_form(self): + # If we specify fields or fieldsets, it should exclude fields on the Form class + # to the fields specified. This may cause errors to be raised in the db layer if + # required model fields arent in fields/fieldsets, but that's preferable to + # ghost errors where you have a field in your Form class that isn't being + # displayed because you forgot to add it to fields/fieldsets + + # Using `fields`. + class BandAdmin(ModelAdmin): + fields = ['name'] + + ma = BandAdmin(Band, self.site) + self.assertEqual(ma.get_form(request).base_fields.keys(), ['name']) + self.assertEqual(ma.get_form(request, self.band).base_fields.keys(), + ['name']) + + # Using `fieldsets`. + class BandAdmin(ModelAdmin): + fieldsets = [(None, {'fields': ['name']})] + + ma = BandAdmin(Band, self.site) + self.assertEqual(ma.get_form(request).base_fields.keys(), ['name']) + self.assertEqual(ma.get_form(request, self.band).base_fields.keys(), + ['name']) + + # Using `exclude`. + class BandAdmin(ModelAdmin): + exclude = ['bio'] + + ma = BandAdmin(Band, self.site) + self.assertEqual(ma.get_form(request).base_fields.keys(), + ['name', 'sign_date']) + + # You can also pass a tuple to `exclude`. + class BandAdmin(ModelAdmin): + exclude = ('bio',) + + ma = BandAdmin(Band, self.site) + self.assertEqual(ma.get_form(request).base_fields.keys(), + ['name', 'sign_date']) + + # Using `fields` and `exclude`. + class BandAdmin(ModelAdmin): + fields = ['name', 'bio'] + exclude = ['bio'] + + ma = BandAdmin(Band, self.site) + self.assertEqual(ma.get_form(request).base_fields.keys(), + ['name']) + + def test_custom_form_validation(self): + # If we specify a form, it should use it allowing custom validation to work + # properly. This won't, however, break any of the admin widgets or media. + + class AdminBandForm(forms.ModelForm): + delete = forms.BooleanField() + + class Meta: + model = Band + + class BandAdmin(ModelAdmin): + form = AdminBandForm + + ma = BandAdmin(Band, self.site) + self.assertEqual(ma.get_form(request).base_fields.keys(), + ['name', 'bio', 'sign_date', 'delete']) + + self.assertEqual( + type(ma.get_form(request).base_fields['sign_date'].widget), + AdminDateWidget) + + def test_queryset_override(self): + # If we need to override the queryset of a ModelChoiceField in our custom form + # make sure that RelatedFieldWidgetWrapper doesn't mess that up. + + band2 = Band(name='The Beatles', bio='', sign_date=date(1962, 1, 1)) + band2.save() + + class ConcertAdmin(ModelAdmin): + pass + ma = ConcertAdmin(Concert, self.site) + form = ma.get_form(request)() + + self.assertEqual(str(form["main_band"]), + '<select name="main_band" id="id_main_band">\n' + '<option value="" selected="selected">---------</option>\n' + '<option value="%d">The Doors</option>\n' + '<option value="%d">The Beatles</option>\n' + '</select>' % (self.band.id, band2.id)) + + class AdminConcertForm(forms.ModelForm): + class Meta: + model = Concert + + def __init__(self, *args, **kwargs): + super(AdminConcertForm, self).__init__(*args, **kwargs) + self.fields["main_band"].queryset = Band.objects.filter(name='The Doors') + + class ConcertAdmin(ModelAdmin): + form = AdminConcertForm + + ma = ConcertAdmin(Concert, self.site) + form = ma.get_form(request)() + + self.assertEqual(str(form["main_band"]), + '<select name="main_band" id="id_main_band">\n' + '<option value="" selected="selected">---------</option>\n' + '<option value="%d">The Doors</option>\n' + '</select>' % self.band.id) + + # radio_fields behavior ########################################### + + def test_default_foreign_key_widget(self): + # First, without any radio_fields specified, the widgets for ForeignKey + # and fields with choices specified ought to be a basic Select widget. + # ForeignKey widgets in the admin are wrapped with RelatedFieldWidgetWrapper so + # they need to be handled properly when type checking. For Select fields, all of + # the choices lists have a first entry of dashes. + + cma = ModelAdmin(Concert, self.site) + cmafa = cma.get_form(request) + + self.assertEqual(type(cmafa.base_fields['main_band'].widget.widget), + Select) + self.assertEqual( + list(cmafa.base_fields['main_band'].widget.choices), + [(u'', u'---------'), (self.band.id, u'The Doors')]) + + self.assertEqual( + type(cmafa.base_fields['opening_band'].widget.widget), Select) + self.assertEqual( + list(cmafa.base_fields['opening_band'].widget.choices), + [(u'', u'---------'), (self.band.id, u'The Doors')]) + + self.assertEqual(type(cmafa.base_fields['day'].widget), Select) + self.assertEqual(list(cmafa.base_fields['day'].widget.choices), + [('', '---------'), (1, 'Fri'), (2, 'Sat')]) + + self.assertEqual(type(cmafa.base_fields['transport'].widget), + Select) + self.assertEqual( + list(cmafa.base_fields['transport'].widget.choices), + [('', '---------'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')]) + + def test_foreign_key_as_radio_field(self): + # Now specify all the fields as radio_fields. Widgets should now be + # RadioSelect, and the choices list should have a first entry of 'None' if + # blank=True for the model field. Finally, the widget should have the + # 'radiolist' attr, and 'inline' as well if the field is specified HORIZONTAL. + + class ConcertAdmin(ModelAdmin): + radio_fields = { + 'main_band': HORIZONTAL, + 'opening_band': VERTICAL, + 'day': VERTICAL, + 'transport': HORIZONTAL, + } + + cma = ConcertAdmin(Concert, self.site) + cmafa = cma.get_form(request) + + self.assertEqual(type(cmafa.base_fields['main_band'].widget.widget), + AdminRadioSelect) + self.assertEqual(cmafa.base_fields['main_band'].widget.attrs, + {'class': 'radiolist inline'}) + self.assertEqual(list(cmafa.base_fields['main_band'].widget.choices), + [(self.band.id, u'The Doors')]) + + self.assertEqual( + type(cmafa.base_fields['opening_band'].widget.widget), + AdminRadioSelect) + self.assertEqual(cmafa.base_fields['opening_band'].widget.attrs, + {'class': 'radiolist'}) + self.assertEqual( + list(cmafa.base_fields['opening_band'].widget.choices), + [(u'', u'None'), (self.band.id, u'The Doors')]) + + self.assertEqual(type(cmafa.base_fields['day'].widget), + AdminRadioSelect) + self.assertEqual(cmafa.base_fields['day'].widget.attrs, + {'class': 'radiolist'}) + self.assertEqual(list(cmafa.base_fields['day'].widget.choices), + [(1, 'Fri'), (2, 'Sat')]) + + self.assertEqual(type(cmafa.base_fields['transport'].widget), + AdminRadioSelect) + self.assertEqual(cmafa.base_fields['transport'].widget.attrs, + {'class': 'radiolist inline'}) + self.assertEqual(list(cmafa.base_fields['transport'].widget.choices), + [('', u'None'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')]) + + class AdminConcertForm(forms.ModelForm): + class Meta: + model = Concert + exclude = ('transport',) + + class ConcertAdmin(ModelAdmin): + form = AdminConcertForm + + ma = ConcertAdmin(Concert, self.site) + self.assertEqual(ma.get_form(request).base_fields.keys(), + ['main_band', 'opening_band', 'day']) + + class AdminConcertForm(forms.ModelForm): + extra = forms.CharField() + + class Meta: + model = Concert + fields = ['extra', 'transport'] + + class ConcertAdmin(ModelAdmin): + form = AdminConcertForm + + ma = ConcertAdmin(Concert, self.site) + self.assertEqual(ma.get_form(request).base_fields.keys(), + ['extra', 'transport']) + + class ConcertInline(TabularInline): + form = AdminConcertForm + model = Concert + fk_name = 'main_band' + can_delete = True + + class BandAdmin(ModelAdmin): + inlines = [ + ConcertInline + ] + + ma = BandAdmin(Band, self.site) + self.assertEqual( + list(ma.get_formsets(request))[0]().forms[0].fields.keys(), + ['extra', 'transport', 'id', 'DELETE', 'main_band']) + + +class ValidationTests(unittest.TestCase): + def assertRaisesErrorWithMessage(self, error, message, callable, *args, **kwargs): + self.assertRaises(error, callable, *args, **kwargs) + try: + callable(*args, **kwargs) + except error, e: + self.assertEqual(message, str(e)) + + def test_validation_only_runs_in_debug(self): + # Ensure validation only runs when DEBUG = True + try: + settings.DEBUG = True + + class ValidationTestModelAdmin(ModelAdmin): + raw_id_fields = 10 + + site = AdminSite() + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.raw_id_fields' must be a list or tuple.", + site.register, + ValidationTestModel, + ValidationTestModelAdmin, + ) + finally: + settings.DEBUG = False + + site = AdminSite() + site.register(ValidationTestModel, ValidationTestModelAdmin) + + def test_raw_id_fields_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + raw_id_fields = 10 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.raw_id_fields' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + raw_id_fields = ('non_existent_field',) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.raw_id_fields' refers to field 'non_existent_field' that is missing from model 'ValidationTestModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + raw_id_fields = ('name',) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.raw_id_fields[0]', 'name' must be either a ForeignKey or ManyToManyField.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + raw_id_fields = ('users',) + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_fieldsets_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + fieldsets = 10 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.fieldsets' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + fieldsets = ({},) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.fieldsets[0]' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + fieldsets = ((),) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.fieldsets[0]' does not have exactly two elements.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + fieldsets = (("General", ()),) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.fieldsets[0][1]' must be a dictionary.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + fieldsets = (("General", {}),) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'fields' key is required in ValidationTestModelAdmin.fieldsets[0][1] field options dict.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + fieldsets = (("General", {"fields": ("non_existent_field",)}),) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.fieldsets[0][1]['fields']' refers to field 'non_existent_field' that is missing from the form.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + fieldsets = (("General", {"fields": ("name",)}),) + + validate(ValidationTestModelAdmin, ValidationTestModel) + + class ValidationTestModelAdmin(ModelAdmin): + fieldsets = (("General", {"fields": ("name",)}),) + fields = ["name",] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "Both fieldsets and fields are specified in ValidationTestModelAdmin.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + fieldsets = [(None, {'fields': ['name', 'name']})] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "There are duplicate field(s) in ValidationTestModelAdmin.fieldsets", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + fields = ["name", "name"] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "There are duplicate field(s) in ValidationTestModelAdmin.fields", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + def test_form_validation(self): + + class FakeForm(object): + pass + + class ValidationTestModelAdmin(ModelAdmin): + form = FakeForm + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "ValidationTestModelAdmin.form does not inherit from BaseModelForm.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + def test_fieldsets_with_custom_form_validation(self): + + class BandAdmin(ModelAdmin): + + fieldsets = ( + ('Band', { + 'fields': ('non_existent_field',) + }), + ) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'BandAdmin.fieldsets[0][1]['fields']' refers to field 'non_existent_field' that is missing from the form.", + validate, + BandAdmin, + Band, + ) + + class BandAdmin(ModelAdmin): + fieldsets = ( + ('Band', { + 'fields': ('name',) + }), + ) + + validate(BandAdmin, Band) + + class AdminBandForm(forms.ModelForm): + class Meta: + model = Band + + class BandAdmin(ModelAdmin): + form = AdminBandForm + + fieldsets = ( + ('Band', { + 'fields': ('non_existent_field',) + }), + ) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'BandAdmin.fieldsets[0][1]['fields']' refers to field 'non_existent_field' that is missing from the form.", + validate, + BandAdmin, + Band, + ) + + class AdminBandForm(forms.ModelForm): + delete = forms.BooleanField() + + class Meta: + model = Band + + class BandAdmin(ModelAdmin): + form = AdminBandForm + + fieldsets = ( + ('Band', { + 'fields': ('name', 'bio', 'sign_date', 'delete') + }), + ) + + validate(BandAdmin, Band) + + def test_filter_vertical_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + filter_vertical = 10 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.filter_vertical' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + filter_vertical = ("non_existent_field",) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.filter_vertical' refers to field 'non_existent_field' that is missing from model 'ValidationTestModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + filter_vertical = ("name",) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.filter_vertical[0]' must be a ManyToManyField.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + filter_vertical = ("users",) + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_filter_horizontal_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + filter_horizontal = 10 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.filter_horizontal' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + filter_horizontal = ("non_existent_field",) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.filter_horizontal' refers to field 'non_existent_field' that is missing from model 'ValidationTestModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + filter_horizontal = ("name",) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.filter_horizontal[0]' must be a ManyToManyField.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + filter_horizontal = ("users",) + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_radio_fields_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + radio_fields = () + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.radio_fields' must be a dictionary.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + radio_fields = {"non_existent_field": None} + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.radio_fields' refers to field 'non_existent_field' that is missing from model 'ValidationTestModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + radio_fields = {"name": None} + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.radio_fields['name']' is neither an instance of ForeignKey nor does have choices set.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + radio_fields = {"state": None} + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.radio_fields['state']' is neither admin.HORIZONTAL nor admin.VERTICAL.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + radio_fields = {"state": VERTICAL} + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_prepopulated_fields_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + prepopulated_fields = () + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.prepopulated_fields' must be a dictionary.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + prepopulated_fields = {"non_existent_field": None} + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.prepopulated_fields' refers to field 'non_existent_field' that is missing from model 'ValidationTestModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + prepopulated_fields = {"slug": ("non_existent_field",)} + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.prepopulated_fields['slug'][0]' refers to field 'non_existent_field' that is missing from model 'ValidationTestModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + prepopulated_fields = {"users": ("name",)} + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.prepopulated_fields['users']' is either a DateTimeField, ForeignKey or ManyToManyField. This isn't allowed.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + prepopulated_fields = {"slug": ("name",)} + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_list_display_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + list_display = 10 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.list_display' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + list_display = ('non_existent_field',) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "ValidationTestModelAdmin.list_display[0], 'non_existent_field' is not a callable or an attribute of 'ValidationTestModelAdmin' or found in the model 'ValidationTestModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + list_display = ('users',) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.list_display[0]', 'users' is a ManyToManyField which is not supported.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + list_display = ('name',) + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_list_display_links_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + list_display_links = 10 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.list_display_links' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + list_display_links = ('non_existent_field',) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.list_display_links[0]' refers to 'non_existent_field' that is neither a field, method or property of model 'ValidationTestModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + list_display_links = ('name',) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.list_display_links[0]'refers to 'name' which is not defined in 'list_display'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + list_display = ('name',) + list_display_links = ('name',) + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_list_filter_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + list_filter = 10 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.list_filter' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + list_filter = ('non_existent_field',) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.list_filter[0]' refers to field 'non_existent_field' that is missing from model 'ValidationTestModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + list_filter = ('is_active',) + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_list_per_page_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + list_per_page = 'hello' + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.list_per_page' should be a integer.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + list_per_page = 100 + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_search_fields_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + search_fields = 10 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.search_fields' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + def test_date_hierarchy_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + date_hierarchy = 'non_existent_field' + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.date_hierarchy' refers to field 'non_existent_field' that is missing from model 'ValidationTestModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + date_hierarchy = 'name' + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.date_hierarchy is neither an instance of DateField nor DateTimeField.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + date_hierarchy = 'pub_date' + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_ordering_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + ordering = 10 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.ordering' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + ordering = ('non_existent_field',) + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.ordering[0]' refers to field 'non_existent_field' that is missing from model 'ValidationTestModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + ordering = ('?', 'name') + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.ordering' has the random ordering marker '?', but contains other fields as well. Please either remove '?' or the other fields.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + ordering = ('?',) + + validate(ValidationTestModelAdmin, ValidationTestModel) + + class ValidationTestModelAdmin(ModelAdmin): + ordering = ('band__name',) + + validate(ValidationTestModelAdmin, ValidationTestModel) + + class ValidationTestModelAdmin(ModelAdmin): + ordering = ('name',) + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_list_select_related_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + list_select_related = 1 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.list_select_related' should be a boolean.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + list_select_related = False + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_save_as_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + save_as = 1 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.save_as' should be a boolean.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + save_as = True + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_save_on_top_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + save_on_top = 1 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.save_on_top' should be a boolean.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestModelAdmin(ModelAdmin): + save_on_top = True + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_inlines_validation(self): + + class ValidationTestModelAdmin(ModelAdmin): + inlines = 10 + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.inlines' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestInline(object): + pass + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.inlines[0]' does not inherit from BaseModelAdmin.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestInline(TabularInline): + pass + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'model' is a required attribute of 'ValidationTestModelAdmin.inlines[0]'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class SomethingBad(object): + pass + + class ValidationTestInline(TabularInline): + model = SomethingBad + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestModelAdmin.inlines[0].model' does not inherit from models.Model.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_fields_validation(self): + + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + fields = 10 + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestInline.fields' must be a list or tuple.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + fields = ("non_existent_field",) + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestInline.fields' refers to field 'non_existent_field' that is missing from the form.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + def test_fk_name_validation(self): + + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + fk_name = "non_existent_field" + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestInline.fk_name' refers to field 'non_existent_field' that is missing from model 'ValidationTestInlineModel'.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + fk_name = "parent" + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_extra_validation(self): + + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + extra = "hello" + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestInline.extra' should be a integer.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + extra = 2 + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_max_num_validation(self): + + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + max_num = "hello" + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestInline.max_num' should be an integer or None (default).", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + max_num = 2 + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + validate(ValidationTestModelAdmin, ValidationTestModel) + + def test_formset_validation(self): + + class FakeFormSet(object): + pass + + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + formset = FakeFormSet + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + self.assertRaisesErrorWithMessage( + ImproperlyConfigured, + "'ValidationTestInline.formset' does not inherit from BaseModelFormSet.", + validate, + ValidationTestModelAdmin, + ValidationTestModel, + ) + + class RealModelFormSet(BaseModelFormSet): + pass + + class ValidationTestInline(TabularInline): + model = ValidationTestInlineModel + formset = RealModelFormSet + + class ValidationTestModelAdmin(ModelAdmin): + inlines = [ValidationTestInline] + + validate(ValidationTestModelAdmin, ValidationTestModel) diff --git a/parts/django/tests/regressiontests/multiple_database/__init__.py b/parts/django/tests/regressiontests/multiple_database/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/multiple_database/__init__.py diff --git a/parts/django/tests/regressiontests/multiple_database/fixtures/multidb-common.json b/parts/django/tests/regressiontests/multiple_database/fixtures/multidb-common.json new file mode 100644 index 0000000..3313417 --- /dev/null +++ b/parts/django/tests/regressiontests/multiple_database/fixtures/multidb-common.json @@ -0,0 +1,10 @@ +[ + { + "pk": 1, + "model": "multiple_database.book", + "fields": { + "title": "The Definitive Guide to Django", + "published": "2009-7-8" + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/multiple_database/fixtures/multidb.default.json b/parts/django/tests/regressiontests/multiple_database/fixtures/multidb.default.json new file mode 100644 index 0000000..379b18a --- /dev/null +++ b/parts/django/tests/regressiontests/multiple_database/fixtures/multidb.default.json @@ -0,0 +1,26 @@ +[ + { + "pk": 1, + "model": "multiple_database.person", + "fields": { + "name": "Marty Alchin" + } + }, + { + "pk": 2, + "model": "multiple_database.person", + "fields": { + "name": "George Vilches" + } + }, + { + "pk": 2, + "model": "multiple_database.book", + "fields": { + "title": "Pro Django", + "published": "2008-12-16", + "authors": [["Marty Alchin"]], + "editor": ["George Vilches"] + } + } +] diff --git a/parts/django/tests/regressiontests/multiple_database/fixtures/multidb.other.json b/parts/django/tests/regressiontests/multiple_database/fixtures/multidb.other.json new file mode 100644 index 0000000..c64f490 --- /dev/null +++ b/parts/django/tests/regressiontests/multiple_database/fixtures/multidb.other.json @@ -0,0 +1,26 @@ +[ + { + "pk": 1, + "model": "multiple_database.person", + "fields": { + "name": "Mark Pilgrim" + } + }, + { + "pk": 2, + "model": "multiple_database.person", + "fields": { + "name": "Chris Mills" + } + }, + { + "pk": 2, + "model": "multiple_database.book", + "fields": { + "title": "Dive into Python", + "published": "2009-5-4", + "authors": [["Mark Pilgrim"]], + "editor": ["Chris Mills"] + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/multiple_database/fixtures/pets.json b/parts/django/tests/regressiontests/multiple_database/fixtures/pets.json new file mode 100644 index 0000000..89756a3 --- /dev/null +++ b/parts/django/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/parts/django/tests/regressiontests/multiple_database/models.py b/parts/django/tests/regressiontests/multiple_database/models.py new file mode 100644 index 0000000..ce71828 --- /dev/null +++ b/parts/django/tests/regressiontests/multiple_database/models.py @@ -0,0 +1,76 @@ +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from django.db import models + +class Review(models.Model): + source = models.CharField(max_length=100) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey() + + def __unicode__(self): + return self.source + + class Meta: + ordering = ('source',) + +class PersonManager(models.Manager): + def get_by_natural_key(self, name): + return self.get(name=name) + +class Person(models.Model): + objects = PersonManager() + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + + class Meta: + ordering = ('name',) + +# This book manager doesn't do anything interesting; it just +# exists to strip out the 'extra_arg' argument to certain +# calls. This argument is used to establish that the BookManager +# is actually getting used when it should be. +class BookManager(models.Manager): + def create(self, *args, **kwargs): + kwargs.pop('extra_arg', None) + return super(BookManager, self).create(*args, **kwargs) + + def get_or_create(self, *args, **kwargs): + kwargs.pop('extra_arg', None) + return super(BookManager, self).get_or_create(*args, **kwargs) + +class Book(models.Model): + objects = BookManager() + title = models.CharField(max_length=100) + published = models.DateField() + authors = models.ManyToManyField(Person) + editor = models.ForeignKey(Person, null=True, related_name='edited') + reviews = generic.GenericRelation(Review) + pages = models.IntegerField(default=100) + + def __unicode__(self): + return self.title + + class Meta: + ordering = ('title',) + +class Pet(models.Model): + name = models.CharField(max_length=100) + owner = models.ForeignKey(Person) + + def __unicode__(self): + return self.name + + class Meta: + ordering = ('name',) + +class UserProfile(models.Model): + user = models.OneToOneField(User, null=True) + flavor = models.CharField(max_length=100) + + class Meta: + ordering = ('flavor',) diff --git a/parts/django/tests/regressiontests/multiple_database/tests.py b/parts/django/tests/regressiontests/multiple_database/tests.py new file mode 100644 index 0000000..05dca26 --- /dev/null +++ b/parts/django/tests/regressiontests/multiple_database/tests.py @@ -0,0 +1,1681 @@ +import datetime +import pickle +import sys +from StringIO import StringIO + +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.utils import ConnectionRouter +from django.test import TestCase + +from models import Book, Person, Pet, Review, UserProfile + +try: + # we only have these models if the user is using multi-db, it's safe the + # run the tests without them though. + from models import Article, article_using +except ImportError: + pass + +class QueryTestCase(TestCase): + multi_db = True + + def test_db_selection(self): + "Check that querysets will use the default databse by default" + self.assertEquals(Book.objects.db, DEFAULT_DB_ALIAS) + self.assertEquals(Book.objects.all().db, DEFAULT_DB_ALIAS) + + self.assertEquals(Book.objects.using('other').db, 'other') + + self.assertEquals(Book.objects.db_manager('other').db, 'other') + self.assertEquals(Book.objects.db_manager('other').all().db, 'other') + + def test_default_creation(self): + "Objects created on the default database don't leak onto other databases" + # Create a book on the default database using create() + Book.objects.create(title="Pro Django", + published=datetime.date(2008, 12, 16)) + + # Create a book on the default database using a save + dive = Book() + dive.title="Dive into Python" + dive.published = datetime.date(2009, 5, 4) + dive.save() + + # Check that book exists on the default database, but not on other database + try: + Book.objects.get(title="Pro Django") + Book.objects.using('default').get(title="Pro Django") + except Book.DoesNotExist: + self.fail('"Dive Into Python" should exist on default database') + + self.assertRaises(Book.DoesNotExist, + Book.objects.using('other').get, + title="Pro Django" + ) + + try: + Book.objects.get(title="Dive into Python") + Book.objects.using('default').get(title="Dive into Python") + except Book.DoesNotExist: + self.fail('"Dive into Python" should exist on default database') + + self.assertRaises(Book.DoesNotExist, + Book.objects.using('other').get, + title="Dive into Python" + ) + + + def test_other_creation(self): + "Objects created on another database don't leak onto the default database" + # Create a book on the second database + Book.objects.using('other').create(title="Pro Django", + published=datetime.date(2008, 12, 16)) + + # Create a book on the default database using a save + dive = Book() + dive.title="Dive into Python" + dive.published = datetime.date(2009, 5, 4) + dive.save(using='other') + + # Check that book exists on the default database, but not on other database + try: + Book.objects.using('other').get(title="Pro Django") + except Book.DoesNotExist: + self.fail('"Dive Into Python" should exist on other database') + + self.assertRaises(Book.DoesNotExist, + Book.objects.get, + title="Pro Django" + ) + self.assertRaises(Book.DoesNotExist, + Book.objects.using('default').get, + title="Pro Django" + ) + + try: + Book.objects.using('other').get(title="Dive into Python") + except Book.DoesNotExist: + self.fail('"Dive into Python" should exist on other database') + + self.assertRaises(Book.DoesNotExist, + Book.objects.get, + title="Dive into Python" + ) + self.assertRaises(Book.DoesNotExist, + Book.objects.using('default').get, + title="Dive into Python" + ) + + def test_basic_queries(self): + "Queries are constrained to a single database" + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + dive = Book.objects.using('other').get(published=datetime.date(2009, 5, 4)) + self.assertEqual(dive.title, "Dive into Python") + self.assertRaises(Book.DoesNotExist, Book.objects.using('default').get, published=datetime.date(2009, 5, 4)) + + dive = Book.objects.using('other').get(title__icontains="dive") + self.assertEqual(dive.title, "Dive into Python") + self.assertRaises(Book.DoesNotExist, Book.objects.using('default').get, title__icontains="dive") + + dive = Book.objects.using('other').get(title__iexact="dive INTO python") + self.assertEqual(dive.title, "Dive into Python") + self.assertRaises(Book.DoesNotExist, Book.objects.using('default').get, title__iexact="dive INTO python") + + dive = Book.objects.using('other').get(published__year=2009) + self.assertEqual(dive.title, "Dive into Python") + self.assertEqual(dive.published, datetime.date(2009, 5, 4)) + self.assertRaises(Book.DoesNotExist, Book.objects.using('default').get, published__year=2009) + + years = Book.objects.using('other').dates('published', 'year') + self.assertEqual([o.year for o in years], [2009]) + years = Book.objects.using('default').dates('published', 'year') + self.assertEqual([o.year for o in years], []) + + months = Book.objects.using('other').dates('published', 'month') + self.assertEqual([o.month for o in months], [5]) + months = Book.objects.using('default').dates('published', 'month') + self.assertEqual([o.month for o in months], []) + + def test_m2m_separation(self): + "M2M fields are constrained to a single database" + # Create a book and author on the default database + pro = Book.objects.create(title="Pro Django", + published=datetime.date(2008, 12, 16)) + + marty = Person.objects.create(name="Marty Alchin") + + # Create a book and author on the other database + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + mark = Person.objects.using('other').create(name="Mark Pilgrim") + + # Save the author relations + pro.authors = [marty] + dive.authors = [mark] + + # Inspect the m2m tables directly. + # There should be 1 entry in each database + self.assertEquals(Book.authors.through.objects.using('default').count(), 1) + self.assertEquals(Book.authors.through.objects.using('other').count(), 1) + + # Check that queries work across m2m joins + self.assertEquals(list(Book.objects.using('default').filter(authors__name='Marty Alchin').values_list('title', flat=True)), + [u'Pro Django']) + self.assertEquals(list(Book.objects.using('other').filter(authors__name='Marty Alchin').values_list('title', flat=True)), + []) + + self.assertEquals(list(Book.objects.using('default').filter(authors__name='Mark Pilgrim').values_list('title', flat=True)), + []) + self.assertEquals(list(Book.objects.using('other').filter(authors__name='Mark Pilgrim').values_list('title', flat=True)), + [u'Dive into Python']) + + # Reget the objects to clear caches + dive = Book.objects.using('other').get(title="Dive into Python") + mark = Person.objects.using('other').get(name="Mark Pilgrim") + + # Retrive related object by descriptor. Related objects should be database-baound + self.assertEquals(list(dive.authors.all().values_list('name', flat=True)), + [u'Mark Pilgrim']) + + self.assertEquals(list(mark.book_set.all().values_list('title', flat=True)), + [u'Dive into Python']) + + def test_m2m_forward_operations(self): + "M2M forward manipulations are all constrained to a single DB" + # Create a book and author on the other database + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + mark = Person.objects.using('other').create(name="Mark Pilgrim") + + # Save the author relations + dive.authors = [mark] + + # Add a second author + john = Person.objects.using('other').create(name="John Smith") + self.assertEquals(list(Book.objects.using('other').filter(authors__name='John Smith').values_list('title', flat=True)), + []) + + + dive.authors.add(john) + self.assertEquals(list(Book.objects.using('other').filter(authors__name='Mark Pilgrim').values_list('title', flat=True)), + [u'Dive into Python']) + self.assertEquals(list(Book.objects.using('other').filter(authors__name='John Smith').values_list('title', flat=True)), + [u'Dive into Python']) + + # Remove the second author + dive.authors.remove(john) + self.assertEquals(list(Book.objects.using('other').filter(authors__name='Mark Pilgrim').values_list('title', flat=True)), + [u'Dive into Python']) + self.assertEquals(list(Book.objects.using('other').filter(authors__name='John Smith').values_list('title', flat=True)), + []) + + # Clear all authors + dive.authors.clear() + self.assertEquals(list(Book.objects.using('other').filter(authors__name='Mark Pilgrim').values_list('title', flat=True)), + []) + self.assertEquals(list(Book.objects.using('other').filter(authors__name='John Smith').values_list('title', flat=True)), + []) + + # Create an author through the m2m interface + dive.authors.create(name='Jane Brown') + self.assertEquals(list(Book.objects.using('other').filter(authors__name='Mark Pilgrim').values_list('title', flat=True)), + []) + self.assertEquals(list(Book.objects.using('other').filter(authors__name='Jane Brown').values_list('title', flat=True)), + [u'Dive into Python']) + + def test_m2m_reverse_operations(self): + "M2M reverse manipulations are all constrained to a single DB" + # Create a book and author on the other database + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + mark = Person.objects.using('other').create(name="Mark Pilgrim") + + # Save the author relations + dive.authors = [mark] + + # Create a second book on the other database + grease = Book.objects.using('other').create(title="Greasemonkey Hacks", + published=datetime.date(2005, 11, 1)) + + # Add a books to the m2m + mark.book_set.add(grease) + self.assertEquals(list(Person.objects.using('other').filter(book__title='Dive into Python').values_list('name', flat=True)), + [u'Mark Pilgrim']) + self.assertEquals(list(Person.objects.using('other').filter(book__title='Greasemonkey Hacks').values_list('name', flat=True)), + [u'Mark Pilgrim']) + + # Remove a book from the m2m + mark.book_set.remove(grease) + self.assertEquals(list(Person.objects.using('other').filter(book__title='Dive into Python').values_list('name', flat=True)), + [u'Mark Pilgrim']) + self.assertEquals(list(Person.objects.using('other').filter(book__title='Greasemonkey Hacks').values_list('name', flat=True)), + []) + + # Clear the books associated with mark + mark.book_set.clear() + self.assertEquals(list(Person.objects.using('other').filter(book__title='Dive into Python').values_list('name', flat=True)), + []) + self.assertEquals(list(Person.objects.using('other').filter(book__title='Greasemonkey Hacks').values_list('name', flat=True)), + []) + + # Create a book through the m2m interface + mark.book_set.create(title="Dive into HTML5", published=datetime.date(2020, 1, 1)) + self.assertEquals(list(Person.objects.using('other').filter(book__title='Dive into Python').values_list('name', flat=True)), + []) + self.assertEquals(list(Person.objects.using('other').filter(book__title='Dive into HTML5').values_list('name', flat=True)), + [u'Mark Pilgrim']) + + def test_m2m_cross_database_protection(self): + "Operations that involve sharing M2M objects across databases raise an error" + # Create a book and author on the default database + pro = Book.objects.create(title="Pro Django", + published=datetime.date(2008, 12, 16)) + + marty = Person.objects.create(name="Marty Alchin") + + # Create a book and author on the other database + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + mark = Person.objects.using('other').create(name="Mark Pilgrim") + # Set a foreign key set with an object from a different database + try: + marty.book_set = [pro, dive] + self.fail("Shouldn't be able to assign across databases") + except ValueError: + pass + + # Add to an m2m with an object from a different database + try: + marty.book_set.add(dive) + self.fail("Shouldn't be able to assign across databases") + except ValueError: + pass + + # Set a m2m with an object from a different database + try: + marty.book_set = [pro, dive] + self.fail("Shouldn't be able to assign across databases") + except ValueError: + pass + + # Add to a reverse m2m with an object from a different database + try: + dive.authors.add(marty) + self.fail("Shouldn't be able to assign across databases") + except ValueError: + pass + + # Set a reverse m2m with an object from a different database + try: + dive.authors = [mark, marty] + self.fail("Shouldn't be able to assign across databases") + except ValueError: + pass + + def test_m2m_deletion(self): + "Cascaded deletions of m2m relations issue queries on the right database" + # Create a book and author on the other database + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + mark = Person.objects.using('other').create(name="Mark Pilgrim") + dive.authors = [mark] + + # Check the initial state + self.assertEquals(Person.objects.using('default').count(), 0) + self.assertEquals(Book.objects.using('default').count(), 0) + self.assertEquals(Book.authors.through.objects.using('default').count(), 0) + + self.assertEquals(Person.objects.using('other').count(), 1) + self.assertEquals(Book.objects.using('other').count(), 1) + self.assertEquals(Book.authors.through.objects.using('other').count(), 1) + + # Delete the object on the other database + dive.delete(using='other') + + self.assertEquals(Person.objects.using('default').count(), 0) + self.assertEquals(Book.objects.using('default').count(), 0) + self.assertEquals(Book.authors.through.objects.using('default').count(), 0) + + # The person still exists ... + self.assertEquals(Person.objects.using('other').count(), 1) + # ... but the book has been deleted + self.assertEquals(Book.objects.using('other').count(), 0) + # ... and the relationship object has also been deleted. + self.assertEquals(Book.authors.through.objects.using('other').count(), 0) + + # Now try deletion in the reverse direction. Set up the relation again + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + dive.authors = [mark] + + # Check the initial state + self.assertEquals(Person.objects.using('default').count(), 0) + self.assertEquals(Book.objects.using('default').count(), 0) + self.assertEquals(Book.authors.through.objects.using('default').count(), 0) + + self.assertEquals(Person.objects.using('other').count(), 1) + self.assertEquals(Book.objects.using('other').count(), 1) + self.assertEquals(Book.authors.through.objects.using('other').count(), 1) + + # Delete the object on the other database + mark.delete(using='other') + + self.assertEquals(Person.objects.using('default').count(), 0) + self.assertEquals(Book.objects.using('default').count(), 0) + self.assertEquals(Book.authors.through.objects.using('default').count(), 0) + + # The person has been deleted ... + self.assertEquals(Person.objects.using('other').count(), 0) + # ... but the book still exists + self.assertEquals(Book.objects.using('other').count(), 1) + # ... and the relationship object has been deleted. + self.assertEquals(Book.authors.through.objects.using('other').count(), 0) + + def test_foreign_key_separation(self): + "FK fields are constrained to a single database" + # Create a book and author on the default database + pro = Book.objects.create(title="Pro Django", + published=datetime.date(2008, 12, 16)) + + marty = Person.objects.create(name="Marty Alchin") + george = Person.objects.create(name="George Vilches") + + # Create a book and author on the other database + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + mark = Person.objects.using('other').create(name="Mark Pilgrim") + chris = Person.objects.using('other').create(name="Chris Mills") + + # Save the author's favourite books + pro.editor = george + pro.save() + + dive.editor = chris + dive.save() + + pro = Book.objects.using('default').get(title="Pro Django") + self.assertEquals(pro.editor.name, "George Vilches") + + dive = Book.objects.using('other').get(title="Dive into Python") + self.assertEquals(dive.editor.name, "Chris Mills") + + # Check that queries work across foreign key joins + self.assertEquals(list(Person.objects.using('default').filter(edited__title='Pro Django').values_list('name', flat=True)), + [u'George Vilches']) + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Pro Django').values_list('name', flat=True)), + []) + + self.assertEquals(list(Person.objects.using('default').filter(edited__title='Dive into Python').values_list('name', flat=True)), + []) + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into Python').values_list('name', flat=True)), + [u'Chris Mills']) + + # Reget the objects to clear caches + chris = Person.objects.using('other').get(name="Chris Mills") + dive = Book.objects.using('other').get(title="Dive into Python") + + # Retrive related object by descriptor. Related objects should be database-baound + self.assertEquals(list(chris.edited.values_list('title', flat=True)), + [u'Dive into Python']) + + def test_foreign_key_reverse_operations(self): + "FK reverse manipulations are all constrained to a single DB" + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + mark = Person.objects.using('other').create(name="Mark Pilgrim") + chris = Person.objects.using('other').create(name="Chris Mills") + + # Save the author relations + dive.editor = chris + dive.save() + + # Add a second book edited by chris + html5 = Book.objects.using('other').create(title="Dive into HTML5", published=datetime.date(2010, 3, 15)) + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into HTML5').values_list('name', flat=True)), + []) + + chris.edited.add(html5) + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into HTML5').values_list('name', flat=True)), + [u'Chris Mills']) + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into Python').values_list('name', flat=True)), + [u'Chris Mills']) + + # Remove the second editor + chris.edited.remove(html5) + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into HTML5').values_list('name', flat=True)), + []) + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into Python').values_list('name', flat=True)), + [u'Chris Mills']) + + # Clear all edited books + chris.edited.clear() + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into HTML5').values_list('name', flat=True)), + []) + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into Python').values_list('name', flat=True)), + []) + + # Create an author through the m2m interface + chris.edited.create(title='Dive into Water', published=datetime.date(2010, 3, 15)) + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into HTML5').values_list('name', flat=True)), + []) + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into Water').values_list('name', flat=True)), + [u'Chris Mills']) + self.assertEquals(list(Person.objects.using('other').filter(edited__title='Dive into Python').values_list('name', flat=True)), + []) + + def test_foreign_key_cross_database_protection(self): + "Operations that involve sharing FK objects across databases raise an error" + # Create a book and author on the default database + pro = Book.objects.create(title="Pro Django", + published=datetime.date(2008, 12, 16)) + + marty = Person.objects.create(name="Marty Alchin") + + # Create a book and author on the other database + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + mark = Person.objects.using('other').create(name="Mark Pilgrim") + + # Set a foreign key with an object from a different database + try: + dive.editor = marty + self.fail("Shouldn't be able to assign across databases") + except ValueError: + pass + + # Set a foreign key set with an object from a different database + try: + marty.edited = [pro, dive] + self.fail("Shouldn't be able to assign across databases") + except ValueError: + pass + + # Add to a foreign key set with an object from a different database + try: + marty.edited.add(dive) + self.fail("Shouldn't be able to assign across databases") + except ValueError: + pass + + # BUT! if you assign a FK object when the base object hasn't + # been saved yet, you implicitly assign the database for the + # base object. + chris = Person(name="Chris Mills") + html5 = Book(title="Dive into HTML5", published=datetime.date(2010, 3, 15)) + # initially, no db assigned + self.assertEquals(chris._state.db, None) + self.assertEquals(html5._state.db, None) + + # old object comes from 'other', so the new object is set to use 'other'... + dive.editor = chris + html5.editor = mark + self.assertEquals(chris._state.db, 'other') + self.assertEquals(html5._state.db, 'other') + # ... but it isn't saved yet + self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)), + [u'Mark Pilgrim']) + self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)), + [u'Dive into Python']) + + # When saved (no using required), new objects goes to 'other' + chris.save() + html5.save() + self.assertEquals(list(Person.objects.using('default').values_list('name',flat=True)), + [u'Marty Alchin']) + self.assertEquals(list(Person.objects.using('other').values_list('name',flat=True)), + [u'Chris Mills', u'Mark Pilgrim']) + self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)), + [u'Pro Django']) + self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)), + [u'Dive into HTML5', u'Dive into Python']) + + # This also works if you assign the FK in the constructor + water = Book(title="Dive into Water", published=datetime.date(2001, 1, 1), editor=mark) + self.assertEquals(water._state.db, 'other') + # ... but it isn't saved yet + self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)), + [u'Pro Django']) + self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)), + [u'Dive into HTML5', u'Dive into Python']) + + # When saved, the new book goes to 'other' + water.save() + self.assertEquals(list(Book.objects.using('default').values_list('title',flat=True)), + [u'Pro Django']) + self.assertEquals(list(Book.objects.using('other').values_list('title',flat=True)), + [u'Dive into HTML5', u'Dive into Python', u'Dive into Water']) + + def test_foreign_key_deletion(self): + "Cascaded deletions of Foreign Key relations issue queries on the right database" + mark = Person.objects.using('other').create(name="Mark Pilgrim") + fido = Pet.objects.using('other').create(name="Fido", owner=mark) + + # Check the initial state + self.assertEquals(Person.objects.using('default').count(), 0) + self.assertEquals(Pet.objects.using('default').count(), 0) + + self.assertEquals(Person.objects.using('other').count(), 1) + self.assertEquals(Pet.objects.using('other').count(), 1) + + # Delete the person object, which will cascade onto the pet + mark.delete(using='other') + + self.assertEquals(Person.objects.using('default').count(), 0) + self.assertEquals(Pet.objects.using('default').count(), 0) + + # Both the pet and the person have been deleted from the right database + self.assertEquals(Person.objects.using('other').count(), 0) + self.assertEquals(Pet.objects.using('other').count(), 0) + + def test_foreign_key_validation(self): + "ForeignKey.validate() uses the correct database" + mickey = Person.objects.using('other').create(name="Mickey") + pluto = Pet.objects.using('other').create(name="Pluto", owner=mickey) + self.assertEquals(None, pluto.full_clean()) + + def test_o2o_separation(self): + "OneToOne fields are constrained to a single database" + # Create a user and profile on the default database + alice = User.objects.db_manager('default').create_user('alice', 'alice@example.com') + alice_profile = UserProfile.objects.using('default').create(user=alice, flavor='chocolate') + + # Create a user and profile on the other database + bob = User.objects.db_manager('other').create_user('bob', 'bob@example.com') + bob_profile = UserProfile.objects.using('other').create(user=bob, flavor='crunchy frog') + + # Retrieve related objects; queries should be database constrained + alice = User.objects.using('default').get(username="alice") + self.assertEquals(alice.userprofile.flavor, "chocolate") + + bob = User.objects.using('other').get(username="bob") + self.assertEquals(bob.userprofile.flavor, "crunchy frog") + + # Check that queries work across joins + self.assertEquals(list(User.objects.using('default').filter(userprofile__flavor='chocolate').values_list('username', flat=True)), + [u'alice']) + self.assertEquals(list(User.objects.using('other').filter(userprofile__flavor='chocolate').values_list('username', flat=True)), + []) + + self.assertEquals(list(User.objects.using('default').filter(userprofile__flavor='crunchy frog').values_list('username', flat=True)), + []) + self.assertEquals(list(User.objects.using('other').filter(userprofile__flavor='crunchy frog').values_list('username', flat=True)), + [u'bob']) + + # Reget the objects to clear caches + alice_profile = UserProfile.objects.using('default').get(flavor='chocolate') + bob_profile = UserProfile.objects.using('other').get(flavor='crunchy frog') + + # Retrive related object by descriptor. Related objects should be database-baound + self.assertEquals(alice_profile.user.username, 'alice') + self.assertEquals(bob_profile.user.username, 'bob') + + def test_o2o_cross_database_protection(self): + "Operations that involve sharing FK objects across databases raise an error" + # Create a user and profile on the default database + alice = User.objects.db_manager('default').create_user('alice', 'alice@example.com') + + # Create a user and profile on the other database + bob = User.objects.db_manager('other').create_user('bob', 'bob@example.com') + + # Set a one-to-one relation with an object from a different database + alice_profile = UserProfile.objects.using('default').create(user=alice, flavor='chocolate') + try: + bob.userprofile = alice_profile + self.fail("Shouldn't be able to assign across databases") + except ValueError: + pass + + # BUT! if you assign a FK object when the base object hasn't + # been saved yet, you implicitly assign the database for the + # base object. + bob_profile = UserProfile.objects.using('other').create(user=bob, flavor='crunchy frog') + + new_bob_profile = UserProfile(flavor="spring surprise") + + charlie = User(username='charlie',email='charlie@example.com') + charlie.set_unusable_password() + + # initially, no db assigned + self.assertEquals(new_bob_profile._state.db, None) + self.assertEquals(charlie._state.db, None) + + # old object comes from 'other', so the new object is set to use 'other'... + new_bob_profile.user = bob + charlie.userprofile = bob_profile + self.assertEquals(new_bob_profile._state.db, 'other') + self.assertEquals(charlie._state.db, 'other') + + # ... but it isn't saved yet + self.assertEquals(list(User.objects.using('other').values_list('username',flat=True)), + [u'bob']) + self.assertEquals(list(UserProfile.objects.using('other').values_list('flavor',flat=True)), + [u'crunchy frog']) + + # When saved (no using required), new objects goes to 'other' + charlie.save() + bob_profile.save() + new_bob_profile.save() + self.assertEquals(list(User.objects.using('default').values_list('username',flat=True)), + [u'alice']) + self.assertEquals(list(User.objects.using('other').values_list('username',flat=True)), + [u'bob', u'charlie']) + self.assertEquals(list(UserProfile.objects.using('default').values_list('flavor',flat=True)), + [u'chocolate']) + self.assertEquals(list(UserProfile.objects.using('other').values_list('flavor',flat=True)), + [u'crunchy frog', u'spring surprise']) + + # This also works if you assign the O2O relation in the constructor + denise = User.objects.db_manager('other').create_user('denise','denise@example.com') + denise_profile = UserProfile(flavor="tofu", user=denise) + + self.assertEquals(denise_profile._state.db, 'other') + # ... but it isn't saved yet + self.assertEquals(list(UserProfile.objects.using('default').values_list('flavor',flat=True)), + [u'chocolate']) + self.assertEquals(list(UserProfile.objects.using('other').values_list('flavor',flat=True)), + [u'crunchy frog', u'spring surprise']) + + # When saved, the new profile goes to 'other' + denise_profile.save() + self.assertEquals(list(UserProfile.objects.using('default').values_list('flavor',flat=True)), + [u'chocolate']) + self.assertEquals(list(UserProfile.objects.using('other').values_list('flavor',flat=True)), + [u'crunchy frog', u'spring surprise', u'tofu']) + + def test_generic_key_separation(self): + "Generic fields are constrained to a single database" + # Create a book and author on the default database + pro = Book.objects.create(title="Pro Django", + published=datetime.date(2008, 12, 16)) + + review1 = Review.objects.create(source="Python Monthly", content_object=pro) + + # Create a book and author on the other database + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + review2 = Review.objects.using('other').create(source="Python Weekly", content_object=dive) + + review1 = Review.objects.using('default').get(source="Python Monthly") + self.assertEquals(review1.content_object.title, "Pro Django") + + review2 = Review.objects.using('other').get(source="Python Weekly") + self.assertEquals(review2.content_object.title, "Dive into Python") + + # Reget the objects to clear caches + dive = Book.objects.using('other').get(title="Dive into Python") + + # Retrive related object by descriptor. Related objects should be database-bound + self.assertEquals(list(dive.reviews.all().values_list('source', flat=True)), + [u'Python Weekly']) + + def test_generic_key_reverse_operations(self): + "Generic reverse manipulations are all constrained to a single DB" + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + temp = Book.objects.using('other').create(title="Temp", + published=datetime.date(2009, 5, 4)) + + review1 = Review.objects.using('other').create(source="Python Weekly", content_object=dive) + review2 = Review.objects.using('other').create(source="Python Monthly", content_object=temp) + + self.assertEquals(list(Review.objects.using('default').filter(object_id=dive.pk).values_list('source', flat=True)), + []) + self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)), + [u'Python Weekly']) + + # Add a second review + dive.reviews.add(review2) + self.assertEquals(list(Review.objects.using('default').filter(object_id=dive.pk).values_list('source', flat=True)), + []) + self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)), + [u'Python Monthly', u'Python Weekly']) + + # Remove the second author + dive.reviews.remove(review1) + self.assertEquals(list(Review.objects.using('default').filter(object_id=dive.pk).values_list('source', flat=True)), + []) + self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)), + [u'Python Monthly']) + + # Clear all reviews + dive.reviews.clear() + self.assertEquals(list(Review.objects.using('default').filter(object_id=dive.pk).values_list('source', flat=True)), + []) + self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)), + []) + + # Create an author through the generic interface + dive.reviews.create(source='Python Daily') + self.assertEquals(list(Review.objects.using('default').filter(object_id=dive.pk).values_list('source', flat=True)), + []) + self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source', flat=True)), + [u'Python Daily']) + + def test_generic_key_cross_database_protection(self): + "Operations that involve sharing generic key objects across databases raise an error" + # Create a book and author on the default database + pro = Book.objects.create(title="Pro Django", + published=datetime.date(2008, 12, 16)) + + review1 = Review.objects.create(source="Python Monthly", content_object=pro) + + # Create a book and author on the other database + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + review2 = Review.objects.using('other').create(source="Python Weekly", content_object=dive) + + # Set a foreign key with an object from a different database + try: + review1.content_object = dive + self.fail("Shouldn't be able to assign across databases") + except ValueError: + pass + + # Add to a foreign key set with an object from a different database + try: + dive.reviews.add(review1) + self.fail("Shouldn't be able to assign across databases") + except ValueError: + pass + + # BUT! if you assign a FK object when the base object hasn't + # been saved yet, you implicitly assign the database for the + # base object. + review3 = Review(source="Python Daily") + # initially, no db assigned + self.assertEquals(review3._state.db, None) + + # Dive comes from 'other', so review3 is set to use 'other'... + review3.content_object = dive + self.assertEquals(review3._state.db, 'other') + # ... but it isn't saved yet + self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)), + [u'Python Monthly']) + self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)), + [u'Python Weekly']) + + # When saved, John goes to 'other' + review3.save() + self.assertEquals(list(Review.objects.using('default').filter(object_id=pro.pk).values_list('source', flat=True)), + [u'Python Monthly']) + self.assertEquals(list(Review.objects.using('other').filter(object_id=dive.pk).values_list('source',flat=True)), + [u'Python Daily', u'Python Weekly']) + + def test_generic_key_deletion(self): + "Cascaded deletions of Generic Key relations issue queries on the right database" + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + review = Review.objects.using('other').create(source="Python Weekly", content_object=dive) + + # Check the initial state + self.assertEquals(Book.objects.using('default').count(), 0) + self.assertEquals(Review.objects.using('default').count(), 0) + + self.assertEquals(Book.objects.using('other').count(), 1) + self.assertEquals(Review.objects.using('other').count(), 1) + + # Delete the Book object, which will cascade onto the pet + dive.delete(using='other') + + self.assertEquals(Book.objects.using('default').count(), 0) + self.assertEquals(Review.objects.using('default').count(), 0) + + # Both the pet and the person have been deleted from the right database + self.assertEquals(Book.objects.using('other').count(), 0) + self.assertEquals(Review.objects.using('other').count(), 0) + + def test_ordering(self): + "get_next_by_XXX commands stick to a single database" + pro = Book.objects.create(title="Pro Django", + published=datetime.date(2008, 12, 16)) + + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + learn = Book.objects.using('other').create(title="Learning Python", + published=datetime.date(2008, 7, 16)) + + self.assertEquals(learn.get_next_by_published().title, "Dive into Python") + self.assertEquals(dive.get_previous_by_published().title, "Learning Python") + + def test_raw(self): + "test the raw() method across databases" + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + val = Book.objects.db_manager("other").raw('SELECT id FROM multiple_database_book') + self.assertEqual(map(lambda o: o.pk, val), [dive.pk]) + + val = Book.objects.raw('SELECT id FROM multiple_database_book').using('other') + self.assertEqual(map(lambda o: o.pk, val), [dive.pk]) + + def test_select_related(self): + "Database assignment is retained if an object is retrieved with select_related()" + # Create a book and author on the other database + mark = Person.objects.using('other').create(name="Mark Pilgrim") + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4), + editor=mark) + + # Retrieve the Person using select_related() + book = Book.objects.using('other').select_related('editor').get(title="Dive into Python") + + # The editor instance should have a db state + self.assertEqual(book.editor._state.db, 'other') + + def test_subquery(self): + """Make sure as_sql works with subqueries and master/slave.""" + sub = Person.objects.using('other').filter(name='fff') + qs = Book.objects.filter(editor__in=sub) + + # When you call __str__ on the query object, it doesn't know about using + # so it falls back to the default. If the subquery explicitly uses a + # different database, an error should be raised. + self.assertRaises(ValueError, str, qs.query) + + # Evaluating the query shouldn't work, either + try: + for obj in qs: + pass + self.fail('Iterating over query should raise ValueError') + except ValueError: + pass + + def test_related_manager(self): + "Related managers return managers, not querysets" + mark = Person.objects.using('other').create(name="Mark Pilgrim") + + # extra_arg is removed by the BookManager's implementation of + # create(); but the BookManager's implementation won't get called + # unless edited returns a Manager, not a queryset + mark.book_set.create(title="Dive into Python", + published=datetime.date(2009, 5, 4), + extra_arg=True) + + mark.book_set.get_or_create(title="Dive into Python", + published=datetime.date(2009, 5, 4), + extra_arg=True) + + mark.edited.create(title="Dive into Water", + published=datetime.date(2009, 5, 4), + extra_arg=True) + + mark.edited.get_or_create(title="Dive into Water", + published=datetime.date(2009, 5, 4), + extra_arg=True) + +class TestRouter(object): + # A test router. The behaviour is vaguely master/slave, but the + # databases aren't assumed to propagate changes. + def db_for_read(self, model, instance=None, **hints): + if instance: + return instance._state.db or 'other' + return 'other' + + def db_for_write(self, model, **hints): + return DEFAULT_DB_ALIAS + + def allow_relation(self, obj1, obj2, **hints): + return obj1._state.db in ('default', 'other') and obj2._state.db in ('default', 'other') + + def allow_syncdb(self, db, model): + return True + +class AuthRouter(object): + """A router to control all database operations on models in + the contrib.auth application""" + + def db_for_read(self, model, **hints): + "Point all read operations on auth models to 'default'" + if model._meta.app_label == 'auth': + # We use default here to ensure we can tell the difference + # between a read request and a write request for Auth objects + return 'default' + return None + + def db_for_write(self, model, **hints): + "Point all operations on auth models to 'other'" + if model._meta.app_label == 'auth': + return 'other' + return None + + def allow_relation(self, obj1, obj2, **hints): + "Allow any relation if a model in Auth is involved" + if obj1._meta.app_label == 'auth' or obj2._meta.app_label == 'auth': + return True + return None + + def allow_syncdb(self, db, model): + "Make sure the auth app only appears on the 'other' db" + if db == 'other': + return model._meta.app_label == 'auth' + elif model._meta.app_label == 'auth': + return False + return None + +class WriteRouter(object): + # A router that only expresses an opinion on writes + def db_for_write(self, model, **hints): + return 'writer' + +class RouterTestCase(TestCase): + multi_db = True + + def setUp(self): + # Make the 'other' database appear to be a slave of the 'default' + self.old_routers = router.routers + router.routers = [TestRouter()] + + def tearDown(self): + # Restore the 'other' database as an independent database + router.routers = self.old_routers + + def test_db_selection(self): + "Check that querysets obey the router for db suggestions" + self.assertEquals(Book.objects.db, 'other') + self.assertEquals(Book.objects.all().db, 'other') + + self.assertEquals(Book.objects.using('default').db, 'default') + + self.assertEquals(Book.objects.db_manager('default').db, 'default') + self.assertEquals(Book.objects.db_manager('default').all().db, 'default') + + def test_syncdb_selection(self): + "Synchronization behaviour is predicatable" + + self.assertTrue(router.allow_syncdb('default', User)) + self.assertTrue(router.allow_syncdb('default', Book)) + + self.assertTrue(router.allow_syncdb('other', User)) + self.assertTrue(router.allow_syncdb('other', Book)) + + # Add the auth router to the chain. + # TestRouter is a universal synchronizer, so it should have no effect. + router.routers = [TestRouter(), AuthRouter()] + + self.assertTrue(router.allow_syncdb('default', User)) + self.assertTrue(router.allow_syncdb('default', Book)) + + self.assertTrue(router.allow_syncdb('other', User)) + self.assertTrue(router.allow_syncdb('other', Book)) + + # Now check what happens if the router order is the other way around + router.routers = [AuthRouter(), TestRouter()] + + self.assertFalse(router.allow_syncdb('default', User)) + self.assertTrue(router.allow_syncdb('default', Book)) + + self.assertTrue(router.allow_syncdb('other', User)) + self.assertFalse(router.allow_syncdb('other', Book)) + + def test_partial_router(self): + "A router can choose to implement a subset of methods" + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + # First check the baseline behaviour + + self.assertEquals(router.db_for_read(User), 'other') + self.assertEquals(router.db_for_read(Book), 'other') + + self.assertEquals(router.db_for_write(User), 'default') + self.assertEquals(router.db_for_write(Book), 'default') + + self.assertTrue(router.allow_relation(dive, dive)) + + self.assertTrue(router.allow_syncdb('default', User)) + self.assertTrue(router.allow_syncdb('default', Book)) + + router.routers = [WriteRouter(), AuthRouter(), TestRouter()] + + self.assertEquals(router.db_for_read(User), 'default') + self.assertEquals(router.db_for_read(Book), 'other') + + self.assertEquals(router.db_for_write(User), 'writer') + self.assertEquals(router.db_for_write(Book), 'writer') + + self.assertTrue(router.allow_relation(dive, dive)) + + self.assertFalse(router.allow_syncdb('default', User)) + self.assertTrue(router.allow_syncdb('default', Book)) + + + def test_database_routing(self): + marty = Person.objects.using('default').create(name="Marty Alchin") + pro = Book.objects.using('default').create(title="Pro Django", + published=datetime.date(2008, 12, 16), + editor=marty) + pro.authors = [marty] + + # Create a book and author on the other database + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + # An update query will be routed to the default database + Book.objects.filter(title='Pro Django').update(pages=200) + + try: + # By default, the get query will be directed to 'other' + Book.objects.get(title='Pro Django') + self.fail("Shouldn't be able to find the book") + except Book.DoesNotExist: + pass + + # But the same query issued explicitly at a database will work. + pro = Book.objects.using('default').get(title='Pro Django') + + # Check that the update worked. + self.assertEquals(pro.pages, 200) + + # An update query with an explicit using clause will be routed + # to the requested database. + Book.objects.using('other').filter(title='Dive into Python').update(pages=300) + self.assertEquals(Book.objects.get(title='Dive into Python').pages, 300) + + # Related object queries stick to the same database + # as the original object, regardless of the router + self.assertEquals(list(pro.authors.values_list('name', flat=True)), [u'Marty Alchin']) + self.assertEquals(pro.editor.name, u'Marty Alchin') + + # get_or_create is a special case. The get needs to be targetted at + # the write database in order to avoid potential transaction + # consistency problems + book, created = Book.objects.get_or_create(title="Pro Django") + self.assertFalse(created) + + book, created = Book.objects.get_or_create(title="Dive Into Python", + defaults={'published':datetime.date(2009, 5, 4)}) + self.assertTrue(created) + + # Check the head count of objects + self.assertEquals(Book.objects.using('default').count(), 2) + self.assertEquals(Book.objects.using('other').count(), 1) + # If a database isn't specified, the read database is used + self.assertEquals(Book.objects.count(), 1) + + # A delete query will also be routed to the default database + Book.objects.filter(pages__gt=150).delete() + + # The default database has lost the book. + self.assertEquals(Book.objects.using('default').count(), 1) + self.assertEquals(Book.objects.using('other').count(), 1) + + def test_foreign_key_cross_database_protection(self): + "Foreign keys can cross databases if they two databases have a common source" + # Create a book and author on the default database + pro = Book.objects.using('default').create(title="Pro Django", + published=datetime.date(2008, 12, 16)) + + marty = Person.objects.using('default').create(name="Marty Alchin") + + # Create a book and author on the other database + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + mark = Person.objects.using('other').create(name="Mark Pilgrim") + + # Set a foreign key with an object from a different database + try: + dive.editor = marty + except ValueError: + self.fail("Assignment across master/slave databases with a common source should be ok") + + # Database assignments of original objects haven't changed... + self.assertEquals(marty._state.db, 'default') + self.assertEquals(pro._state.db, 'default') + self.assertEquals(dive._state.db, 'other') + self.assertEquals(mark._state.db, 'other') + + # ... but they will when the affected object is saved. + dive.save() + self.assertEquals(dive._state.db, 'default') + + # ...and the source database now has a copy of any object saved + try: + Book.objects.using('default').get(title='Dive into Python').delete() + except Book.DoesNotExist: + self.fail('Source database should have a copy of saved object') + + # This isn't a real master-slave database, so restore the original from other + dive = Book.objects.using('other').get(title='Dive into Python') + self.assertEquals(dive._state.db, 'other') + + # Set a foreign key set with an object from a different database + try: + marty.edited = [pro, dive] + except ValueError: + self.fail("Assignment across master/slave databases with a common source should be ok") + + # Assignment implies a save, so database assignments of original objects have changed... + self.assertEquals(marty._state.db, 'default') + self.assertEquals(pro._state.db, 'default') + self.assertEquals(dive._state.db, 'default') + self.assertEquals(mark._state.db, 'other') + + # ...and the source database now has a copy of any object saved + try: + Book.objects.using('default').get(title='Dive into Python').delete() + except Book.DoesNotExist: + self.fail('Source database should have a copy of saved object') + + # This isn't a real master-slave database, so restore the original from other + dive = Book.objects.using('other').get(title='Dive into Python') + self.assertEquals(dive._state.db, 'other') + + # Add to a foreign key set with an object from a different database + try: + marty.edited.add(dive) + except ValueError: + self.fail("Assignment across master/slave databases with a common source should be ok") + + # Add implies a save, so database assignments of original objects have changed... + self.assertEquals(marty._state.db, 'default') + self.assertEquals(pro._state.db, 'default') + self.assertEquals(dive._state.db, 'default') + self.assertEquals(mark._state.db, 'other') + + # ...and the source database now has a copy of any object saved + try: + Book.objects.using('default').get(title='Dive into Python').delete() + except Book.DoesNotExist: + self.fail('Source database should have a copy of saved object') + + # This isn't a real master-slave database, so restore the original from other + dive = Book.objects.using('other').get(title='Dive into Python') + + # If you assign a FK object when the base object hasn't + # been saved yet, you implicitly assign the database for the + # base object. + chris = Person(name="Chris Mills") + html5 = Book(title="Dive into HTML5", published=datetime.date(2010, 3, 15)) + # initially, no db assigned + self.assertEquals(chris._state.db, None) + self.assertEquals(html5._state.db, None) + + # old object comes from 'other', so the new object is set to use the + # source of 'other'... + self.assertEquals(dive._state.db, 'other') + dive.editor = chris + html5.editor = mark + + self.assertEquals(dive._state.db, 'other') + self.assertEquals(mark._state.db, 'other') + self.assertEquals(chris._state.db, 'default') + self.assertEquals(html5._state.db, 'default') + + # This also works if you assign the FK in the constructor + water = Book(title="Dive into Water", published=datetime.date(2001, 1, 1), editor=mark) + self.assertEquals(water._state.db, 'default') + + # If you create an object through a FK relation, it will be + # written to the write database, even if the original object + # was on the read database + cheesecake = mark.edited.create(title='Dive into Cheesecake', published=datetime.date(2010, 3, 15)) + self.assertEquals(cheesecake._state.db, 'default') + + # Same goes for get_or_create, regardless of whether getting or creating + cheesecake, created = mark.edited.get_or_create(title='Dive into Cheesecake', published=datetime.date(2010, 3, 15)) + self.assertEquals(cheesecake._state.db, 'default') + + puddles, created = mark.edited.get_or_create(title='Dive into Puddles', published=datetime.date(2010, 3, 15)) + self.assertEquals(puddles._state.db, 'default') + + def test_m2m_cross_database_protection(self): + "M2M relations can cross databases if the database share a source" + # Create books and authors on the inverse to the usual database + pro = Book.objects.using('other').create(pk=1, title="Pro Django", + published=datetime.date(2008, 12, 16)) + + marty = Person.objects.using('other').create(pk=1, name="Marty Alchin") + + dive = Book.objects.using('default').create(pk=2, title="Dive into Python", + published=datetime.date(2009, 5, 4)) + + mark = Person.objects.using('default').create(pk=2, name="Mark Pilgrim") + + # Now save back onto the usual databse. + # This simulates master/slave - the objects exist on both database, + # but the _state.db is as it is for all other tests. + pro.save(using='default') + marty.save(using='default') + dive.save(using='other') + mark.save(using='other') + + # Check that we have 2 of both types of object on both databases + self.assertEquals(Book.objects.using('default').count(), 2) + self.assertEquals(Book.objects.using('other').count(), 2) + self.assertEquals(Person.objects.using('default').count(), 2) + self.assertEquals(Person.objects.using('other').count(), 2) + + # Set a m2m set with an object from a different database + try: + marty.book_set = [pro, dive] + except ValueError: + self.fail("Assignment across master/slave databases with a common source should be ok") + + # Database assignments don't change + self.assertEquals(marty._state.db, 'default') + self.assertEquals(pro._state.db, 'default') + self.assertEquals(dive._state.db, 'other') + self.assertEquals(mark._state.db, 'other') + + # All m2m relations should be saved on the default database + self.assertEquals(Book.authors.through.objects.using('default').count(), 2) + self.assertEquals(Book.authors.through.objects.using('other').count(), 0) + + # Reset relations + Book.authors.through.objects.using('default').delete() + + # Add to an m2m with an object from a different database + try: + marty.book_set.add(dive) + except ValueError: + self.fail("Assignment across master/slave databases with a common source should be ok") + + # Database assignments don't change + self.assertEquals(marty._state.db, 'default') + self.assertEquals(pro._state.db, 'default') + self.assertEquals(dive._state.db, 'other') + self.assertEquals(mark._state.db, 'other') + + # All m2m relations should be saved on the default database + self.assertEquals(Book.authors.through.objects.using('default').count(), 1) + self.assertEquals(Book.authors.through.objects.using('other').count(), 0) + + # Reset relations + Book.authors.through.objects.using('default').delete() + + # Set a reverse m2m with an object from a different database + try: + dive.authors = [mark, marty] + except ValueError: + self.fail("Assignment across master/slave databases with a common source should be ok") + + # Database assignments don't change + self.assertEquals(marty._state.db, 'default') + self.assertEquals(pro._state.db, 'default') + self.assertEquals(dive._state.db, 'other') + self.assertEquals(mark._state.db, 'other') + + # All m2m relations should be saved on the default database + self.assertEquals(Book.authors.through.objects.using('default').count(), 2) + self.assertEquals(Book.authors.through.objects.using('other').count(), 0) + + # Reset relations + Book.authors.through.objects.using('default').delete() + + self.assertEquals(Book.authors.through.objects.using('default').count(), 0) + self.assertEquals(Book.authors.through.objects.using('other').count(), 0) + + # Add to a reverse m2m with an object from a different database + try: + dive.authors.add(marty) + except ValueError: + self.fail("Assignment across master/slave databases with a common source should be ok") + + # Database assignments don't change + self.assertEquals(marty._state.db, 'default') + self.assertEquals(pro._state.db, 'default') + self.assertEquals(dive._state.db, 'other') + self.assertEquals(mark._state.db, 'other') + + # All m2m relations should be saved on the default database + self.assertEquals(Book.authors.through.objects.using('default').count(), 1) + self.assertEquals(Book.authors.through.objects.using('other').count(), 0) + + # If you create an object through a M2M relation, it will be + # written to the write database, even if the original object + # was on the read database + alice = dive.authors.create(name='Alice') + self.assertEquals(alice._state.db, 'default') + + # Same goes for get_or_create, regardless of whether getting or creating + alice, created = dive.authors.get_or_create(name='Alice') + self.assertEquals(alice._state.db, 'default') + + bob, created = dive.authors.get_or_create(name='Bob') + self.assertEquals(bob._state.db, 'default') + + def test_o2o_cross_database_protection(self): + "Operations that involve sharing FK objects across databases raise an error" + # Create a user and profile on the default database + alice = User.objects.db_manager('default').create_user('alice', 'alice@example.com') + + # Create a user and profile on the other database + bob = User.objects.db_manager('other').create_user('bob', 'bob@example.com') + + # Set a one-to-one relation with an object from a different database + alice_profile = UserProfile.objects.create(user=alice, flavor='chocolate') + try: + bob.userprofile = alice_profile + except ValueError: + self.fail("Assignment across master/slave databases with a common source should be ok") + + # Database assignments of original objects haven't changed... + self.assertEquals(alice._state.db, 'default') + self.assertEquals(alice_profile._state.db, 'default') + self.assertEquals(bob._state.db, 'other') + + # ... but they will when the affected object is saved. + bob.save() + self.assertEquals(bob._state.db, 'default') + + def test_generic_key_cross_database_protection(self): + "Generic Key operations can span databases if they share a source" + # Create a book and author on the default database + pro = Book.objects.using('default' + ).create(title="Pro Django", published=datetime.date(2008, 12, 16)) + + review1 = Review.objects.using('default' + ).create(source="Python Monthly", content_object=pro) + + # Create a book and author on the other database + dive = Book.objects.using('other' + ).create(title="Dive into Python", published=datetime.date(2009, 5, 4)) + + review2 = Review.objects.using('other' + ).create(source="Python Weekly", content_object=dive) + + # Set a generic foreign key with an object from a different database + try: + review1.content_object = dive + except ValueError: + self.fail("Assignment across master/slave databases with a common source should be ok") + + # Database assignments of original objects haven't changed... + self.assertEquals(pro._state.db, 'default') + self.assertEquals(review1._state.db, 'default') + self.assertEquals(dive._state.db, 'other') + self.assertEquals(review2._state.db, 'other') + + # ... but they will when the affected object is saved. + dive.save() + self.assertEquals(review1._state.db, 'default') + self.assertEquals(dive._state.db, 'default') + + # ...and the source database now has a copy of any object saved + try: + Book.objects.using('default').get(title='Dive into Python').delete() + except Book.DoesNotExist: + self.fail('Source database should have a copy of saved object') + + # This isn't a real master-slave database, so restore the original from other + dive = Book.objects.using('other').get(title='Dive into Python') + self.assertEquals(dive._state.db, 'other') + + # Add to a generic foreign key set with an object from a different database + try: + dive.reviews.add(review1) + except ValueError: + self.fail("Assignment across master/slave databases with a common source should be ok") + + # Database assignments of original objects haven't changed... + self.assertEquals(pro._state.db, 'default') + self.assertEquals(review1._state.db, 'default') + self.assertEquals(dive._state.db, 'other') + self.assertEquals(review2._state.db, 'other') + + # ... but they will when the affected object is saved. + dive.save() + self.assertEquals(dive._state.db, 'default') + + # ...and the source database now has a copy of any object saved + try: + Book.objects.using('default').get(title='Dive into Python').delete() + except Book.DoesNotExist: + self.fail('Source database should have a copy of saved object') + + # BUT! if you assign a FK object when the base object hasn't + # been saved yet, you implicitly assign the database for the + # base object. + review3 = Review(source="Python Daily") + # initially, no db assigned + self.assertEquals(review3._state.db, None) + + # Dive comes from 'other', so review3 is set to use the source of 'other'... + review3.content_object = dive + self.assertEquals(review3._state.db, 'default') + + # If you create an object through a M2M relation, it will be + # written to the write database, even if the original object + # was on the read database + dive = Book.objects.using('other').get(title='Dive into Python') + nyt = dive.reviews.create(source="New York Times", content_object=dive) + self.assertEquals(nyt._state.db, 'default') + + def test_m2m_managers(self): + "M2M relations are represented by managers, and can be controlled like managers" + pro = Book.objects.using('other').create(pk=1, title="Pro Django", + published=datetime.date(2008, 12, 16)) + + marty = Person.objects.using('other').create(pk=1, name="Marty Alchin") + pro.authors = [marty] + + self.assertEquals(pro.authors.db, 'other') + self.assertEquals(pro.authors.db_manager('default').db, 'default') + self.assertEquals(pro.authors.db_manager('default').all().db, 'default') + + self.assertEquals(marty.book_set.db, 'other') + self.assertEquals(marty.book_set.db_manager('default').db, 'default') + self.assertEquals(marty.book_set.db_manager('default').all().db, 'default') + + def test_foreign_key_managers(self): + "FK reverse relations are represented by managers, and can be controlled like managers" + marty = Person.objects.using('other').create(pk=1, name="Marty Alchin") + pro = Book.objects.using('other').create(pk=1, title="Pro Django", + published=datetime.date(2008, 12, 16), + editor=marty) + + self.assertEquals(marty.edited.db, 'other') + self.assertEquals(marty.edited.db_manager('default').db, 'default') + self.assertEquals(marty.edited.db_manager('default').all().db, 'default') + + def test_generic_key_managers(self): + "Generic key relations are represented by managers, and can be controlled like managers" + pro = Book.objects.using('other').create(title="Pro Django", + published=datetime.date(2008, 12, 16)) + + review1 = Review.objects.using('other').create(source="Python Monthly", + content_object=pro) + + self.assertEquals(pro.reviews.db, 'other') + self.assertEquals(pro.reviews.db_manager('default').db, 'default') + self.assertEquals(pro.reviews.db_manager('default').all().db, 'default') + + def test_subquery(self): + """Make sure as_sql works with subqueries and master/slave.""" + # Create a book and author on the other database + + mark = Person.objects.using('other').create(name="Mark Pilgrim") + dive = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4), + editor=mark) + + sub = Person.objects.filter(name='Mark Pilgrim') + qs = Book.objects.filter(editor__in=sub) + + # When you call __str__ on the query object, it doesn't know about using + # so it falls back to the default. Don't let routing instructions + # force the subquery to an incompatible database. + str(qs.query) + + # If you evaluate the query, it should work, running on 'other' + self.assertEquals(list(qs.values_list('title', flat=True)), [u'Dive into Python']) + +class AuthTestCase(TestCase): + multi_db = True + + def setUp(self): + # Make the 'other' database appear to be a slave of the 'default' + self.old_routers = router.routers + router.routers = [AuthRouter()] + + def tearDown(self): + # Restore the 'other' database as an independent database + router.routers = self.old_routers + + def test_auth_manager(self): + "The methods on the auth manager obey database hints" + # Create one user using default allocation policy + User.objects.create_user('alice', 'alice@example.com') + + # Create another user, explicitly specifying the database + User.objects.db_manager('default').create_user('bob', 'bob@example.com') + + # The second user only exists on the other database + alice = User.objects.using('other').get(username='alice') + + self.assertEquals(alice.username, 'alice') + self.assertEquals(alice._state.db, 'other') + + self.assertRaises(User.DoesNotExist, User.objects.using('default').get, username='alice') + + # The second user only exists on the default database + bob = User.objects.using('default').get(username='bob') + + self.assertEquals(bob.username, 'bob') + self.assertEquals(bob._state.db, 'default') + + self.assertRaises(User.DoesNotExist, User.objects.using('other').get, username='bob') + + # That is... there is one user on each database + self.assertEquals(User.objects.using('default').count(), 1) + self.assertEquals(User.objects.using('other').count(), 1) + + def test_dumpdata(self): + "Check that dumpdata honors allow_syncdb restrictions on the router" + User.objects.create_user('alice', 'alice@example.com') + User.objects.db_manager('default').create_user('bob', 'bob@example.com') + + # Check that dumping the default database doesn't try to include auth + # because allow_syncdb prohibits auth on default + 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 + 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) + +_missing = object() +class UserProfileTestCase(TestCase): + def setUp(self): + self.old_auth_profile_module = getattr(settings, 'AUTH_PROFILE_MODULE', _missing) + settings.AUTH_PROFILE_MODULE = 'multiple_database.UserProfile' + + def tearDown(self): + if self.old_auth_profile_module is _missing: + del settings.AUTH_PROFILE_MODULE + else: + settings.AUTH_PROFILE_MODULE = self.old_auth_profile_module + + def test_user_profiles(self): + + alice = User.objects.create_user('alice', 'alice@example.com') + bob = User.objects.db_manager('other').create_user('bob', 'bob@example.com') + + alice_profile = UserProfile(user=alice, flavor='chocolate') + alice_profile.save() + + bob_profile = UserProfile(user=bob, flavor='crunchy frog') + bob_profile.save() + + 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 + try: + Book.objects.get(title="Pro Django") + Book.objects.using('default').get(title="Pro Django") + except Book.DoesNotExist: + self.fail('"Pro Django" should exist on default database') + + self.assertRaises(Book.DoesNotExist, + Book.objects.using('other').get, + title="Pro Django" + ) + + # Check that "Dive into Python" exists on the default database, but not on other database + try: + Book.objects.using('other').get(title="Dive into Python") + except Book.DoesNotExist: + self.fail('"Dive into Python" should exist on other database') + + self.assertRaises(Book.DoesNotExist, + Book.objects.get, + title="Dive into Python" + ) + self.assertRaises(Book.DoesNotExist, + Book.objects.using('default').get, + title="Dive into Python" + ) + + # Check that "Definitive Guide" exists on the both databases + try: + Book.objects.get(title="The Definitive Guide to Django") + Book.objects.using('default').get(title="The Definitive Guide to Django") + Book.objects.using('other').get(title="The Definitive Guide to Django") + 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.assertTrue("Installed 0 object(s) (of 2) from 1 fixture(s)" in command_output) + +class PickleQuerySetTestCase(TestCase): + multi_db = True + + def test_pickling(self): + for db in connections: + 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) diff --git a/parts/django/tests/regressiontests/null_fk/__init__.py b/parts/django/tests/regressiontests/null_fk/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/null_fk/__init__.py diff --git a/parts/django/tests/regressiontests/null_fk/models.py b/parts/django/tests/regressiontests/null_fk/models.py new file mode 100644 index 0000000..3cce319 --- /dev/null +++ b/parts/django/tests/regressiontests/null_fk/models.py @@ -0,0 +1,33 @@ +""" +Regression tests for proper working of ForeignKey(null=True). +""" + +from django.db import models + +class SystemDetails(models.Model): + details = models.TextField() + +class SystemInfo(models.Model): + system_details = models.ForeignKey(SystemDetails) + system_name = models.CharField(max_length=32) + +class Forum(models.Model): + system_info = models.ForeignKey(SystemInfo) + forum_name = models.CharField(max_length=32) + +class Post(models.Model): + forum = models.ForeignKey(Forum, null=True) + title = models.CharField(max_length=32) + + def __unicode__(self): + return self.title + +class Comment(models.Model): + post = models.ForeignKey(Post, null=True) + comment_text = models.CharField(max_length=250) + + class Meta: + ordering = ('comment_text',) + + def __unicode__(self): + return self.comment_text diff --git a/parts/django/tests/regressiontests/null_fk/tests.py b/parts/django/tests/regressiontests/null_fk/tests.py new file mode 100644 index 0000000..449f343 --- /dev/null +++ b/parts/django/tests/regressiontests/null_fk/tests.py @@ -0,0 +1,42 @@ +from django.test import TestCase + +from regressiontests.null_fk.models import * + +class NullFkTests(TestCase): + + def test_null_fk(self): + d = SystemDetails.objects.create(details='First details') + s = SystemInfo.objects.create(system_name='First forum', system_details=d) + f = Forum.objects.create(system_info=s, forum_name='First forum') + p = Post.objects.create(forum=f, title='First Post') + c1 = Comment.objects.create(post=p, comment_text='My first comment') + c2 = Comment.objects.create(comment_text='My second comment') + + # Starting from comment, make sure that a .select_related(...) with a specified + # set of fields will properly LEFT JOIN multiple levels of NULLs (and the things + # that come after the NULLs, or else data that should exist won't). Regression + # test for #7369. + c = Comment.objects.select_related().get(id=1) + self.assertEquals(c.post, p) + self.assertEquals(Comment.objects.select_related().get(id=2).post, None) + + self.assertQuerysetEqual( + Comment.objects.select_related('post__forum__system_info').all(), + [ + (1, u'My first comment', '<Post: First Post>'), + (2, u'My second comment', 'None') + ], + transform = lambda c: (c.id, c.comment_text, repr(c.post)) + ) + + # Regression test for #7530, #7716. + self.assertTrue(Comment.objects.select_related('post').filter(post__isnull=True)[0].post is None) + + self.assertQuerysetEqual( + Comment.objects.select_related('post__forum__system_info__system_details'), + [ + (1, u'My first comment', '<Post: First Post>'), + (2, u'My second comment', 'None') + ], + transform = lambda c: (c.id, c.comment_text, repr(c.post)) + ) diff --git a/parts/django/tests/regressiontests/null_fk_ordering/__init__.py b/parts/django/tests/regressiontests/null_fk_ordering/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/null_fk_ordering/__init__.py diff --git a/parts/django/tests/regressiontests/null_fk_ordering/models.py b/parts/django/tests/regressiontests/null_fk_ordering/models.py new file mode 100644 index 0000000..d0635e8 --- /dev/null +++ b/parts/django/tests/regressiontests/null_fk_ordering/models.py @@ -0,0 +1,49 @@ +""" +Regression tests for proper working of ForeignKey(null=True). Tests these bugs: + + * #7512: including a nullable foreign key reference in Meta ordering has un +xpected results + +""" + +from django.db import models + +# The first two models represent a very simple null FK ordering case. +class Author(models.Model): + name = models.CharField(max_length=150) + +class Article(models.Model): + title = models.CharField(max_length=150) + author = models.ForeignKey(Author, null=True) + + def __unicode__(self): + return u'Article titled: %s' % (self.title, ) + + class Meta: + ordering = ['author__name', ] + + +# These following 4 models represent a far more complex ordering case. +class SystemInfo(models.Model): + system_name = models.CharField(max_length=32) + +class Forum(models.Model): + system_info = models.ForeignKey(SystemInfo) + forum_name = models.CharField(max_length=32) + +class Post(models.Model): + forum = models.ForeignKey(Forum, null=True) + title = models.CharField(max_length=32) + + def __unicode__(self): + return self.title + +class Comment(models.Model): + post = models.ForeignKey(Post, null=True) + comment_text = models.CharField(max_length=250) + + class Meta: + ordering = ['post__forum__system_info__system_name', 'comment_text'] + + def __unicode__(self): + return self.comment_text diff --git a/parts/django/tests/regressiontests/null_fk_ordering/tests.py b/parts/django/tests/regressiontests/null_fk_ordering/tests.py new file mode 100644 index 0000000..c9ee4f7 --- /dev/null +++ b/parts/django/tests/regressiontests/null_fk_ordering/tests.py @@ -0,0 +1,39 @@ +from django.test import TestCase + +from regressiontests.null_fk_ordering.models import * + +class NullFkOrderingTests(TestCase): + + def test_ordering_across_null_fk(self): + """ + Regression test for #7512 + + ordering across nullable Foreign Keys shouldn't exclude results + """ + author_1 = Author.objects.create(name='Tom Jones') + author_2 = Author.objects.create(name='Bob Smith') + article_1 = Article.objects.create(title='No author on this article') + article_2 = Article.objects.create(author=author_1, title='This article written by Tom Jones') + article_3 = Article.objects.create(author=author_2, title='This article written by Bob Smith') + + # We can't compare results directly (since different databases sort NULLs to + # different ends of the ordering), but we can check that all results are + # returned. + self.assertTrue(len(list(Article.objects.all())) == 3) + + s = SystemInfo.objects.create(system_name='System Info') + f = Forum.objects.create(system_info=s, forum_name='First forum') + p = Post.objects.create(forum=f, title='First Post') + c1 = Comment.objects.create(post=p, comment_text='My first comment') + c2 = Comment.objects.create(comment_text='My second comment') + s2 = SystemInfo.objects.create(system_name='More System Info') + f2 = Forum.objects.create(system_info=s2, forum_name='Second forum') + p2 = Post.objects.create(forum=f2, title='Second Post') + c3 = Comment.objects.create(comment_text='Another first comment') + c4 = Comment.objects.create(post=p2, comment_text='Another second comment') + + # We have to test this carefully. Some databases sort NULL values before + # everything else, some sort them afterwards. So we extract the ordered list + # and check the length. Before the fix, this list was too short (some values + # were omitted). + self.assertTrue(len(list(Comment.objects.all())) == 4) diff --git a/parts/django/tests/regressiontests/null_queries/__init__.py b/parts/django/tests/regressiontests/null_queries/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/null_queries/__init__.py diff --git a/parts/django/tests/regressiontests/null_queries/models.py b/parts/django/tests/regressiontests/null_queries/models.py new file mode 100644 index 0000000..442535c --- /dev/null +++ b/parts/django/tests/regressiontests/null_queries/models.py @@ -0,0 +1,25 @@ +from django.db import models + +class Poll(models.Model): + question = models.CharField(max_length=200) + + def __unicode__(self): + return u"Q: %s " % self.question + +class Choice(models.Model): + poll = models.ForeignKey(Poll) + choice = models.CharField(max_length=200) + + def __unicode__(self): + return u"Choice: %s in poll %s" % (self.choice, self.poll) + +# A set of models with an inner one pointing to two outer ones. +class OuterA(models.Model): + pass + +class OuterB(models.Model): + data = models.CharField(max_length=10) + +class Inner(models.Model): + first = models.ForeignKey(OuterA) + second = models.ForeignKey(OuterB, null=True) diff --git a/parts/django/tests/regressiontests/null_queries/tests.py b/parts/django/tests/regressiontests/null_queries/tests.py new file mode 100644 index 0000000..72dcd51 --- /dev/null +++ b/parts/django/tests/regressiontests/null_queries/tests.py @@ -0,0 +1,69 @@ +from django.test import TestCase +from django.core.exceptions import FieldError + +from regressiontests.null_queries.models import * + + +class NullQueriesTests(TestCase): + + def test_none_as_null(self): + """ + Regression test for the use of None as a query value. + + None is interpreted as an SQL NULL, but only in __exact queries. + Set up some initial polls and choices + """ + p1 = Poll(question='Why?') + p1.save() + c1 = Choice(poll=p1, choice='Because.') + c1.save() + c2 = Choice(poll=p1, choice='Why Not?') + c2.save() + + # Exact query with value None returns nothing ("is NULL" in sql, + # but every 'id' field has a value). + self.assertQuerysetEqual(Choice.objects.filter(choice__exact=None), []) + + # Excluding the previous result returns everything. + self.assertQuerysetEqual( + Choice.objects.exclude(choice=None).order_by('id'), + [ + '<Choice: Choice: Because. in poll Q: Why? >', + '<Choice: Choice: Why Not? in poll Q: Why? >' + ] + ) + + # Valid query, but fails because foo isn't a keyword + self.assertRaises(FieldError, Choice.objects.filter, foo__exact=None) + + # Can't use None on anything other than __exact + self.assertRaises(ValueError, Choice.objects.filter, id__gt=None) + + # Can't use None on anything other than __exact + self.assertRaises(ValueError, Choice.objects.filter, foo__gt=None) + + # Related managers use __exact=None implicitly if the object hasn't been saved. + p2 = Poll(question="How?") + self.assertEquals(repr(p2.choice_set.all()), '[]') + + def test_reverse_relations(self): + """ + Querying across reverse relations and then another relation should + insert outer joins correctly so as not to exclude results. + """ + obj = OuterA.objects.create() + self.assertQuerysetEqual( + OuterA.objects.filter(inner__second=None), + ['<OuterA: OuterA object>'] + ) + self.assertQuerysetEqual( + OuterA.objects.filter(inner__second__data=None), + ['<OuterA: OuterA object>'] + ) + + inner_obj = Inner.objects.create(first=obj) + self.assertQuerysetEqual( + Inner.objects.filter(first__inner__second=None), + ['<Inner: Inner object>'] + ) + diff --git a/parts/django/tests/regressiontests/one_to_one_regress/__init__.py b/parts/django/tests/regressiontests/one_to_one_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/one_to_one_regress/__init__.py diff --git a/parts/django/tests/regressiontests/one_to_one_regress/models.py b/parts/django/tests/regressiontests/one_to_one_regress/models.py new file mode 100644 index 0000000..a7edbc0 --- /dev/null +++ b/parts/django/tests/regressiontests/one_to_one_regress/models.py @@ -0,0 +1,43 @@ +from django.db import models + +class Place(models.Model): + name = models.CharField(max_length=50) + address = models.CharField(max_length=80) + + def __unicode__(self): + return u"%s the place" % self.name + +class Restaurant(models.Model): + place = models.OneToOneField(Place) + serves_hot_dogs = models.BooleanField() + serves_pizza = models.BooleanField() + + def __unicode__(self): + return u"%s the restaurant" % self.place.name + +class Bar(models.Model): + place = models.OneToOneField(Place) + serves_cocktails = models.BooleanField() + + def __unicode__(self): + return u"%s the bar" % self.place.name + +class UndergroundBar(models.Model): + place = models.OneToOneField(Place, null=True) + serves_cocktails = models.BooleanField() + +class Favorites(models.Model): + name = models.CharField(max_length = 50) + restaurants = models.ManyToManyField(Restaurant) + + def __unicode__(self): + return u"Favorites for %s" % self.name + +class Target(models.Model): + pass + +class Pointer(models.Model): + other = models.OneToOneField(Target, primary_key=True) + +class Pointer2(models.Model): + other = models.OneToOneField(Target) diff --git a/parts/django/tests/regressiontests/one_to_one_regress/tests.py b/parts/django/tests/regressiontests/one_to_one_regress/tests.py new file mode 100644 index 0000000..8787575 --- /dev/null +++ b/parts/django/tests/regressiontests/one_to_one_regress/tests.py @@ -0,0 +1,130 @@ +from django.test import TestCase +from regressiontests.one_to_one_regress.models import * + +class OneToOneRegressionTests(TestCase): + + def setUp(self): + self.p1 = Place(name='Demon Dogs', address='944 W. Fullerton') + self.p1.save() + self.r1 = Restaurant(place=self.p1, serves_hot_dogs=True, serves_pizza=False) + self.r1.save() + self.b1 = Bar(place=self.p1, serves_cocktails=False) + self.b1.save() + + def test_reverse_relationship_cache_cascade(self): + """ + Regression test for #9023: accessing the reverse relationship shouldn't + result in a cascading delete(). + """ + bar = UndergroundBar.objects.create(place=self.p1, serves_cocktails=False) + + # The bug in #9023: if you access the one-to-one relation *before* + # setting to None and deleting, the cascade happens anyway. + self.p1.undergroundbar + bar.place.name='foo' + bar.place = None + bar.save() + self.p1.delete() + + self.assertEqual(Place.objects.all().count(), 0) + self.assertEqual(UndergroundBar.objects.all().count(), 1) + + def test_create_models_m2m(self): + """ + Regression test for #1064 and #1506 + + Check that we create models via the m2m relation if the remote model + has a OneToOneField. + """ + f = Favorites(name = 'Fred') + f.save() + f.restaurants = [self.r1] + self.assertQuerysetEqual( + f.restaurants.all(), + ['<Restaurant: Demon Dogs the restaurant>'] + ) + + def test_reverse_object_cache(self): + """ + Regression test for #7173 + + Check that the name of the cache for the reverse object is correct. + """ + self.assertEquals(self.p1.restaurant, self.r1) + self.assertEquals(self.p1.bar, self.b1) + + def test_related_object_cache(self): + """ Regression test for #6886 (the related-object cache) """ + + # Look up the objects again so that we get "fresh" objects + p = Place.objects.get(name="Demon Dogs") + r = p.restaurant + + # Accessing the related object again returns the exactly same object + self.assertTrue(p.restaurant is r) + + # But if we kill the cache, we get a new object + del p._restaurant_cache + self.assertFalse(p.restaurant is r) + + # Reassigning the Restaurant object results in an immediate cache update + # We can't use a new Restaurant because that'll violate one-to-one, but + # with a new *instance* the is test below will fail if #6886 regresses. + r2 = Restaurant.objects.get(pk=r.pk) + p.restaurant = r2 + self.assertTrue(p.restaurant is r2) + + # Assigning None succeeds if field is null=True. + ug_bar = UndergroundBar.objects.create(place=p, serves_cocktails=False) + ug_bar.place = None + self.assertTrue(ug_bar.place is None) + + # Assigning None fails: Place.restaurant is null=False + self.assertRaises(ValueError, setattr, p, 'restaurant', None) + + # You also can't assign an object of the wrong type here + self.assertRaises(ValueError, setattr, p, 'restaurant', p) + + # Creation using keyword argument should cache the related object. + p = Place.objects.get(name="Demon Dogs") + r = Restaurant(place=p) + self.assertTrue(r.place is p) + + # Creation using keyword argument and unsaved related instance (#8070). + p = Place() + r = Restaurant(place=p) + self.assertTrue(r.place is p) + + # Creation using attname keyword argument and an id will cause the related + # object to be fetched. + p = Place.objects.get(name="Demon Dogs") + r = Restaurant(place_id=p.id) + self.assertFalse(r.place is p) + self.assertEqual(r.place, p) + + def test_filter_one_to_one_relations(self): + """ + Regression test for #9968 + + filtering reverse one-to-one relations with primary_key=True was + misbehaving. We test both (primary_key=True & False) cases here to + prevent any reappearance of the problem. + """ + t = Target.objects.create() + + self.assertQuerysetEqual( + Target.objects.filter(pointer=None), + ['<Target: Target object>'] + ) + self.assertQuerysetEqual( + Target.objects.exclude(pointer=None), + [] + ) + self.assertQuerysetEqual( + Target.objects.filter(pointer2=None), + ['<Target: Target object>'] + ) + self.assertQuerysetEqual( + Target.objects.exclude(pointer2=None), + [] + ) diff --git a/parts/django/tests/regressiontests/pagination_regress/__init__.py b/parts/django/tests/regressiontests/pagination_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/pagination_regress/__init__.py diff --git a/parts/django/tests/regressiontests/pagination_regress/models.py b/parts/django/tests/regressiontests/pagination_regress/models.py new file mode 100644 index 0000000..cde172d --- /dev/null +++ b/parts/django/tests/regressiontests/pagination_regress/models.py @@ -0,0 +1 @@ +# Models file for tests to run. diff --git a/parts/django/tests/regressiontests/pagination_regress/tests.py b/parts/django/tests/regressiontests/pagination_regress/tests.py new file mode 100644 index 0000000..08436df --- /dev/null +++ b/parts/django/tests/regressiontests/pagination_regress/tests.py @@ -0,0 +1,157 @@ +from unittest import TestCase + +from django.core.paginator import Paginator, EmptyPage + +class PaginatorTests(TestCase): + """ + Tests for the Paginator and Page classes. + """ + + def check_paginator(self, params, output): + """ + Helper method that instantiates a Paginator object from the passed + params and then checks that its attributes match the passed output. + """ + count, num_pages, page_range = output + paginator = Paginator(*params) + self.check_attribute('count', paginator, count, params) + self.check_attribute('num_pages', paginator, num_pages, params) + self.check_attribute('page_range', paginator, page_range, params) + + def check_attribute(self, name, paginator, expected, params): + """ + Helper method that checks a single attribute and gives a nice error + message upon test failure. + """ + got = getattr(paginator, name) + self.assertEqual(expected, got, + "For '%s', expected %s but got %s. Paginator parameters were: %s" + % (name, expected, got, params)) + + def test_paginator(self): + """ + Tests the paginator attributes using varying inputs. + """ + nine = [1, 2, 3, 4, 5, 6, 7, 8, 9] + ten = nine + [10] + eleven = ten + [11] + tests = ( + # Each item is two tuples: + # First tuple is Paginator parameters - object_list, per_page, + # orphans, and allow_empty_first_page. + # Second tuple is resulting Paginator attributes - count, + # num_pages, and page_range. + # Ten items, varying orphans, no empty first page. + ((ten, 4, 0, False), (10, 3, [1, 2, 3])), + ((ten, 4, 1, False), (10, 3, [1, 2, 3])), + ((ten, 4, 2, False), (10, 2, [1, 2])), + ((ten, 4, 5, False), (10, 2, [1, 2])), + ((ten, 4, 6, False), (10, 1, [1])), + # Ten items, varying orphans, allow empty first page. + ((ten, 4, 0, True), (10, 3, [1, 2, 3])), + ((ten, 4, 1, True), (10, 3, [1, 2, 3])), + ((ten, 4, 2, True), (10, 2, [1, 2])), + ((ten, 4, 5, True), (10, 2, [1, 2])), + ((ten, 4, 6, True), (10, 1, [1])), + # One item, varying orphans, no empty first page. + (([1], 4, 0, False), (1, 1, [1])), + (([1], 4, 1, False), (1, 1, [1])), + (([1], 4, 2, False), (1, 1, [1])), + # One item, varying orphans, allow empty first page. + (([1], 4, 0, True), (1, 1, [1])), + (([1], 4, 1, True), (1, 1, [1])), + (([1], 4, 2, True), (1, 1, [1])), + # Zero items, varying orphans, no empty first page. + (([], 4, 0, False), (0, 0, [])), + (([], 4, 1, False), (0, 0, [])), + (([], 4, 2, False), (0, 0, [])), + # Zero items, varying orphans, allow empty first page. + (([], 4, 0, True), (0, 1, [1])), + (([], 4, 1, True), (0, 1, [1])), + (([], 4, 2, True), (0, 1, [1])), + # Number if items one less than per_page. + (([], 1, 0, True), (0, 1, [1])), + (([], 1, 0, False), (0, 0, [])), + (([1], 2, 0, True), (1, 1, [1])), + ((nine, 10, 0, True), (9, 1, [1])), + # Number if items equal to per_page. + (([1], 1, 0, True), (1, 1, [1])), + (([1, 2], 2, 0, True), (2, 1, [1])), + ((ten, 10, 0, True), (10, 1, [1])), + # Number if items one more than per_page. + (([1, 2], 1, 0, True), (2, 2, [1, 2])), + (([1, 2, 3], 2, 0, True), (3, 2, [1, 2])), + ((eleven, 10, 0, True), (11, 2, [1, 2])), + # Number if items one more than per_page with one orphan. + (([1, 2], 1, 1, True), (2, 1, [1])), + (([1, 2, 3], 2, 1, True), (3, 1, [1])), + ((eleven, 10, 1, True), (11, 1, [1])), + ) + for params, output in tests: + self.check_paginator(params, output) + + def check_indexes(self, params, page_num, indexes): + """ + Helper method that instantiates a Paginator object from the passed + params and then checks that the start and end indexes of the passed + page_num match those given as a 2-tuple in indexes. + """ + paginator = Paginator(*params) + if page_num == 'first': + page_num = 1 + elif page_num == 'last': + page_num = paginator.num_pages + page = paginator.page(page_num) + start, end = indexes + msg = ("For %s of page %s, expected %s but got %s." + " Paginator parameters were: %s") + self.assertEqual(start, page.start_index(), + msg % ('start index', page_num, start, page.start_index(), params)) + self.assertEqual(end, page.end_index(), + msg % ('end index', page_num, end, page.end_index(), params)) + + def test_page_indexes(self): + """ + Tests that paginator pages have the correct start and end indexes. + """ + ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + tests = ( + # Each item is three tuples: + # First tuple is Paginator parameters - object_list, per_page, + # orphans, and allow_empty_first_page. + # Second tuple is the start and end indexes of the first page. + # Third tuple is the start and end indexes of the last page. + # Ten items, varying per_page, no orphans. + ((ten, 1, 0, True), (1, 1), (10, 10)), + ((ten, 2, 0, True), (1, 2), (9, 10)), + ((ten, 3, 0, True), (1, 3), (10, 10)), + ((ten, 5, 0, True), (1, 5), (6, 10)), + # Ten items, varying per_page, with orphans. + ((ten, 1, 1, True), (1, 1), (9, 10)), + ((ten, 1, 2, True), (1, 1), (8, 10)), + ((ten, 3, 1, True), (1, 3), (7, 10)), + ((ten, 3, 2, True), (1, 3), (7, 10)), + ((ten, 3, 4, True), (1, 3), (4, 10)), + ((ten, 5, 1, True), (1, 5), (6, 10)), + ((ten, 5, 2, True), (1, 5), (6, 10)), + ((ten, 5, 5, True), (1, 10), (1, 10)), + # One item, varying orphans, no empty first page. + (([1], 4, 0, False), (1, 1), (1, 1)), + (([1], 4, 1, False), (1, 1), (1, 1)), + (([1], 4, 2, False), (1, 1), (1, 1)), + # One item, varying orphans, allow empty first page. + (([1], 4, 0, True), (1, 1), (1, 1)), + (([1], 4, 1, True), (1, 1), (1, 1)), + (([1], 4, 2, True), (1, 1), (1, 1)), + # Zero items, varying orphans, allow empty first page. + (([], 4, 0, True), (0, 0), (0, 0)), + (([], 4, 1, True), (0, 0), (0, 0)), + (([], 4, 2, True), (0, 0), (0, 0)), + ) + for params, first, last in tests: + self.check_indexes(params, 'first', first) + self.check_indexes(params, 'last', last) + # When no items and no empty first page, we should get EmptyPage error. + self.assertRaises(EmptyPage, self.check_indexes, ([], 4, 0, False), 1, None) + self.assertRaises(EmptyPage, self.check_indexes, ([], 4, 1, False), 1, None) + self.assertRaises(EmptyPage, self.check_indexes, ([], 4, 2, False), 1, None) diff --git a/parts/django/tests/regressiontests/queries/__init__.py b/parts/django/tests/regressiontests/queries/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/queries/__init__.py diff --git a/parts/django/tests/regressiontests/queries/models.py b/parts/django/tests/regressiontests/queries/models.py new file mode 100644 index 0000000..5247ef9 --- /dev/null +++ b/parts/django/tests/regressiontests/queries/models.py @@ -0,0 +1,276 @@ +""" +Various complex queries that have been problematic in the past. +""" + +import threading + +from django.db import models + +class DumbCategory(models.Model): + pass + +class NamedCategory(DumbCategory): + name = models.CharField(max_length=10) + +class Tag(models.Model): + name = models.CharField(max_length=10) + parent = models.ForeignKey('self', blank=True, null=True, + related_name='children') + category = models.ForeignKey(NamedCategory, null=True, default=None) + + class Meta: + ordering = ['name'] + + def __unicode__(self): + return self.name + +class Note(models.Model): + note = models.CharField(max_length=100) + misc = models.CharField(max_length=10) + + class Meta: + ordering = ['note'] + + def __unicode__(self): + return self.note + + def __init__(self, *args, **kwargs): + super(Note, self).__init__(*args, **kwargs) + # Regression for #13227 -- having an attribute that + # is unpickleable doesn't stop you from cloning queries + # that use objects of that type as an argument. + self.lock = threading.Lock() + +class Annotation(models.Model): + name = models.CharField(max_length=10) + tag = models.ForeignKey(Tag) + notes = models.ManyToManyField(Note) + + def __unicode__(self): + return self.name + +class ExtraInfo(models.Model): + info = models.CharField(max_length=100) + note = models.ForeignKey(Note) + + class Meta: + ordering = ['info'] + + def __unicode__(self): + return self.info + +class Author(models.Model): + name = models.CharField(max_length=10) + num = models.IntegerField(unique=True) + extra = models.ForeignKey(ExtraInfo) + + class Meta: + ordering = ['name'] + + def __unicode__(self): + return self.name + +class Item(models.Model): + name = models.CharField(max_length=10) + created = models.DateTimeField() + modified = models.DateTimeField(blank=True, null=True) + tags = models.ManyToManyField(Tag, blank=True, null=True) + creator = models.ForeignKey(Author) + note = models.ForeignKey(Note) + + class Meta: + ordering = ['-note', 'name'] + + def __unicode__(self): + return self.name + +class Report(models.Model): + name = models.CharField(max_length=10) + creator = models.ForeignKey(Author, to_field='num', null=True) + + def __unicode__(self): + return self.name + +class Ranking(models.Model): + rank = models.IntegerField() + author = models.ForeignKey(Author) + + class Meta: + # A complex ordering specification. Should stress the system a bit. + ordering = ('author__extra__note', 'author__name', 'rank') + + def __unicode__(self): + return '%d: %s' % (self.rank, self.author.name) + +class Cover(models.Model): + title = models.CharField(max_length=50) + item = models.ForeignKey(Item) + + class Meta: + ordering = ['item'] + + def __unicode__(self): + return self.title + +class Number(models.Model): + num = models.IntegerField() + + def __unicode__(self): + return unicode(self.num) + +# Symmetrical m2m field with a normal field using the reverse accesor name +# ("valid"). +class Valid(models.Model): + valid = models.CharField(max_length=10) + parent = models.ManyToManyField('self') + + class Meta: + ordering = ['valid'] + +# Some funky cross-linked models for testing a couple of infinite recursion +# cases. +class X(models.Model): + y = models.ForeignKey('Y') + +class Y(models.Model): + x1 = models.ForeignKey(X, related_name='y1') + +# Some models with a cycle in the default ordering. This would be bad if we +# didn't catch the infinite loop. +class LoopX(models.Model): + y = models.ForeignKey('LoopY') + + class Meta: + ordering = ['y'] + +class LoopY(models.Model): + x = models.ForeignKey(LoopX) + + class Meta: + ordering = ['x'] + +class LoopZ(models.Model): + z = models.ForeignKey('self') + + class Meta: + ordering = ['z'] + +# A model and custom default manager combination. +class CustomManager(models.Manager): + def get_query_set(self): + qs = super(CustomManager, self).get_query_set() + return qs.filter(public=True, tag__name='t1') + +class ManagedModel(models.Model): + data = models.CharField(max_length=10) + tag = models.ForeignKey(Tag) + public = models.BooleanField(default=True) + + objects = CustomManager() + normal_manager = models.Manager() + + def __unicode__(self): + return self.data + +# An inter-related setup with multiple paths from Child to Detail. +class Detail(models.Model): + data = models.CharField(max_length=10) + +class MemberManager(models.Manager): + def get_query_set(self): + return super(MemberManager, self).get_query_set().select_related("details") + +class Member(models.Model): + name = models.CharField(max_length=10) + details = models.OneToOneField(Detail, primary_key=True) + + objects = MemberManager() + +class Child(models.Model): + person = models.OneToOneField(Member, primary_key=True) + parent = models.ForeignKey(Member, related_name="children") + +# Custom primary keys interfered with ordering in the past. +class CustomPk(models.Model): + name = models.CharField(max_length=10, primary_key=True) + extra = models.CharField(max_length=10) + + class Meta: + ordering = ['name', 'extra'] + +class Related(models.Model): + custom = models.ForeignKey(CustomPk) + +# An inter-related setup with a model subclass that has a nullable +# path to another model, and a return path from that model. + +class Celebrity(models.Model): + name = models.CharField("Name", max_length=20) + greatest_fan = models.ForeignKey("Fan", null=True, unique=True) + +class TvChef(Celebrity): + pass + +class Fan(models.Model): + fan_of = models.ForeignKey(Celebrity) + +# Multiple foreign keys +class LeafA(models.Model): + data = models.CharField(max_length=10) + + def __unicode__(self): + return self.data + +class LeafB(models.Model): + data = models.CharField(max_length=10) + +class Join(models.Model): + a = models.ForeignKey(LeafA) + b = models.ForeignKey(LeafB) + +class ReservedName(models.Model): + name = models.CharField(max_length=20) + order = models.IntegerField() + + def __unicode__(self): + return self.name + +# A simpler shared-foreign-key setup that can expose some problems. +class SharedConnection(models.Model): + data = models.CharField(max_length=10) + +class PointerA(models.Model): + connection = models.ForeignKey(SharedConnection) + +class PointerB(models.Model): + connection = models.ForeignKey(SharedConnection) + +# Multi-layer ordering +class SingleObject(models.Model): + name = models.CharField(max_length=10) + + class Meta: + ordering = ['name'] + + def __unicode__(self): + return self.name + +class RelatedObject(models.Model): + single = models.ForeignKey(SingleObject) + + class Meta: + ordering = ['single'] + +class Plaything(models.Model): + name = models.CharField(max_length=10) + others = models.ForeignKey(RelatedObject, null=True) + + class Meta: + ordering = ['others'] + + def __unicode__(self): + return self.name + +class Article(models.Model): + name = models.CharField(max_length=20) + created = models.DateTimeField() diff --git a/parts/django/tests/regressiontests/queries/tests.py b/parts/django/tests/regressiontests/queries/tests.py new file mode 100644 index 0000000..741b33c --- /dev/null +++ b/parts/django/tests/regressiontests/queries/tests.py @@ -0,0 +1,1586 @@ +import datetime +import pickle +import sys +import unittest + +from django.conf import settings +from django.core.exceptions import FieldError +from django.db import DatabaseError, connection, connections, DEFAULT_DB_ALIAS +from django.db.models import Count +from django.db.models.query import Q, ITER_CHUNK_SIZE, EmptyQuerySet +from django.test import TestCase +from django.utils.datastructures import SortedDict + +from models import (Annotation, Article, Author, Celebrity, Child, Cover, Detail, + DumbCategory, ExtraInfo, Fan, Item, LeafA, LoopX, LoopZ, ManagedModel, + Member, NamedCategory, Note, Number, Plaything, PointerA, Ranking, Related, + Report, ReservedName, Tag, TvChef, Valid, X) + + +class BaseQuerysetTest(TestCase): + def assertValueQuerysetEqual(self, qs, values): + return self.assertQuerysetEqual(qs, values, transform=lambda x: x) + + def assertRaisesMessage(self, exc, msg, func, *args, **kwargs): + try: + func(*args, **kwargs) + except Exception, e: + self.assertEqual(msg, str(e)) + self.assertTrue(isinstance(e, exc), "Expected %s, got %s" % (exc, type(e))) + else: + if hasattr(exc, '__name__'): + excName = exc.__name__ + else: + excName = str(exc) + raise AssertionError, "%s not raised" % excName + + +class Queries1Tests(BaseQuerysetTest): + def setUp(self): + generic = NamedCategory.objects.create(name="Generic") + self.t1 = Tag.objects.create(name='t1', category=generic) + self.t2 = Tag.objects.create(name='t2', parent=self.t1, category=generic) + self.t3 = Tag.objects.create(name='t3', parent=self.t1) + t4 = Tag.objects.create(name='t4', parent=self.t3) + self.t5 = Tag.objects.create(name='t5', parent=self.t3) + + self.n1 = Note.objects.create(note='n1', misc='foo', id=1) + n2 = Note.objects.create(note='n2', misc='bar', id=2) + self.n3 = Note.objects.create(note='n3', misc='foo', id=3) + + ann1 = Annotation.objects.create(name='a1', tag=self.t1) + ann1.notes.add(self.n1) + ann2 = Annotation.objects.create(name='a2', tag=t4) + ann2.notes.add(n2, self.n3) + + # Create these out of order so that sorting by 'id' will be different to sorting + # by 'info'. Helps detect some problems later. + self.e2 = ExtraInfo.objects.create(info='e2', note=n2) + e1 = ExtraInfo.objects.create(info='e1', note=self.n1) + + self.a1 = Author.objects.create(name='a1', num=1001, extra=e1) + self.a2 = Author.objects.create(name='a2', num=2002, extra=e1) + a3 = Author.objects.create(name='a3', num=3003, extra=self.e2) + self.a4 = Author.objects.create(name='a4', num=4004, extra=self.e2) + + self.time1 = datetime.datetime(2007, 12, 19, 22, 25, 0) + self.time2 = datetime.datetime(2007, 12, 19, 21, 0, 0) + time3 = datetime.datetime(2007, 12, 20, 22, 25, 0) + time4 = datetime.datetime(2007, 12, 20, 21, 0, 0) + self.i1 = Item.objects.create(name='one', created=self.time1, modified=self.time1, creator=self.a1, note=self.n3) + self.i1.tags = [self.t1, self.t2] + self.i2 = Item.objects.create(name='two', created=self.time2, creator=self.a2, note=n2) + self.i2.tags = [self.t1, self.t3] + self.i3 = Item.objects.create(name='three', created=time3, creator=self.a2, note=self.n3) + i4 = Item.objects.create(name='four', created=time4, creator=self.a4, note=self.n3) + i4.tags = [t4] + + self.r1 = Report.objects.create(name='r1', creator=self.a1) + Report.objects.create(name='r2', creator=a3) + Report.objects.create(name='r3') + + # Ordering by 'rank' gives us rank2, rank1, rank3. Ordering by the Meta.ordering + # will be rank3, rank2, rank1. + self.rank1 = Ranking.objects.create(rank=2, author=self.a2) + + Cover.objects.create(title="first", item=i4) + Cover.objects.create(title="second", item=self.i2) + + def test_ticket1050(self): + self.assertQuerysetEqual( + Item.objects.filter(tags__isnull=True), + ['<Item: three>'] + ) + self.assertQuerysetEqual( + Item.objects.filter(tags__id__isnull=True), + ['<Item: three>'] + ) + + def test_ticket1801(self): + self.assertQuerysetEqual( + Author.objects.filter(item=self.i2), + ['<Author: a2>'] + ) + self.assertQuerysetEqual( + Author.objects.filter(item=self.i3), + ['<Author: a2>'] + ) + self.assertQuerysetEqual( + Author.objects.filter(item=self.i2) & Author.objects.filter(item=self.i3), + ['<Author: a2>'] + ) + + def test_ticket2306(self): + # Checking that no join types are "left outer" joins. + query = Item.objects.filter(tags=self.t2).query + self.assertTrue(query.LOUTER not in [x[2] for x in query.alias_map.values()]) + + self.assertQuerysetEqual( + Item.objects.filter(Q(tags=self.t1)).order_by('name'), + ['<Item: one>', '<Item: two>'] + ) + self.assertQuerysetEqual( + Item.objects.filter(Q(tags=self.t1)).filter(Q(tags=self.t2)), + ['<Item: one>'] + ) + self.assertQuerysetEqual( + Item.objects.filter(Q(tags=self.t1)).filter(Q(creator__name='fred')|Q(tags=self.t2)), + ['<Item: one>'] + ) + + # Each filter call is processed "at once" against a single table, so this is + # different from the previous example as it tries to find tags that are two + # things at once (rather than two tags). + self.assertQuerysetEqual( + Item.objects.filter(Q(tags=self.t1) & Q(tags=self.t2)), + [] + ) + self.assertQuerysetEqual( + Item.objects.filter(Q(tags=self.t1), Q(creator__name='fred')|Q(tags=self.t2)), + [] + ) + + qs = Author.objects.filter(ranking__rank=2, ranking__id=self.rank1.id) + self.assertQuerysetEqual(list(qs), ['<Author: a2>']) + self.assertEqual(2, qs.query.count_active_tables(), 2) + qs = Author.objects.filter(ranking__rank=2).filter(ranking__id=self.rank1.id) + self.assertEqual(qs.query.count_active_tables(), 3) + + def test_ticket4464(self): + self.assertQuerysetEqual( + Item.objects.filter(tags=self.t1).filter(tags=self.t2), + ['<Item: one>'] + ) + self.assertQuerysetEqual( + Item.objects.filter(tags__in=[self.t1, self.t2]).distinct().order_by('name'), + ['<Item: one>', '<Item: two>'] + ) + self.assertQuerysetEqual( + Item.objects.filter(tags__in=[self.t1, self.t2]).filter(tags=self.t3), + ['<Item: two>'] + ) + + # Make sure .distinct() works with slicing (this was broken in Oracle). + self.assertQuerysetEqual( + Item.objects.filter(tags__in=[self.t1, self.t2]).order_by('name')[:3], + ['<Item: one>', '<Item: one>', '<Item: two>'] + ) + self.assertQuerysetEqual( + Item.objects.filter(tags__in=[self.t1, self.t2]).distinct().order_by('name')[:3], + ['<Item: one>', '<Item: two>'] + ) + + def test_tickets_2080_3592(self): + self.assertQuerysetEqual( + Author.objects.filter(item__name='one') | Author.objects.filter(name='a3'), + ['<Author: a1>', '<Author: a3>'] + ) + self.assertQuerysetEqual( + Author.objects.filter(Q(item__name='one') | Q(name='a3')), + ['<Author: a1>', '<Author: a3>'] + ) + self.assertQuerysetEqual( + Author.objects.filter(Q(name='a3') | Q(item__name='one')), + ['<Author: a1>', '<Author: a3>'] + ) + self.assertQuerysetEqual( + Author.objects.filter(Q(item__name='three') | Q(report__name='r3')), + ['<Author: a2>'] + ) + + def test_ticket6074(self): + # Merging two empty result sets shouldn't leave a queryset with no constraints + # (which would match everything). + self.assertQuerysetEqual(Author.objects.filter(Q(id__in=[])), []) + self.assertQuerysetEqual( + Author.objects.filter(Q(id__in=[])|Q(id__in=[])), + [] + ) + + def test_tickets_1878_2939(self): + self.assertEqual(Item.objects.values('creator').distinct().count(), 3) + + # Create something with a duplicate 'name' so that we can test multi-column + # cases (which require some tricky SQL transformations under the covers). + xx = Item(name='four', created=self.time1, creator=self.a2, note=self.n1) + xx.save() + self.assertEqual( + Item.objects.exclude(name='two').values('creator', 'name').distinct().count(), + 4 + ) + self.assertEqual( + Item.objects.exclude(name='two').extra(select={'foo': '%s'}, select_params=(1,)).values('creator', 'name', 'foo').distinct().count(), + 4 + ) + self.assertEqual( + Item.objects.exclude(name='two').extra(select={'foo': '%s'}, select_params=(1,)).values('creator', 'name').distinct().count(), + 4 + ) + xx.delete() + + def test_ticket7323(self): + self.assertEqual(Item.objects.values('creator', 'name').count(), 4) + + def test_ticket2253(self): + q1 = Item.objects.order_by('name') + q2 = Item.objects.filter(id=self.i1.id) + self.assertQuerysetEqual( + q1, + ['<Item: four>', '<Item: one>', '<Item: three>', '<Item: two>'] + ) + self.assertQuerysetEqual(q2, ['<Item: one>']) + self.assertQuerysetEqual( + (q1 | q2).order_by('name'), + ['<Item: four>', '<Item: one>', '<Item: three>', '<Item: two>'] + ) + self.assertQuerysetEqual((q1 & q2).order_by('name'), ['<Item: one>']) + + # FIXME: This is difficult to fix and very much an edge case, so punt for now. + # This is related to the order_by() tests, below, but the old bug exhibited + # itself here (q2 was pulling too many tables into the combined query with the + # new ordering, but only because we have evaluated q2 already). + # + #self.assertEqual(len((q1 & q2).order_by('name').query.tables), 1) + + q1 = Item.objects.filter(tags=self.t1) + q2 = Item.objects.filter(note=self.n3, tags=self.t2) + q3 = Item.objects.filter(creator=self.a4) + self.assertQuerysetEqual( + ((q1 & q2) | q3).order_by('name'), + ['<Item: four>', '<Item: one>'] + ) + + def test_tickets_4088_4306(self): + self.assertQuerysetEqual( + Report.objects.filter(creator=1001), + ['<Report: r1>'] + ) + self.assertQuerysetEqual( + Report.objects.filter(creator__num=1001), + ['<Report: r1>'] + ) + self.assertQuerysetEqual(Report.objects.filter(creator__id=1001), []) + self.assertQuerysetEqual( + Report.objects.filter(creator__id=self.a1.id), + ['<Report: r1>'] + ) + self.assertQuerysetEqual( + Report.objects.filter(creator__name='a1'), + ['<Report: r1>'] + ) + + def test_ticket4510(self): + self.assertQuerysetEqual( + Author.objects.filter(report__name='r1'), + ['<Author: a1>'] + ) + + def test_ticket7378(self): + self.assertQuerysetEqual(self.a1.report_set.all(), ['<Report: r1>']) + + def test_tickets_5324_6704(self): + self.assertQuerysetEqual( + Item.objects.filter(tags__name='t4'), + ['<Item: four>'] + ) + self.assertQuerysetEqual( + Item.objects.exclude(tags__name='t4').order_by('name').distinct(), + ['<Item: one>', '<Item: three>', '<Item: two>'] + ) + self.assertQuerysetEqual( + Item.objects.exclude(tags__name='t4').order_by('name').distinct().reverse(), + ['<Item: two>', '<Item: three>', '<Item: one>'] + ) + self.assertQuerysetEqual( + Author.objects.exclude(item__name='one').distinct().order_by('name'), + ['<Author: a2>', '<Author: a3>', '<Author: a4>'] + ) + + # Excluding across a m2m relation when there is more than one related + # object associated was problematic. + self.assertQuerysetEqual( + Item.objects.exclude(tags__name='t1').order_by('name'), + ['<Item: four>', '<Item: three>'] + ) + self.assertQuerysetEqual( + Item.objects.exclude(tags__name='t1').exclude(tags__name='t4'), + ['<Item: three>'] + ) + + # Excluding from a relation that cannot be NULL should not use outer joins. + query = Item.objects.exclude(creator__in=[self.a1, self.a2]).query + self.assertTrue(query.LOUTER not in [x[2] for x in query.alias_map.values()]) + + # Similarly, when one of the joins cannot possibly, ever, involve NULL + # values (Author -> ExtraInfo, in the following), it should never be + # promoted to a left outer join. So the following query should only + # involve one "left outer" join (Author -> Item is 0-to-many). + qs = Author.objects.filter(id=self.a1.id).filter(Q(extra__note=self.n1)|Q(item__note=self.n3)) + self.assertEqual( + len([x[2] for x in qs.query.alias_map.values() if x[2] == query.LOUTER and qs.query.alias_refcount[x[1]]]), + 1 + ) + + # The previous changes shouldn't affect nullable foreign key joins. + self.assertQuerysetEqual( + Tag.objects.filter(parent__isnull=True).order_by('name'), + ['<Tag: t1>'] + ) + self.assertQuerysetEqual( + Tag.objects.exclude(parent__isnull=True).order_by('name'), + ['<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'] + ) + self.assertQuerysetEqual( + Tag.objects.exclude(Q(parent__name='t1') | Q(parent__isnull=True)).order_by('name'), + ['<Tag: t4>', '<Tag: t5>'] + ) + self.assertQuerysetEqual( + Tag.objects.exclude(Q(parent__isnull=True) | Q(parent__name='t1')).order_by('name'), + ['<Tag: t4>', '<Tag: t5>'] + ) + self.assertQuerysetEqual( + Tag.objects.exclude(Q(parent__parent__isnull=True)).order_by('name'), + ['<Tag: t4>', '<Tag: t5>'] + ) + self.assertQuerysetEqual( + Tag.objects.filter(~Q(parent__parent__isnull=True)).order_by('name'), + ['<Tag: t4>', '<Tag: t5>'] + ) + + def test_ticket2091(self): + t = Tag.objects.get(name='t4') + self.assertQuerysetEqual( + Item.objects.filter(tags__in=[t]), + ['<Item: four>'] + ) + + def test_heterogeneous_qs_combination(self): + # Combining querysets built on different models should behave in a well-defined + # fashion. We raise an error. + self.assertRaisesMessage( + AssertionError, + 'Cannot combine queries on two different base models.', + lambda: Author.objects.all() & Tag.objects.all() + ) + self.assertRaisesMessage( + AssertionError, + 'Cannot combine queries on two different base models.', + lambda: Author.objects.all() | Tag.objects.all() + ) + + def test_ticket3141(self): + self.assertEqual(Author.objects.extra(select={'foo': '1'}).count(), 4) + self.assertEqual( + Author.objects.extra(select={'foo': '%s'}, select_params=(1,)).count(), + 4 + ) + + def test_ticket2400(self): + self.assertQuerysetEqual( + Author.objects.filter(item__isnull=True), + ['<Author: a3>'] + ) + self.assertQuerysetEqual( + Tag.objects.filter(item__isnull=True), + ['<Tag: t5>'] + ) + + def test_ticket2496(self): + self.assertQuerysetEqual( + Item.objects.extra(tables=['queries_author']).select_related().order_by('name')[:1], + ['<Item: four>'] + ) + + def test_tickets_2076_7256(self): + # Ordering on related tables should be possible, even if the table is + # not otherwise involved. + self.assertQuerysetEqual( + Item.objects.order_by('note__note', 'name'), + ['<Item: two>', '<Item: four>', '<Item: one>', '<Item: three>'] + ) + + # Ordering on a related field should use the remote model's default + # ordering as a final step. + self.assertQuerysetEqual( + Author.objects.order_by('extra', '-name'), + ['<Author: a2>', '<Author: a1>', '<Author: a4>', '<Author: a3>'] + ) + + # Using remote model default ordering can span multiple models (in this + # case, Cover is ordered by Item's default, which uses Note's default). + self.assertQuerysetEqual( + Cover.objects.all(), + ['<Cover: first>', '<Cover: second>'] + ) + + # If the remote model does not have a default ordering, we order by its 'id' + # field. + self.assertQuerysetEqual( + Item.objects.order_by('creator', 'name'), + ['<Item: one>', '<Item: three>', '<Item: two>', '<Item: four>'] + ) + + # Ordering by a many-valued attribute (e.g. a many-to-many or reverse + # ForeignKey) is legal, but the results might not make sense. That + # isn't Django's problem. Garbage in, garbage out. + self.assertQuerysetEqual( + Item.objects.filter(tags__isnull=False).order_by('tags', 'id'), + ['<Item: one>', '<Item: two>', '<Item: one>', '<Item: two>', '<Item: four>'] + ) + + # If we replace the default ordering, Django adjusts the required + # tables automatically. Item normally requires a join with Note to do + # the default ordering, but that isn't needed here. + qs = Item.objects.order_by('name') + self.assertQuerysetEqual( + qs, + ['<Item: four>', '<Item: one>', '<Item: three>', '<Item: two>'] + ) + self.assertEqual(len(qs.query.tables), 1) + + def test_tickets_2874_3002(self): + qs = Item.objects.select_related().order_by('note__note', 'name') + self.assertQuerysetEqual( + qs, + ['<Item: two>', '<Item: four>', '<Item: one>', '<Item: three>'] + ) + + # This is also a good select_related() test because there are multiple + # Note entries in the SQL. The two Note items should be different. + self.assertTrue(repr(qs[0].note), '<Note: n2>') + self.assertEqual(repr(qs[0].creator.extra.note), '<Note: n1>') + + def test_ticket3037(self): + self.assertQuerysetEqual( + Item.objects.filter(Q(creator__name='a3', name='two')|Q(creator__name='a4', name='four')), + ['<Item: four>'] + ) + + def test_tickets_5321_7070(self): + # Ordering columns must be included in the output columns. Note that + # this means results that might otherwise be distinct are not (if there + # are multiple values in the ordering cols), as in this example. This + # isn't a bug; it's a warning to be careful with the selection of + # ordering columns. + self.assertValueQuerysetEqual( + Note.objects.values('misc').distinct().order_by('note', '-misc'), + [{'misc': u'foo'}, {'misc': u'bar'}, {'misc': u'foo'}] + ) + + def test_ticket4358(self): + # If you don't pass any fields to values(), relation fields are + # returned as "foo_id" keys, not "foo". For consistency, you should be + # able to pass "foo_id" in the fields list and have it work, too. We + # actually allow both "foo" and "foo_id". + + # The *_id version is returned by default. + self.assertTrue('note_id' in ExtraInfo.objects.values()[0]) + + # You can also pass it in explicitly. + self.assertValueQuerysetEqual( + ExtraInfo.objects.values('note_id'), + [{'note_id': 1}, {'note_id': 2}] + ) + + # ...or use the field name. + self.assertValueQuerysetEqual( + ExtraInfo.objects.values('note'), + [{'note': 1}, {'note': 2}] + ) + + def test_ticket2902(self): + # Parameters can be given to extra_select, *if* you use a SortedDict. + + # (First we need to know which order the keys fall in "naturally" on + # your system, so we can put things in the wrong way around from + # normal. A normal dict would thus fail.) + s = [('a', '%s'), ('b', '%s')] + params = ['one', 'two'] + if {'a': 1, 'b': 2}.keys() == ['a', 'b']: + s.reverse() + params.reverse() + + # This slightly odd comparison works around the fact that PostgreSQL will + # return 'one' and 'two' as strings, not Unicode objects. It's a side-effect of + # using constants here and not a real concern. + d = Item.objects.extra(select=SortedDict(s), select_params=params).values('a', 'b')[0] + self.assertEqual(d, {'a': u'one', 'b': u'two'}) + + # Order by the number of tags attached to an item. + l = Item.objects.extra(select={'count': 'select count(*) from queries_item_tags where queries_item_tags.item_id = queries_item.id'}).order_by('-count') + self.assertEqual([o.count for o in l], [2, 2, 1, 0]) + + def test_ticket6154(self): + # Multiple filter statements are joined using "AND" all the time. + + self.assertQuerysetEqual( + Author.objects.filter(id=self.a1.id).filter(Q(extra__note=self.n1)|Q(item__note=self.n3)), + ['<Author: a1>'] + ) + self.assertQuerysetEqual( + Author.objects.filter(Q(extra__note=self.n1)|Q(item__note=self.n3)).filter(id=self.a1.id), + ['<Author: a1>'] + ) + + def test_ticket6981(self): + self.assertQuerysetEqual( + Tag.objects.select_related('parent').order_by('name'), + ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'] + ) + + def test_ticket9926(self): + self.assertQuerysetEqual( + Tag.objects.select_related("parent", "category").order_by('name'), + ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'] + ) + self.assertQuerysetEqual( + Tag.objects.select_related('parent', "parent__category").order_by('name'), + ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'] + ) + + def test_tickets_6180_6203(self): + # Dates with limits and/or counts + self.assertEqual(Item.objects.count(), 4) + self.assertEqual(Item.objects.dates('created', 'month').count(), 1) + self.assertEqual(Item.objects.dates('created', 'day').count(), 2) + self.assertEqual(len(Item.objects.dates('created', 'day')), 2) + self.assertEqual(Item.objects.dates('created', 'day')[0], datetime.datetime(2007, 12, 19, 0, 0)) + + def test_tickets_7087_12242(self): + # Dates with extra select columns + self.assertQuerysetEqual( + Item.objects.dates('created', 'day').extra(select={'a': 1}), + ['datetime.datetime(2007, 12, 19, 0, 0)', 'datetime.datetime(2007, 12, 20, 0, 0)'] + ) + self.assertQuerysetEqual( + Item.objects.extra(select={'a': 1}).dates('created', 'day'), + ['datetime.datetime(2007, 12, 19, 0, 0)', 'datetime.datetime(2007, 12, 20, 0, 0)'] + ) + + name="one" + self.assertQuerysetEqual( + Item.objects.dates('created', 'day').extra(where=['name=%s'], params=[name]), + ['datetime.datetime(2007, 12, 19, 0, 0)'] + ) + + self.assertQuerysetEqual( + Item.objects.extra(where=['name=%s'], params=[name]).dates('created', 'day'), + ['datetime.datetime(2007, 12, 19, 0, 0)'] + ) + + def test_ticket7155(self): + # Nullable dates + self.assertQuerysetEqual( + Item.objects.dates('modified', 'day'), + ['datetime.datetime(2007, 12, 19, 0, 0)'] + ) + + def test_ticket7098(self): + # Make sure semi-deprecated ordering by related models syntax still + # works. + self.assertValueQuerysetEqual( + Item.objects.values('note__note').order_by('queries_note.note', 'id'), + [{'note__note': u'n2'}, {'note__note': u'n3'}, {'note__note': u'n3'}, {'note__note': u'n3'}] + ) + + def test_ticket7096(self): + # Make sure exclude() with multiple conditions continues to work. + self.assertQuerysetEqual( + Tag.objects.filter(parent=self.t1, name='t3').order_by('name'), + ['<Tag: t3>'] + ) + self.assertQuerysetEqual( + Tag.objects.exclude(parent=self.t1, name='t3').order_by('name'), + ['<Tag: t1>', '<Tag: t2>', '<Tag: t4>', '<Tag: t5>'] + ) + self.assertQuerysetEqual( + Item.objects.exclude(tags__name='t1', name='one').order_by('name').distinct(), + ['<Item: four>', '<Item: three>', '<Item: two>'] + ) + self.assertQuerysetEqual( + Item.objects.filter(name__in=['three', 'four']).exclude(tags__name='t1').order_by('name'), + ['<Item: four>', '<Item: three>'] + ) + + # More twisted cases, involving nested negations. + self.assertQuerysetEqual( + Item.objects.exclude(~Q(tags__name='t1', name='one')), + ['<Item: one>'] + ) + self.assertQuerysetEqual( + Item.objects.filter(~Q(tags__name='t1', name='one'), name='two'), + ['<Item: two>'] + ) + self.assertQuerysetEqual( + Item.objects.exclude(~Q(tags__name='t1', name='one'), name='two'), + ['<Item: four>', '<Item: one>', '<Item: three>'] + ) + + def test_tickets_7204_7506(self): + # Make sure querysets with related fields can be pickled. If this + # doesn't crash, it's a Good Thing. + pickle.dumps(Item.objects.all()) + + def test_ticket7813(self): + # We should also be able to pickle things that use select_related(). + # The only tricky thing here is to ensure that we do the related + # selections properly after unpickling. + qs = Item.objects.select_related() + query = qs.query.get_compiler(qs.db).as_sql()[0] + query2 = pickle.loads(pickle.dumps(qs.query)) + self.assertEqual( + query2.get_compiler(qs.db).as_sql()[0], + query + ) + + def test_deferred_load_qs_pickling(self): + # Check pickling of deferred-loading querysets + qs = Item.objects.defer('name', 'creator') + q2 = pickle.loads(pickle.dumps(qs)) + self.assertEqual(list(qs), list(q2)) + q3 = pickle.loads(pickle.dumps(qs, pickle.HIGHEST_PROTOCOL)) + self.assertEqual(list(qs), list(q3)) + + def test_ticket7277(self): + self.assertQuerysetEqual( + self.n1.annotation_set.filter(Q(tag=self.t5) | Q(tag__children=self.t5) | Q(tag__children__children=self.t5)), + ['<Annotation: a1>'] + ) + + def test_tickets_7448_7707(self): + # Complex objects should be converted to strings before being used in + # lookups. + self.assertQuerysetEqual( + Item.objects.filter(created__in=[self.time1, self.time2]), + ['<Item: one>', '<Item: two>'] + ) + + def test_ticket7235(self): + # An EmptyQuerySet should not raise exceptions if it is filtered. + q = EmptyQuerySet() + self.assertQuerysetEqual(q.all(), []) + self.assertQuerysetEqual(q.filter(x=10), []) + self.assertQuerysetEqual(q.exclude(y=3), []) + self.assertQuerysetEqual(q.complex_filter({'pk': 1}), []) + self.assertQuerysetEqual(q.select_related('spam', 'eggs'), []) + self.assertQuerysetEqual(q.annotate(Count('eggs')), []) + self.assertQuerysetEqual(q.order_by('-pub_date', 'headline'), []) + self.assertQuerysetEqual(q.distinct(), []) + self.assertQuerysetEqual( + q.extra(select={'is_recent': "pub_date > '2006-01-01'"}), + [] + ) + q.query.low_mark = 1 + self.assertRaisesMessage( + AssertionError, + 'Cannot change a query once a slice has been taken', + q.extra, select={'is_recent': "pub_date > '2006-01-01'"} + ) + self.assertQuerysetEqual(q.reverse(), []) + self.assertQuerysetEqual(q.defer('spam', 'eggs'), []) + self.assertQuerysetEqual(q.only('spam', 'eggs'), []) + + def test_ticket7791(self): + # There were "issues" when ordering and distinct-ing on fields related + # via ForeignKeys. + self.assertEqual( + len(Note.objects.order_by('extrainfo__info').distinct()), + 3 + ) + + # Pickling of DateQuerySets used to fail + qs = Item.objects.dates('created', 'month') + _ = pickle.loads(pickle.dumps(qs)) + + def test_ticket9997(self): + # If a ValuesList or Values queryset is passed as an inner query, we + # make sure it's only requesting a single value and use that as the + # thing to select. + self.assertQuerysetEqual( + Tag.objects.filter(name__in=Tag.objects.filter(parent=self.t1).values('name')), + ['<Tag: t2>', '<Tag: t3>'] + ) + + # Multi-valued values() and values_list() querysets should raise errors. + self.assertRaisesMessage( + TypeError, + 'Cannot use a multi-field ValuesQuerySet as a filter value.', + lambda: Tag.objects.filter(name__in=Tag.objects.filter(parent=self.t1).values('name', 'id')) + ) + self.assertRaisesMessage( + TypeError, + 'Cannot use a multi-field ValuesListQuerySet as a filter value.', + lambda: Tag.objects.filter(name__in=Tag.objects.filter(parent=self.t1).values_list('name', 'id')) + ) + + def test_ticket9985(self): + # qs.values_list(...).values(...) combinations should work. + self.assertValueQuerysetEqual( + Note.objects.values_list("note", flat=True).values("id").order_by("id"), + [{'id': 1}, {'id': 2}, {'id': 3}] + ) + self.assertQuerysetEqual( + Annotation.objects.filter(notes__in=Note.objects.filter(note="n1").values_list('note').values('id')), + ['<Annotation: a1>'] + ) + + def test_ticket10205(self): + # When bailing out early because of an empty "__in" filter, we need + # to set things up correctly internally so that subqueries can continue properly. + self.assertEqual(Tag.objects.filter(name__in=()).update(name="foo"), 0) + + def test_ticket10432(self): + # Testing an empty "__in" filter with a generator as the value. + def f(): + return iter([]) + n_obj = Note.objects.all()[0] + def g(): + for i in [n_obj.pk]: + yield i + self.assertQuerysetEqual(Note.objects.filter(pk__in=f()), []) + self.assertEqual(list(Note.objects.filter(pk__in=g())), [n_obj]) + + def test_ticket10742(self): + # Queries used in an __in clause don't execute subqueries + + subq = Author.objects.filter(num__lt=3000) + qs = Author.objects.filter(pk__in=subq) + self.assertQuerysetEqual(qs, ['<Author: a1>', '<Author: a2>']) + + # The subquery result cache should not be populated + self.assertTrue(subq._result_cache is None) + + subq = Author.objects.filter(num__lt=3000) + qs = Author.objects.exclude(pk__in=subq) + self.assertQuerysetEqual(qs, ['<Author: a3>', '<Author: a4>']) + + # The subquery result cache should not be populated + self.assertTrue(subq._result_cache is None) + + subq = Author.objects.filter(num__lt=3000) + self.assertQuerysetEqual( + Author.objects.filter(Q(pk__in=subq) & Q(name='a1')), + ['<Author: a1>'] + ) + + # The subquery result cache should not be populated + self.assertTrue(subq._result_cache is None) + + def test_ticket7076(self): + # Excluding shouldn't eliminate NULL entries. + self.assertQuerysetEqual( + Item.objects.exclude(modified=self.time1).order_by('name'), + ['<Item: four>', '<Item: three>', '<Item: two>'] + ) + self.assertQuerysetEqual( + Tag.objects.exclude(parent__name=self.t1.name), + ['<Tag: t1>', '<Tag: t4>', '<Tag: t5>'] + ) + + def test_ticket7181(self): + # Ordering by related tables should accomodate nullable fields (this + # test is a little tricky, since NULL ordering is database dependent. + # Instead, we just count the number of results). + self.assertEqual(len(Tag.objects.order_by('parent__name')), 5) + + # Empty querysets can be merged with others. + self.assertQuerysetEqual( + Note.objects.none() | Note.objects.all(), + ['<Note: n1>', '<Note: n2>', '<Note: n3>'] + ) + self.assertQuerysetEqual( + Note.objects.all() | Note.objects.none(), + ['<Note: n1>', '<Note: n2>', '<Note: n3>'] + ) + self.assertQuerysetEqual(Note.objects.none() & Note.objects.all(), []) + self.assertQuerysetEqual(Note.objects.all() & Note.objects.none(), []) + + def test_ticket9411(self): + # Make sure bump_prefix() (an internal Query method) doesn't (re-)break. It's + # sufficient that this query runs without error. + qs = Tag.objects.values_list('id', flat=True).order_by('id') + qs.query.bump_prefix() + first = qs[0] + self.assertEqual(list(qs), range(first, first+5)) + + def test_ticket8439(self): + # Complex combinations of conjunctions, disjunctions and nullable + # relations. + self.assertQuerysetEqual( + Author.objects.filter(Q(item__note__extrainfo=self.e2)|Q(report=self.r1, name='xyz')), + ['<Author: a2>'] + ) + self.assertQuerysetEqual( + Author.objects.filter(Q(report=self.r1, name='xyz')|Q(item__note__extrainfo=self.e2)), + ['<Author: a2>'] + ) + self.assertQuerysetEqual( + Annotation.objects.filter(Q(tag__parent=self.t1)|Q(notes__note='n1', name='a1')), + ['<Annotation: a1>'] + ) + xx = ExtraInfo.objects.create(info='xx', note=self.n3) + self.assertQuerysetEqual( + Note.objects.filter(Q(extrainfo__author=self.a1)|Q(extrainfo=xx)), + ['<Note: n1>', '<Note: n3>'] + ) + xx.delete() + q = Note.objects.filter(Q(extrainfo__author=self.a1)|Q(extrainfo=xx)).query + self.assertEqual( + len([x[2] for x in q.alias_map.values() if x[2] == q.LOUTER and q.alias_refcount[x[1]]]), + 1 + ) + + +class Queries2Tests(TestCase): + def setUp(self): + Number.objects.create(num=4) + Number.objects.create(num=8) + Number.objects.create(num=12) + + def test_ticket4289(self): + # A slight variation on the restricting the filtering choices by the + # lookup constraints. + self.assertQuerysetEqual(Number.objects.filter(num__lt=4), []) + self.assertQuerysetEqual(Number.objects.filter(num__gt=8, num__lt=12), []) + self.assertQuerysetEqual( + Number.objects.filter(num__gt=8, num__lt=13), + ['<Number: 12>'] + ) + self.assertQuerysetEqual( + Number.objects.filter(Q(num__lt=4) | Q(num__gt=8, num__lt=12)), + [] + ) + self.assertQuerysetEqual( + Number.objects.filter(Q(num__gt=8, num__lt=12) | Q(num__lt=4)), + [] + ) + self.assertQuerysetEqual( + Number.objects.filter(Q(num__gt=8) & Q(num__lt=12) | Q(num__lt=4)), + [] + ) + self.assertQuerysetEqual( + Number.objects.filter(Q(num__gt=7) & Q(num__lt=12) | Q(num__lt=4)), + ['<Number: 8>'] + ) + + def test_ticket12239(self): + # Float was being rounded to integer on gte queries on integer field. Tests + # show that gt, lt, gte, and lte work as desired. Note that the fix changes + # get_prep_lookup for gte and lt queries only. + self.assertQuerysetEqual( + Number.objects.filter(num__gt=11.9), + ['<Number: 12>'] + ) + self.assertQuerysetEqual(Number.objects.filter(num__gt=12), []) + self.assertQuerysetEqual(Number.objects.filter(num__gt=12.0), []) + self.assertQuerysetEqual(Number.objects.filter(num__gt=12.1), []) + self.assertQuerysetEqual( + Number.objects.filter(num__lt=12), + ['<Number: 4>', '<Number: 8>'] + ) + self.assertQuerysetEqual( + Number.objects.filter(num__lt=12.0), + ['<Number: 4>', '<Number: 8>'] + ) + self.assertQuerysetEqual( + Number.objects.filter(num__lt=12.1), + ['<Number: 4>', '<Number: 8>', '<Number: 12>'] + ) + self.assertQuerysetEqual( + Number.objects.filter(num__gte=11.9), + ['<Number: 12>'] + ) + self.assertQuerysetEqual( + Number.objects.filter(num__gte=12), + ['<Number: 12>'] + ) + self.assertQuerysetEqual( + Number.objects.filter(num__gte=12.0), + ['<Number: 12>'] + ) + self.assertQuerysetEqual(Number.objects.filter(num__gte=12.1), []) + self.assertQuerysetEqual(Number.objects.filter(num__gte=12.9), []) + self.assertQuerysetEqual( + Number.objects.filter(num__lte=11.9), + ['<Number: 4>', '<Number: 8>'] + ) + self.assertQuerysetEqual( + Number.objects.filter(num__lte=12), + ['<Number: 4>', '<Number: 8>', '<Number: 12>'] + ) + self.assertQuerysetEqual( + Number.objects.filter(num__lte=12.0), + ['<Number: 4>', '<Number: 8>', '<Number: 12>'] + ) + self.assertQuerysetEqual( + Number.objects.filter(num__lte=12.1), + ['<Number: 4>', '<Number: 8>', '<Number: 12>'] + ) + self.assertQuerysetEqual( + Number.objects.filter(num__lte=12.9), + ['<Number: 4>', '<Number: 8>', '<Number: 12>'] + ) + + def test_ticket7411(self): + # Saving to db must work even with partially read result set in another + # cursor. + for num in range(2 * ITER_CHUNK_SIZE + 1): + _ = Number.objects.create(num=num) + + for i, obj in enumerate(Number.objects.all()): + obj.save() + if i > 10: break + + def test_ticket7759(self): + # Count should work with a partially read result set. + count = Number.objects.count() + qs = Number.objects.all() + def run(): + for obj in qs: + return qs.count() == count + self.assertTrue(run()) + + +class Queries3Tests(BaseQuerysetTest): + def test_ticket7107(self): + # This shouldn't create an infinite loop. + self.assertQuerysetEqual(Valid.objects.all(), []) + + def test_ticket8683(self): + # Raise proper error when a DateQuerySet gets passed a wrong type of + # field + self.assertRaisesMessage( + AssertionError, + "'name' isn't a DateField.", + Item.objects.dates, 'name', 'month' + ) + +class Queries4Tests(BaseQuerysetTest): + def setUp(self): + generic = NamedCategory.objects.create(name="Generic") + self.t1 = Tag.objects.create(name='t1', category=generic) + + n1 = Note.objects.create(note='n1', misc='foo', id=1) + n2 = Note.objects.create(note='n2', misc='bar', id=2) + + e1 = ExtraInfo.objects.create(info='e1', note=n1) + e2 = ExtraInfo.objects.create(info='e2', note=n2) + + a1 = Author.objects.create(name='a1', num=1001, extra=e1) + a3 = Author.objects.create(name='a3', num=3003, extra=e2) + + Report.objects.create(name='r1', creator=a1) + Report.objects.create(name='r2', creator=a3) + Report.objects.create(name='r3') + + def test_ticket7095(self): + # Updates that are filtered on the model being updated are somewhat + # tricky in MySQL. This exercises that case. + ManagedModel.objects.create(data='mm1', tag=self.t1, public=True) + self.assertEqual(ManagedModel.objects.update(data='mm'), 1) + + # A values() or values_list() query across joined models must use outer + # joins appropriately. + # Note: In Oracle, we expect a null CharField to return u'' instead of + # None. + if connection.features.interprets_empty_strings_as_nulls: + expected_null_charfield_repr = u'' + else: + expected_null_charfield_repr = None + self.assertValueQuerysetEqual( + Report.objects.values_list("creator__extra__info", flat=True).order_by("name"), + [u'e1', u'e2', expected_null_charfield_repr], + ) + + # Similarly for select_related(), joins beyond an initial nullable join + # must use outer joins so that all results are included. + self.assertQuerysetEqual( + Report.objects.select_related("creator", "creator__extra").order_by("name"), + ['<Report: r1>', '<Report: r2>', '<Report: r3>'] + ) + + # When there are multiple paths to a table from another table, we have + # to be careful not to accidentally reuse an inappropriate join when + # using select_related(). We used to return the parent's Detail record + # here by mistake. + + d1 = Detail.objects.create(data="d1") + d2 = Detail.objects.create(data="d2") + m1 = Member.objects.create(name="m1", details=d1) + m2 = Member.objects.create(name="m2", details=d2) + Child.objects.create(person=m2, parent=m1) + obj = m1.children.select_related("person__details")[0] + self.assertEqual(obj.person.details.data, u'd2') + + def test_order_by_resetting(self): + # Calling order_by() with no parameters removes any existing ordering on the + # model. But it should still be possible to add new ordering after that. + qs = Author.objects.order_by().order_by('name') + self.assertTrue('ORDER BY' in qs.query.get_compiler(qs.db).as_sql()[0]) + + def test_ticket10181(self): + # Avoid raising an EmptyResultSet if an inner query is probably + # empty (and hence, not executed). + self.assertQuerysetEqual( + Tag.objects.filter(id__in=Tag.objects.filter(id__in=[])), + [] + ) + + +class Queries5Tests(TestCase): + def setUp(self): + # Ordering by 'rank' gives us rank2, rank1, rank3. Ordering by the Meta.ordering + # will be rank3, rank2, rank1. + n1 = Note.objects.create(note='n1', misc='foo', id=1) + n2 = Note.objects.create(note='n2', misc='bar', id=2) + e1 = ExtraInfo.objects.create(info='e1', note=n1) + e2 = ExtraInfo.objects.create(info='e2', note=n2) + a1 = Author.objects.create(name='a1', num=1001, extra=e1) + a2 = Author.objects.create(name='a2', num=2002, extra=e1) + a3 = Author.objects.create(name='a3', num=3003, extra=e2) + self.rank1 = Ranking.objects.create(rank=2, author=a2) + Ranking.objects.create(rank=1, author=a3) + Ranking.objects.create(rank=3, author=a1) + + def test_ordering(self): + # Cross model ordering is possible in Meta, too. + self.assertQuerysetEqual( + Ranking.objects.all(), + ['<Ranking: 3: a1>', '<Ranking: 2: a2>', '<Ranking: 1: a3>'] + ) + self.assertQuerysetEqual( + Ranking.objects.all().order_by('rank'), + ['<Ranking: 1: a3>', '<Ranking: 2: a2>', '<Ranking: 3: a1>'] + ) + + + # Ordering of extra() pieces is possible, too and you can mix extra + # fields and model fields in the ordering. + self.assertQuerysetEqual( + Ranking.objects.extra(tables=['django_site'], order_by=['-django_site.id', 'rank']), + ['<Ranking: 1: a3>', '<Ranking: 2: a2>', '<Ranking: 3: a1>'] + ) + + qs = Ranking.objects.extra(select={'good': 'case when rank > 2 then 1 else 0 end'}) + self.assertEqual( + [o.good for o in qs.extra(order_by=('-good',))], + [True, False, False] + ) + self.assertQuerysetEqual( + qs.extra(order_by=('-good', 'id')), + ['<Ranking: 3: a1>', '<Ranking: 2: a2>', '<Ranking: 1: a3>'] + ) + + # Despite having some extra aliases in the query, we can still omit + # them in a values() query. + dicts = qs.values('id', 'rank').order_by('id') + self.assertEqual( + [d.items()[1] for d in dicts], + [('rank', 2), ('rank', 1), ('rank', 3)] + ) + + def test_ticket7256(self): + # An empty values() call includes all aliases, including those from an + # extra() + qs = Ranking.objects.extra(select={'good': 'case when rank > 2 then 1 else 0 end'}) + dicts = qs.values().order_by('id') + for d in dicts: del d['id']; del d['author_id'] + self.assertEqual( + [sorted(d.items()) for d in dicts], + [[('good', 0), ('rank', 2)], [('good', 0), ('rank', 1)], [('good', 1), ('rank', 3)]] + ) + + def test_ticket7045(self): + # Extra tables used to crash SQL construction on the second use. + qs = Ranking.objects.extra(tables=['django_site']) + qs.query.get_compiler(qs.db).as_sql() + # test passes if this doesn't raise an exception. + qs.query.get_compiler(qs.db).as_sql() + + def test_ticket9848(self): + # Make sure that updates which only filter on sub-tables don't + # inadvertently update the wrong records (bug #9848). + + # Make sure that the IDs from different tables don't happen to match. + self.assertQuerysetEqual( + Ranking.objects.filter(author__name='a1'), + ['<Ranking: 3: a1>'] + ) + self.assertEqual( + Ranking.objects.filter(author__name='a1').update(rank='4'), + 1 + ) + r = Ranking.objects.filter(author__name='a1')[0] + self.assertNotEqual(r.id, r.author.id) + self.assertEqual(r.rank, 4) + r.rank = 3 + r.save() + self.assertQuerysetEqual( + Ranking.objects.all(), + ['<Ranking: 3: a1>', '<Ranking: 2: a2>', '<Ranking: 1: a3>'] + ) + + def test_ticket5261(self): + self.assertQuerysetEqual( + Note.objects.exclude(Q()), + ['<Note: n1>', '<Note: n2>'] + ) + + +class SelectRelatedTests(TestCase): + def test_tickets_3045_3288(self): + # Once upon a time, select_related() with circular relations would loop + # infinitely if you forgot to specify "depth". Now we set an arbitrary + # default upper bound. + self.assertQuerysetEqual(X.objects.all(), []) + self.assertQuerysetEqual(X.objects.select_related(), []) + + +class SubclassFKTests(TestCase): + def test_ticket7778(self): + # Model subclasses could not be deleted if a nullable foreign key + # relates to a model that relates back. + + num_celebs = Celebrity.objects.count() + tvc = TvChef.objects.create(name="Huey") + self.assertEqual(Celebrity.objects.count(), num_celebs + 1) + Fan.objects.create(fan_of=tvc) + Fan.objects.create(fan_of=tvc) + tvc.delete() + + # The parent object should have been deleted as well. + self.assertEqual(Celebrity.objects.count(), num_celebs) + + +class CustomPkTests(TestCase): + def test_ticket7371(self): + self.assertQuerysetEqual(Related.objects.order_by('custom'), []) + + +class NullableRelOrderingTests(TestCase): + def test_ticket10028(self): + # Ordering by model related to nullable relations(!) should use outer + # joins, so that all results are included. + _ = Plaything.objects.create(name="p1") + self.assertQuerysetEqual( + Plaything.objects.all(), + ['<Plaything: p1>'] + ) + + +class DisjunctiveFilterTests(TestCase): + def setUp(self): + self.n1 = Note.objects.create(note='n1', misc='foo', id=1) + ExtraInfo.objects.create(info='e1', note=self.n1) + + def test_ticket7872(self): + # Another variation on the disjunctive filtering theme. + + # For the purposes of this regression test, it's important that there is no + # Join object releated to the LeafA we create. + LeafA.objects.create(data='first') + self.assertQuerysetEqual(LeafA.objects.all(), ['<LeafA: first>']) + self.assertQuerysetEqual( + LeafA.objects.filter(Q(data='first')|Q(join__b__data='second')), + ['<LeafA: first>'] + ) + + def test_ticket8283(self): + # Checking that applying filters after a disjunction works correctly. + self.assertQuerysetEqual( + (ExtraInfo.objects.filter(note=self.n1)|ExtraInfo.objects.filter(info='e2')).filter(note=self.n1), + ['<ExtraInfo: e1>'] + ) + self.assertQuerysetEqual( + (ExtraInfo.objects.filter(info='e2')|ExtraInfo.objects.filter(note=self.n1)).filter(note=self.n1), + ['<ExtraInfo: e1>'] + ) + + +class Queries6Tests(TestCase): + def setUp(self): + generic = NamedCategory.objects.create(name="Generic") + t1 = Tag.objects.create(name='t1', category=generic) + t2 = Tag.objects.create(name='t2', parent=t1, category=generic) + t3 = Tag.objects.create(name='t3', parent=t1) + t4 = Tag.objects.create(name='t4', parent=t3) + t5 = Tag.objects.create(name='t5', parent=t3) + n1 = Note.objects.create(note='n1', misc='foo', id=1) + ann1 = Annotation.objects.create(name='a1', tag=t1) + ann1.notes.add(n1) + ann2 = Annotation.objects.create(name='a2', tag=t4) + + # FIXME!! This next test causes really weird PostgreSQL behaviour, but it's + # only apparent much later when the full test suite runs. I don't understand + # what's going on here yet. + ##def test_slicing_and_cache_interaction(self): + ## # We can do slicing beyond what is currently in the result cache, + ## # too. + ## + ## # We need to mess with the implementation internals a bit here to decrease the + ## # cache fill size so that we don't read all the results at once. + ## from django.db.models import query + ## query.ITER_CHUNK_SIZE = 2 + ## qs = Tag.objects.all() + ## + ## # Fill the cache with the first chunk. + ## self.assertTrue(bool(qs)) + ## self.assertEqual(len(qs._result_cache), 2) + ## + ## # Query beyond the end of the cache and check that it is filled out as required. + ## self.assertEqual(repr(qs[4]), '<Tag: t5>') + ## self.assertEqual(len(qs._result_cache), 5) + ## + ## # But querying beyond the end of the result set will fail. + ## self.assertRaises(IndexError, lambda: qs[100]) + + def test_parallel_iterators(self): + # Test that parallel iterators work. + qs = Tag.objects.all() + i1, i2 = iter(qs), iter(qs) + self.assertEqual(repr(i1.next()), '<Tag: t1>') + self.assertEqual(repr(i1.next()), '<Tag: t2>') + self.assertEqual(repr(i2.next()), '<Tag: t1>') + self.assertEqual(repr(i2.next()), '<Tag: t2>') + self.assertEqual(repr(i2.next()), '<Tag: t3>') + self.assertEqual(repr(i1.next()), '<Tag: t3>') + + qs = X.objects.all() + self.assertEqual(bool(qs), False) + self.assertEqual(bool(qs), False) + + def test_nested_queries_sql(self): + # Nested queries should not evaluate the inner query as part of constructing the + # SQL (so we should see a nested query here, indicated by two "SELECT" calls). + qs = Annotation.objects.filter(notes__in=Note.objects.filter(note="xyzzy")) + self.assertEqual( + qs.query.get_compiler(qs.db).as_sql()[0].count('SELECT'), + 2 + ) + + def test_tickets_8921_9188(self): + # Incorrect SQL was being generated for certain types of exclude() + # queries that crossed multi-valued relations (#8921, #9188 and some + # pre-emptively discovered cases). + + self.assertQuerysetEqual( + PointerA.objects.filter(connection__pointerb__id=1), + [] + ) + self.assertQuerysetEqual( + PointerA.objects.exclude(connection__pointerb__id=1), + [] + ) + + self.assertQuerysetEqual( + Tag.objects.exclude(children=None), + ['<Tag: t1>', '<Tag: t3>'] + ) + + # This example is tricky because the parent could be NULL, so only checking + # parents with annotations omits some results (tag t1, in this case). + self.assertQuerysetEqual( + Tag.objects.exclude(parent__annotation__name="a1"), + ['<Tag: t1>', '<Tag: t4>', '<Tag: t5>'] + ) + + # The annotation->tag link is single values and tag->children links is + # multi-valued. So we have to split the exclude filter in the middle + # and then optimise the inner query without losing results. + self.assertQuerysetEqual( + Annotation.objects.exclude(tag__children__name="t2"), + ['<Annotation: a2>'] + ) + + # Nested queries are possible (although should be used with care, since + # they have performance problems on backends like MySQL. + + self.assertQuerysetEqual( + Annotation.objects.filter(notes__in=Note.objects.filter(note="n1")), + ['<Annotation: a1>'] + ) + + def test_ticket3739(self): + # The all() method on querysets returns a copy of the queryset. + q1 = Tag.objects.order_by('name') + self.assertTrue(q1 is not q1.all()) + + +class GeneratorExpressionTests(TestCase): + def test_ticket10432(self): + # Using an empty generator expression as the rvalue for an "__in" + # lookup is legal. + self.assertQuerysetEqual( + Note.objects.filter(pk__in=(x for x in ())), + [] + ) + + +class ComparisonTests(TestCase): + def setUp(self): + self.n1 = Note.objects.create(note='n1', misc='foo', id=1) + e1 = ExtraInfo.objects.create(info='e1', note=self.n1) + self.a2 = Author.objects.create(name='a2', num=2002, extra=e1) + + def test_ticket8597(self): + # Regression tests for case-insensitive comparisons + _ = Item.objects.create(name="a_b", created=datetime.datetime.now(), creator=self.a2, note=self.n1) + _ = Item.objects.create(name="x%y", created=datetime.datetime.now(), creator=self.a2, note=self.n1) + self.assertQuerysetEqual( + Item.objects.filter(name__iexact="A_b"), + ['<Item: a_b>'] + ) + self.assertQuerysetEqual( + Item.objects.filter(name__iexact="x%Y"), + ['<Item: x%y>'] + ) + self.assertQuerysetEqual( + Item.objects.filter(name__istartswith="A_b"), + ['<Item: a_b>'] + ) + self.assertQuerysetEqual( + Item.objects.filter(name__iendswith="A_b"), + ['<Item: a_b>'] + ) + + +class ExistsSql(TestCase): + def setUp(self): + settings.DEBUG = True + + def test_exists(self): + self.assertFalse(Tag.objects.exists()) + # Ok - so the exist query worked - but did it include too many columns? + self.assertTrue("id" not in connection.queries[-1]['sql'] and "name" not in connection.queries[-1]['sql']) + + def tearDown(self): + settings.DEBUG = False + + +class QuerysetOrderedTests(unittest.TestCase): + """ + Tests for the Queryset.ordered attribute. + """ + + def test_no_default_or_explicit_ordering(self): + self.assertEqual(Annotation.objects.all().ordered, False) + + def test_cleared_default_ordering(self): + self.assertEqual(Tag.objects.all().ordered, True) + self.assertEqual(Tag.objects.all().order_by().ordered, False) + + def test_explicit_ordering(self): + self.assertEqual(Annotation.objects.all().order_by('id').ordered, True) + + def test_order_by_extra(self): + self.assertEqual(Annotation.objects.all().extra(order_by=['id']).ordered, True) + + def test_annotated_ordering(self): + qs = Annotation.objects.annotate(num_notes=Count('notes')) + self.assertEqual(qs.ordered, False) + self.assertEqual(qs.order_by('num_notes').ordered, True) + + +class SubqueryTests(TestCase): + def setUp(self): + DumbCategory.objects.create(id=1) + DumbCategory.objects.create(id=2) + DumbCategory.objects.create(id=3) + + def test_ordered_subselect(self): + "Subselects honor any manual ordering" + try: + query = DumbCategory.objects.filter(id__in=DumbCategory.objects.order_by('-id')[0:2]) + self.assertEquals(set(query.values_list('id', flat=True)), set([2,3])) + + query = DumbCategory.objects.filter(id__in=DumbCategory.objects.order_by('-id')[:2]) + self.assertEquals(set(query.values_list('id', flat=True)), set([2,3])) + + query = DumbCategory.objects.filter(id__in=DumbCategory.objects.order_by('-id')[2:]) + self.assertEquals(set(query.values_list('id', flat=True)), set([1])) + except DatabaseError: + # Oracle and MySQL both have problems with sliced subselects. + # This prevents us from even evaluating this test case at all. + # Refs #10099 + self.assertFalse(connections[DEFAULT_DB_ALIAS].features.allow_sliced_subqueries) + + def test_sliced_delete(self): + "Delete queries can safely contain sliced subqueries" + try: + DumbCategory.objects.filter(id__in=DumbCategory.objects.order_by('-id')[0:1]).delete() + self.assertEquals(set(DumbCategory.objects.values_list('id', flat=True)), set([1,2])) + except DatabaseError: + # Oracle and MySQL both have problems with sliced subselects. + # This prevents us from even evaluating this test case at all. + # Refs #10099 + self.assertFalse(connections[DEFAULT_DB_ALIAS].features.allow_sliced_subqueries) + + +class CloneTests(TestCase): + def test_evaluated_queryset_as_argument(self): + "#13227 -- If a queryset is already evaluated, it can still be used as a query arg" + n = Note(note='Test1', misc='misc') + n.save() + e = ExtraInfo(info='good', note=n) + e.save() + + n_list = Note.objects.all() + # Evaluate the Note queryset, populating the query cache + list(n_list) + # Use the note queryset in a query, and evalute + # that query in a way that involves cloning. + try: + self.assertEquals(ExtraInfo.objects.filter(note__in=n_list)[0].info, 'good') + except: + self.fail('Query should be clonable') + + +class EmptyQuerySetTests(TestCase): + def test_emptyqueryset_values(self): + # #14366 -- Calling .values() on an EmptyQuerySet and then cloning that + # should not cause an error" + self.assertQuerysetEqual( + Number.objects.none().values('num').order_by('num'), [] + ) + + def test_values_subquery(self): + self.assertQuerysetEqual( + Number.objects.filter(pk__in=Number.objects.none().values("pk")), + [] + ) + self.assertQuerysetEqual( + Number.objects.filter(pk__in=Number.objects.none().values_list("pk")), + [] + ) + + +class ValuesQuerysetTests(BaseQuerysetTest): + def test_flat_values_lits(self): + Number.objects.create(num=72) + qs = Number.objects.values_list("num") + qs = qs.values_list("num", flat=True) + self.assertValueQuerysetEqual( + qs, [72] + ) + + +class WeirdQuerysetSlicingTests(BaseQuerysetTest): + def setUp(self): + Number.objects.create(num=1) + Number.objects.create(num=2) + + Article.objects.create(name='one', created=datetime.datetime.now()) + Article.objects.create(name='two', created=datetime.datetime.now()) + Article.objects.create(name='three', created=datetime.datetime.now()) + Article.objects.create(name='four', created=datetime.datetime.now()) + + def test_tickets_7698_10202(self): + # People like to slice with '0' as the high-water mark. + self.assertQuerysetEqual(Article.objects.all()[0:0], []) + self.assertQuerysetEqual(Article.objects.all()[0:0][:10], []) + self.assertEqual(Article.objects.all()[:0].count(), 0) + self.assertRaisesMessage( + AssertionError, + 'Cannot change a query once a slice has been taken.', + Article.objects.all()[:0].latest, 'created' + ) + + +class EscapingTests(TestCase): + def test_ticket_7302(self): + # Reserved names are appropriately escaped + _ = ReservedName.objects.create(name='a', order=42) + ReservedName.objects.create(name='b', order=37) + self.assertQuerysetEqual( + ReservedName.objects.all().order_by('order'), + ['<ReservedName: b>', '<ReservedName: a>'] + ) + self.assertQuerysetEqual( + ReservedName.objects.extra(select={'stuff':'name'}, order_by=('order','stuff')), + ['<ReservedName: b>', '<ReservedName: a>'] + ) + + +# In Python 2.6 beta releases, exceptions raised in __len__ are swallowed +# (Python issue 1242657), so these cases return an empty list, rather than +# raising an exception. Not a lot we can do about that, unfortunately, due to +# the way Python handles list() calls internally. Thus, we skip the tests for +# Python 2.6. +if sys.version_info[:2] != (2, 6): + class OrderingLoopTests(BaseQuerysetTest): + def setUp(self): + generic = NamedCategory.objects.create(name="Generic") + t1 = Tag.objects.create(name='t1', category=generic) + t2 = Tag.objects.create(name='t2', parent=t1, category=generic) + t3 = Tag.objects.create(name='t3', parent=t1) + t4 = Tag.objects.create(name='t4', parent=t3) + t5 = Tag.objects.create(name='t5', parent=t3) + + def test_infinite_loop(self): + # If you're not careful, it's possible to introduce infinite loops via + # default ordering on foreign keys in a cycle. We detect that. + self.assertRaisesMessage( + FieldError, + 'Infinite loop caused by ordering.', + lambda: list(LoopX.objects.all()) # Force queryset evaluation with list() + ) + self.assertRaisesMessage( + FieldError, + 'Infinite loop caused by ordering.', + lambda: list(LoopZ.objects.all()) # Force queryset evaluation with list() + ) + + # Note that this doesn't cause an infinite loop, since the default + # ordering on the Tag model is empty (and thus defaults to using "id" + # for the related field). + self.assertEqual(len(Tag.objects.order_by('parent')), 5) + + # ... but you can still order in a non-recursive fashion amongst linked + # fields (the previous test failed because the default ordering was + # recursive). + self.assertQuerysetEqual( + LoopX.objects.all().order_by('y__x__y__x__id'), + [] + ) + + +# When grouping without specifying ordering, we add an explicit "ORDER BY NULL" +# portion in MySQL to prevent unnecessary sorting. +if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == "django.db.backends.mysql": + class GroupingTests(TestCase): + def test_null_ordering_added(self): + query = Tag.objects.values_list('parent_id', flat=True).order_by().query + query.group_by = ['parent_id'] + sql = query.get_compiler(DEFAULT_DB_ALIAS).as_sql()[0] + fragment = "ORDER BY " + pos = sql.find(fragment) + self.assertEqual(sql.find(fragment, pos + 1), -1) + self.assertEqual(sql.find("NULL", pos + len(fragment)), pos + len(fragment)) + + +# Sqlite 3 does not support passing in more than 1000 parameters except by +# changing a parameter at compilation time. +if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != "django.db.backends.sqlite3": + class InLookupTests(TestCase): + def test_ticket14244(self): + # Test that the "in" lookup works with lists of 1000 items or more. + Number.objects.all().delete() + numbers = range(2500) + for num in numbers: + _ = Number.objects.create(num=num) + self.assertEqual( + Number.objects.filter(num__in=numbers[:1000]).count(), + 1000 + ) + self.assertEqual( + Number.objects.filter(num__in=numbers[:1001]).count(), + 1001 + ) + self.assertEqual( + Number.objects.filter(num__in=numbers[:2000]).count(), + 2000 + ) + self.assertEqual( + Number.objects.filter(num__in=numbers).count(), + 2500 + ) diff --git a/parts/django/tests/regressiontests/queryset_pickle/__init__.py b/parts/django/tests/regressiontests/queryset_pickle/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/queryset_pickle/__init__.py diff --git a/parts/django/tests/regressiontests/queryset_pickle/models.py b/parts/django/tests/regressiontests/queryset_pickle/models.py new file mode 100644 index 0000000..df0a6e6 --- /dev/null +++ b/parts/django/tests/regressiontests/queryset_pickle/models.py @@ -0,0 +1,34 @@ +import datetime +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +def standalone_number(self): + return 1 + +class Numbers(object): + @staticmethod + def get_static_number(self): + return 2 + + @classmethod + def get_class_number(self): + return 3 + + def get_member_number(self): + return 4 + +nn = Numbers() + +class Group(models.Model): + name = models.CharField(_('name'), max_length=100) + +class Event(models.Model): + group = models.ForeignKey(Group) + +class Happening(models.Model): + when = models.DateTimeField(blank=True, default=datetime.datetime.now) + name = models.CharField(blank=True, max_length=100, default=lambda:"test") + number1 = models.IntegerField(blank=True, default=standalone_number) + number2 = models.IntegerField(blank=True, default=Numbers.get_static_number) + number3 = models.IntegerField(blank=True, default=Numbers.get_class_number) + number4 = models.IntegerField(blank=True, default=nn.get_member_number) diff --git a/parts/django/tests/regressiontests/queryset_pickle/tests.py b/parts/django/tests/regressiontests/queryset_pickle/tests.py new file mode 100644 index 0000000..5c64687 --- /dev/null +++ b/parts/django/tests/regressiontests/queryset_pickle/tests.py @@ -0,0 +1,36 @@ +import pickle +import datetime + +from django.test import TestCase + +from models import Group, Event, Happening + + +class PickleabilityTestCase(TestCase): + def assert_pickles(self, qs): + self.assertEqual(list(pickle.loads(pickle.dumps(qs))), list(qs)) + + def test_related_field(self): + g = Group.objects.create(name="Ponies Who Own Maybachs") + self.assert_pickles(Event.objects.filter(group=g.id)) + + def test_datetime_callable_default_all(self): + self.assert_pickles(Happening.objects.all()) + + def test_datetime_callable_default_filter(self): + self.assert_pickles(Happening.objects.filter(when=datetime.datetime.now())) + + def test_lambda_as_default(self): + self.assert_pickles(Happening.objects.filter(name="test")) + + def test_standalone_method_as_default(self): + self.assert_pickles(Happening.objects.filter(number1=1)) + + def test_staticmethod_as_default(self): + self.assert_pickles(Happening.objects.filter(number2=1)) + + def test_classmethod_as_default(self): + self.assert_pickles(Happening.objects.filter(number3=1)) + + def test_membermethod_as_default(self): + self.assert_pickles(Happening.objects.filter(number4=1)) diff --git a/parts/django/tests/regressiontests/requests/__init__.py b/parts/django/tests/regressiontests/requests/__init__.py new file mode 100644 index 0000000..3a32885 --- /dev/null +++ b/parts/django/tests/regressiontests/requests/__init__.py @@ -0,0 +1,3 @@ +""" +Tests for Django's various Request objects. +""" diff --git a/parts/django/tests/regressiontests/requests/models.py b/parts/django/tests/regressiontests/requests/models.py new file mode 100644 index 0000000..19f81d6 --- /dev/null +++ b/parts/django/tests/regressiontests/requests/models.py @@ -0,0 +1 @@ +# Need a models module for the test runner. diff --git a/parts/django/tests/regressiontests/requests/tests.py b/parts/django/tests/regressiontests/requests/tests.py new file mode 100644 index 0000000..556d61e --- /dev/null +++ b/parts/django/tests/regressiontests/requests/tests.py @@ -0,0 +1,59 @@ +from datetime import datetime, timedelta +import time +import unittest + +from django.http import HttpRequest, HttpResponse, parse_cookie +from django.core.handlers.wsgi import WSGIRequest +from django.core.handlers.modpython import ModPythonRequest +from django.utils.http import cookie_date + +class RequestsTests(unittest.TestCase): + + def test_httprequest(self): + request = HttpRequest() + self.assertEqual(request.GET.keys(), []) + self.assertEqual(request.POST.keys(), []) + self.assertEqual(request.COOKIES.keys(), []) + self.assertEqual(request.META.keys(), []) + + def test_wsgirequest(self): + request = WSGIRequest({'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus'}) + self.assertEqual(request.GET.keys(), []) + self.assertEqual(request.POST.keys(), []) + self.assertEqual(request.COOKIES.keys(), []) + self.assertEqual(set(request.META.keys()), set(['PATH_INFO', 'REQUEST_METHOD', 'SCRIPT_NAME'])) + self.assertEqual(request.META['PATH_INFO'], 'bogus') + self.assertEqual(request.META['REQUEST_METHOD'], 'bogus') + self.assertEqual(request.META['SCRIPT_NAME'], '') + + def test_modpythonrequest(self): + class FakeModPythonRequest(ModPythonRequest): + def __init__(self, *args, **kwargs): + super(FakeModPythonRequest, self).__init__(*args, **kwargs) + self._get = self._post = self._meta = self._cookies = {} + + class Dummy: + def get_options(self): + return {} + + req = Dummy() + req.uri = 'bogus' + request = FakeModPythonRequest(req) + self.assertEqual(request.path, 'bogus') + self.assertEqual(request.GET.keys(), []) + self.assertEqual(request.POST.keys(), []) + self.assertEqual(request.COOKIES.keys(), []) + self.assertEqual(request.META.keys(), []) + + def test_parse_cookie(self): + self.assertEqual(parse_cookie('invalid:key=true'), {}) + + def test_httprequest_location(self): + request = HttpRequest() + self.assertEqual(request.build_absolute_uri(location="https://www.example.com/asdf"), + 'https://www.example.com/asdf') + + request.get_host = lambda: 'www.example.com' + request.path = '' + self.assertEqual(request.build_absolute_uri(location="/path/with:colons"), + 'http://www.example.com/path/with:colons') diff --git a/parts/django/tests/regressiontests/reverse_single_related/__init__.py b/parts/django/tests/regressiontests/reverse_single_related/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/reverse_single_related/__init__.py diff --git a/parts/django/tests/regressiontests/reverse_single_related/models.py b/parts/django/tests/regressiontests/reverse_single_related/models.py new file mode 100644 index 0000000..a2d8fb0 --- /dev/null +++ b/parts/django/tests/regressiontests/reverse_single_related/models.py @@ -0,0 +1,12 @@ +from django.db import models + +class SourceManager(models.Manager): + def get_query_set(self): + return super(SourceManager, self).get_query_set().filter(is_public=True) + +class Source(models.Model): + is_public = models.BooleanField() + objects = SourceManager() + +class Item(models.Model): + source = models.ForeignKey(Source) diff --git a/parts/django/tests/regressiontests/reverse_single_related/tests.py b/parts/django/tests/regressiontests/reverse_single_related/tests.py new file mode 100644 index 0000000..14f3a66 --- /dev/null +++ b/parts/django/tests/regressiontests/reverse_single_related/tests.py @@ -0,0 +1,36 @@ +from django.test import TestCase + +from regressiontests.reverse_single_related.models import * + +class ReverseSingleRelatedTests(TestCase): + """ + Regression tests for an object that cannot access a single related + object due to a restrictive default manager. + """ + + def test_reverse_single_related(self): + + public_source = Source.objects.create(is_public=True) + public_item = Item.objects.create(source=public_source) + + private_source = Source.objects.create(is_public=False) + private_item = Item.objects.create(source=private_source) + + # Only one source is available via all() due to the custom default manager. + self.assertQuerysetEqual( + Source.objects.all(), + ["<Source: Source object>"] + ) + + self.assertEquals(public_item.source, public_source) + + # Make sure that an item can still access its related source even if the default + # manager doesn't normally allow it. + self.assertEquals(private_item.source, private_source) + + # If the manager is marked "use_for_related_fields", it'll get used instead + # of the "bare" queryset. Usually you'd define this as a property on the class, + # but this approximates that in a way that's easier in tests. + Source.objects.use_for_related_fields = True + private_item = Item.objects.get(pk=private_item.pk) + self.assertRaises(Source.DoesNotExist, lambda: private_item.source) diff --git a/parts/django/tests/regressiontests/select_related_onetoone/__init__.py b/parts/django/tests/regressiontests/select_related_onetoone/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/select_related_onetoone/__init__.py diff --git a/parts/django/tests/regressiontests/select_related_onetoone/models.py b/parts/django/tests/regressiontests/select_related_onetoone/models.py new file mode 100644 index 0000000..3d6da9b --- /dev/null +++ b/parts/django/tests/regressiontests/select_related_onetoone/models.py @@ -0,0 +1,54 @@ +from django.db import models + + +class User(models.Model): + username = models.CharField(max_length=100) + email = models.EmailField() + + def __unicode__(self): + return self.username + + +class UserProfile(models.Model): + user = models.OneToOneField(User) + city = models.CharField(max_length=100) + state = models.CharField(max_length=2) + + def __unicode__(self): + return "%s, %s" % (self.city, self.state) + + +class UserStatResult(models.Model): + results = models.CharField(max_length=50) + + def __unicode__(self): + return 'UserStatResults, results = %s' % (self.results,) + + +class UserStat(models.Model): + user = models.OneToOneField(User, primary_key=True) + posts = models.IntegerField() + results = models.ForeignKey(UserStatResult) + + def __unicode__(self): + return 'UserStat, posts = %s' % (self.posts,) + + +class StatDetails(models.Model): + base_stats = models.OneToOneField(UserStat) + comments = models.IntegerField() + + def __unicode__(self): + return 'StatDetails, comments = %s' % (self.comments,) + + +class AdvancedUserStat(UserStat): + karma = models.IntegerField() + +class Image(models.Model): + name = models.CharField(max_length=100) + + +class Product(models.Model): + name = models.CharField(max_length=100) + image = models.OneToOneField(Image, null=True) diff --git a/parts/django/tests/regressiontests/select_related_onetoone/tests.py b/parts/django/tests/regressiontests/select_related_onetoone/tests.py new file mode 100644 index 0000000..4ccb584 --- /dev/null +++ b/parts/django/tests/regressiontests/select_related_onetoone/tests.py @@ -0,0 +1,94 @@ +from django import db +from django.conf import settings +from django.test import TestCase + +from models import (User, UserProfile, UserStat, UserStatResult, StatDetails, + AdvancedUserStat, Image, Product) + +class ReverseSelectRelatedTestCase(TestCase): + def setUp(self): + # Explicitly enable debug for these tests - we need to count + # the queries that have been issued. + self.old_debug = settings.DEBUG + settings.DEBUG = True + + user = User.objects.create(username="test") + userprofile = UserProfile.objects.create(user=user, state="KS", + city="Lawrence") + results = UserStatResult.objects.create(results='first results') + userstat = UserStat.objects.create(user=user, posts=150, + results=results) + details = StatDetails.objects.create(base_stats=userstat, comments=259) + + user2 = User.objects.create(username="bob") + results2 = UserStatResult.objects.create(results='moar results') + advstat = AdvancedUserStat.objects.create(user=user2, posts=200, karma=5, + results=results2) + StatDetails.objects.create(base_stats=advstat, comments=250) + + db.reset_queries() + + def assertQueries(self, queries): + self.assertEqual(len(db.connection.queries), queries) + + def tearDown(self): + settings.DEBUG = self.old_debug + + def test_basic(self): + u = User.objects.select_related("userprofile").get(username="test") + self.assertEqual(u.userprofile.state, "KS") + self.assertQueries(1) + + def test_follow_next_level(self): + u = User.objects.select_related("userstat__results").get(username="test") + self.assertEqual(u.userstat.posts, 150) + self.assertEqual(u.userstat.results.results, 'first results') + self.assertQueries(1) + + def test_follow_two(self): + u = User.objects.select_related("userprofile", "userstat").get(username="test") + self.assertEqual(u.userprofile.state, "KS") + self.assertEqual(u.userstat.posts, 150) + self.assertQueries(1) + + def test_follow_two_next_level(self): + u = User.objects.select_related("userstat__results", "userstat__statdetails").get(username="test") + self.assertEqual(u.userstat.results.results, 'first results') + self.assertEqual(u.userstat.statdetails.comments, 259) + self.assertQueries(1) + + def test_forward_and_back(self): + stat = UserStat.objects.select_related("user__userprofile").get(user__username="test") + self.assertEqual(stat.user.userprofile.state, 'KS') + self.assertEqual(stat.user.userstat.posts, 150) + self.assertQueries(1) + + def test_back_and_forward(self): + u = User.objects.select_related("userstat").get(username="test") + self.assertEqual(u.userstat.user.username, 'test') + self.assertQueries(1) + + def test_not_followed_by_default(self): + u = User.objects.select_related().get(username="test") + self.assertEqual(u.userstat.posts, 150) + self.assertQueries(2) + + def test_follow_from_child_class(self): + stat = AdvancedUserStat.objects.select_related('user', 'statdetails').get(posts=200) + self.assertEqual(stat.statdetails.comments, 250) + self.assertEqual(stat.user.username, 'bob') + self.assertQueries(1) + + def test_follow_inheritance(self): + stat = UserStat.objects.select_related('user', 'advanceduserstat').get(posts=200) + self.assertEqual(stat.advanceduserstat.posts, 200) + self.assertEqual(stat.user.username, 'bob') + self.assertEqual(stat.advanceduserstat.user.username, 'bob') + self.assertQueries(1) + + def test_nullable_relation(self): + im = Image.objects.create(name="imag1") + p1 = Product.objects.create(name="Django Plushie", image=im) + p2 = Product.objects.create(name="Talking Django Plushie") + + self.assertEqual(len(Product.objects.select_related("image")), 2) diff --git a/parts/django/tests/regressiontests/select_related_regress/__init__.py b/parts/django/tests/regressiontests/select_related_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/select_related_regress/__init__.py diff --git a/parts/django/tests/regressiontests/select_related_regress/models.py b/parts/django/tests/regressiontests/select_related_regress/models.py new file mode 100644 index 0000000..3efb19e --- /dev/null +++ b/parts/django/tests/regressiontests/select_related_regress/models.py @@ -0,0 +1,86 @@ +from django.db import models + +class Building(models.Model): + name = models.CharField(max_length=10) + + def __unicode__(self): + return u"Building: %s" % self.name + +class Device(models.Model): + building = models.ForeignKey('Building') + name = models.CharField(max_length=10) + + def __unicode__(self): + return u"device '%s' in building %s" % (self.name, self.building) + +class Port(models.Model): + device = models.ForeignKey('Device') + port_number = models.CharField(max_length=10) + + def __unicode__(self): + return u"%s/%s" % (self.device.name, self.port_number) + +class Connection(models.Model): + start = models.ForeignKey(Port, related_name='connection_start', + unique=True) + end = models.ForeignKey(Port, related_name='connection_end', unique=True) + + def __unicode__(self): + return u"%s to %s" % (self.start, self.end) + +# Another non-tree hierarchy that exercises code paths similar to the above +# example, but in a slightly different configuration. +class TUser(models.Model): + name = models.CharField(max_length=200) + +class Person(models.Model): + user = models.ForeignKey(TUser, unique=True) + +class Organizer(models.Model): + person = models.ForeignKey(Person) + +class Student(models.Model): + person = models.ForeignKey(Person) + +class Class(models.Model): + org = models.ForeignKey(Organizer) + +class Enrollment(models.Model): + std = models.ForeignKey(Student) + cls = models.ForeignKey(Class) + +# Models for testing bug #8036. +class Country(models.Model): + name = models.CharField(max_length=50) + +class State(models.Model): + name = models.CharField(max_length=50) + country = models.ForeignKey(Country) + +class ClientStatus(models.Model): + name = models.CharField(max_length=50) + +class Client(models.Model): + name = models.CharField(max_length=50) + state = models.ForeignKey(State, null=True) + status = models.ForeignKey(ClientStatus) + +class SpecialClient(Client): + value = models.IntegerField() + +# Some model inheritance exercises +class Parent(models.Model): + name = models.CharField(max_length=10) + + def __unicode__(self): + return self.name + +class Child(Parent): + value = models.IntegerField() + +class Item(models.Model): + name = models.CharField(max_length=10) + child = models.ForeignKey(Child, null=True) + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/regressiontests/select_related_regress/tests.py b/parts/django/tests/regressiontests/select_related_regress/tests.py new file mode 100644 index 0000000..bfa1e21 --- /dev/null +++ b/parts/django/tests/regressiontests/select_related_regress/tests.py @@ -0,0 +1,134 @@ +from django.test import TestCase +from regressiontests.select_related_regress.models import * + +class SelectRelatedRegressTests(TestCase): + + def test_regression_7110(self): + """ + Regression test for bug #7110. + + When using select_related(), we must query the + Device and Building tables using two different aliases (each) in order to + differentiate the start and end Connection fields. The net result is that + both the "connections = ..." queries here should give the same results + without pulling in more than the absolute minimum number of tables + (history has shown that it's easy to make a mistake in the implementation + and include some unnecessary bonus joins). + """ + + b=Building.objects.create(name='101') + dev1=Device.objects.create(name="router", building=b) + dev2=Device.objects.create(name="switch", building=b) + dev3=Device.objects.create(name="server", building=b) + port1=Port.objects.create(port_number='4',device=dev1) + port2=Port.objects.create(port_number='7',device=dev2) + port3=Port.objects.create(port_number='1',device=dev3) + c1=Connection.objects.create(start=port1, end=port2) + c2=Connection.objects.create(start=port2, end=port3) + + connections=Connection.objects.filter(start__device__building=b, end__device__building=b).order_by('id') + self.assertEquals([(c.id, unicode(c.start), unicode(c.end)) for c in connections], + [(1, u'router/4', u'switch/7'), (2, u'switch/7', u'server/1')]) + + connections=Connection.objects.filter(start__device__building=b, end__device__building=b).select_related().order_by('id') + self.assertEquals([(c.id, unicode(c.start), unicode(c.end)) for c in connections], + [(1, u'router/4', u'switch/7'), (2, u'switch/7', u'server/1')]) + + # This final query should only join seven tables (port, device and building + # twice each, plus connection once). + self.assertEquals(connections.query.count_active_tables(), 7) + + + def test_regression_8106(self): + """ + Regression test for bug #8106. + + Same sort of problem as the previous test, but this time there are + more extra tables to pull in as part of the select_related() and some + of them could potentially clash (so need to be kept separate). + """ + + us = TUser.objects.create(name="std") + usp = Person.objects.create(user=us) + uo = TUser.objects.create(name="org") + uop = Person.objects.create(user=uo) + s = Student.objects.create(person = usp) + o = Organizer.objects.create(person = uop) + c = Class.objects.create(org=o) + e = Enrollment.objects.create(std=s, cls=c) + + e_related = Enrollment.objects.all().select_related()[0] + self.assertEquals(e_related.std.person.user.name, u"std") + self.assertEquals(e_related.cls.org.person.user.name, u"org") + + def test_regression_8036(self): + """ + Regression test for bug #8036 + + the first related model in the tests below + ("state") is empty and we try to select the more remotely related + state__country. The regression here was not skipping the empty column results + for country before getting status. + """ + + australia = Country.objects.create(name='Australia') + active = ClientStatus.objects.create(name='active') + client = Client.objects.create(name='client', status=active) + + self.assertEquals(client.status, active) + self.assertEquals(Client.objects.select_related()[0].status, active) + self.assertEquals(Client.objects.select_related('state')[0].status, active) + self.assertEquals(Client.objects.select_related('state', 'status')[0].status, active) + self.assertEquals(Client.objects.select_related('state__country')[0].status, active) + self.assertEquals(Client.objects.select_related('state__country', 'status')[0].status, active) + self.assertEquals(Client.objects.select_related('status')[0].status, active) + + def test_multi_table_inheritance(self): + """ Exercising select_related() with multi-table model inheritance. """ + c1 = Child.objects.create(name="child1", value=42) + i1 = Item.objects.create(name="item1", child=c1) + i2 = Item.objects.create(name="item2") + + self.assertQuerysetEqual( + Item.objects.select_related("child").order_by("name"), + ["<Item: item1>", "<Item: item2>"] + ) + + def test_regression_12851(self): + """ + Regression for #12851 + + Deferred fields are used correctly if you select_related a subset + of fields. + """ + australia = Country.objects.create(name='Australia') + active = ClientStatus.objects.create(name='active') + + wa = State.objects.create(name="Western Australia", country=australia) + c1 = Client.objects.create(name='Brian Burke', state=wa, status=active) + burke = Client.objects.select_related('state').defer('state__name').get(name='Brian Burke') + + self.assertEquals(burke.name, u'Brian Burke') + self.assertEquals(burke.state.name, u'Western Australia') + + # Still works if we're dealing with an inherited class + sc1 = SpecialClient.objects.create(name='Troy Buswell', state=wa, status=active, value=42) + troy = SpecialClient.objects.select_related('state').defer('state__name').get(name='Troy Buswell') + + self.assertEquals(troy.name, u'Troy Buswell') + self.assertEquals(troy.value, 42) + self.assertEquals(troy.state.name, u'Western Australia') + + # Still works if we defer an attribute on the inherited class + troy = SpecialClient.objects.select_related('state').defer('value', 'state__name').get(name='Troy Buswell') + + self.assertEquals(troy.name, u'Troy Buswell') + self.assertEquals(troy.value, 42) + self.assertEquals(troy.state.name, u'Western Australia') + + # Also works if you use only, rather than defer + troy = SpecialClient.objects.select_related('state').only('name').get(name='Troy Buswell') + + self.assertEquals(troy.name, u'Troy Buswell') + self.assertEquals(troy.value, 42) + self.assertEquals(troy.state.name, u'Western Australia') diff --git a/parts/django/tests/regressiontests/serializers_regress/__init__.py b/parts/django/tests/regressiontests/serializers_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/serializers_regress/__init__.py diff --git a/parts/django/tests/regressiontests/serializers_regress/models.py b/parts/django/tests/regressiontests/serializers_regress/models.py new file mode 100644 index 0000000..bec0a98 --- /dev/null +++ b/parts/django/tests/regressiontests/serializers_regress/models.py @@ -0,0 +1,266 @@ +""" +A test spanning all the capabilities of all the serializers. + +This class sets up a model for each model field type +(except for image types, because of the PIL dependency). +""" + +from django.db import models +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType +from django.contrib.localflavor.us.models import USStateField, PhoneNumberField + +# The following classes are for testing basic data +# marshalling, including NULL values, where allowed. + +class BooleanData(models.Model): + data = models.BooleanField() + +class CharData(models.Model): + data = models.CharField(max_length=30, null=True) + +class DateData(models.Model): + data = models.DateField(null=True) + +class DateTimeData(models.Model): + data = models.DateTimeField(null=True) + +class DecimalData(models.Model): + data = models.DecimalField(null=True, decimal_places=3, max_digits=5) + +class EmailData(models.Model): + data = models.EmailField(null=True) + +class FileData(models.Model): + data = models.FileField(null=True, upload_to='/foo/bar') + +class FilePathData(models.Model): + data = models.FilePathField(null=True) + +class FloatData(models.Model): + data = models.FloatField(null=True) + +class IntegerData(models.Model): + data = models.IntegerField(null=True) + +class BigIntegerData(models.Model): + data = models.BigIntegerField(null=True) + +# class ImageData(models.Model): +# data = models.ImageField(null=True) + +class IPAddressData(models.Model): + data = models.IPAddressField(null=True) + +class NullBooleanData(models.Model): + data = models.NullBooleanField(null=True) + +class PhoneData(models.Model): + data = PhoneNumberField(null=True) + +class PositiveIntegerData(models.Model): + data = models.PositiveIntegerField(null=True) + +class PositiveSmallIntegerData(models.Model): + data = models.PositiveSmallIntegerField(null=True) + +class SlugData(models.Model): + data = models.SlugField(null=True) + +class SmallData(models.Model): + data = models.SmallIntegerField(null=True) + +class TextData(models.Model): + data = models.TextField(null=True) + +class TimeData(models.Model): + data = models.TimeField(null=True) + +class USStateData(models.Model): + data = USStateField(null=True) + +class XMLData(models.Model): + data = models.XMLField(null=True) + +class Tag(models.Model): + """A tag on an item.""" + data = models.SlugField() + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + + content_object = generic.GenericForeignKey() + + class Meta: + ordering = ["data"] + +class GenericData(models.Model): + data = models.CharField(max_length=30) + + tags = generic.GenericRelation(Tag) + +# The following test classes are all for validation +# of related objects; in particular, forward, backward, +# and self references. + +class Anchor(models.Model): + """This is a model that can be used as + something for other models to point at""" + + data = models.CharField(max_length=30) + + class Meta: + ordering = ('id',) + +class UniqueAnchor(models.Model): + """This is a model that can be used as + something for other models to point at""" + + data = models.CharField(unique=True, max_length=30) + +class FKData(models.Model): + data = models.ForeignKey(Anchor, null=True) + +class M2MData(models.Model): + data = models.ManyToManyField(Anchor, null=True) + +class O2OData(models.Model): + # One to one field can't be null here, since it is a PK. + data = models.OneToOneField(Anchor, primary_key=True) + +class FKSelfData(models.Model): + data = models.ForeignKey('self', null=True) + +class M2MSelfData(models.Model): + data = models.ManyToManyField('self', null=True, symmetrical=False) + +class FKDataToField(models.Model): + data = models.ForeignKey(UniqueAnchor, null=True, to_field='data') + +class FKDataToO2O(models.Model): + data = models.ForeignKey(O2OData, null=True) + +class M2MIntermediateData(models.Model): + data = models.ManyToManyField(Anchor, null=True, through='Intermediate') + +class Intermediate(models.Model): + left = models.ForeignKey(M2MIntermediateData) + right = models.ForeignKey(Anchor) + extra = models.CharField(max_length=30, blank=True, default="doesn't matter") + +# The following test classes are for validating the +# deserialization of objects that use a user-defined +# field as the primary key. +# Some of these data types have been commented out +# because they can't be used as a primary key on one +# or all database backends. + +class BooleanPKData(models.Model): + data = models.BooleanField(primary_key=True) + +class CharPKData(models.Model): + data = models.CharField(max_length=30, primary_key=True) + +# class DatePKData(models.Model): +# data = models.DateField(primary_key=True) + +# class DateTimePKData(models.Model): +# data = models.DateTimeField(primary_key=True) + +class DecimalPKData(models.Model): + data = models.DecimalField(primary_key=True, decimal_places=3, max_digits=5) + +class EmailPKData(models.Model): + data = models.EmailField(primary_key=True) + +# class FilePKData(models.Model): +# data = models.FileField(primary_key=True, upload_to='/foo/bar') + +class FilePathPKData(models.Model): + data = models.FilePathField(primary_key=True) + +class FloatPKData(models.Model): + data = models.FloatField(primary_key=True) + +class IntegerPKData(models.Model): + data = models.IntegerField(primary_key=True) + +# class ImagePKData(models.Model): +# data = models.ImageField(primary_key=True) + +class IPAddressPKData(models.Model): + data = models.IPAddressField(primary_key=True) + +# This is just a Boolean field with null=True, and we can't test a PK value of NULL. +# class NullBooleanPKData(models.Model): +# data = models.NullBooleanField(primary_key=True) + +class PhonePKData(models.Model): + data = PhoneNumberField(primary_key=True) + +class PositiveIntegerPKData(models.Model): + data = models.PositiveIntegerField(primary_key=True) + +class PositiveSmallIntegerPKData(models.Model): + data = models.PositiveSmallIntegerField(primary_key=True) + +class SlugPKData(models.Model): + data = models.SlugField(primary_key=True) + +class SmallPKData(models.Model): + data = models.SmallIntegerField(primary_key=True) + +# class TextPKData(models.Model): +# data = models.TextField(primary_key=True) + +# class TimePKData(models.Model): +# data = models.TimeField(primary_key=True) + +class USStatePKData(models.Model): + data = USStateField(primary_key=True) + +# class XMLPKData(models.Model): +# data = models.XMLField(primary_key=True) + +class ComplexModel(models.Model): + field1 = models.CharField(max_length=10) + field2 = models.CharField(max_length=10) + field3 = models.CharField(max_length=10) + +# Tests for handling fields with pre_save functions, or +# models with save functions that modify data +class AutoNowDateTimeData(models.Model): + data = models.DateTimeField(null=True, auto_now=True) + +class ModifyingSaveData(models.Model): + data = models.IntegerField(null=True) + + def save(self): + "A save method that modifies the data in the object" + self.data = 666 + super(ModifyingSaveData, self).save(raw) + +# Tests for serialization of models using inheritance. +# Regression for #7202, #7350 +class AbstractBaseModel(models.Model): + parent_data = models.IntegerField() + class Meta: + abstract = True + +class InheritAbstractModel(AbstractBaseModel): + child_data = models.IntegerField() + +class BaseModel(models.Model): + parent_data = models.IntegerField() + +class InheritBaseModel(BaseModel): + child_data = models.IntegerField() + +class ExplicitInheritBaseModel(BaseModel): + parent = models.OneToOneField(BaseModel) + child_data = models.IntegerField() + +class LengthModel(models.Model): + data = models.IntegerField() + + def __len__(self): + return self.data diff --git a/parts/django/tests/regressiontests/serializers_regress/tests.py b/parts/django/tests/regressiontests/serializers_regress/tests.py new file mode 100644 index 0000000..be920c6 --- /dev/null +++ b/parts/django/tests/regressiontests/serializers_regress/tests.py @@ -0,0 +1,421 @@ +""" +A test spanning all the capabilities of all the serializers. + +This class defines sample data and a dynamically generated +test case that is capable of testing the capabilities of +the serializers. This includes all valid data values, plus +forward, backwards and self references. +""" + + +import datetime +import decimal +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +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 * + +# A set of functions that can be used to recreate +# test data objects of various kinds. +# The save method is a raw base model save, to make +# sure that the data in the database matches the +# exact test case. +def data_create(pk, klass, data): + instance = klass(id=pk) + instance.data = data + models.Model.save_base(instance, raw=True) + return [instance] + +def generic_create(pk, klass, data): + instance = klass(id=pk) + instance.data = data[0] + models.Model.save_base(instance, raw=True) + for tag in data[1:]: + instance.tags.create(data=tag) + return [instance] + +def fk_create(pk, klass, data): + instance = klass(id=pk) + setattr(instance, 'data_id', data) + models.Model.save_base(instance, raw=True) + return [instance] + +def m2m_create(pk, klass, data): + instance = klass(id=pk) + models.Model.save_base(instance, raw=True) + instance.data = data + return [instance] + +def im2m_create(pk, klass, data): + instance = klass(id=pk) + models.Model.save_base(instance, raw=True) + return [instance] + +def im_create(pk, klass, data): + instance = klass(id=pk) + instance.right_id = data['right'] + instance.left_id = data['left'] + if 'extra' in data: + instance.extra = data['extra'] + models.Model.save_base(instance, raw=True) + return [instance] + +def o2o_create(pk, klass, data): + instance = klass() + instance.data_id = data + models.Model.save_base(instance, raw=True) + return [instance] + +def pk_create(pk, klass, data): + instance = klass() + instance.data = data + models.Model.save_base(instance, raw=True) + return [instance] + +def inherited_create(pk, klass, data): + instance = klass(id=pk,**data) + # This isn't a raw save because: + # 1) we're testing inheritance, not field behaviour, so none + # of the field values need to be protected. + # 2) saving the child class and having the parent created + # automatically is easier than manually creating both. + models.Model.save(instance) + created = [instance] + for klass,field in instance._meta.parents.items(): + created.append(klass.objects.get(id=pk)) + return created + +# A set of functions that can be used to compare +# test data objects of various kinds +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)) + ) + +def generic_compare(testcase, pk, klass, data): + instance = klass.objects.get(id=pk) + testcase.assertEqual(data[0], instance.data) + testcase.assertEqual(data[1:], [t.data for t in instance.tags.order_by('id')]) + +def fk_compare(testcase, pk, klass, data): + instance = klass.objects.get(id=pk) + testcase.assertEqual(data, instance.data_id) + +def m2m_compare(testcase, pk, klass, data): + instance = klass.objects.get(id=pk) + testcase.assertEqual(data, [obj.id for obj in instance.data.order_by('id')]) + +def im2m_compare(testcase, pk, klass, data): + instance = klass.objects.get(id=pk) + #actually nothing else to check, the instance just should exist + +def im_compare(testcase, pk, klass, data): + instance = klass.objects.get(id=pk) + testcase.assertEqual(data['left'], instance.left_id) + testcase.assertEqual(data['right'], instance.right_id) + if 'extra' in data: + testcase.assertEqual(data['extra'], instance.extra) + else: + testcase.assertEqual("doesn't matter", instance.extra) + +def o2o_compare(testcase, pk, klass, data): + instance = klass.objects.get(data=data) + testcase.assertEqual(data, instance.data_id) + +def pk_compare(testcase, pk, klass, data): + instance = klass.objects.get(data=data) + testcase.assertEqual(data, instance.data) + +def inherited_compare(testcase, pk, klass, data): + instance = klass.objects.get(id=pk) + for key,value in data.items(): + testcase.assertEqual(value, getattr(instance,key)) + +# Define some data types. Each data type is +# actually a pair of functions; one to create +# and one to compare objects of that type +data_obj = (data_create, data_compare) +generic_obj = (generic_create, generic_compare) +fk_obj = (fk_create, fk_compare) +m2m_obj = (m2m_create, m2m_compare) +im2m_obj = (im2m_create, im2m_compare) +im_obj = (im_create, im_compare) +o2o_obj = (o2o_create, o2o_compare) +pk_obj = (pk_create, pk_compare) +inherited_obj = (inherited_create, inherited_compare) + +test_data = [ + # Format: (data type, PK value, Model Class, data) + (data_obj, 1, BooleanData, True), + (data_obj, 2, BooleanData, False), + (data_obj, 10, CharData, "Test Char Data"), + (data_obj, 11, CharData, ""), + (data_obj, 12, CharData, "None"), + (data_obj, 13, CharData, "null"), + (data_obj, 14, CharData, "NULL"), + (data_obj, 15, CharData, None), + # (We use something that will fit into a latin1 database encoding here, + # because that is still the default used on many system setups.) + (data_obj, 16, CharData, u'\xa5'), + (data_obj, 20, DateData, datetime.date(2006,6,16)), + (data_obj, 21, DateData, None), + (data_obj, 30, DateTimeData, datetime.datetime(2006,6,16,10,42,37)), + (data_obj, 31, DateTimeData, None), + (data_obj, 40, EmailData, "hovercraft@example.com"), + (data_obj, 41, EmailData, None), + (data_obj, 42, EmailData, ""), + (data_obj, 50, FileData, 'file:///foo/bar/whiz.txt'), +# (data_obj, 51, FileData, None), + (data_obj, 52, FileData, ""), + (data_obj, 60, FilePathData, "/foo/bar/whiz.txt"), + (data_obj, 61, FilePathData, None), + (data_obj, 62, FilePathData, ""), + (data_obj, 70, DecimalData, decimal.Decimal('12.345')), + (data_obj, 71, DecimalData, decimal.Decimal('-12.345')), + (data_obj, 72, DecimalData, decimal.Decimal('0.0')), + (data_obj, 73, DecimalData, None), + (data_obj, 74, FloatData, 12.345), + (data_obj, 75, FloatData, -12.345), + (data_obj, 76, FloatData, 0.0), + (data_obj, 77, FloatData, None), + (data_obj, 80, IntegerData, 123456789), + (data_obj, 81, IntegerData, -123456789), + (data_obj, 82, IntegerData, 0), + (data_obj, 83, IntegerData, None), + #(XX, ImageData + (data_obj, 90, IPAddressData, "127.0.0.1"), + (data_obj, 91, IPAddressData, None), + (data_obj, 100, NullBooleanData, True), + (data_obj, 101, NullBooleanData, False), + (data_obj, 102, NullBooleanData, None), + (data_obj, 110, PhoneData, "212-634-5789"), + (data_obj, 111, PhoneData, None), + (data_obj, 120, PositiveIntegerData, 123456789), + (data_obj, 121, PositiveIntegerData, None), + (data_obj, 130, PositiveSmallIntegerData, 12), + (data_obj, 131, PositiveSmallIntegerData, None), + (data_obj, 140, SlugData, "this-is-a-slug"), + (data_obj, 141, SlugData, None), + (data_obj, 142, SlugData, ""), + (data_obj, 150, SmallData, 12), + (data_obj, 151, SmallData, -12), + (data_obj, 152, SmallData, 0), + (data_obj, 153, SmallData, None), + (data_obj, 160, TextData, """This is a long piece of text. +It contains line breaks. +Several of them. +The end."""), + (data_obj, 161, TextData, ""), + (data_obj, 162, TextData, None), + (data_obj, 170, TimeData, datetime.time(10,42,37)), + (data_obj, 171, TimeData, None), + (data_obj, 180, USStateData, "MA"), + (data_obj, 181, USStateData, None), + (data_obj, 182, USStateData, ""), + (data_obj, 190, XMLData, "<foo></foo>"), + (data_obj, 191, XMLData, None), + (data_obj, 192, XMLData, ""), + + (generic_obj, 200, GenericData, ['Generic Object 1', 'tag1', 'tag2']), + (generic_obj, 201, GenericData, ['Generic Object 2', 'tag2', 'tag3']), + + (data_obj, 300, Anchor, "Anchor 1"), + (data_obj, 301, Anchor, "Anchor 2"), + (data_obj, 302, UniqueAnchor, "UAnchor 1"), + + (fk_obj, 400, FKData, 300), # Post reference + (fk_obj, 401, FKData, 500), # Pre reference + (fk_obj, 402, FKData, None), # Empty reference + + (m2m_obj, 410, M2MData, []), # Empty set + (m2m_obj, 411, M2MData, [300,301]), # Post reference + (m2m_obj, 412, M2MData, [500,501]), # Pre reference + (m2m_obj, 413, M2MData, [300,301,500,501]), # Pre and Post reference + + (o2o_obj, None, O2OData, 300), # Post reference + (o2o_obj, None, O2OData, 500), # Pre reference + + (fk_obj, 430, FKSelfData, 431), # Pre reference + (fk_obj, 431, FKSelfData, 430), # Post reference + (fk_obj, 432, FKSelfData, None), # Empty reference + + (m2m_obj, 440, M2MSelfData, []), + (m2m_obj, 441, M2MSelfData, []), + (m2m_obj, 442, M2MSelfData, [440, 441]), + (m2m_obj, 443, M2MSelfData, [445, 446]), + (m2m_obj, 444, M2MSelfData, [440, 441, 445, 446]), + (m2m_obj, 445, M2MSelfData, []), + (m2m_obj, 446, M2MSelfData, []), + + (fk_obj, 450, FKDataToField, "UAnchor 1"), + (fk_obj, 451, FKDataToField, "UAnchor 2"), + (fk_obj, 452, FKDataToField, None), + + (fk_obj, 460, FKDataToO2O, 300), + + (im2m_obj, 470, M2MIntermediateData, None), + + #testing post- and prereferences and extra fields + (im_obj, 480, Intermediate, {'right': 300, 'left': 470}), + (im_obj, 481, Intermediate, {'right': 300, 'left': 490}), + (im_obj, 482, Intermediate, {'right': 500, 'left': 470}), + (im_obj, 483, Intermediate, {'right': 500, 'left': 490}), + (im_obj, 484, Intermediate, {'right': 300, 'left': 470, 'extra': "extra"}), + (im_obj, 485, Intermediate, {'right': 300, 'left': 490, 'extra': "extra"}), + (im_obj, 486, Intermediate, {'right': 500, 'left': 470, 'extra': "extra"}), + (im_obj, 487, Intermediate, {'right': 500, 'left': 490, 'extra': "extra"}), + + (im2m_obj, 490, M2MIntermediateData, []), + + (data_obj, 500, Anchor, "Anchor 3"), + (data_obj, 501, Anchor, "Anchor 4"), + (data_obj, 502, UniqueAnchor, "UAnchor 2"), + + (pk_obj, 601, BooleanPKData, True), + (pk_obj, 602, BooleanPKData, False), + (pk_obj, 610, CharPKData, "Test Char PKData"), +# (pk_obj, 620, DatePKData, datetime.date(2006,6,16)), +# (pk_obj, 630, DateTimePKData, datetime.datetime(2006,6,16,10,42,37)), + (pk_obj, 640, EmailPKData, "hovercraft@example.com"), +# (pk_obj, 650, FilePKData, 'file:///foo/bar/whiz.txt'), + (pk_obj, 660, FilePathPKData, "/foo/bar/whiz.txt"), + (pk_obj, 670, DecimalPKData, decimal.Decimal('12.345')), + (pk_obj, 671, DecimalPKData, decimal.Decimal('-12.345')), + (pk_obj, 672, DecimalPKData, decimal.Decimal('0.0')), + (pk_obj, 673, FloatPKData, 12.345), + (pk_obj, 674, FloatPKData, -12.345), + (pk_obj, 675, FloatPKData, 0.0), + (pk_obj, 680, IntegerPKData, 123456789), + (pk_obj, 681, IntegerPKData, -123456789), + (pk_obj, 682, IntegerPKData, 0), +# (XX, ImagePKData + (pk_obj, 690, IPAddressPKData, "127.0.0.1"), + # (pk_obj, 700, NullBooleanPKData, True), + # (pk_obj, 701, NullBooleanPKData, False), + (pk_obj, 710, PhonePKData, "212-634-5789"), + (pk_obj, 720, PositiveIntegerPKData, 123456789), + (pk_obj, 730, PositiveSmallIntegerPKData, 12), + (pk_obj, 740, SlugPKData, "this-is-a-slug"), + (pk_obj, 750, SmallPKData, 12), + (pk_obj, 751, SmallPKData, -12), + (pk_obj, 752, SmallPKData, 0), +# (pk_obj, 760, TextPKData, """This is a long piece of text. +# It contains line breaks. +# Several of them. +# The end."""), +# (pk_obj, 770, TimePKData, datetime.time(10,42,37)), + (pk_obj, 780, USStatePKData, "MA"), +# (pk_obj, 790, XMLPKData, "<foo></foo>"), + + (data_obj, 800, AutoNowDateTimeData, datetime.datetime(2006,6,16,10,42,37)), + (data_obj, 810, ModifyingSaveData, 42), + + (inherited_obj, 900, InheritAbstractModel, {'child_data':37,'parent_data':42}), + (inherited_obj, 910, ExplicitInheritBaseModel, {'child_data':37,'parent_data':42}), + (inherited_obj, 920, InheritBaseModel, {'child_data':37,'parent_data':42}), + + (data_obj, 1000, BigIntegerData, 9223372036854775807), + (data_obj, 1001, BigIntegerData, -9223372036854775808), + (data_obj, 1002, BigIntegerData, 0), + (data_obj, 1003, BigIntegerData, None), + (data_obj, 1004, LengthModel, 0), + (data_obj, 1005, LengthModel, 1), +] + +# Because Oracle treats the empty string as NULL, Oracle is expected to fail +# when field.empty_strings_allowed is True and the value is None; skip these +# tests. +if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.oracle': + test_data = [data for data in test_data + if not (data[0] == data_obj and + data[2]._meta.get_field('data').empty_strings_allowed and + data[3] is None)] + +# Regression test for #8651 -- a FK to an object iwth PK of 0 +# This won't work on MySQL since it won't let you create an object +# with a primary key of 0, +if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.mysql': + test_data.extend([ + (data_obj, 0, Anchor, "Anchor 0"), + (fk_obj, 465, FKData, 0), + ]) + +# Dynamically create serializer tests to ensure that all +# registered serializers are automatically tested. +class SerializerTests(TestCase): + pass + +def serializerTest(format, self): + + # Create all the objects defined in the test data + objects = [] + instance_count = {} + 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: + instance_count[klass] = klass.objects.count() + + # Add the generic tagged objects to the object list + objects.extend(Tag.objects.all()) + + # Serialize the test database + serialized_data = serializers.serialize(format, objects, indent=2) + + for obj in serializers.deserialize(format, serialized_data): + obj.save() + + # Assert that the deserialized data is the same + # as the original source + for (func, pk, klass, datum) in test_data: + func[1](self, pk, klass, datum) + + # Assert that the number of objects deserialized is the + # same as the number that was serialized. + for klass, count in instance_count.items(): + self.assertEquals(count, klass.objects.count()) + +def fieldsTest(format, self): + obj = ComplexModel(field1='first', field2='second', field3='third') + obj.save_base(raw=True) + + # Serialize then deserialize the test database + serialized_data = serializers.serialize(format, [obj], indent=2, fields=('field1','field3')) + result = serializers.deserialize(format, serialized_data).next() + + # Check that the deserialized object contains data in only the serialized fields. + self.assertEqual(result.object.field1, 'first') + self.assertEqual(result.object.field2, '') + self.assertEqual(result.object.field3, 'third') + +def streamTest(format, self): + obj = ComplexModel(field1='first',field2='second',field3='third') + obj.save_base(raw=True) + + # Serialize the test database to a stream + stream = StringIO() + serializers.serialize(format, [obj], indent=2, stream=stream) + + # Serialize normally for a comparison + string_data = serializers.serialize(format, [obj], indent=2) + + # Check that the two are the same + self.assertEqual(string_data, stream.getvalue()) + 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)) + if format != 'python': + setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format)) diff --git a/parts/django/tests/regressiontests/servers/__init__.py b/parts/django/tests/regressiontests/servers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/servers/__init__.py diff --git a/parts/django/tests/regressiontests/servers/models.py b/parts/django/tests/regressiontests/servers/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/servers/models.py diff --git a/parts/django/tests/regressiontests/servers/tests.py b/parts/django/tests/regressiontests/servers/tests.py new file mode 100644 index 0000000..4763982 --- /dev/null +++ b/parts/django/tests/regressiontests/servers/tests.py @@ -0,0 +1,68 @@ +""" +Tests for django.core.servers. +""" + +import os + +import django +from django.test import TestCase +from django.core.handlers.wsgi import WSGIHandler +from django.core.servers.basehttp import AdminMediaHandler + + +class AdminMediaHandlerTests(TestCase): + + def setUp(self): + self.admin_media_file_path = os.path.abspath( + os.path.join(django.__path__[0], 'contrib', 'admin', 'media') + ) + self.handler = AdminMediaHandler(WSGIHandler()) + + def test_media_urls(self): + """ + Tests that URLs that look like absolute file paths after the + settings.ADMIN_MEDIA_PREFIX don't turn into absolute file paths. + """ + # Cases that should work on all platforms. + data = ( + ('/media/css/base.css', ('css', 'base.css')), + ) + # Cases that should raise an exception. + bad_data = () + + # Add platform-specific cases. + if os.sep == '/': + data += ( + # URL, tuple of relative path parts. + ('/media/\\css/base.css', ('\\css', 'base.css')), + ) + bad_data += ( + '/media//css/base.css', + '/media////css/base.css', + '/media/../css/base.css', + ) + elif os.sep == '\\': + bad_data += ( + '/media/C:\css/base.css', + '/media//\\css/base.css', + '/media/\\css/base.css', + '/media/\\\\css/base.css' + ) + for url, path_tuple in data: + try: + output = self.handler.file_path(url) + except ValueError: + self.fail("Got a ValueError exception, but wasn't expecting" + " one. URL was: %s" % url) + rel_path = os.path.join(*path_tuple) + desired = os.path.normcase( + os.path.join(self.admin_media_file_path, rel_path)) + self.assertEqual(output, desired, + "Got: %s, Expected: %s, URL was: %s" % (output, desired, url)) + for url in bad_data: + try: + output = self.handler.file_path(url) + except ValueError: + continue + self.fail('URL: %s should have caused a ValueError exception.' + % url) diff --git a/parts/django/tests/regressiontests/settings_tests/__init__.py b/parts/django/tests/regressiontests/settings_tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/settings_tests/__init__.py diff --git a/parts/django/tests/regressiontests/settings_tests/models.py b/parts/django/tests/regressiontests/settings_tests/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/settings_tests/models.py diff --git a/parts/django/tests/regressiontests/settings_tests/tests.py b/parts/django/tests/regressiontests/settings_tests/tests.py new file mode 100644 index 0000000..fa217b1 --- /dev/null +++ b/parts/django/tests/regressiontests/settings_tests/tests.py @@ -0,0 +1,17 @@ +import unittest +from django.conf import settings + +class SettingsTests(unittest.TestCase): + + # + # Regression tests for #10130: deleting settings. + # + + def test_settings_delete(self): + settings.TEST = 'test' + self.assertEqual('test', settings.TEST) + del settings.TEST + self.assertRaises(AttributeError, getattr, settings, 'TEST') + + def test_settings_delete_wrapped(self): + self.assertRaises(TypeError, delattr, settings, '_wrapped') diff --git a/parts/django/tests/regressiontests/signals_regress/__init__.py b/parts/django/tests/regressiontests/signals_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/signals_regress/__init__.py diff --git a/parts/django/tests/regressiontests/signals_regress/models.py b/parts/django/tests/regressiontests/signals_regress/models.py new file mode 100644 index 0000000..e7879d8 --- /dev/null +++ b/parts/django/tests/regressiontests/signals_regress/models.py @@ -0,0 +1,14 @@ +from django.db import models + +class Author(models.Model): + name = models.CharField(max_length=20) + + def __unicode__(self): + return self.name + +class Book(models.Model): + name = models.CharField(max_length=20) + authors = models.ManyToManyField(Author) + + def __unicode__(self): + return self.name diff --git a/parts/django/tests/regressiontests/signals_regress/tests.py b/parts/django/tests/regressiontests/signals_regress/tests.py new file mode 100644 index 0000000..234893f --- /dev/null +++ b/parts/django/tests/regressiontests/signals_regress/tests.py @@ -0,0 +1,96 @@ +import sys +from StringIO import StringIO +from django.test import TestCase + +from django.db import models +from regressiontests.signals_regress.models import Author, Book + +signal_output = [] + +def pre_save_test(signal, sender, instance, **kwargs): + signal_output.append('pre_save signal, %s' % instance) + if kwargs.get('raw'): + signal_output.append('Is raw') + +def post_save_test(signal, sender, instance, **kwargs): + signal_output.append('post_save signal, %s' % instance) + if 'created' in kwargs: + if kwargs['created']: + signal_output.append('Is created') + else: + signal_output.append('Is updated') + if kwargs.get('raw'): + signal_output.append('Is raw') + +def pre_delete_test(signal, sender, instance, **kwargs): + signal_output.append('pre_save signal, %s' % instance) + signal_output.append('instance.id is not None: %s' % (instance.id != None)) + +def post_delete_test(signal, sender, instance, **kwargs): + signal_output.append('post_delete signal, %s' % instance) + signal_output.append('instance.id is not None: %s' % (instance.id != None)) + +class SignalsRegressTests(TestCase): + """ + Testing signals before/after saving and deleting. + """ + + def get_signal_output(self, fn, *args, **kwargs): + # Flush any existing signal output + global signal_output + signal_output = [] + fn(*args, **kwargs) + return signal_output + + def setUp(self): + # Save up the number of connected signals so that we can check at the end + # that all the signals we register get properly unregistered (#9989) + self.pre_signals = (len(models.signals.pre_save.receivers), + len(models.signals.post_save.receivers), + len(models.signals.pre_delete.receivers), + len(models.signals.post_delete.receivers)) + + models.signals.pre_save.connect(pre_save_test) + models.signals.post_save.connect(post_save_test) + models.signals.pre_delete.connect(pre_delete_test) + models.signals.post_delete.connect(post_delete_test) + + def tearDown(self): + models.signals.post_delete.disconnect(post_delete_test) + models.signals.pre_delete.disconnect(pre_delete_test) + models.signals.post_save.disconnect(post_save_test) + models.signals.pre_save.disconnect(pre_save_test) + + # Check that all our signals got disconnected properly. + post_signals = (len(models.signals.pre_save.receivers), + len(models.signals.post_save.receivers), + len(models.signals.pre_delete.receivers), + len(models.signals.post_delete.receivers)) + + self.assertEquals(self.pre_signals, post_signals) + + def test_model_signals(self): + """ Model saves should throw some signals. """ + a1 = Author(name='Neal Stephenson') + self.assertEquals(self.get_signal_output(a1.save), [ + "pre_save signal, Neal Stephenson", + "post_save signal, Neal Stephenson", + "Is created" + ]) + + b1 = Book(name='Snow Crash') + self.assertEquals(self.get_signal_output(b1.save), [ + "pre_save signal, Snow Crash", + "post_save signal, Snow Crash", + "Is created" + ]) + + def test_m2m_signals(self): + """ Assigning and removing to/from m2m shouldn't generate an m2m signal """ + + b1 = Book(name='Snow Crash') + self.get_signal_output(b1.save) + a1 = Author(name='Neal Stephenson') + self.get_signal_output(a1.save) + self.assertEquals(self.get_signal_output(setattr, b1, 'authors', [a1]), []) + self.assertEquals(self.get_signal_output(setattr, b1, 'authors', []), []) diff --git a/parts/django/tests/regressiontests/sites_framework/__init__.py b/parts/django/tests/regressiontests/sites_framework/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/sites_framework/__init__.py diff --git a/parts/django/tests/regressiontests/sites_framework/models.py b/parts/django/tests/regressiontests/sites_framework/models.py new file mode 100644 index 0000000..9ecc3e6 --- /dev/null +++ b/parts/django/tests/regressiontests/sites_framework/models.py @@ -0,0 +1,36 @@ +from django.contrib.sites.managers import CurrentSiteManager +from django.contrib.sites.models import Site +from django.db import models + +class AbstractArticle(models.Model): + title = models.CharField(max_length=50) + + objects = models.Manager() + on_site = CurrentSiteManager() + + class Meta: + abstract = True + + def __unicode__(self): + return self.title + +class SyndicatedArticle(AbstractArticle): + sites = models.ManyToManyField(Site) + +class ExclusiveArticle(AbstractArticle): + site = models.ForeignKey(Site) + +class CustomArticle(AbstractArticle): + places_this_article_should_appear = models.ForeignKey(Site) + + objects = models.Manager() + on_site = CurrentSiteManager("places_this_article_should_appear") + +class InvalidArticle(AbstractArticle): + site = models.ForeignKey(Site) + + objects = models.Manager() + on_site = CurrentSiteManager("places_this_article_should_appear") + +class ConfusedArticle(AbstractArticle): + site = models.IntegerField() diff --git a/parts/django/tests/regressiontests/sites_framework/tests.py b/parts/django/tests/regressiontests/sites_framework/tests.py new file mode 100644 index 0000000..b737727 --- /dev/null +++ b/parts/django/tests/regressiontests/sites_framework/tests.py @@ -0,0 +1,34 @@ +from django.conf import settings +from django.contrib.sites.models import Site +from django.test import TestCase + +from models import SyndicatedArticle, ExclusiveArticle, CustomArticle, InvalidArticle, ConfusedArticle + +class SitesFrameworkTestCase(TestCase): + def setUp(self): + Site.objects.get_or_create(id=settings.SITE_ID, domain="example.com", name="example.com") + Site.objects.create(id=settings.SITE_ID+1, domain="example2.com", name="example2.com") + + def test_site_fk(self): + article = ExclusiveArticle.objects.create(title="Breaking News!", site_id=settings.SITE_ID) + self.assertEqual(ExclusiveArticle.on_site.all().get(), article) + + def test_sites_m2m(self): + article = SyndicatedArticle.objects.create(title="Fresh News!") + article.sites.add(Site.objects.get(id=settings.SITE_ID)) + article.sites.add(Site.objects.get(id=settings.SITE_ID+1)) + article2 = SyndicatedArticle.objects.create(title="More News!") + article2.sites.add(Site.objects.get(id=settings.SITE_ID+1)) + self.assertEqual(SyndicatedArticle.on_site.all().get(), article) + + def test_custom_named_field(self): + article = CustomArticle.objects.create(title="Tantalizing News!", places_this_article_should_appear_id=settings.SITE_ID) + self.assertEqual(CustomArticle.on_site.all().get(), article) + + def test_invalid_name(self): + article = InvalidArticle.objects.create(title="Bad News!", site_id=settings.SITE_ID) + self.assertRaises(ValueError, InvalidArticle.on_site.all) + + def test_invalid_field_type(self): + article = ConfusedArticle.objects.create(title="More Bad News!", site=settings.SITE_ID) + self.assertRaises(TypeError, ConfusedArticle.on_site.all) diff --git a/parts/django/tests/regressiontests/special_headers/__init__.py b/parts/django/tests/regressiontests/special_headers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/special_headers/__init__.py diff --git a/parts/django/tests/regressiontests/special_headers/fixtures/data.xml b/parts/django/tests/regressiontests/special_headers/fixtures/data.xml new file mode 100644 index 0000000..7e60d45 --- /dev/null +++ b/parts/django/tests/regressiontests/special_headers/fixtures/data.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="100" model="auth.user"> + <field type="CharField" name="username">super</field> + <field type="CharField" name="first_name">Super</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">super@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">True</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="1" model="special_headers.article"> + <field type="TextField" name="text">text</field> + </object> +</django-objects> diff --git a/parts/django/tests/regressiontests/special_headers/models.py b/parts/django/tests/regressiontests/special_headers/models.py new file mode 100644 index 0000000..0c12675 --- /dev/null +++ b/parts/django/tests/regressiontests/special_headers/models.py @@ -0,0 +1,4 @@ +from django.db import models + +class Article(models.Model): + text = models.TextField() diff --git a/parts/django/tests/regressiontests/special_headers/templates/special_headers/article_detail.html b/parts/django/tests/regressiontests/special_headers/templates/special_headers/article_detail.html new file mode 100644 index 0000000..3cbd38c --- /dev/null +++ b/parts/django/tests/regressiontests/special_headers/templates/special_headers/article_detail.html @@ -0,0 +1 @@ +{{ object }} diff --git a/parts/django/tests/regressiontests/special_headers/tests.py b/parts/django/tests/regressiontests/special_headers/tests.py new file mode 100644 index 0000000..f304bfa --- /dev/null +++ b/parts/django/tests/regressiontests/special_headers/tests.py @@ -0,0 +1,40 @@ +from django.test import TestCase +from django.contrib.auth.models import User + +class SpecialHeadersTest(TestCase): + fixtures = ['data.xml'] + + def test_xheaders(self): + user = User.objects.get(username='super') + response = self.client.get('/special_headers/article/1/') + # import pdb; pdb.set_trace() + self.failUnless('X-Object-Type' not in response) + self.client.login(username='super', password='secret') + response = self.client.get('/special_headers/article/1/') + self.failUnless('X-Object-Type' in response) + user.is_staff = False + user.save() + response = self.client.get('/special_headers/article/1/') + self.failUnless('X-Object-Type' not in response) + user.is_staff = True + user.is_active = False + user.save() + response = self.client.get('/special_headers/article/1/') + self.failUnless('X-Object-Type' not in response) + + def test_xview(self): + user = User.objects.get(username='super') + response = self.client.head('/special_headers/xview/') + self.failUnless('X-View' not in response) + self.client.login(username='super', password='secret') + response = self.client.head('/special_headers/xview/') + self.failUnless('X-View' in response) + user.is_staff = False + user.save() + response = self.client.head('/special_headers/xview/') + self.failUnless('X-View' not in response) + user.is_staff = True + user.is_active = False + user.save() + response = self.client.head('/special_headers/xview/') + self.failUnless('X-View' not in response) diff --git a/parts/django/tests/regressiontests/special_headers/urls.py b/parts/django/tests/regressiontests/special_headers/urls.py new file mode 100644 index 0000000..721f60a --- /dev/null +++ b/parts/django/tests/regressiontests/special_headers/urls.py @@ -0,0 +1,10 @@ +# coding: utf-8 +from django.conf.urls.defaults import * +from django.views.generic.list_detail import object_detail +from models import Article +import views + +urlpatterns = patterns('', + (r'^article/(?P<object_id>\d+)/$', object_detail, {'queryset': Article.objects.all()}), + (r'^xview/$', views.xview), +) diff --git a/parts/django/tests/regressiontests/special_headers/views.py b/parts/django/tests/regressiontests/special_headers/views.py new file mode 100644 index 0000000..7a01203 --- /dev/null +++ b/parts/django/tests/regressiontests/special_headers/views.py @@ -0,0 +1,10 @@ +# -*- coding:utf-8 -*- +from django.http import HttpResponse +from django.utils.decorators import decorator_from_middleware +from django.middleware.doc import XViewMiddleware + +xview_dec = decorator_from_middleware(XViewMiddleware) + +def xview(request): + return HttpResponse() +xview = xview_dec(xview) diff --git a/parts/django/tests/regressiontests/string_lookup/__init__.py b/parts/django/tests/regressiontests/string_lookup/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/string_lookup/__init__.py diff --git a/parts/django/tests/regressiontests/string_lookup/models.py b/parts/django/tests/regressiontests/string_lookup/models.py new file mode 100644 index 0000000..037854d --- /dev/null +++ b/parts/django/tests/regressiontests/string_lookup/models.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from django.db import models + +class Foo(models.Model): + name = models.CharField(max_length=50) + friend = models.CharField(max_length=50, blank=True) + + def __unicode__(self): + return "Foo %s" % self.name + +class Bar(models.Model): + name = models.CharField(max_length=50) + normal = models.ForeignKey(Foo, related_name='normal_foo') + fwd = models.ForeignKey("Whiz") + back = models.ForeignKey("Foo") + + def __unicode__(self): + return "Bar %s" % self.place.name + +class Whiz(models.Model): + name = models.CharField(max_length=50) + + def __unicode__(self): + return "Whiz %s" % self.name + +class Child(models.Model): + parent = models.OneToOneField('Base') + name = models.CharField(max_length=50) + + def __unicode__(self): + return "Child %s" % self.name + +class Base(models.Model): + name = models.CharField(max_length=50) + + def __unicode__(self): + return "Base %s" % self.name + +class Article(models.Model): + name = models.CharField(max_length=50) + text = models.TextField() + submitted_from = models.IPAddressField(blank=True, null=True) + + def __str__(self): + return "Article %s" % self.name diff --git a/parts/django/tests/regressiontests/string_lookup/tests.py b/parts/django/tests/regressiontests/string_lookup/tests.py new file mode 100644 index 0000000..ddf7a8a --- /dev/null +++ b/parts/django/tests/regressiontests/string_lookup/tests.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +from django.test import TestCase +from regressiontests.string_lookup.models import Foo, Whiz, Bar, Article, Base, Child + +class StringLookupTests(TestCase): + + def test_string_form_referencing(self): + """ + Regression test for #1661 and #1662 + + Check that string form referencing of + models works, both as pre and post reference, on all RelatedField types. + """ + + f1 = Foo(name="Foo1") + f1.save() + f2 = Foo(name="Foo2") + f2.save() + + w1 = Whiz(name="Whiz1") + w1.save() + + b1 = Bar(name="Bar1", normal=f1, fwd=w1, back=f2) + b1.save() + + self.assertEquals(b1.normal, f1) + + self.assertEquals(b1.fwd, w1) + + self.assertEquals(b1.back, f2) + + base1 = Base(name="Base1") + base1.save() + + child1 = Child(name="Child1", parent=base1) + child1.save() + + self.assertEquals(child1.parent, base1) + + def test_unicode_chars_in_queries(self): + """ + Regression tests for #3937 + + make sure we can use unicode characters in queries. + If these tests fail on MySQL, it's a problem with the test setup. + A properly configured UTF-8 database can handle this. + """ + + fx = Foo(name='Bjorn', friend=u'François') + fx.save() + self.assertEquals(Foo.objects.get(friend__contains=u'\xe7'), fx) + + # We can also do the above query using UTF-8 strings. + self.assertEquals(Foo.objects.get(friend__contains='\xc3\xa7'), fx) + + def test_queries_on_textfields(self): + """ + Regression tests for #5087 + + make sure we can perform queries on TextFields. + """ + + a = Article(name='Test', text='The quick brown fox jumps over the lazy dog.') + a.save() + self.assertEquals(Article.objects.get(text__exact='The quick brown fox jumps over the lazy dog.'), a) + + self.assertEquals(Article.objects.get(text__contains='quick brown fox'), a) + + def test_ipaddress_on_postgresql(self): + """ + Regression test for #708 + + "like" queries on IP address fields require casting to text (on PostgreSQL). + """ + a = Article(name='IP test', text='The body', submitted_from='192.0.2.100') + a.save() + self.assertEquals(repr(Article.objects.filter(submitted_from__contains='192.0.2')), + repr([a])) diff --git a/parts/django/tests/regressiontests/syndication/__init__.py b/parts/django/tests/regressiontests/syndication/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/syndication/__init__.py diff --git a/parts/django/tests/regressiontests/syndication/feeds.py b/parts/django/tests/regressiontests/syndication/feeds.py new file mode 100644 index 0000000..5563170 --- /dev/null +++ b/parts/django/tests/regressiontests/syndication/feeds.py @@ -0,0 +1,142 @@ +from django.contrib.syndication import feeds, views +from django.core.exceptions import ObjectDoesNotExist +from django.utils import feedgenerator, tzinfo +from models import Article, Entry + + +class ComplexFeed(views.Feed): + def get_object(self, request, foo=None): + if foo is not None: + raise ObjectDoesNotExist + return None + + +class TestRss2Feed(views.Feed): + title = 'My blog' + description = 'A more thorough description of my blog.' + link = '/blog/' + feed_guid = '/foo/bar/1234' + author_name = 'Sally Smith' + author_email = 'test@example.com' + author_link = 'http://www.example.com/' + categories = ('python', 'django') + feed_copyright = 'Copyright (c) 2007, Sally Smith' + ttl = 600 + + def items(self): + return Entry.objects.all() + + def item_description(self, item): + return "Overridden description: %s" % item + + def item_pubdate(self, item): + return item.date + + item_author_name = 'Sally Smith' + item_author_email = 'test@example.com' + item_author_link = 'http://www.example.com/' + item_categories = ('python', 'testing') + item_copyright = 'Copyright (c) 2007, Sally Smith' + + +class TestRss091Feed(TestRss2Feed): + feed_type = feedgenerator.RssUserland091Feed + + +class TestAtomFeed(TestRss2Feed): + feed_type = feedgenerator.Atom1Feed + subtitle = TestRss2Feed.description + + +class ArticlesFeed(TestRss2Feed): + """ + A feed to test no link being defined. Articles have no get_absolute_url() + method, and item_link() is not defined. + """ + def items(self): + return Article.objects.all() + + +class TestEnclosureFeed(TestRss2Feed): + pass + + +class TemplateFeed(TestRss2Feed): + """ + A feed to test defining item titles and descriptions with templates. + """ + title_template = 'syndication/title.html' + description_template = 'syndication/description.html' + + # Defining a template overrides any item_title definition + def item_title(self): + return "Not in a template" + + +class NaiveDatesFeed(TestAtomFeed): + """ + A feed with naive (non-timezone-aware) dates. + """ + def item_pubdate(self, item): + return item.date + + +class TZAwareDatesFeed(TestAtomFeed): + """ + A feed with timezone-aware dates. + """ + def item_pubdate(self, item): + # Provide a weird offset so that the test can know it's getting this + # specific offset and not accidentally getting on from + # settings.TIME_ZONE. + return item.date.replace(tzinfo=tzinfo.FixedOffset(42)) + + +class TestFeedUrlFeed(TestAtomFeed): + feed_url = 'http://example.com/customfeedurl/' + + +class MyCustomAtom1Feed(feedgenerator.Atom1Feed): + """ + Test of a custom feed generator class. + """ + def root_attributes(self): + attrs = super(MyCustomAtom1Feed, self).root_attributes() + attrs[u'django'] = u'rocks' + return attrs + + def add_root_elements(self, handler): + super(MyCustomAtom1Feed, self).add_root_elements(handler) + handler.addQuickElement(u'spam', u'eggs') + + def item_attributes(self, item): + attrs = super(MyCustomAtom1Feed, self).item_attributes(item) + attrs[u'bacon'] = u'yum' + return attrs + + def add_item_elements(self, handler, item): + super(MyCustomAtom1Feed, self).add_item_elements(handler, item) + handler.addQuickElement(u'ministry', u'silly walks') + + +class TestCustomFeed(TestAtomFeed): + feed_type = MyCustomAtom1Feed + + +class DeprecatedComplexFeed(feeds.Feed): + def get_object(self, bits): + if len(bits) != 1: + raise ObjectDoesNotExist + return None + + +class DeprecatedRssFeed(feeds.Feed): + link = "/blog/" + title = 'My blog' + + def items(self): + return Entry.objects.all() + + def item_link(self, item): + return "/blog/%s/" % item.pk + diff --git a/parts/django/tests/regressiontests/syndication/fixtures/feeddata.json b/parts/django/tests/regressiontests/syndication/fixtures/feeddata.json new file mode 100644 index 0000000..4a5c022 --- /dev/null +++ b/parts/django/tests/regressiontests/syndication/fixtures/feeddata.json @@ -0,0 +1,42 @@ +[ + { + "model": "syndication.entry", + "pk": 1, + "fields": { + "title": "My first entry", + "date": "2008-01-01 12:30:00" + } + }, + { + "model": "syndication.entry", + "pk": 2, + "fields": { + "title": "My second entry", + "date": "2008-01-02 12:30:00" + } + }, + { + "model": "syndication.entry", + "pk": 3, + "fields": { + "title": "My third entry", + "date": "2008-01-02 13:30:00" + } + }, + { + "model": "syndication.entry", + "pk": 4, + "fields": { + "title": "A & B < C > D", + "date": "2008-01-03 13:30:00" + } + }, + { + "model": "syndication.article", + "pk": 1, + "fields": { + "title": "My first article", + "entry": "1" + } + } +] diff --git a/parts/django/tests/regressiontests/syndication/models.py b/parts/django/tests/regressiontests/syndication/models.py new file mode 100644 index 0000000..54230b9 --- /dev/null +++ b/parts/django/tests/regressiontests/syndication/models.py @@ -0,0 +1,23 @@ +from django.db import models + +class Entry(models.Model): + title = models.CharField(max_length=200) + date = models.DateTimeField() + + class Meta: + ordering = ('date',) + + def __unicode__(self): + return self.title + + def get_absolute_url(self): + return "/blog/%s/" % self.pk + + +class Article(models.Model): + title = models.CharField(max_length=200) + entry = models.ForeignKey(Entry) + + def __unicode__(self): + return self.title + diff --git a/parts/django/tests/regressiontests/syndication/templates/syndication/description.html b/parts/django/tests/regressiontests/syndication/templates/syndication/description.html new file mode 100644 index 0000000..85ec82c --- /dev/null +++ b/parts/django/tests/regressiontests/syndication/templates/syndication/description.html @@ -0,0 +1 @@ +Description in your templates: {{ obj }}
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/syndication/templates/syndication/title.html b/parts/django/tests/regressiontests/syndication/templates/syndication/title.html new file mode 100644 index 0000000..eb17969 --- /dev/null +++ b/parts/django/tests/regressiontests/syndication/templates/syndication/title.html @@ -0,0 +1 @@ +Title in your templates: {{ obj }}
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/syndication/tests.py b/parts/django/tests/regressiontests/syndication/tests.py new file mode 100644 index 0000000..76a6c88 --- /dev/null +++ b/parts/django/tests/regressiontests/syndication/tests.py @@ -0,0 +1,356 @@ +import datetime +from django.contrib.syndication import feeds, views +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase +from django.utils import tzinfo +from django.utils.feedgenerator import rfc2822_date, rfc3339_date +from models import Entry +from xml.dom import minidom + +try: + set +except NameError: + from sets import Set as set + +class FeedTestCase(TestCase): + fixtures = ['feeddata.json'] + + def assertChildNodes(self, elem, expected): + actual = set([n.nodeName for n in elem.childNodes]) + expected = set(expected) + self.assertEqual(actual, expected) + + def assertChildNodeContent(self, elem, expected): + for k, v in expected.items(): + self.assertEqual( + elem.getElementsByTagName(k)[0].firstChild.wholeText, v) + + def assertCategories(self, elem, expected): + self.assertEqual(set(i.firstChild.wholeText for i in elem.childNodes if i.nodeName == 'category'), set(expected)); + +###################################### +# Feed view +###################################### + +class SyndicationFeedTest(FeedTestCase): + """ + Tests for the high-level syndication feed framework. + """ + + def test_rss2_feed(self): + """ + Test the structure and content of feeds generated by Rss201rev2Feed. + """ + response = self.client.get('/syndication/rss2/') + doc = minidom.parseString(response.content) + + # Making sure there's only 1 `rss` element and that the correct + # RSS version was specified. + feed_elem = doc.getElementsByTagName('rss') + self.assertEqual(len(feed_elem), 1) + feed = feed_elem[0] + self.assertEqual(feed.getAttribute('version'), '2.0') + + # Making sure there's only one `channel` element w/in the + # `rss` element. + chan_elem = feed.getElementsByTagName('channel') + self.assertEqual(len(chan_elem), 1) + chan = chan_elem[0] + + # Find the last build date + d = Entry.objects.latest('date').date + ltz = tzinfo.LocalTimezone(d) + last_build_date = rfc2822_date(d.replace(tzinfo=ltz)) + + self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category']) + self.assertChildNodeContent(chan, { + 'title': 'My blog', + 'description': 'A more thorough description of my blog.', + 'link': 'http://example.com/blog/', + 'language': 'en', + 'lastBuildDate': last_build_date, + #'atom:link': '', + 'ttl': '600', + 'copyright': 'Copyright (c) 2007, Sally Smith', + }) + self.assertCategories(chan, ['python', 'django']); + + # Ensure the content of the channel is correct + self.assertChildNodeContent(chan, { + 'title': 'My blog', + 'link': 'http://example.com/blog/', + }) + + # Check feed_url is passed + self.assertEqual( + chan.getElementsByTagName('atom:link')[0].getAttribute('href'), + 'http://example.com/syndication/rss2/' + ) + + # Find the pubdate of the first feed item + d = Entry.objects.get(pk=1).date + ltz = tzinfo.LocalTimezone(d) + pub_date = rfc2822_date(d.replace(tzinfo=ltz)) + + items = chan.getElementsByTagName('item') + self.assertEqual(len(items), Entry.objects.count()) + self.assertChildNodeContent(items[0], { + 'title': 'My first entry', + 'description': 'Overridden description: My first entry', + 'link': 'http://example.com/blog/1/', + 'guid': 'http://example.com/blog/1/', + 'pubDate': pub_date, + 'author': 'test@example.com (Sally Smith)', + }) + self.assertCategories(items[0], ['python', 'testing']); + + for item in items: + self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'category', 'pubDate', 'author']) + + def test_rss091_feed(self): + """ + Test the structure and content of feeds generated by RssUserland091Feed. + """ + response = self.client.get('/syndication/rss091/') + doc = minidom.parseString(response.content) + + # Making sure there's only 1 `rss` element and that the correct + # RSS version was specified. + feed_elem = doc.getElementsByTagName('rss') + self.assertEqual(len(feed_elem), 1) + feed = feed_elem[0] + self.assertEqual(feed.getAttribute('version'), '0.91') + + # Making sure there's only one `channel` element w/in the + # `rss` element. + chan_elem = feed.getElementsByTagName('channel') + self.assertEqual(len(chan_elem), 1) + chan = chan_elem[0] + self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link', 'ttl', 'copyright', 'category']) + + # Ensure the content of the channel is correct + self.assertChildNodeContent(chan, { + 'title': 'My blog', + 'link': 'http://example.com/blog/', + }) + self.assertCategories(chan, ['python', 'django']) + + # Check feed_url is passed + self.assertEqual( + chan.getElementsByTagName('atom:link')[0].getAttribute('href'), + 'http://example.com/syndication/rss091/' + ) + + items = chan.getElementsByTagName('item') + self.assertEqual(len(items), Entry.objects.count()) + self.assertChildNodeContent(items[0], { + 'title': 'My first entry', + 'description': 'Overridden description: My first entry', + 'link': 'http://example.com/blog/1/', + }) + for item in items: + self.assertChildNodes(item, ['title', 'link', 'description']) + self.assertCategories(item, []) + + def test_atom_feed(self): + """ + Test the structure and content of feeds generated by Atom1Feed. + """ + response = self.client.get('/syndication/atom/') + feed = minidom.parseString(response.content).firstChild + + self.assertEqual(feed.nodeName, 'feed') + self.assertEqual(feed.getAttribute('xmlns'), 'http://www.w3.org/2005/Atom') + self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'rights', 'category', 'author']) + for link in feed.getElementsByTagName('link'): + if link.getAttribute('rel') == 'self': + self.assertEqual(link.getAttribute('href'), 'http://example.com/syndication/atom/') + + entries = feed.getElementsByTagName('entry') + self.assertEqual(len(entries), Entry.objects.count()) + for entry in entries: + self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'category', 'updated', 'rights', 'author']) + summary = entry.getElementsByTagName('summary')[0] + self.assertEqual(summary.getAttribute('type'), 'html') + + def test_custom_feed_generator(self): + response = self.client.get('/syndication/custom/') + feed = minidom.parseString(response.content).firstChild + + self.assertEqual(feed.nodeName, 'feed') + self.assertEqual(feed.getAttribute('django'), 'rocks') + self.assertChildNodes(feed, ['title', 'subtitle', 'link', 'id', 'updated', 'entry', 'spam', 'rights', 'category', 'author']) + + entries = feed.getElementsByTagName('entry') + self.assertEqual(len(entries), Entry.objects.count()) + for entry in entries: + self.assertEqual(entry.getAttribute('bacon'), 'yum') + self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry', 'rights', 'author', 'updated', 'category']) + summary = entry.getElementsByTagName('summary')[0] + self.assertEqual(summary.getAttribute('type'), 'html') + + def test_title_escaping(self): + """ + Tests that titles are escaped correctly in RSS feeds. + """ + response = self.client.get('/syndication/rss2/') + doc = minidom.parseString(response.content) + for item in doc.getElementsByTagName('item'): + link = item.getElementsByTagName('link')[0] + if link.firstChild.wholeText == 'http://example.com/blog/4/': + title = item.getElementsByTagName('title')[0] + self.assertEquals(title.firstChild.wholeText, u'A & B < C > D') + + def test_naive_datetime_conversion(self): + """ + Test that datetimes are correctly converted to the local time zone. + """ + # Naive date times passed in get converted to the local time zone, so + # check the recived zone offset against the local offset. + response = self.client.get('/syndication/naive-dates/') + doc = minidom.parseString(response.content) + updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText + + d = Entry.objects.latest('date').date + ltz = tzinfo.LocalTimezone(d) + latest = rfc3339_date(d.replace(tzinfo=ltz)) + + self.assertEqual(updated, latest) + + def test_aware_datetime_conversion(self): + """ + Test that datetimes with timezones don't get trodden on. + """ + response = self.client.get('/syndication/aware-dates/') + doc = minidom.parseString(response.content) + updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText + self.assertEqual(updated[-6:], '+00:42') + + def test_feed_url(self): + """ + Test that the feed_url can be overridden. + """ + response = self.client.get('/syndication/feedurl/') + doc = minidom.parseString(response.content) + for link in doc.getElementsByTagName('link'): + if link.getAttribute('rel') == 'self': + self.assertEqual(link.getAttribute('href'), 'http://example.com/customfeedurl/') + + def test_secure_urls(self): + """ + Test URLs are prefixed with https:// when feed is requested over HTTPS. + """ + response = self.client.get('/syndication/rss2/', **{ + 'wsgi.url_scheme': 'https', + }) + doc = minidom.parseString(response.content) + chan = doc.getElementsByTagName('channel')[0] + self.assertEqual( + chan.getElementsByTagName('link')[0].firstChild.wholeText[0:5], + 'https' + ) + atom_link = chan.getElementsByTagName('atom:link')[0] + self.assertEqual(atom_link.getAttribute('href')[0:5], 'https') + for link in doc.getElementsByTagName('link'): + if link.getAttribute('rel') == 'self': + self.assertEqual(link.getAttribute('href')[0:5], 'https') + + def test_item_link_error(self): + """ + Test that a ImproperlyConfigured is raised if no link could be found + for the item(s). + """ + self.assertRaises(ImproperlyConfigured, + self.client.get, + '/syndication/articles/') + + def test_template_feed(self): + """ + Test that the item title and description can be overridden with + templates. + """ + response = self.client.get('/syndication/template/') + doc = minidom.parseString(response.content) + feed = doc.getElementsByTagName('rss')[0] + chan = feed.getElementsByTagName('channel')[0] + items = chan.getElementsByTagName('item') + + self.assertChildNodeContent(items[0], { + 'title': 'Title in your templates: My first entry', + 'description': 'Description in your templates: My first entry', + 'link': 'http://example.com/blog/1/', + }) + + def test_add_domain(self): + """ + Test add_domain() prefixes domains onto the correct URLs. + """ + self.assertEqual( + views.add_domain('example.com', '/foo/?arg=value'), + 'http://example.com/foo/?arg=value' + ) + self.assertEqual( + views.add_domain('example.com', '/foo/?arg=value', True), + 'https://example.com/foo/?arg=value' + ) + self.assertEqual( + views.add_domain('example.com', 'http://djangoproject.com/doc/'), + 'http://djangoproject.com/doc/' + ) + self.assertEqual( + views.add_domain('example.com', 'https://djangoproject.com/doc/'), + 'https://djangoproject.com/doc/' + ) + self.assertEqual( + views.add_domain('example.com', 'mailto:uhoh@djangoproject.com'), + 'mailto:uhoh@djangoproject.com' + ) + + +###################################### +# Deprecated feeds +###################################### + +class DeprecatedSyndicationFeedTest(FeedTestCase): + """ + Tests for the deprecated API (feed() view and the feed_dict etc). + """ + + def test_empty_feed_dict(self): + """ + Test that an empty feed_dict raises a 404. + """ + response = self.client.get('/syndication/depr-feeds-empty/aware-dates/') + self.assertEquals(response.status_code, 404) + + def test_nonexistent_slug(self): + """ + Test that a non-existent slug raises a 404. + """ + response = self.client.get('/syndication/depr-feeds/foobar/') + self.assertEquals(response.status_code, 404) + + def test_rss_feed(self): + """ + A simple test for Rss201rev2Feed feeds generated by the deprecated + system. + """ + response = self.client.get('/syndication/depr-feeds/rss/') + doc = minidom.parseString(response.content) + feed = doc.getElementsByTagName('rss')[0] + self.assertEqual(feed.getAttribute('version'), '2.0') + + chan = feed.getElementsByTagName('channel')[0] + self.assertChildNodes(chan, ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'atom:link']) + + items = chan.getElementsByTagName('item') + self.assertEqual(len(items), Entry.objects.count()) + + def test_complex_base_url(self): + """ + Tests that the base url for a complex feed doesn't raise a 500 + exception. + """ + response = self.client.get('/syndication/depr-feeds/complex/') + self.assertEquals(response.status_code, 404) + diff --git a/parts/django/tests/regressiontests/syndication/urls.py b/parts/django/tests/regressiontests/syndication/urls.py new file mode 100644 index 0000000..881fa48 --- /dev/null +++ b/parts/django/tests/regressiontests/syndication/urls.py @@ -0,0 +1,24 @@ +from django.conf.urls.defaults import * + +import feeds + +feed_dict = { + 'complex': feeds.DeprecatedComplexFeed, + 'rss': feeds.DeprecatedRssFeed, +} + +urlpatterns = patterns('django.contrib.syndication.views', + (r'^complex/(?P<foo>.*)/$', feeds.ComplexFeed()), + (r'^rss2/$', feeds.TestRss2Feed()), + (r'^rss091/$', feeds.TestRss091Feed()), + (r'^atom/$', feeds.TestAtomFeed()), + (r'^custom/$', feeds.TestCustomFeed()), + (r'^naive-dates/$', feeds.NaiveDatesFeed()), + (r'^aware-dates/$', feeds.TZAwareDatesFeed()), + (r'^feedurl/$', feeds.TestFeedUrlFeed()), + (r'^articles/$', feeds.ArticlesFeed()), + (r'^template/$', feeds.TemplateFeed()), + + (r'^depr-feeds/(?P<url>.*)/$', 'feed', {'feed_dict': feed_dict}), + (r'^depr-feeds-empty/(?P<url>.*)/$', 'feed', {'feed_dict': None}), +) diff --git a/parts/django/tests/regressiontests/templates/__init__.py b/parts/django/tests/regressiontests/templates/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/__init__.py diff --git a/parts/django/tests/regressiontests/templates/context.py b/parts/django/tests/regressiontests/templates/context.py new file mode 100644 index 0000000..394de94 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/context.py @@ -0,0 +1,17 @@ +# coding: utf-8 +from unittest import TestCase + +from django.template import Context + + +class ContextTests(TestCase): + def test_context(self): + c = Context({"a": 1, "b": "xyzzy"}) + self.assertEqual(c["a"], 1) + self.assertEqual(c.push(), {}) + c["a"] = 2 + self.assertEqual(c["a"], 2) + self.assertEqual(c.get("a"), 2) + self.assertEqual(c.pop(), {"a": 2}) + self.assertEqual(c["a"], 1) + self.assertEqual(c.get("foo", 42), 42) diff --git a/parts/django/tests/regressiontests/templates/custom.py b/parts/django/tests/regressiontests/templates/custom.py new file mode 100644 index 0000000..b346198 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/custom.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from django import template + + +class CustomTests(TestCase): + def test_filter(self): + t = template.Template("{% load custom %}{{ string|trim:5 }}") + self.assertEqual( + t.render(template.Context({"string": "abcdefghijklmnopqrstuvwxyz"})), + u"abcde" + ) diff --git a/parts/django/tests/regressiontests/templates/eggs/tagsegg.egg b/parts/django/tests/regressiontests/templates/eggs/tagsegg.egg Binary files differnew file mode 100755 index 0000000..3941914 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/eggs/tagsegg.egg diff --git a/parts/django/tests/regressiontests/templates/filters.py b/parts/django/tests/regressiontests/templates/filters.py new file mode 100644 index 0000000..af34c58 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/filters.py @@ -0,0 +1,350 @@ +# coding: utf-8 +""" +Tests for template filters (as opposed to template tags). + +The tests are hidden inside a function so that things like timestamps and +timezones are only evaluated at the moment of execution and will therefore be +consistent. +""" + +from datetime import date, datetime, timedelta + +from django.utils.tzinfo import LocalTimezone, FixedOffset +from django.utils.safestring import mark_safe + +# These two classes are used to test auto-escaping of __unicode__ output. +class UnsafeClass: + def __unicode__(self): + return u'you & me' + +class SafeClass: + def __unicode__(self): + return mark_safe(u'you > me') + +# RESULT SYNTAX -- +# 'template_name': ('template contents', 'context dict', +# 'expected string output' or Exception class) +def get_filter_tests(): + now = datetime.now() + now_tz = datetime.now(LocalTimezone(now)) + now_tz_i = datetime.now(FixedOffset((3 * 60) + 15)) # imaginary time zone + today = date.today() + + return { + # Default compare with datetime.now() + 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), + 'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1 day'), + 'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1 hour, 25 minutes'), + + # Compare to a given parameter + 'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=1)}, '1 day'), + 'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2, minutes=1), 'b':now - timedelta(days=2)}, '1 minute'), + + # Check that timezone is respected + 'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz - timedelta(hours=8), 'b':now_tz}, '8 hours'), + + # Regression for #7443 + 'filter-timesince07': ('{{ earlier|timesince }}', { 'earlier': now - timedelta(days=7) }, '1 week'), + 'filter-timesince08': ('{{ earlier|timesince:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '1 week'), + 'filter-timesince09': ('{{ later|timesince }}', { 'later': now + timedelta(days=7) }, '0 minutes'), + 'filter-timesince10': ('{{ later|timesince:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '0 minutes'), + + # Ensures that differing timezones are calculated correctly + 'filter-timesince11' : ('{{ a|timesince }}', {'a': now}, '0 minutes'), + 'filter-timesince12' : ('{{ a|timesince }}', {'a': now_tz}, '0 minutes'), + 'filter-timesince13' : ('{{ a|timesince }}', {'a': now_tz_i}, '0 minutes'), + 'filter-timesince14' : ('{{ a|timesince:b }}', {'a': now_tz, 'b': now_tz_i}, '0 minutes'), + 'filter-timesince15' : ('{{ a|timesince:b }}', {'a': now, 'b': now_tz_i}, ''), + 'filter-timesince16' : ('{{ a|timesince:b }}', {'a': now_tz_i, 'b': now}, ''), + + # Regression for #9065 (two date objects). + 'filter-timesince17' : ('{{ a|timesince:b }}', {'a': today, 'b': today}, '0 minutes'), + 'filter-timesince18' : ('{{ a|timesince:b }}', {'a': today, 'b': today + timedelta(hours=24)}, '1 day'), + + # Default compare with datetime.now() + 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), + 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), + 'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'), + + # Compare to a given parameter + 'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1 day'), + 'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1 minute'), + + # Regression for #7443 + 'filter-timeuntil06': ('{{ earlier|timeuntil }}', { 'earlier': now - timedelta(days=7) }, '0 minutes'), + 'filter-timeuntil07': ('{{ earlier|timeuntil:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '0 minutes'), + 'filter-timeuntil08': ('{{ later|timeuntil }}', { 'later': now + timedelta(days=7, hours=1) }, '1 week'), + 'filter-timeuntil09': ('{{ later|timeuntil:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '1 week'), + + # Ensures that differing timezones are calculated correctly + 'filter-timeuntil10' : ('{{ a|timeuntil }}', {'a': now_tz_i}, '0 minutes'), + 'filter-timeuntil11' : ('{{ a|timeuntil:b }}', {'a': now_tz_i, 'b': now_tz}, '0 minutes'), + + # Regression for #9065 (two date objects). + 'filter-timeuntil12' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today}, '0 minutes'), + 'filter-timeuntil13' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today - timedelta(hours=24)}, '1 day'), + + 'filter-addslash01': ("{% autoescape off %}{{ a|addslashes }} {{ b|addslashes }}{% endautoescape %}", {"a": "<a>'", "b": mark_safe("<a>'")}, ur"<a>\' <a>\'"), + 'filter-addslash02': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "<a>'", "b": mark_safe("<a>'")}, ur"<a>\' <a>\'"), + + 'filter-capfirst01': ("{% autoescape off %}{{ a|capfirst }} {{ b|capfirst }}{% endautoescape %}", {"a": "fred>", "b": mark_safe("fred>")}, u"Fred> Fred>"), + 'filter-capfirst02': ("{{ a|capfirst }} {{ b|capfirst }}", {"a": "fred>", "b": mark_safe("fred>")}, u"Fred> Fred>"), + + # Note that applying fix_ampsersands in autoescape mode leads to + # double escaping. + 'filter-fix_ampersands01': ("{% autoescape off %}{{ a|fix_ampersands }} {{ b|fix_ampersands }}{% endautoescape %}", {"a": "a&b", "b": mark_safe("a&b")}, u"a&b a&b"), + 'filter-fix_ampersands02': ("{{ a|fix_ampersands }} {{ b|fix_ampersands }}", {"a": "a&b", "b": mark_safe("a&b")}, u"a&amp;b a&b"), + + 'filter-floatformat01': ("{% autoescape off %}{{ a|floatformat }} {{ b|floatformat }}{% endautoescape %}", {"a": "1.42", "b": mark_safe("1.42")}, u"1.4 1.4"), + 'filter-floatformat02': ("{{ a|floatformat }} {{ b|floatformat }}", {"a": "1.42", "b": mark_safe("1.42")}, u"1.4 1.4"), + + # The contents of "linenumbers" is escaped according to the current + # autoescape setting. + 'filter-linenumbers01': ("{{ a|linenumbers }} {{ b|linenumbers }}", {"a": "one\n<two>\nthree", "b": mark_safe("one\n<two>\nthree")}, u"1. one\n2. <two>\n3. three 1. one\n2. <two>\n3. three"), + 'filter-linenumbers02': ("{% autoescape off %}{{ a|linenumbers }} {{ b|linenumbers }}{% endautoescape %}", {"a": "one\n<two>\nthree", "b": mark_safe("one\n<two>\nthree")}, u"1. one\n2. <two>\n3. three 1. one\n2. <two>\n3. three"), + + 'filter-lower01': ("{% autoescape off %}{{ a|lower }} {{ b|lower }}{% endautoescape %}", {"a": "Apple & banana", "b": mark_safe("Apple & banana")}, u"apple & banana apple & banana"), + 'filter-lower02': ("{{ a|lower }} {{ b|lower }}", {"a": "Apple & banana", "b": mark_safe("Apple & banana")}, u"apple & banana apple & banana"), + + # The make_list filter can destroy existing escaping, so the results are + # escaped. + 'filter-make_list01': ("{% autoescape off %}{{ a|make_list }}{% endautoescape %}", {"a": mark_safe("&")}, u"[u'&']"), + 'filter-make_list02': ("{{ a|make_list }}", {"a": mark_safe("&")}, u"[u'&']"), + 'filter-make_list03': ('{% autoescape off %}{{ a|make_list|stringformat:"s"|safe }}{% endautoescape %}', {"a": mark_safe("&")}, u"[u'&']"), + 'filter-make_list04': ('{{ a|make_list|stringformat:"s"|safe }}', {"a": mark_safe("&")}, u"[u'&']"), + + # Running slugify on a pre-escaped string leads to odd behaviour, + # but the result is still safe. + 'filter-slugify01': ("{% autoescape off %}{{ a|slugify }} {{ b|slugify }}{% endautoescape %}", {"a": "a & b", "b": mark_safe("a & b")}, u"a-b a-amp-b"), + 'filter-slugify02': ("{{ a|slugify }} {{ b|slugify }}", {"a": "a & b", "b": mark_safe("a & b")}, u"a-b a-amp-b"), + + # Notice that escaping is applied *after* any filters, so the string + # formatting here only needs to deal with pre-escaped characters. + 'filter-stringformat01': ('{% autoescape off %}.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.{% endautoescape %}', + {"a": "a<b", "b": mark_safe("a<b")}, u". a<b. . a<b."), + 'filter-stringformat02': ('.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.', {"a": "a<b", "b": mark_safe("a<b")}, + u". a<b. . a<b."), + + # Test the title filter + 'filter-title1' : ('{{ a|title }}', {'a' : 'JOE\'S CRAB SHACK'}, u'Joe's Crab Shack'), + 'filter-title2' : ('{{ a|title }}', {'a' : '555 WEST 53RD STREET'}, u'555 West 53rd Street'), + + 'filter-truncatewords01': ('{% autoescape off %}{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}{% endautoescape %}', + {"a": "alpha & bravo", "b": mark_safe("alpha & bravo")}, u"alpha & ... alpha & ..."), + 'filter-truncatewords02': ('{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}', + {"a": "alpha & bravo", "b": mark_safe("alpha & bravo")}, u"alpha & ... alpha & ..."), + + # The "upper" filter messes up entities (which are case-sensitive), + # so it's not safe for non-escaping purposes. + 'filter-upper01': ('{% autoescape off %}{{ a|upper }} {{ b|upper }}{% endautoescape %}', {"a": "a & b", "b": mark_safe("a & b")}, u"A & B A & B"), + 'filter-upper02': ('{{ a|upper }} {{ b|upper }}', {"a": "a & b", "b": mark_safe("a & b")}, u"A & B A &AMP; B"), + + 'filter-urlize01': ('{% autoescape off %}{{ a|urlize }} {{ b|urlize }}{% endautoescape %}', {"a": "http://example.com/?x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'<a href="http://example.com/?x=&y=" rel="nofollow">http://example.com/?x=&y=</a> <a href="http://example.com?x=&y=" rel="nofollow">http://example.com?x=&y=</a>'), + 'filter-urlize02': ('{{ a|urlize }} {{ b|urlize }}', {"a": "http://example.com/?x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'<a href="http://example.com/?x=&y=" rel="nofollow">http://example.com/?x=&y=</a> <a href="http://example.com?x=&y=" rel="nofollow">http://example.com?x=&y=</a>'), + 'filter-urlize03': ('{% autoescape off %}{{ a|urlize }}{% endautoescape %}', {"a": mark_safe("a & b")}, 'a & b'), + 'filter-urlize04': ('{{ a|urlize }}', {"a": mark_safe("a & b")}, 'a & b'), + + # This will lead to a nonsense result, but at least it won't be + # exploitable for XSS purposes when auto-escaping is on. + 'filter-urlize05': ('{% autoescape off %}{{ a|urlize }}{% endautoescape %}', {"a": "<script>alert('foo')</script>"}, "<script>alert('foo')</script>"), + 'filter-urlize06': ('{{ a|urlize }}', {"a": "<script>alert('foo')</script>"}, '<script>alert('foo')</script>'), + + # mailto: testing for urlize + 'filter-urlize07': ('{{ a|urlize }}', {"a": "Email me at me@example.com"}, 'Email me at <a href="mailto:me@example.com">me@example.com</a>'), + 'filter-urlize08': ('{{ a|urlize }}', {"a": "Email me at <me@example.com>"}, 'Email me at <<a href="mailto:me@example.com">me@example.com</a>>'), + + 'filter-urlizetrunc01': ('{% autoescape off %}{{ a|urlizetrunc:"8" }} {{ b|urlizetrunc:"8" }}{% endautoescape %}', {"a": '"Unsafe" http://example.com/x=&y=', "b": mark_safe('"Safe" http://example.com?x=&y=')}, u'"Unsafe" <a href="http://example.com/x=&y=" rel="nofollow">http:...</a> "Safe" <a href="http://example.com?x=&y=" rel="nofollow">http:...</a>'), + 'filter-urlizetrunc02': ('{{ a|urlizetrunc:"8" }} {{ b|urlizetrunc:"8" }}', {"a": '"Unsafe" http://example.com/x=&y=', "b": mark_safe('"Safe" http://example.com?x=&y=')}, u'"Unsafe" <a href="http://example.com/x=&y=" rel="nofollow">http:...</a> "Safe" <a href="http://example.com?x=&y=" rel="nofollow">http:...</a>'), + + 'filter-wordcount01': ('{% autoescape off %}{{ a|wordcount }} {{ b|wordcount }}{% endautoescape %}', {"a": "a & b", "b": mark_safe("a & b")}, "3 3"), + 'filter-wordcount02': ('{{ a|wordcount }} {{ b|wordcount }}', {"a": "a & b", "b": mark_safe("a & b")}, "3 3"), + + 'filter-wordwrap01': ('{% autoescape off %}{{ a|wordwrap:"3" }} {{ b|wordwrap:"3" }}{% endautoescape %}', {"a": "a & b", "b": mark_safe("a & b")}, u"a &\nb a &\nb"), + 'filter-wordwrap02': ('{{ a|wordwrap:"3" }} {{ b|wordwrap:"3" }}', {"a": "a & b", "b": mark_safe("a & b")}, u"a &\nb a &\nb"), + + 'filter-ljust01': ('{% autoescape off %}.{{ a|ljust:"5" }}. .{{ b|ljust:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u".a&b . .a&b ."), + 'filter-ljust02': ('.{{ a|ljust:"5" }}. .{{ b|ljust:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u".a&b . .a&b ."), + + 'filter-rjust01': ('{% autoescape off %}.{{ a|rjust:"5" }}. .{{ b|rjust:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b. . a&b."), + 'filter-rjust02': ('.{{ a|rjust:"5" }}. .{{ b|rjust:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b. . a&b."), + + 'filter-center01': ('{% autoescape off %}.{{ a|center:"5" }}. .{{ b|center:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b . . a&b ."), + 'filter-center02': ('.{{ a|center:"5" }}. .{{ b|center:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b . . a&b ."), + + 'filter-cut01': ('{% autoescape off %}{{ a|cut:"x" }} {{ b|cut:"x" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, u"&y &y"), + 'filter-cut02': ('{{ a|cut:"x" }} {{ b|cut:"x" }}', {"a": "x&y", "b": mark_safe("x&y")}, u"&y &y"), + 'filter-cut03': ('{% autoescape off %}{{ a|cut:"&" }} {{ b|cut:"&" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, u"xy xamp;y"), + 'filter-cut04': ('{{ a|cut:"&" }} {{ b|cut:"&" }}', {"a": "x&y", "b": mark_safe("x&y")}, u"xy xamp;y"), + # Passing ';' to cut can break existing HTML entities, so those strings + # are auto-escaped. + 'filter-cut05': ('{% autoescape off %}{{ a|cut:";" }} {{ b|cut:";" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, u"x&y x&y"), + 'filter-cut06': ('{{ a|cut:";" }} {{ b|cut:";" }}', {"a": "x&y", "b": mark_safe("x&y")}, u"x&y x&ampy"), + + # The "escape" filter works the same whether autoescape is on or off, + # but it has no effect on strings already marked as safe. + 'filter-escape01': ('{{ a|escape }} {{ b|escape }}', {"a": "x&y", "b": mark_safe("x&y")}, u"x&y x&y"), + 'filter-escape02': ('{% autoescape off %}{{ a|escape }} {{ b|escape }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, "x&y x&y"), + + # It is only applied once, regardless of the number of times it + # appears in a chain. + 'filter-escape03': ('{% autoescape off %}{{ a|escape|escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-escape04': ('{{ a|escape|escape }}', {"a": "x&y"}, u"x&y"), + + # Force_escape is applied immediately. It can be used to provide + # double-escaping, for example. + 'filter-force-escape01': ('{% autoescape off %}{{ a|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape02': ('{{ a|force_escape }}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape03': ('{% autoescape off %}{{ a|force_escape|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&amp;y"), + 'filter-force-escape04': ('{{ a|force_escape|force_escape }}', {"a": "x&y"}, u"x&amp;y"), + + # Because the result of force_escape is "safe", an additional + # escape filter has no effect. + 'filter-force-escape05': ('{% autoescape off %}{{ a|force_escape|escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape06': ('{{ a|force_escape|escape }}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape07': ('{% autoescape off %}{{ a|escape|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&y"), + 'filter-force-escape08': ('{{ a|escape|force_escape }}', {"a": "x&y"}, u"x&y"), + + # The contents in "linebreaks" and "linebreaksbr" are escaped + # according to the current autoescape setting. + 'filter-linebreaks01': ('{{ a|linebreaks }} {{ b|linebreaks }}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"<p>x&<br />y</p> <p>x&<br />y</p>"), + 'filter-linebreaks02': ('{% autoescape off %}{{ a|linebreaks }} {{ b|linebreaks }}{% endautoescape %}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"<p>x&<br />y</p> <p>x&<br />y</p>"), + + 'filter-linebreaksbr01': ('{{ a|linebreaksbr }} {{ b|linebreaksbr }}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"x&<br />y x&<br />y"), + 'filter-linebreaksbr02': ('{% autoescape off %}{{ a|linebreaksbr }} {{ b|linebreaksbr }}{% endautoescape %}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"x&<br />y x&<br />y"), + + 'filter-safe01': ("{{ a }} -- {{ a|safe }}", {"a": u"<b>hello</b>"}, "<b>hello</b> -- <b>hello</b>"), + 'filter-safe02': ("{% autoescape off %}{{ a }} -- {{ a|safe }}{% endautoescape %}", {"a": "<b>hello</b>"}, u"<b>hello</b> -- <b>hello</b>"), + + 'filter-safeseq01': ('{{ a|join:", " }} -- {{ a|safeseq|join:", " }}', {"a": ["&", "<"]}, "&, < -- &, <"), + 'filter-safeseq02': ('{% autoescape off %}{{ a|join:", " }} -- {{ a|safeseq|join:", " }}{% endautoescape %}', {"a": ["&", "<"]}, "&, < -- &, <"), + + 'filter-removetags01': ('{{ a|removetags:"a b" }} {{ b|removetags:"a b" }}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, u"x <p>y</p> x <p>y</p>"), + 'filter-removetags02': ('{% autoescape off %}{{ a|removetags:"a b" }} {{ b|removetags:"a b" }}{% endautoescape %}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, u"x <p>y</p> x <p>y</p>"), + + 'filter-striptags01': ('{{ a|striptags }} {{ b|striptags }}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, "x y x y"), + 'filter-striptags02': ('{% autoescape off %}{{ a|striptags }} {{ b|striptags }}{% endautoescape %}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, "x y x y"), + + 'filter-first01': ('{{ a|first }} {{ b|first }}', {"a": ["a&b", "x"], "b": [mark_safe("a&b"), "x"]}, "a&b a&b"), + 'filter-first02': ('{% autoescape off %}{{ a|first }} {{ b|first }}{% endautoescape %}', {"a": ["a&b", "x"], "b": [mark_safe("a&b"), "x"]}, "a&b a&b"), + + 'filter-last01': ('{{ a|last }} {{ b|last }}', {"a": ["x", "a&b"], "b": ["x", mark_safe("a&b")]}, "a&b a&b"), + 'filter-last02': ('{% autoescape off %}{{ a|last }} {{ b|last }}{% endautoescape %}', {"a": ["x", "a&b"], "b": ["x", mark_safe("a&b")]}, "a&b a&b"), + + 'filter-random01': ('{{ a|random }} {{ b|random }}', {"a": ["a&b", "a&b"], "b": [mark_safe("a&b"), mark_safe("a&b")]}, "a&b a&b"), + 'filter-random02': ('{% autoescape off %}{{ a|random }} {{ b|random }}{% endautoescape %}', {"a": ["a&b", "a&b"], "b": [mark_safe("a&b"), mark_safe("a&b")]}, "a&b a&b"), + + 'filter-slice01': ('{{ a|slice:"1:3" }} {{ b|slice:"1:3" }}', {"a": "a&b", "b": mark_safe("a&b")}, "&b &b"), + 'filter-slice02': ('{% autoescape off %}{{ a|slice:"1:3" }} {{ b|slice:"1:3" }}{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, "&b &b"), + + 'filter-unordered_list01': ('{{ a|unordered_list }}', {"a": ["x>", [["<y", []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"), + 'filter-unordered_list02': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"a": ["x>", [["<y", []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"), + 'filter-unordered_list03': ('{{ a|unordered_list }}', {"a": ["x>", [[mark_safe("<y"), []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"), + 'filter-unordered_list04': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"a": ["x>", [[mark_safe("<y"), []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"), + 'filter-unordered_list05': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"a": ["x>", [["<y", []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"), + + # Literal string arguments to the default filter are always treated as + # safe strings, regardless of the auto-escaping state. + # + # Note: we have to use {"a": ""} here, otherwise the invalid template + # variable string interferes with the test result. + 'filter-default01': ('{{ a|default:"x<" }}', {"a": ""}, "x<"), + 'filter-default02': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": ""}, "x<"), + 'filter-default03': ('{{ a|default:"x<" }}', {"a": mark_safe("x>")}, "x>"), + 'filter-default04': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": mark_safe("x>")}, "x>"), + + 'filter-default_if_none01': ('{{ a|default:"x<" }}', {"a": None}, "x<"), + 'filter-default_if_none02': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": None}, "x<"), + + 'filter-phone2numeric01': ('{{ a|phone2numeric }} {{ b|phone2numeric }}', {"a": "<1-800-call-me>", "b": mark_safe("<1-800-call-me>") }, "<1-800-2255-63> <1-800-2255-63>"), + 'filter-phone2numeric02': ('{% autoescape off %}{{ a|phone2numeric }} {{ b|phone2numeric }}{% endautoescape %}', {"a": "<1-800-call-me>", "b": mark_safe("<1-800-call-me>") }, "<1-800-2255-63> <1-800-2255-63>"), + 'filter-phone2numeric03': ('{{ a|phone2numeric }}', {"a": "How razorback-jumping frogs can level six piqued gymnasts!"}, "469 729672225-5867464 37647 226 53835 749 747833 49662787!"), + + # Ensure iriencode keeps safe strings: + 'filter-iriencode01': ('{{ url|iriencode }}', {'url': '?test=1&me=2'}, '?test=1&me=2'), + 'filter-iriencode02': ('{% autoescape off %}{{ url|iriencode }}{% endautoescape %}', {'url': '?test=1&me=2'}, '?test=1&me=2'), + 'filter-iriencode03': ('{{ url|iriencode }}', {'url': mark_safe('?test=1&me=2')}, '?test=1&me=2'), + 'filter-iriencode04': ('{% autoescape off %}{{ url|iriencode }}{% endautoescape %}', {'url': mark_safe('?test=1&me=2')}, '?test=1&me=2'), + + # Chaining a bunch of safeness-preserving filters should not alter + # the safe status either way. + 'chaining01': ('{{ a|capfirst|center:"7" }}.{{ b|capfirst|center:"7" }}', {"a": "a < b", "b": mark_safe("a < b")}, " A < b . A < b "), + 'chaining02': ('{% autoescape off %}{{ a|capfirst|center:"7" }}.{{ b|capfirst|center:"7" }}{% endautoescape %}', {"a": "a < b", "b": mark_safe("a < b")}, " A < b . A < b "), + + # Using a filter that forces a string back to unsafe: + 'chaining03': ('{{ a|cut:"b"|capfirst }}.{{ b|cut:"b"|capfirst }}', {"a": "a < b", "b": mark_safe("a < b")}, "A < .A < "), + 'chaining04': ('{% autoescape off %}{{ a|cut:"b"|capfirst }}.{{ b|cut:"b"|capfirst }}{% endautoescape %}', {"a": "a < b", "b": mark_safe("a < b")}, "A < .A < "), + + # Using a filter that forces safeness does not lead to double-escaping + 'chaining05': ('{{ a|escape|capfirst }}', {"a": "a < b"}, "A < b"), + 'chaining06': ('{% autoescape off %}{{ a|escape|capfirst }}{% endautoescape %}', {"a": "a < b"}, "A < b"), + + # Force to safe, then back (also showing why using force_escape too + # early in a chain can lead to unexpected results). + 'chaining07': ('{{ a|force_escape|cut:";" }}', {"a": "a < b"}, "a &lt b"), + 'chaining08': ('{% autoescape off %}{{ a|force_escape|cut:";" }}{% endautoescape %}', {"a": "a < b"}, "a < b"), + 'chaining09': ('{{ a|cut:";"|force_escape }}', {"a": "a < b"}, "a < b"), + 'chaining10': ('{% autoescape off %}{{ a|cut:";"|force_escape }}{% endautoescape %}', {"a": "a < b"}, "a < b"), + 'chaining11': ('{{ a|cut:"b"|safe }}', {"a": "a < b"}, "a < "), + 'chaining12': ('{% autoescape off %}{{ a|cut:"b"|safe }}{% endautoescape %}', {"a": "a < b"}, "a < "), + 'chaining13': ('{{ a|safe|force_escape }}', {"a": "a < b"}, "a < b"), + 'chaining14': ('{% autoescape off %}{{ a|safe|force_escape }}{% endautoescape %}', {"a": "a < b"}, "a < b"), + + # Filters decorated with stringfilter still respect is_safe. + 'autoescape-stringfilter01': (r'{{ unsafe|capfirst }}', {'unsafe': UnsafeClass()}, 'You & me'), + 'autoescape-stringfilter02': (r'{% autoescape off %}{{ unsafe|capfirst }}{% endautoescape %}', {'unsafe': UnsafeClass()}, 'You & me'), + 'autoescape-stringfilter03': (r'{{ safe|capfirst }}', {'safe': SafeClass()}, 'You > me'), + 'autoescape-stringfilter04': (r'{% autoescape off %}{{ safe|capfirst }}{% endautoescape %}', {'safe': SafeClass()}, 'You > me'), + + 'escapejs01': (r'{{ a|escapejs }}', {'a': 'testing\r\njavascript \'string" <b>escaping</b>'}, 'testing\\u000D\\u000Ajavascript \\u0027string\\u0022 \\u003Cb\\u003Eescaping\\u003C/b\\u003E'), + 'escapejs02': (r'{% autoescape off %}{{ a|escapejs }}{% endautoescape %}', {'a': 'testing\r\njavascript \'string" <b>escaping</b>'}, 'testing\\u000D\\u000Ajavascript \\u0027string\\u0022 \\u003Cb\\u003Eescaping\\u003C/b\\u003E'), + + + # length filter. + 'length01': ('{{ list|length }}', {'list': ['4', None, True, {}]}, '4'), + 'length02': ('{{ list|length }}', {'list': []}, '0'), + 'length03': ('{{ string|length }}', {'string': ''}, '0'), + 'length04': ('{{ string|length }}', {'string': 'django'}, '6'), + # Invalid uses that should fail silently. + 'length05': ('{{ int|length }}', {'int': 7}, ''), + 'length06': ('{{ None|length }}', {'None': None}, ''), + + # length_is filter. + 'length_is01': ('{% if some_list|length_is:"4" %}Four{% endif %}', {'some_list': ['4', None, True, {}]}, 'Four'), + 'length_is02': ('{% if some_list|length_is:"4" %}Four{% else %}Not Four{% endif %}', {'some_list': ['4', None, True, {}, 17]}, 'Not Four'), + 'length_is03': ('{% if mystring|length_is:"4" %}Four{% endif %}', {'mystring': 'word'}, 'Four'), + 'length_is04': ('{% if mystring|length_is:"4" %}Four{% else %}Not Four{% endif %}', {'mystring': 'Python'}, 'Not Four'), + 'length_is05': ('{% if mystring|length_is:"4" %}Four{% else %}Not Four{% endif %}', {'mystring': ''}, 'Not Four'), + 'length_is06': ('{% with var|length as my_length %}{{ my_length }}{% endwith %}', {'var': 'django'}, '6'), + # Boolean return value from length_is should not be coerced to a string + 'length_is07': (r'{% if "X"|length_is:0 %}Length is 0{% else %}Length not 0{% endif %}', {}, 'Length not 0'), + 'length_is08': (r'{% if "X"|length_is:1 %}Length is 1{% else %}Length not 1{% endif %}', {}, 'Length is 1'), + # Invalid uses that should fail silently. + 'length_is09': ('{{ var|length_is:"fish" }}', {'var': 'django'}, ''), + 'length_is10': ('{{ int|length_is:"1" }}', {'int': 7}, ''), + 'length_is11': ('{{ none|length_is:"1" }}', {'none': None}, ''), + + 'join01': (r'{{ a|join:", " }}', {'a': ['alpha', 'beta & me']}, 'alpha, beta & me'), + 'join02': (r'{% autoescape off %}{{ a|join:", " }}{% endautoescape %}', {'a': ['alpha', 'beta & me']}, 'alpha, beta & me'), + 'join03': (r'{{ a|join:" & " }}', {'a': ['alpha', 'beta & me']}, 'alpha & beta & me'), + 'join04': (r'{% autoescape off %}{{ a|join:" & " }}{% endautoescape %}', {'a': ['alpha', 'beta & me']}, 'alpha & 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 & beta & me'), + 'join06': (r'{{ a|join:var }}', {'a': ['alpha', 'beta & me'], 'var': mark_safe(' & ')}, 'alpha & beta & me'), + 'join07': (r'{{ a|join:var|lower }}', {'a': ['Alpha', 'Beta & me'], 'var': ' & ' }, 'alpha & beta & me'), + 'join08': (r'{{ a|join:var|lower }}', {'a': ['Alpha', 'Beta & me'], 'var': mark_safe(' & ')}, 'alpha & beta & 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 + 'date03': (r'{{ d|date:"m" }}', {'d': 'fail_string'}, ''), + + # Tests for #11687 + 'add01': (r'{{ i|add:"5" }}', {'i': 2000}, '2005'), + 'add02': (r'{{ i|add:"napis" }}', {'i': 2000}, '2000'), + 'add03': (r'{{ i|add:16 }}', {'i': 'not_an_int'}, 'not_an_int'), + '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)}, 'Jan. 11, 2000'), + } diff --git a/parts/django/tests/regressiontests/templates/loaders.py b/parts/django/tests/regressiontests/templates/loaders.py new file mode 100644 index 0000000..47cd18a --- /dev/null +++ b/parts/django/tests/regressiontests/templates/loaders.py @@ -0,0 +1,150 @@ +""" +Test cases for the template loaders + +Note: This test requires setuptools! +""" + +from django.conf import settings + +if __name__ == '__main__': + settings.configure() + +import unittest +import sys +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 +from django.test.utils import get_warnings_state, restore_warnings_state + +# Mock classes and objects for pkg_resources functions. +class MockProvider(pkg_resources.NullProvider): + def __init__(self, module): + pkg_resources.NullProvider.__init__(self, module) + self.module = module + + def _has(self, path): + return path in self.module._resources + + def _isdir(self,path): + return False + + def get_resource_stream(self, manager, resource_name): + return self.module._resources[resource_name] + + def _get(self, path): + return self.module._resources[path].read() + +class MockLoader(object): + pass + +def create_egg(name, resources): + """ + Creates a mock egg with a list of resources. + + name: The name of the module. + resources: A dictionary of resources. Keys are the names and values the data. + """ + egg = imp.new_module(name) + egg.__loader__ = MockLoader() + egg._resources = resources + sys.modules[name] = egg + +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 = [] + self._warnings_state = get_warnings_state() + warnings.simplefilter("ignore", PendingDeprecationWarning) + + def tearDown(self): + settings.INSTALLED_APPS = self._old_installed_apps + restore_warnings_state(self._warnings_state) + + 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 + + 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 = [] + + def tearDown(self): + settings.INSTALLED_APPS = self._old_installed_apps + + def test_empty(self): + "Loading any template on an empty egg should fail" + settings.INSTALLED_APPS = ['egg_empty'] + 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'] + 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'] + 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 = [] + egg_loader = EggLoader() + self.assertRaises(TemplateDoesNotExist, egg_loader.load_template_source, "y.html") + +class CachedLoader(unittest.TestCase): + def setUp(self): + self.old_TEMPLATE_LOADERS = settings.TEMPLATE_LOADERS + settings.TEMPLATE_LOADERS = ( + ('django.template.loaders.cached.Loader', ( + 'django.template.loaders.filesystem.Loader', + ) + ), + ) + def tearDown(self): + settings.TEMPLATE_LOADERS = self.old_TEMPLATE_LOADERS + + def test_templatedir_caching(self): + "Check that the template directories form part of the template cache key. Refs #13573" + # Retrive a template specifying a template directory to check + t1, name = loader.find_template('test.html', (os.path.join(os.path.dirname(__file__), 'templates', 'first'),)) + # Now retrieve the same template name, but from a different directory + t2, name = loader.find_template('test.html', (os.path.join(os.path.dirname(__file__), 'templates', 'second'),)) + + # The two templates should not have the same content + self.assertNotEqual(t1.render(Context({})), t2.render(Context({}))) + +if __name__ == "__main__": + unittest.main() diff --git a/parts/django/tests/regressiontests/templates/models.py b/parts/django/tests/regressiontests/templates/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/models.py diff --git a/parts/django/tests/regressiontests/templates/nodelist.py b/parts/django/tests/regressiontests/templates/nodelist.py new file mode 100644 index 0000000..89fac97 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/nodelist.py @@ -0,0 +1,30 @@ +from unittest import TestCase +from django.template.loader import get_template_from_string +from django.template import VariableNode + + +class NodelistTest(TestCase): + + def test_for(self): + source = '{% for i in 1 %}{{ a }}{% endfor %}' + template = get_template_from_string(source) + vars = template.nodelist.get_nodes_by_type(VariableNode) + self.assertEqual(len(vars), 1) + + def test_if(self): + source = '{% if x %}{{ a }}{% endif %}' + template = get_template_from_string(source) + vars = template.nodelist.get_nodes_by_type(VariableNode) + self.assertEqual(len(vars), 1) + + def test_ifequal(self): + source = '{% ifequal x y %}{{ a }}{% endifequal %}' + template = get_template_from_string(source) + vars = template.nodelist.get_nodes_by_type(VariableNode) + self.assertEqual(len(vars), 1) + + def test_ifchanged(self): + source = '{% ifchanged x %}{{ a }}{% endifchanged %}' + template = get_template_from_string(source) + vars = template.nodelist.get_nodes_by_type(VariableNode) + self.assertEqual(len(vars), 1) diff --git a/parts/django/tests/regressiontests/templates/parser.py b/parts/django/tests/regressiontests/templates/parser.py new file mode 100644 index 0000000..93e8118 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/parser.py @@ -0,0 +1,84 @@ +""" +Testing some internals of the template processing. These are *not* examples to be copied in user code. +""" +from unittest import TestCase + +from django.template import (TokenParser, FilterExpression, Parser, Variable, + TemplateSyntaxError) + + +class ParserTests(TestCase): + def test_token_parsing(self): + # Tests for TokenParser behavior in the face of quoted strings with + # spaces. + + p = TokenParser("tag thevar|filter sometag") + self.assertEqual(p.tagname, "tag") + self.assertEqual(p.value(), "thevar|filter") + self.assertTrue(p.more()) + self.assertEqual(p.tag(), "sometag") + self.assertFalse(p.more()) + + p = TokenParser('tag "a value"|filter sometag') + self.assertEqual(p.tagname, "tag") + self.assertEqual(p.value(), '"a value"|filter') + self.assertTrue(p.more()) + self.assertEqual(p.tag(), "sometag") + self.assertFalse(p.more()) + + p = TokenParser("tag 'a value'|filter sometag") + self.assertEqual(p.tagname, "tag") + self.assertEqual(p.value(), "'a value'|filter") + self.assertTrue(p.more()) + self.assertEqual(p.tag(), "sometag") + self.assertFalse(p.more()) + + def test_filter_parsing(self): + c = {"article": {"section": u"News"}} + p = Parser("") + + def fe_test(s, val): + self.assertEqual(FilterExpression(s, p).resolve(c), val) + + fe_test("article.section", u"News") + fe_test("article.section|upper", u"NEWS") + fe_test(u'"News"', u"News") + fe_test(u"'News'", u"News") + fe_test(ur'"Some \"Good\" News"', u'Some "Good" News') + fe_test(ur'"Some \"Good\" News"', u'Some "Good" News') + fe_test(ur"'Some \'Bad\' News'", u"Some 'Bad' News") + + fe = FilterExpression(ur'"Some \"Good\" News"', p) + self.assertEqual(fe.filters, []) + self.assertEqual(fe.var, u'Some "Good" News') + + # Filtered variables should reject access of attributes beginning with + # underscores. + self.assertRaises(TemplateSyntaxError, + FilterExpression, "article._hidden|upper", p + ) + + def test_variable_parsing(self): + c = {"article": {"section": u"News"}} + self.assertEqual(Variable("article.section").resolve(c), "News") + self.assertEqual(Variable(u'"News"').resolve(c), "News") + self.assertEqual(Variable(u"'News'").resolve(c), "News") + + # Translated strings are handled correctly. + self.assertEqual(Variable("_(article.section)").resolve(c), "News") + self.assertEqual(Variable('_("Good News")').resolve(c), "Good News") + self.assertEqual(Variable("_('Better News')").resolve(c), "Better News") + + # Escaped quotes work correctly as well. + self.assertEqual( + Variable(ur'"Some \"Good\" News"').resolve(c), 'Some "Good" News' + ) + self.assertEqual( + Variable(ur"'Some \'Better\' News'").resolve(c), "Some 'Better' News" + ) + + # Variables should reject access of attributes beginning with + # underscores. + self.assertRaises(TemplateSyntaxError, + Variable, "article._hidden" + ) diff --git a/parts/django/tests/regressiontests/templates/smartif.py b/parts/django/tests/regressiontests/templates/smartif.py new file mode 100644 index 0000000..5e5d770 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/smartif.py @@ -0,0 +1,53 @@ +import unittest +from django.template.smartif import IfParser, Literal + +class SmartIfTests(unittest.TestCase): + + def assertCalcEqual(self, expected, tokens): + self.assertEqual(expected, IfParser(tokens).parse().eval({})) + + # We only test things here that are difficult to test elsewhere + # Many other tests are found in the main tests for builtin template tags + # Test parsing via the printed parse tree + def test_not(self): + var = IfParser(["not", False]).parse() + self.assertEqual("(not (literal False))", repr(var)) + self.assert_(var.eval({})) + + self.assertFalse(IfParser(["not", True]).parse().eval({})) + + def test_or(self): + var = IfParser([True, "or", False]).parse() + self.assertEqual("(or (literal True) (literal False))", repr(var)) + self.assert_(var.eval({})) + + def test_in(self): + list_ = [1,2,3] + self.assertCalcEqual(True, [1, 'in', list_]) + self.assertCalcEqual(False, [1, 'in', None]) + self.assertCalcEqual(False, [None, 'in', list_]) + + def test_not_in(self): + list_ = [1,2,3] + self.assertCalcEqual(False, [1, 'not', 'in', list_]) + self.assertCalcEqual(True, [4, 'not', 'in', list_]) + self.assertCalcEqual(False, [1, 'not', 'in', None]) + self.assertCalcEqual(True, [None, 'not', 'in', list_]) + + def test_precedence(self): + # (False and False) or True == True <- we want this one, like Python + # False and (False or True) == False + self.assertCalcEqual(True, [False, 'and', False, 'or', True]) + + # True or (False and False) == True <- we want this one, like Python + # (True or False) and False == False + self.assertCalcEqual(True, [True, 'or', False, 'and', False]) + + # (1 or 1) == 2 -> False + # 1 or (1 == 2) -> True <- we want this one + self.assertCalcEqual(True, [1, 'or', 1, '==', 2]) + + self.assertCalcEqual(True, [True, '==', True, 'or', True, '==', False]) + + self.assertEqual("(or (and (== (literal 1) (literal 2)) (literal 3)) (literal 4))", + repr(IfParser([1, '==', 2, 'and', 3, 'or', 4]).parse())) diff --git a/parts/django/tests/regressiontests/templates/templates/broken_base.html b/parts/django/tests/regressiontests/templates/templates/broken_base.html new file mode 100644 index 0000000..aa41f44 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/templates/broken_base.html @@ -0,0 +1 @@ +{% include "missing.html" %} diff --git a/parts/django/tests/regressiontests/templates/templates/first/test.html b/parts/django/tests/regressiontests/templates/templates/first/test.html new file mode 100644 index 0000000..6029fe5 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/templates/first/test.html @@ -0,0 +1 @@ +First template diff --git a/parts/django/tests/regressiontests/templates/templates/second/test.html b/parts/django/tests/regressiontests/templates/templates/second/test.html new file mode 100644 index 0000000..d9b316f --- /dev/null +++ b/parts/django/tests/regressiontests/templates/templates/second/test.html @@ -0,0 +1 @@ +Second template diff --git a/parts/django/tests/regressiontests/templates/templates/test_extends_error.html b/parts/django/tests/regressiontests/templates/templates/test_extends_error.html new file mode 100755 index 0000000..fc74690 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/templates/test_extends_error.html @@ -0,0 +1 @@ +{% extends "broken_base.html" %} diff --git a/parts/django/tests/regressiontests/templates/templatetags/__init__.py b/parts/django/tests/regressiontests/templates/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/templatetags/__init__.py diff --git a/parts/django/tests/regressiontests/templates/templatetags/broken_tag.py b/parts/django/tests/regressiontests/templates/templatetags/broken_tag.py new file mode 100644 index 0000000..c70e183 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/templatetags/broken_tag.py @@ -0,0 +1 @@ +from django import Xtemplate diff --git a/parts/django/tests/regressiontests/templates/templatetags/custom.py b/parts/django/tests/regressiontests/templates/templatetags/custom.py new file mode 100644 index 0000000..fdf8d10 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/templatetags/custom.py @@ -0,0 +1,11 @@ +from django import template +from django.template.defaultfilters import stringfilter + +register = template.Library() + +def trim(value, num): + return value[:num] +trim = stringfilter(trim) + +register.filter(trim) + diff --git a/parts/django/tests/regressiontests/templates/tests.py b/parts/django/tests/regressiontests/templates/tests.py new file mode 100644 index 0000000..62b2237 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/tests.py @@ -0,0 +1,1389 @@ +# -*- coding: utf-8 -*- +from django.conf import settings + +if __name__ == '__main__': + # When running this file in isolation, we need to set up the configuration + # before importing 'template'. + settings.configure() + +from datetime import datetime, timedelta +import time +import os +import sys +import traceback +import unittest + +from django import template +from django.core import urlresolvers +from django.template import loader +from django.template.loaders import app_directories, filesystem, cached +from django.utils.translation import activate, deactivate, ugettext as _ +from django.utils.safestring import mark_safe +from django.utils.tzinfo import LocalTimezone + +from context import ContextTests +from custom import CustomTests +from parser import ParserTests +from unicode import UnicodeTests +from nodelist import NodelistTest +from smartif import * + +try: + from loaders import * +except ImportError: + pass # If setuptools isn't installed, that's fine. Just move on. + +import filters + +################################# +# Custom template tag for tests # +################################# + +register = template.Library() + +class EchoNode(template.Node): + def __init__(self, contents): + self.contents = contents + + def render(self, context): + return " ".join(self.contents) + +def do_echo(parser, token): + return EchoNode(token.contents.split()[1:]) + +register.tag("echo", do_echo) + +template.libraries['testtags'] = register + +##################################### +# Helper objects for template tests # +##################################### + +class SomeException(Exception): + silent_variable_failure = True + +class SomeOtherException(Exception): + pass + +class ContextStackException(Exception): + pass + +class SomeClass: + def __init__(self): + self.otherclass = OtherClass() + + def method(self): + return "SomeClass.method" + + def method2(self, o): + return o + + def method3(self): + raise SomeException + + def method4(self): + raise SomeOtherException + +class OtherClass: + def method(self): + return "OtherClass.method" + +class TestObj(object): + def is_true(self): + return True + + def is_false(self): + return False + + def is_bad(self): + time.sleep(0.3) + return True + +class SilentGetItemClass(object): + def __getitem__(self, key): + raise SomeException + +class SilentAttrClass(object): + def b(self): + raise SomeException + b = property(b) + +class UTF8Class: + "Class whose __str__ returns non-ASCII data" + def __str__(self): + return u'ŠĐĆŽćžšđ'.encode('utf-8') + +class Templates(unittest.TestCase): + def test_loaders_security(self): + ad_loader = app_directories.Loader() + fs_loader = filesystem.Loader() + def test_template_sources(path, template_dirs, expected_sources): + if isinstance(expected_sources, list): + # Fix expected sources so they are normcased and abspathed + expected_sources = [os.path.normcase(os.path.abspath(s)) for s in expected_sources] + # Test the two loaders (app_directores and filesystem). + func1 = lambda p, t: list(ad_loader.get_template_sources(p, t)) + func2 = lambda p, t: list(fs_loader.get_template_sources(p, t)) + for func in (func1, func2): + if isinstance(expected_sources, list): + self.assertEqual(func(path, template_dirs), expected_sources) + else: + self.assertRaises(expected_sources, func, path, template_dirs) + + template_dirs = ['/dir1', '/dir2'] + test_template_sources('index.html', template_dirs, + ['/dir1/index.html', '/dir2/index.html']) + test_template_sources('/etc/passwd', template_dirs, []) + test_template_sources('etc/passwd', template_dirs, + ['/dir1/etc/passwd', '/dir2/etc/passwd']) + test_template_sources('../etc/passwd', template_dirs, []) + test_template_sources('../../../etc/passwd', template_dirs, []) + test_template_sources('/dir1/index.html', template_dirs, + ['/dir1/index.html']) + test_template_sources('../dir2/index.html', template_dirs, + ['/dir2/index.html']) + test_template_sources('/dir1blah', template_dirs, []) + test_template_sources('../dir1blah', template_dirs, []) + + # UTF-8 bytestrings are permitted. + test_template_sources('\xc3\x85ngstr\xc3\xb6m', template_dirs, + [u'/dir1/Ångström', u'/dir2/Ångström']) + # Unicode strings are permitted. + test_template_sources(u'Ångström', template_dirs, + [u'/dir1/Ångström', u'/dir2/Ångström']) + test_template_sources(u'Ångström', ['/Straße'], [u'/Straße/Ångström']) + test_template_sources('\xc3\x85ngstr\xc3\xb6m', ['/Straße'], + [u'/Straße/Ångström']) + # Invalid UTF-8 encoding in bytestrings is not. Should raise a + # semi-useful error message. + test_template_sources('\xc3\xc3', template_dirs, UnicodeDecodeError) + + # Case insensitive tests (for win32). Not run unless we're on + # a case insensitive operating system. + if os.path.normcase('/TEST') == os.path.normpath('/test'): + template_dirs = ['/dir1', '/DIR2'] + test_template_sources('index.html', template_dirs, + ['/dir1/index.html', '/dir2/index.html']) + test_template_sources('/DIR1/index.HTML', template_dirs, + ['/dir1/index.html']) + + def test_loader_debug_origin(self): + # Turn TEMPLATE_DEBUG on, so that the origin file name will be kept with + # the compiled templates. + old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, True + old_loaders = loader.template_source_loaders + + try: + loader.template_source_loaders = (filesystem.Loader(),) + + # We rely on the fact that runtests.py sets up TEMPLATE_DIRS to + # point to a directory containing a 404.html file. Also that + # the file system and app directories loaders both inherit the + # load_template method from the BaseLoader class, so we only need + # to test one of them. + load_name = '404.html' + template = loader.get_template(load_name) + template_name = template.nodelist[0].source[0].name + self.assertTrue(template_name.endswith(load_name), + 'Template loaded by filesystem loader has incorrect name for debug page: %s' % template_name) + + # Aso test the cached loader, since it overrides load_template + cache_loader = cached.Loader(('',)) + cache_loader._cached_loaders = loader.template_source_loaders + loader.template_source_loaders = (cache_loader,) + + template = loader.get_template(load_name) + template_name = template.nodelist[0].source[0].name + self.assertTrue(template_name.endswith(load_name), + 'Template loaded through cached loader has incorrect name for debug page: %s' % template_name) + + template = loader.get_template(load_name) + template_name = template.nodelist[0].source[0].name + self.assertTrue(template_name.endswith(load_name), + 'Cached template loaded through cached loader has incorrect name for debug page: %s' % template_name) + finally: + loader.template_source_loaders = old_loaders + settings.TEMPLATE_DEBUG = old_td + + def test_extends_include_missing_baseloader(self): + """ + Tests that the correct template is identified as not existing + when {% extends %} specifies a template that does exist, but + that template has an {% include %} of something that does not + exist. See #12787. + """ + + # TEMPLATE_DEBUG must be true, otherwise the exception raised + # during {% include %} processing will be suppressed. + old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, True + old_loaders = loader.template_source_loaders + + try: + # Test the base loader class via the app loader. load_template + # from base is used by all shipped loaders excepting cached, + # which has its own test. + loader.template_source_loaders = (app_directories.Loader(),) + + load_name = 'test_extends_error.html' + tmpl = loader.get_template(load_name) + r = None + try: + r = tmpl.render(template.Context({})) + except template.TemplateSyntaxError, e: + settings.TEMPLATE_DEBUG = old_td + self.assertEqual(e.args[0], 'Caught TemplateDoesNotExist while rendering: missing.html') + self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r) + finally: + loader.template_source_loaders = old_loaders + settings.TEMPLATE_DEBUG = old_td + + def test_extends_include_missing_cachedloader(self): + """ + Same as test_extends_include_missing_baseloader, only tests + behavior of the cached loader instead of BaseLoader. + """ + + old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, True + old_loaders = loader.template_source_loaders + + try: + cache_loader = cached.Loader(('',)) + cache_loader._cached_loaders = (app_directories.Loader(),) + loader.template_source_loaders = (cache_loader,) + + load_name = 'test_extends_error.html' + tmpl = loader.get_template(load_name) + r = None + try: + r = tmpl.render(template.Context({})) + except template.TemplateSyntaxError, e: + self.assertEqual(e.args[0], 'Caught TemplateDoesNotExist while rendering: missing.html') + self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r) + + # For the cached loader, repeat the test, to ensure the first attempt did not cache a + # result that behaves incorrectly on subsequent attempts. + tmpl = loader.get_template(load_name) + try: + tmpl.render(template.Context({})) + except template.TemplateSyntaxError, e: + self.assertEqual(e.args[0], 'Caught TemplateDoesNotExist while rendering: missing.html') + self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r) + finally: + loader.template_source_loaders = old_loaders + settings.TEMPLATE_DEBUG = old_td + + def test_token_smart_split(self): + # Regression test for #7027 + token = template.Token(template.TOKEN_BLOCK, 'sometag _("Page not found") value|yesno:_("yes,no")') + split = token.split_contents() + self.assertEqual(split, ["sometag", '_("Page not found")', 'value|yesno:_("yes,no")']) + + def test_url_reverse_no_settings_module(self): + # Regression test for #9005 + from django.template import Template, Context, TemplateSyntaxError + + old_settings_module = settings.SETTINGS_MODULE + old_template_debug = settings.TEMPLATE_DEBUG + + settings.SETTINGS_MODULE = None + settings.TEMPLATE_DEBUG = True + + t = Template('{% url will_not_match %}') + c = Context() + try: + rendered = t.render(c) + except TemplateSyntaxError, e: + # Assert that we are getting the template syntax error and not the + # string encoding error. + self.assertEquals(e.args[0], "Caught NoReverseMatch while rendering: Reverse for 'will_not_match' with arguments '()' and keyword arguments '{}' not found.") + + settings.SETTINGS_MODULE = old_settings_module + settings.TEMPLATE_DEBUG = old_template_debug + + def test_invalid_block_suggestion(self): + # See #7876 + from django.template import Template, TemplateSyntaxError + try: + t = Template("{% if 1 %}lala{% endblock %}{% endif %}") + except TemplateSyntaxError, e: + self.assertEquals(e.args[0], "Invalid block tag: 'endblock', expected 'else' or 'endif'") + + def test_templates(self): + template_tests = self.get_template_tests() + filter_tests = filters.get_filter_tests() + + # Quickly check that we aren't accidentally using a name in both + # template and filter tests. + overlapping_names = [name for name in filter_tests if name in template_tests] + assert not overlapping_names, 'Duplicate test name(s): %s' % ', '.join(overlapping_names) + + template_tests.update(filter_tests) + + # Register our custom template loader. + def test_template_loader(template_name, template_dirs=None): + "A custom template loader that loads the unit-test templates." + try: + return (template_tests[template_name][0] , "test:%s" % template_name) + except KeyError: + raise template.TemplateDoesNotExist, template_name + + cache_loader = cached.Loader(('test_template_loader',)) + cache_loader._cached_loaders = (test_template_loader,) + + old_template_loaders = loader.template_source_loaders + loader.template_source_loaders = [cache_loader] + + failures = [] + tests = template_tests.items() + tests.sort() + + # Turn TEMPLATE_DEBUG off, because tests assume that. + old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False + + # Set TEMPLATE_STRING_IF_INVALID to a known string. + old_invalid = settings.TEMPLATE_STRING_IF_INVALID + expected_invalid_str = 'INVALID' + + # Warm the URL reversing cache. This ensures we don't pay the cost + # warming the cache during one of the tests. + urlresolvers.reverse('regressiontests.templates.views.client_action', + kwargs={'id':0,'action':"update"}) + + for name, vals in tests: + if isinstance(vals[2], tuple): + normal_string_result = vals[2][0] + invalid_string_result = vals[2][1] + if isinstance(invalid_string_result, basestring) and '%s' in invalid_string_result: + expected_invalid_str = 'INVALID %s' + invalid_string_result = invalid_string_result % vals[2][2] + template.invalid_var_format_string = True + else: + normal_string_result = vals[2] + invalid_string_result = vals[2] + + if 'LANGUAGE_CODE' in vals[1]: + activate(vals[1]['LANGUAGE_CODE']) + else: + activate('en-us') + + for invalid_str, result in [('', normal_string_result), + (expected_invalid_str, invalid_string_result)]: + settings.TEMPLATE_STRING_IF_INVALID = invalid_str + for is_cached in (False, True): + try: + start = datetime.now() + test_template = loader.get_template(name) + end = datetime.now() + if end-start > timedelta(seconds=0.2): + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Took too long to parse test" % (is_cached, invalid_str, name)) + + start = datetime.now() + output = self.render(test_template, vals) + end = datetime.now() + if end-start > timedelta(seconds=0.2): + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Took too long to render test" % (is_cached, invalid_str, name)) + except ContextStackException: + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Context stack was left imbalanced" % (is_cached, invalid_str, name)) + continue + except Exception: + exc_type, exc_value, exc_tb = sys.exc_info() + if exc_type != result: + tb = '\n'.join(traceback.format_exception(exc_type, exc_value, exc_tb)) + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s\n%s" % (is_cached, invalid_str, name, exc_type, exc_value, tb)) + continue + if output != result: + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (is_cached, invalid_str, name, result, output)) + cache_loader.reset() + + if 'LANGUAGE_CODE' in vals[1]: + deactivate() + + if template.invalid_var_format_string: + expected_invalid_str = 'INVALID' + template.invalid_var_format_string = False + + loader.template_source_loaders = old_template_loaders + deactivate() + settings.TEMPLATE_DEBUG = old_td + settings.TEMPLATE_STRING_IF_INVALID = old_invalid + + self.assertEqual(failures, [], "Tests failed:\n%s\n%s" % + ('-'*70, ("\n%s\n" % ('-'*70)).join(failures))) + + def render(self, test_template, vals): + context = template.Context(vals[1]) + before_stack_size = len(context.dicts) + output = test_template.render(context) + if len(context.dicts) != before_stack_size: + raise ContextStackException + return output + + def get_template_tests(self): + # SYNTAX -- + # 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class) + return { + ### BASIC SYNTAX ################################################ + + # Plain text should go through the template parser untouched + 'basic-syntax01': ("something cool", {}, "something cool"), + + # Variables should be replaced with their value in the current + # context + 'basic-syntax02': ("{{ headline }}", {'headline':'Success'}, "Success"), + + # More than one replacement variable is allowed in a template + 'basic-syntax03': ("{{ first }} --- {{ second }}", {"first" : 1, "second" : 2}, "1 --- 2"), + + # Fail silently when a variable is not found in the current context + 'basic-syntax04': ("as{{ missing }}df", {}, ("asdf","asINVALIDdf")), + + # A variable may not contain more than one word + 'basic-syntax06': ("{{ multi word variable }}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError for empty variable tags + 'basic-syntax07': ("{{ }}", {}, template.TemplateSyntaxError), + 'basic-syntax08': ("{{ }}", {}, template.TemplateSyntaxError), + + # Attribute syntax allows a template to call an object's attribute + 'basic-syntax09': ("{{ var.method }}", {"var": SomeClass()}, "SomeClass.method"), + + # Multiple levels of attribute access are allowed + 'basic-syntax10': ("{{ var.otherclass.method }}", {"var": SomeClass()}, "OtherClass.method"), + + # Fail silently when a variable's attribute isn't found + 'basic-syntax11': ("{{ var.blech }}", {"var": SomeClass()}, ("","INVALID")), + + # Raise TemplateSyntaxError when trying to access a variable beginning with an underscore + 'basic-syntax12': ("{{ var.__dict__ }}", {"var": SomeClass()}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError when trying to access a variable containing an illegal character + 'basic-syntax13': ("{{ va>r }}", {}, template.TemplateSyntaxError), + 'basic-syntax14': ("{{ (var.r) }}", {}, template.TemplateSyntaxError), + 'basic-syntax15': ("{{ sp%am }}", {}, template.TemplateSyntaxError), + 'basic-syntax16': ("{{ eggs! }}", {}, template.TemplateSyntaxError), + 'basic-syntax17': ("{{ moo? }}", {}, template.TemplateSyntaxError), + + # Attribute syntax allows a template to call a dictionary key's value + 'basic-syntax18': ("{{ foo.bar }}", {"foo" : {"bar" : "baz"}}, "baz"), + + # Fail silently when a variable's dictionary key isn't found + 'basic-syntax19': ("{{ foo.spam }}", {"foo" : {"bar" : "baz"}}, ("","INVALID")), + + # Fail silently when accessing a non-simple method + 'basic-syntax20': ("{{ var.method2 }}", {"var": SomeClass()}, ("","INVALID")), + + # Don't get confused when parsing something that is almost, but not + # quite, a template tag. + 'basic-syntax21': ("a {{ moo %} b", {}, "a {{ moo %} b"), + 'basic-syntax22': ("{{ moo #}", {}, "{{ moo #}"), + + # Will try to treat "moo #} {{ cow" as the variable. Not ideal, but + # costly to work around, so this triggers an error. + 'basic-syntax23': ("{{ moo #} {{ cow }}", {"cow": "cow"}, template.TemplateSyntaxError), + + # Embedded newlines make it not-a-tag. + 'basic-syntax24': ("{{ moo\n }}", {}, "{{ moo\n }}"), + + # Literal strings are permitted inside variables, mostly for i18n + # purposes. + 'basic-syntax25': ('{{ "fred" }}', {}, "fred"), + 'basic-syntax26': (r'{{ "\"fred\"" }}', {}, "\"fred\""), + 'basic-syntax27': (r'{{ _("\"fred\"") }}', {}, "\"fred\""), + + # regression test for ticket #12554 + # make sure a silent_variable_failure Exception is supressed + # on dictionary and attribute lookup + '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"), + + # Fail silently when the list index is out of range. + 'list-index02': ("{{ var.5 }}", {"var": ["first item", "second item"]}, ("", "INVALID")), + + # Fail silently when the variable is not a subscriptable object. + 'list-index03': ("{{ var.1 }}", {"var": None}, ("", "INVALID")), + + # Fail silently when variable is a dict without the specified key. + 'list-index04': ("{{ var.1 }}", {"var": {}}, ("", "INVALID")), + + # Dictionary lookup wins out when dict's key is a string. + 'list-index05': ("{{ var.1 }}", {"var": {'1': "hello"}}, "hello"), + + # But list-index lookup wins out when dict's key is an int, which + # behind the scenes is really a dictionary lookup (for a dict) + # after converting the key to an int. + 'list-index06': ("{{ var.1 }}", {"var": {1: "hello"}}, "hello"), + + # Dictionary lookup wins out when there is a string and int version of the key. + 'list-index07': ("{{ var.1 }}", {"var": {'1': "hello", 1: "world"}}, "hello"), + + # Basic filter usage + 'filter-syntax01': ("{{ var|upper }}", {"var": "Django is the greatest!"}, "DJANGO IS THE GREATEST!"), + + # Chained filters + 'filter-syntax02': ("{{ var|upper|lower }}", {"var": "Django is the greatest!"}, "django is the greatest!"), + + # Raise TemplateSyntaxError for space between a variable and filter pipe + 'filter-syntax03': ("{{ var |upper }}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError for space after a filter pipe + 'filter-syntax04': ("{{ var| upper }}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError for a nonexistent filter + 'filter-syntax05': ("{{ var|does_not_exist }}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError when trying to access a filter containing an illegal character + 'filter-syntax06': ("{{ var|fil(ter) }}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError for invalid block tags + 'filter-syntax07': ("{% nothing_to_see_here %}", {}, template.TemplateSyntaxError), + + # Raise TemplateSyntaxError for empty block tags + 'filter-syntax08': ("{% %}", {}, template.TemplateSyntaxError), + + # Chained filters, with an argument to the first one + 'filter-syntax09': ('{{ var|removetags:"b i"|upper|lower }}', {"var": "<b><i>Yes</i></b>"}, "yes"), + + # Literal string as argument is always "safe" from auto-escaping.. + 'filter-syntax10': (r'{{ var|default_if_none:" endquote\" hah" }}', + {"var": None}, ' endquote" hah'), + + # Variable as argument + 'filter-syntax11': (r'{{ var|default_if_none:var2 }}', {"var": None, "var2": "happy"}, 'happy'), + + # Default argument testing + 'filter-syntax12': (r'{{ var|yesno:"yup,nup,mup" }} {{ var|yesno }}', {"var": True}, 'yup yes'), + + # Fail silently for methods that raise an exception with a + # "silent_variable_failure" attribute + 'filter-syntax13': (r'1{{ var.method3 }}2', {"var": SomeClass()}, ("12", "1INVALID2")), + + # In methods that raise an exception without a + # "silent_variable_attribute" set to True, the exception propagates + 'filter-syntax14': (r'1{{ var.method4 }}2', {"var": SomeClass()}, SomeOtherException), + + # Escaped backslash in argument + 'filter-syntax15': (r'{{ var|default_if_none:"foo\bar" }}', {"var": None}, r'foo\bar'), + + # Escaped backslash using known escape char + 'filter-syntax16': (r'{{ var|default_if_none:"foo\now" }}', {"var": None}, r'foo\now'), + + # Empty strings can be passed as arguments to filters + 'filter-syntax17': (r'{{ var|join:"" }}', {'var': ['a', 'b', 'c']}, 'abc'), + + # Make sure that any unicode strings are converted to bytestrings + # in the final output. + 'filter-syntax18': (r'{{ var }}', {'var': UTF8Class()}, u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111'), + + # Numbers as filter arguments should work + 'filter-syntax19': ('{{ var|truncatewords:1 }}', {"var": "hello world"}, "hello ..."), + + #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"), + + # Comments can contain invalid stuff. + 'comment-syntax03': ("foo{# {% if %} #}", {}, "foo"), + 'comment-syntax04': ("foo{# {% endblock %} #}", {}, "foo"), + 'comment-syntax05': ("foo{# {% somerandomtag %} #}", {}, "foo"), + 'comment-syntax06': ("foo{# {% #}", {}, "foo"), + 'comment-syntax07': ("foo{# %} #}", {}, "foo"), + 'comment-syntax08': ("foo{# %} #}bar", {}, "foobar"), + 'comment-syntax09': ("foo{# {{ #}", {}, "foo"), + 'comment-syntax10': ("foo{# }} #}", {}, "foo"), + 'comment-syntax11': ("foo{# { #}", {}, "foo"), + 'comment-syntax12': ("foo{# } #}", {}, "foo"), + + ### COMMENT TAG ########################################################### + 'comment-tag01': ("{% comment %}this is hidden{% endcomment %}hello", {}, "hello"), + 'comment-tag02': ("{% comment %}this is hidden{% endcomment %}hello{% comment %}foo{% endcomment %}", {}, "hello"), + + # Comment tag can contain invalid stuff. + 'comment-tag03': ("foo{% comment %} {% if %} {% endcomment %}", {}, "foo"), + 'comment-tag04': ("foo{% comment %} {% endblock %} {% endcomment %}", {}, "foo"), + 'comment-tag05': ("foo{% comment %} {% somerandomtag %} {% endcomment %}", {}, "foo"), + + ### CYCLE TAG ############################################################# + 'cycle01': ('{% cycle a %}', {}, template.TemplateSyntaxError), + 'cycle02': ('{% cycle a,b,c as abc %}{% cycle abc %}', {}, 'ab'), + 'cycle03': ('{% cycle a,b,c as abc %}{% cycle abc %}{% cycle abc %}', {}, 'abc'), + 'cycle04': ('{% cycle a,b,c as abc %}{% cycle abc %}{% cycle abc %}{% cycle abc %}', {}, 'abca'), + 'cycle05': ('{% cycle %}', {}, template.TemplateSyntaxError), + 'cycle06': ('{% cycle a %}', {}, template.TemplateSyntaxError), + 'cycle07': ('{% cycle a,b,c as foo %}{% cycle bar %}', {}, template.TemplateSyntaxError), + 'cycle08': ('{% cycle a,b,c as foo %}{% cycle foo %}{{ foo }}{{ foo }}{% cycle foo %}{{ foo }}', {}, 'abbbcc'), + 'cycle09': ("{% for i in test %}{% cycle a,b %}{{ i }},{% endfor %}", {'test': range(5)}, 'a0,b1,a2,b3,a4,'), + 'cycle10': ("{% cycle 'a' 'b' 'c' as abc %}{% cycle abc %}", {}, 'ab'), + 'cycle11': ("{% cycle 'a' 'b' 'c' as abc %}{% cycle abc %}{% cycle abc %}", {}, 'abc'), + 'cycle12': ("{% cycle 'a' 'b' 'c' as abc %}{% cycle abc %}{% cycle abc %}{% cycle abc %}", {}, 'abca'), + 'cycle13': ("{% for i in test %}{% cycle 'a' 'b' %}{{ i }},{% endfor %}", {'test': range(5)}, 'a0,b1,a2,b3,a4,'), + 'cycle14': ("{% cycle one two as foo %}{% cycle foo %}", {'one': '1','two': '2'}, '12'), + 'cycle15': ("{% for i in test %}{% cycle aye bee %}{{ i }},{% endfor %}", {'test': range(5), 'aye': 'a', 'bee': 'b'}, 'a0,b1,a2,b3,a4,'), + 'cycle16': ("{% cycle one|lower two as foo %}{% cycle foo %}", {'one': 'A','two': '2'}, 'a2'), + + ### EXCEPTIONS ############################################################ + + # Raise exception for invalid template name + 'exception01': ("{% extends 'nonexistent' %}", {}, template.TemplateDoesNotExist), + + # Raise exception for invalid template name (in variable) + 'exception02': ("{% extends nonexistent %}", {}, (template.TemplateSyntaxError, template.TemplateDoesNotExist)), + + # Raise exception for extra {% extends %} tags + 'exception03': ("{% extends 'inheritance01' %}{% block first %}2{% endblock %}{% extends 'inheritance16' %}", {}, template.TemplateSyntaxError), + + # Raise exception for custom tags used in child with {% load %} tag in parent, not in child + 'exception04': ("{% extends 'inheritance17' %}{% block first %}{% echo 400 %}5678{% endblock %}", {}, template.TemplateSyntaxError), + + ### FILTER TAG ############################################################ + 'filter01': ('{% filter upper %}{% endfilter %}', {}, ''), + 'filter02': ('{% filter upper %}django{% endfilter %}', {}, 'DJANGO'), + 'filter03': ('{% filter upper|lower %}django{% endfilter %}', {}, 'django'), + 'filter04': ('{% filter cut:remove %}djangospam{% endfilter %}', {'remove': 'spam'}, 'django'), + + ### FIRSTOF TAG ########################################################### + 'firstof01': ('{% firstof a b c %}', {'a':0,'b':0,'c':0}, ''), + 'firstof02': ('{% firstof a b c %}', {'a':1,'b':0,'c':0}, '1'), + 'firstof03': ('{% firstof a b c %}', {'a':0,'b':2,'c':0}, '2'), + 'firstof04': ('{% firstof a b c %}', {'a':0,'b':0,'c':3}, '3'), + 'firstof05': ('{% firstof a b c %}', {'a':1,'b':2,'c':3}, '1'), + 'firstof06': ('{% firstof a b c %}', {'b':0,'c':3}, '3'), + 'firstof07': ('{% firstof a b "c" %}', {'a':0}, 'c'), + 'firstof08': ('{% firstof a b "c and d" %}', {'a':0,'b':0}, 'c and d'), + 'firstof09': ('{% firstof %}', {}, template.TemplateSyntaxError), + + ### FOR TAG ############################################################### + 'for-tag01': ("{% for val in values %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "123"), + 'for-tag02': ("{% for val in values reversed %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "321"), + 'for-tag-vars01': ("{% for val in values %}{{ forloop.counter }}{% endfor %}", {"values": [6, 6, 6]}, "123"), + 'for-tag-vars02': ("{% for val in values %}{{ forloop.counter0 }}{% endfor %}", {"values": [6, 6, 6]}, "012"), + 'for-tag-vars03': ("{% for val in values %}{{ forloop.revcounter }}{% endfor %}", {"values": [6, 6, 6]}, "321"), + 'for-tag-vars04': ("{% for val in values %}{{ forloop.revcounter0 }}{% endfor %}", {"values": [6, 6, 6]}, "210"), + 'for-tag-vars05': ("{% for val in values %}{% if forloop.first %}f{% else %}x{% endif %}{% endfor %}", {"values": [6, 6, 6]}, "fxx"), + 'for-tag-vars06': ("{% for val in values %}{% if forloop.last %}l{% else %}x{% endif %}{% endfor %}", {"values": [6, 6, 6]}, "xxl"), + 'for-tag-unpack01': ("{% for key,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), + 'for-tag-unpack03': ("{% for key, value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), + 'for-tag-unpack04': ("{% for key , value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), + 'for-tag-unpack05': ("{% for key ,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), + 'for-tag-unpack06': ("{% for key value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError), + 'for-tag-unpack07': ("{% for key,,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError), + 'for-tag-unpack08': ("{% for key,value, in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError), + # Ensure that a single loopvar doesn't truncate the list in val. + 'for-tag-unpack09': ("{% for val in items %}{{ val.0 }}:{{ val.1 }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), + # Otherwise, silently truncate if the length of loopvars differs to the length of each set of items. + 'for-tag-unpack10': ("{% for x,y in items %}{{ x }}:{{ y }}/{% endfor %}", {"items": (('one', 1, 'carrot'), ('two', 2, 'orange'))}, "one:1/two:2/"), + '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"), + + ### IF TAG ################################################################ + 'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"), + 'if-tag02': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": False}, "no"), + 'if-tag03': ("{% if foo %}yes{% else %}no{% endif %}", {}, "no"), + + # Filters + 'if-tag-filter01': ("{% if foo|length == 5 %}yes{% else %}no{% endif %}", {'foo': 'abcde'}, "yes"), + 'if-tag-filter02': ("{% if foo|upper == 'ABC' %}yes{% else %}no{% endif %}", {}, "no"), + + # Equality + 'if-tag-eq01': ("{% if foo == bar %}yes{% else %}no{% endif %}", {}, "yes"), + 'if-tag-eq02': ("{% if foo == bar %}yes{% else %}no{% endif %}", {'foo': 1}, "no"), + 'if-tag-eq03': ("{% if foo == bar %}yes{% else %}no{% endif %}", {'foo': 1, 'bar': 1}, "yes"), + 'if-tag-eq04': ("{% if foo == bar %}yes{% else %}no{% endif %}", {'foo': 1, 'bar': 2}, "no"), + 'if-tag-eq05': ("{% if foo == '' %}yes{% else %}no{% endif %}", {}, "no"), + + # Comparison + 'if-tag-gt-01': ("{% if 2 > 1 %}yes{% else %}no{% endif %}", {}, "yes"), + 'if-tag-gt-02': ("{% if 1 > 1 %}yes{% else %}no{% endif %}", {}, "no"), + 'if-tag-gte-01': ("{% if 1 >= 1 %}yes{% else %}no{% endif %}", {}, "yes"), + 'if-tag-gte-02': ("{% if 1 >= 2 %}yes{% else %}no{% endif %}", {}, "no"), + 'if-tag-lt-01': ("{% if 1 < 2 %}yes{% else %}no{% endif %}", {}, "yes"), + 'if-tag-lt-02': ("{% if 1 < 1 %}yes{% else %}no{% endif %}", {}, "no"), + 'if-tag-lte-01': ("{% if 1 <= 1 %}yes{% else %}no{% endif %}", {}, "yes"), + 'if-tag-lte-02': ("{% if 2 <= 1 %}yes{% else %}no{% endif %}", {}, "no"), + + # Contains + 'if-tag-in-01': ("{% if 1 in x %}yes{% else %}no{% endif %}", {'x':[1]}, "yes"), + 'if-tag-in-02': ("{% if 2 in x %}yes{% else %}no{% endif %}", {'x':[1]}, "no"), + 'if-tag-not-in-01': ("{% if 1 not in x %}yes{% else %}no{% endif %}", {'x':[1]}, "no"), + 'if-tag-not-in-02': ("{% if 2 not in x %}yes{% else %}no{% endif %}", {'x':[1]}, "yes"), + + # AND + 'if-tag-and01': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), + 'if-tag-and02': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), + 'if-tag-and03': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), + 'if-tag-and04': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), + 'if-tag-and05': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': False}, 'no'), + 'if-tag-and06': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'bar': False}, 'no'), + 'if-tag-and07': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'foo': True}, 'no'), + 'if-tag-and08': ("{% if foo and bar %}yes{% else %}no{% endif %}", {'bar': True}, 'no'), + + # OR + 'if-tag-or01': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), + 'if-tag-or02': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), + 'if-tag-or03': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), + 'if-tag-or04': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), + 'if-tag-or05': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': False}, 'no'), + 'if-tag-or06': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'bar': False}, 'no'), + 'if-tag-or07': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'foo': True}, 'yes'), + 'if-tag-or08': ("{% if foo or bar %}yes{% else %}no{% endif %}", {'bar': True}, 'yes'), + + # multiple ORs + 'if-tag-or09': ("{% if foo or bar or baz %}yes{% else %}no{% endif %}", {'baz': True}, 'yes'), + + # NOT + 'if-tag-not01': ("{% if not foo %}no{% else %}yes{% endif %}", {'foo': True}, 'yes'), + 'if-tag-not02': ("{% if not not foo %}no{% else %}yes{% endif %}", {'foo': True}, 'no'), + # not03 to not05 removed, now TemplateSyntaxErrors + + 'if-tag-not06': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {}, 'no'), + 'if-tag-not07': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), + 'if-tag-not08': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), + 'if-tag-not09': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), + 'if-tag-not10': ("{% if foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), + + 'if-tag-not11': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {}, 'no'), + 'if-tag-not12': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), + 'if-tag-not13': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), + 'if-tag-not14': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), + 'if-tag-not15': ("{% if not foo and bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'no'), + + 'if-tag-not16': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {}, 'yes'), + 'if-tag-not17': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), + 'if-tag-not18': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), + 'if-tag-not19': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), + 'if-tag-not20': ("{% if foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), + + 'if-tag-not21': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {}, 'yes'), + 'if-tag-not22': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'yes'), + 'if-tag-not23': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), + 'if-tag-not24': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), + 'if-tag-not25': ("{% if not foo or bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), + + 'if-tag-not26': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {}, 'yes'), + 'if-tag-not27': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), + 'if-tag-not28': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'no'), + 'if-tag-not29': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'no'), + 'if-tag-not30': ("{% if not foo and not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), + + 'if-tag-not31': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {}, 'yes'), + 'if-tag-not32': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': True}, 'no'), + 'if-tag-not33': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': True, 'bar': False}, 'yes'), + 'if-tag-not34': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': True}, 'yes'), + 'if-tag-not35': ("{% if not foo or not bar %}yes{% else %}no{% endif %}", {'foo': False, 'bar': False}, 'yes'), + + # Various syntax errors + 'if-tag-error01': ("{% if %}yes{% endif %}", {}, template.TemplateSyntaxError), + 'if-tag-error02': ("{% if foo and %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), + 'if-tag-error03': ("{% if foo or %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), + 'if-tag-error04': ("{% if not foo and %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), + 'if-tag-error05': ("{% if not foo or %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError), + 'if-tag-error06': ("{% if abc def %}yes{% endif %}", {}, template.TemplateSyntaxError), + 'if-tag-error07': ("{% if not %}yes{% endif %}", {}, template.TemplateSyntaxError), + 'if-tag-error08': ("{% if and %}yes{% endif %}", {}, template.TemplateSyntaxError), + 'if-tag-error09': ("{% if or %}yes{% endif %}", {}, template.TemplateSyntaxError), + 'if-tag-error10': ("{% if == %}yes{% endif %}", {}, template.TemplateSyntaxError), + 'if-tag-error11': ("{% if 1 == %}yes{% endif %}", {}, template.TemplateSyntaxError), + 'if-tag-error12': ("{% if a not b %}yes{% endif %}", {}, template.TemplateSyntaxError), + + # If evaluations are shortcircuited where possible + # These tests will fail by taking too long to run. When the if clause + # is shortcircuiting correctly, the is_bad() function shouldn't be + # evaluated, and the deliberate sleep won't happen. + 'if-tag-shortcircuit01': ('{% if x.is_true or x.is_bad %}yes{% else %}no{% endif %}', {'x': TestObj()}, "yes"), + 'if-tag-shortcircuit02': ('{% if x.is_false and x.is_bad %}yes{% else %}no{% endif %}', {'x': TestObj()}, "no"), + + # Non-existent args + 'if-tag-badarg01':("{% if x|default_if_none:y %}yes{% endif %}", {}, ''), + 'if-tag-badarg02':("{% if x|default_if_none:y %}yes{% endif %}", {'y': 0}, ''), + 'if-tag-badarg03':("{% if x|default_if_none:y %}yes{% endif %}", {'y': 1}, 'yes'), + 'if-tag-badarg04':("{% if x|default_if_none:y %}yes{% else %}no{% endif %}", {}, 'no'), + + # Additional, more precise parsing tests are in SmartIfTests + + ### IFCHANGED TAG ######################################################### + 'ifchanged01': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', {'num': (1,2,3)}, '123'), + 'ifchanged02': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', {'num': (1,1,3)}, '13'), + 'ifchanged03': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', {'num': (1,1,1)}, '1'), + 'ifchanged04': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', {'num': (1, 2, 3), 'numx': (2, 2, 2)}, '122232'), + 'ifchanged05': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', {'num': (1, 1, 1), 'numx': (1, 2, 3)}, '1123123123'), + 'ifchanged06': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', {'num': (1, 1, 1), 'numx': (2, 2, 2)}, '1222'), + 'ifchanged07': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% for y in numy %}{% ifchanged %}{{ y }}{% endifchanged %}{% endfor %}{% endfor %}{% endfor %}', {'num': (1, 1, 1), 'numx': (2, 2, 2), 'numy': (3, 3, 3)}, '1233323332333'), + 'ifchanged08': ('{% for data in datalist %}{% for c,d in data %}{% if c %}{% ifchanged %}{{ d }}{% endifchanged %}{% endif %}{% endfor %}{% endfor %}', {'datalist': [[(1, 'a'), (1, 'a'), (0, 'b'), (1, 'c')], [(0, 'a'), (1, 'c'), (1, 'd'), (1, 'd'), (0, 'e')]]}, 'accd'), + + # Test one parameter given to ifchanged. + 'ifchanged-param01': ('{% for n in num %}{% ifchanged n %}..{% endifchanged %}{{ n }}{% endfor %}', { 'num': (1,2,3) }, '..1..2..3'), + 'ifchanged-param02': ('{% for n in num %}{% for x in numx %}{% ifchanged n %}..{% endifchanged %}{{ x }}{% endfor %}{% endfor %}', { 'num': (1,2,3), 'numx': (5,6,7) }, '..567..567..567'), + + # Test multiple parameters to ifchanged. + 'ifchanged-param03': ('{% for n in num %}{{ n }}{% for x in numx %}{% ifchanged x n %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1,1,2), 'numx': (5,6,6) }, '156156256'), + + # Test a date+hour like construct, where the hour of the last day + # is the same but the date had changed, so print the hour anyway. + 'ifchanged-param04': ('{% for d in days %}{% ifchanged %}{{ d.day }}{% endifchanged %}{% for h in d.hours %}{% ifchanged d h %}{{ h }}{% endifchanged %}{% endfor %}{% endfor %}', {'days':[{'day':1, 'hours':[1,2,3]},{'day':2, 'hours':[3]},] }, '112323'), + + # Logically the same as above, just written with explicit + # ifchanged for the day. + 'ifchanged-param05': ('{% for d in days %}{% ifchanged d.day %}{{ d.day }}{% endifchanged %}{% for h in d.hours %}{% ifchanged d.day h %}{{ h }}{% endifchanged %}{% endfor %}{% endfor %}', {'days':[{'day':1, 'hours':[1,2,3]},{'day':2, 'hours':[3]},] }, '112323'), + + # Test the else clause of ifchanged. + 'ifchanged-else01': ('{% for id in ids %}{{ id }}{% ifchanged id %}-first{% else %}-other{% endifchanged %},{% endfor %}', {'ids': [1,1,2,2,2,3]}, '1-first,1-other,2-first,2-other,2-other,3-first,'), + + 'ifchanged-else02': ('{% for id in ids %}{{ id }}-{% ifchanged id %}{% cycle red,blue %}{% else %}grey{% endifchanged %},{% endfor %}', {'ids': [1,1,2,2,2,3]}, '1-red,1-grey,2-blue,2-grey,2-grey,3-red,'), + 'ifchanged-else03': ('{% for id in ids %}{{ id }}{% ifchanged id %}-{% cycle red,blue %}{% else %}{% endifchanged %},{% endfor %}', {'ids': [1,1,2,2,2,3]}, '1-red,1,2-blue,2,2,3-red,'), + + 'ifchanged-else04': ('{% for id in ids %}{% ifchanged %}***{{ id }}*{% else %}...{% endifchanged %}{{ forloop.counter }}{% endfor %}', {'ids': [1,1,2,2,2,3,4]}, '***1*1...2***2*3...4...5***3*6***4*7'), + + ### IFEQUAL TAG ########################################################### + 'ifequal01': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 2}, ""), + 'ifequal02': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 1}, "yes"), + 'ifequal03': ("{% ifequal a b %}yes{% else %}no{% endifequal %}", {"a": 1, "b": 2}, "no"), + 'ifequal04': ("{% ifequal a b %}yes{% else %}no{% endifequal %}", {"a": 1, "b": 1}, "yes"), + 'ifequal05': ("{% ifequal a 'test' %}yes{% else %}no{% endifequal %}", {"a": "test"}, "yes"), + 'ifequal06': ("{% ifequal a 'test' %}yes{% else %}no{% endifequal %}", {"a": "no"}, "no"), + 'ifequal07': ('{% ifequal a "test" %}yes{% else %}no{% endifequal %}', {"a": "test"}, "yes"), + 'ifequal08': ('{% ifequal a "test" %}yes{% else %}no{% endifequal %}', {"a": "no"}, "no"), + 'ifequal09': ('{% ifequal a "test" %}yes{% else %}no{% endifequal %}', {}, "no"), + 'ifequal10': ('{% ifequal a b %}yes{% else %}no{% endifequal %}', {}, "yes"), + + # SMART SPLITTING + 'ifequal-split01': ('{% ifequal a "test man" %}yes{% else %}no{% endifequal %}', {}, "no"), + 'ifequal-split02': ('{% ifequal a "test man" %}yes{% else %}no{% endifequal %}', {'a': 'foo'}, "no"), + 'ifequal-split03': ('{% ifequal a "test man" %}yes{% else %}no{% endifequal %}', {'a': 'test man'}, "yes"), + 'ifequal-split04': ("{% ifequal a 'test man' %}yes{% else %}no{% endifequal %}", {'a': 'test man'}, "yes"), + 'ifequal-split05': ("{% ifequal a 'i \"love\" you' %}yes{% else %}no{% endifequal %}", {'a': ''}, "no"), + 'ifequal-split06': ("{% ifequal a 'i \"love\" you' %}yes{% else %}no{% endifequal %}", {'a': 'i "love" you'}, "yes"), + 'ifequal-split07': ("{% ifequal a 'i \"love\" you' %}yes{% else %}no{% endifequal %}", {'a': 'i love you'}, "no"), + 'ifequal-split08': (r"{% ifequal a 'I\'m happy' %}yes{% else %}no{% endifequal %}", {'a': "I'm happy"}, "yes"), + 'ifequal-split09': (r"{% ifequal a 'slash\man' %}yes{% else %}no{% endifequal %}", {'a': r"slash\man"}, "yes"), + 'ifequal-split10': (r"{% ifequal a 'slash\man' %}yes{% else %}no{% endifequal %}", {'a': r"slashman"}, "no"), + + # NUMERIC RESOLUTION + 'ifequal-numeric01': ('{% ifequal x 5 %}yes{% endifequal %}', {'x': '5'}, ''), + 'ifequal-numeric02': ('{% ifequal x 5 %}yes{% endifequal %}', {'x': 5}, 'yes'), + 'ifequal-numeric03': ('{% ifequal x 5.2 %}yes{% endifequal %}', {'x': 5}, ''), + 'ifequal-numeric04': ('{% ifequal x 5.2 %}yes{% endifequal %}', {'x': 5.2}, 'yes'), + 'ifequal-numeric05': ('{% ifequal x 0.2 %}yes{% endifequal %}', {'x': .2}, 'yes'), + 'ifequal-numeric06': ('{% ifequal x .2 %}yes{% endifequal %}', {'x': .2}, 'yes'), + 'ifequal-numeric07': ('{% ifequal x 2. %}yes{% endifequal %}', {'x': 2}, ''), + 'ifequal-numeric08': ('{% ifequal x "5" %}yes{% endifequal %}', {'x': 5}, ''), + 'ifequal-numeric09': ('{% ifequal x "5" %}yes{% endifequal %}', {'x': '5'}, 'yes'), + 'ifequal-numeric10': ('{% ifequal x -5 %}yes{% endifequal %}', {'x': -5}, 'yes'), + 'ifequal-numeric11': ('{% ifequal x -5.2 %}yes{% endifequal %}', {'x': -5.2}, 'yes'), + 'ifequal-numeric12': ('{% ifequal x +5 %}yes{% endifequal %}', {'x': 5}, 'yes'), + + # FILTER EXPRESSIONS AS ARGUMENTS + 'ifequal-filter01': ('{% ifequal a|upper "A" %}x{% endifequal %}', {'a': 'a'}, 'x'), + 'ifequal-filter02': ('{% ifequal "A" a|upper %}x{% endifequal %}', {'a': 'a'}, 'x'), + 'ifequal-filter03': ('{% ifequal a|upper b|upper %}x{% endifequal %}', {'a': 'x', 'b': 'X'}, 'x'), + 'ifequal-filter04': ('{% ifequal x|slice:"1" "a" %}x{% endifequal %}', {'x': 'aaa'}, 'x'), + 'ifequal-filter05': ('{% ifequal x|slice:"1"|upper "A" %}x{% endifequal %}', {'x': 'aaa'}, 'x'), + + ### IFNOTEQUAL TAG ######################################################## + 'ifnotequal01': ("{% ifnotequal a b %}yes{% endifnotequal %}", {"a": 1, "b": 2}, "yes"), + 'ifnotequal02': ("{% ifnotequal a b %}yes{% endifnotequal %}", {"a": 1, "b": 1}, ""), + 'ifnotequal03': ("{% ifnotequal a b %}yes{% else %}no{% endifnotequal %}", {"a": 1, "b": 2}, "yes"), + 'ifnotequal04': ("{% ifnotequal a b %}yes{% else %}no{% endifnotequal %}", {"a": 1, "b": 1}, "no"), + + ### INCLUDE TAG ########################################################### + 'include01': ('{% include "basic-syntax01" %}', {}, "something cool"), + 'include02': ('{% include "basic-syntax02" %}', {'headline': 'Included'}, "Included"), + 'include03': ('{% include template_name %}', {'template_name': 'basic-syntax02', 'headline': 'Included'}, "Included"), + 'include04': ('a{% include "nonexistent" %}b', {}, "ab"), + 'include 05': ('template with a space', {}, 'template with a space'), + 'include06': ('{% include "include 05"%}', {}, 'template with a space'), + + ### NAMED ENDBLOCKS ####################################################### + + # Basic test + 'namedendblocks01': ("1{% block first %}_{% block second %}2{% endblock second %}_{% endblock first %}3", {}, '1_2_3'), + + # Unbalanced blocks + 'namedendblocks02': ("1{% block first %}_{% block second %}2{% endblock first %}_{% endblock second %}3", {}, template.TemplateSyntaxError), + 'namedendblocks03': ("1{% block first %}_{% block second %}2{% endblock %}_{% endblock second %}3", {}, template.TemplateSyntaxError), + 'namedendblocks04': ("1{% block first %}_{% block second %}2{% endblock second %}_{% endblock third %}3", {}, template.TemplateSyntaxError), + 'namedendblocks05': ("1{% block first %}_{% block second %}2{% endblock first %}", {}, template.TemplateSyntaxError), + + # Mixed named and unnamed endblocks + 'namedendblocks06': ("1{% block first %}_{% block second %}2{% endblock %}_{% endblock first %}3", {}, '1_2_3'), + 'namedendblocks07': ("1{% block first %}_{% block second %}2{% endblock second %}_{% endblock %}3", {}, '1_2_3'), + + ### INHERITANCE ########################################################### + + # Standard template with no inheritance + 'inheritance01': ("1{% block first %}&{% endblock %}3{% block second %}_{% endblock %}", {}, '1&3_'), + + # Standard two-level inheritance + 'inheritance02': ("{% extends 'inheritance01' %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {}, '1234'), + + # Three-level with no redefinitions on third level + 'inheritance03': ("{% extends 'inheritance02' %}", {}, '1234'), + + # Two-level with no redefinitions on second level + 'inheritance04': ("{% extends 'inheritance01' %}", {}, '1&3_'), + + # Two-level with double quotes instead of single quotes + 'inheritance05': ('{% extends "inheritance02" %}', {}, '1234'), + + # Three-level with variable parent-template name + 'inheritance06': ("{% extends foo %}", {'foo': 'inheritance02'}, '1234'), + + # Two-level with one block defined, one block not defined + 'inheritance07': ("{% extends 'inheritance01' %}{% block second %}5{% endblock %}", {}, '1&35'), + + # Three-level with one block defined on this level, two blocks defined next level + 'inheritance08': ("{% extends 'inheritance02' %}{% block second %}5{% endblock %}", {}, '1235'), + + # Three-level with second and third levels blank + 'inheritance09': ("{% extends 'inheritance04' %}", {}, '1&3_'), + + # Three-level with space NOT in a block -- should be ignored + 'inheritance10': ("{% extends 'inheritance04' %} ", {}, '1&3_'), + + # Three-level with both blocks defined on this level, but none on second level + 'inheritance11': ("{% extends 'inheritance04' %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {}, '1234'), + + # Three-level with this level providing one and second level providing the other + 'inheritance12': ("{% extends 'inheritance07' %}{% block first %}2{% endblock %}", {}, '1235'), + + # Three-level with this level overriding second level + 'inheritance13': ("{% extends 'inheritance02' %}{% block first %}a{% endblock %}{% block second %}b{% endblock %}", {}, '1a3b'), + + # A block defined only in a child template shouldn't be displayed + 'inheritance14': ("{% extends 'inheritance01' %}{% block newblock %}NO DISPLAY{% endblock %}", {}, '1&3_'), + + # A block within another block + 'inheritance15': ("{% extends 'inheritance01' %}{% block first %}2{% block inner %}inner{% endblock %}{% endblock %}", {}, '12inner3_'), + + # A block within another block (level 2) + 'inheritance16': ("{% extends 'inheritance15' %}{% block inner %}out{% endblock %}", {}, '12out3_'), + + # {% load %} tag (parent -- setup for exception04) + 'inheritance17': ("{% load testtags %}{% block first %}1234{% endblock %}", {}, '1234'), + + # {% load %} tag (standard usage, without inheritance) + 'inheritance18': ("{% load testtags %}{% echo this that theother %}5678", {}, 'this that theother5678'), + + # {% load %} tag (within a child template) + 'inheritance19': ("{% extends 'inheritance01' %}{% block first %}{% load testtags %}{% echo 400 %}5678{% endblock %}", {}, '140056783_'), + + # Two-level inheritance with {{ block.super }} + 'inheritance20': ("{% extends 'inheritance01' %}{% block first %}{{ block.super }}a{% endblock %}", {}, '1&a3_'), + + # Three-level inheritance with {{ block.super }} from parent + 'inheritance21': ("{% extends 'inheritance02' %}{% block first %}{{ block.super }}a{% endblock %}", {}, '12a34'), + + # Three-level inheritance with {{ block.super }} from grandparent + 'inheritance22': ("{% extends 'inheritance04' %}{% block first %}{{ block.super }}a{% endblock %}", {}, '1&a3_'), + + # Three-level inheritance with {{ block.super }} from parent and grandparent + 'inheritance23': ("{% extends 'inheritance20' %}{% block first %}{{ block.super }}b{% endblock %}", {}, '1&ab3_'), + + # Inheritance from local context without use of template loader + 'inheritance24': ("{% extends context_template %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {'context_template': template.Template("1{% block first %}_{% endblock %}3{% block second %}_{% endblock %}")}, '1234'), + + # Inheritance from local context with variable parent template + 'inheritance25': ("{% extends context_template.1 %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {'context_template': [template.Template("Wrong"), template.Template("1{% block first %}_{% endblock %}3{% block second %}_{% endblock %}")]}, '1234'), + + # Set up a base template to extend + 'inheritance26': ("no tags", {}, 'no tags'), + + # Inheritance from a template that doesn't have any blocks + 'inheritance27': ("{% extends 'inheritance26' %}", {}, 'no tags'), + + # Set up a base template with a space in it. + 'inheritance 28': ("{% block first %}!{% endblock %}", {}, '!'), + + # Inheritance from a template with a space in its name should work. + 'inheritance29': ("{% extends 'inheritance 28' %}", {}, '!'), + + # Base template, putting block in a conditional {% if %} tag + 'inheritance30': ("1{% if optional %}{% block opt %}2{% endblock %}{% endif %}3", {'optional': True}, '123'), + + # Inherit from a template with block wrapped in an {% if %} tag (in parent), still gets overridden + 'inheritance31': ("{% extends 'inheritance30' %}{% block opt %}two{% endblock %}", {'optional': True}, '1two3'), + 'inheritance32': ("{% extends 'inheritance30' %}{% block opt %}two{% endblock %}", {}, '13'), + + # Base template, putting block in a conditional {% ifequal %} tag + 'inheritance33': ("1{% ifequal optional 1 %}{% block opt %}2{% endblock %}{% endifequal %}3", {'optional': 1}, '123'), + + # Inherit from a template with block wrapped in an {% ifequal %} tag (in parent), still gets overridden + 'inheritance34': ("{% extends 'inheritance33' %}{% block opt %}two{% endblock %}", {'optional': 1}, '1two3'), + 'inheritance35': ("{% extends 'inheritance33' %}{% block opt %}two{% endblock %}", {'optional': 2}, '13'), + + # Base template, putting block in a {% for %} tag + 'inheritance36': ("{% for n in numbers %}_{% block opt %}{{ n }}{% endblock %}{% endfor %}_", {'numbers': '123'}, '_1_2_3_'), + + # Inherit from a template with block wrapped in an {% for %} tag (in parent), still gets overridden + 'inheritance37': ("{% extends 'inheritance36' %}{% block opt %}X{% endblock %}", {'numbers': '123'}, '_X_X_X_'), + 'inheritance38': ("{% extends 'inheritance36' %}{% block opt %}X{% endblock %}", {}, '_'), + + # The super block will still be found. + 'inheritance39': ("{% extends 'inheritance30' %}{% block opt %}new{{ block.super }}{% endblock %}", {'optional': True}, '1new23'), + 'inheritance40': ("{% extends 'inheritance33' %}{% block opt %}new{{ block.super }}{% endblock %}", {'optional': 1}, '1new23'), + 'inheritance41': ("{% extends 'inheritance36' %}{% block opt %}new{{ block.super }}{% endblock %}", {'numbers': '123'}, '_new1_new2_new3_'), + + ### I18N ################################################################## + + # {% spaceless %} tag + 'spaceless01': ("{% spaceless %} <b> <i> text </i> </b> {% endspaceless %}", {}, "<b><i> text </i></b>"), + 'spaceless02': ("{% spaceless %} <b> \n <i> text </i> \n </b> {% endspaceless %}", {}, "<b><i> text </i></b>"), + 'spaceless03': ("{% spaceless %}<b><i>text</i></b>{% endspaceless %}", {}, "<b><i>text</i></b>"), + + # simple translation of a string delimited by ' + 'i18n01': ("{% load i18n %}{% trans 'xxxyyyxxx' %}", {}, "xxxyyyxxx"), + + # simple translation of a string delimited by " + 'i18n02': ('{% load i18n %}{% trans "xxxyyyxxx" %}', {}, "xxxyyyxxx"), + + # simple translation of a variable + 'i18n03': ('{% load i18n %}{% blocktrans %}{{ anton }}{% endblocktrans %}', {'anton': '\xc3\x85'}, u"Å"), + + # simple translation of a variable and filter + 'i18n04': ('{% load i18n %}{% blocktrans with anton|lower as berta %}{{ berta }}{% endblocktrans %}', {'anton': '\xc3\x85'}, u'å'), + + # simple translation of a string with interpolation + 'i18n05': ('{% load i18n %}{% blocktrans %}xxx{{ anton }}xxx{% endblocktrans %}', {'anton': 'yyy'}, "xxxyyyxxx"), + + # simple translation of a string to german + 'i18n06': ('{% load i18n %}{% trans "Page not found" %}', {'LANGUAGE_CODE': 'de'}, "Seite nicht gefunden"), + + # translation of singular form + 'i18n07': ('{% load i18n %}{% blocktrans count number as counter %}singular{% plural %}{{ counter }} plural{% endblocktrans %}', {'number': 1}, "singular"), + + # translation of plural form + 'i18n08': ('{% load i18n %}{% blocktrans count number as counter %}singular{% plural %}{{ counter }} plural{% endblocktrans %}', {'number': 2}, "2 plural"), + + # simple non-translation (only marking) of a string to german + 'i18n09': ('{% load i18n %}{% trans "Page not found" noop %}', {'LANGUAGE_CODE': 'de'}, "Page not found"), + + # translation of a variable with a translated filter + 'i18n10': ('{{ bool|yesno:_("yes,no,maybe") }}', {'bool': True, 'LANGUAGE_CODE': 'de'}, 'Ja'), + + # translation of a variable with a non-translated filter + 'i18n11': ('{{ bool|yesno:"ja,nein" }}', {'bool': True}, 'ja'), + + # usage of the get_available_languages tag + 'i18n12': ('{% load i18n %}{% get_available_languages as langs %}{% for lang in langs %}{% ifequal lang.0 "de" %}{{ lang.0 }}{% endifequal %}{% endfor %}', {}, 'de'), + + # translation of constant strings + 'i18n13': ('{{ _("Password") }}', {'LANGUAGE_CODE': 'de'}, 'Passwort'), + 'i18n14': ('{% cycle "foo" _("Password") _(\'Password\') as c %} {% cycle c %} {% cycle c %}', {'LANGUAGE_CODE': 'de'}, 'foo Passwort Passwort'), + 'i18n15': ('{{ absent|default:_("Password") }}', {'LANGUAGE_CODE': 'de', 'absent': ""}, 'Passwort'), + 'i18n16': ('{{ _("<") }}', {'LANGUAGE_CODE': 'de'}, '<'), + + # Escaping inside blocktrans and trans works as if it was directly in the + # template. + 'i18n17': ('{% load i18n %}{% blocktrans with anton|escape as berta %}{{ berta }}{% endblocktrans %}', {'anton': 'α & β'}, u'α & β'), + 'i18n18': ('{% load i18n %}{% blocktrans with anton|force_escape as berta %}{{ berta }}{% endblocktrans %}', {'anton': 'α & β'}, u'α & β'), + 'i18n19': ('{% load i18n %}{% blocktrans %}{{ andrew }}{% endblocktrans %}', {'andrew': 'a & b'}, u'a & b'), + 'i18n20': ('{% load i18n %}{% trans andrew %}', {'andrew': 'a & b'}, u'a & b'), + 'i18n21': ('{% load i18n %}{% blocktrans %}{{ andrew }}{% endblocktrans %}', {'andrew': mark_safe('a & b')}, u'a & b'), + 'i18n22': ('{% load i18n %}{% trans andrew %}', {'andrew': mark_safe('a & b')}, u'a & b'), + + # Use filters with the {% trans %} tag, #5972 + 'i18n23': ('{% load i18n %}{% trans "Page not found"|capfirst|slice:"6:" %}', {'LANGUAGE_CODE': 'de'}, u'nicht gefunden'), + 'i18n24': ("{% load i18n %}{% trans 'Page not found'|upper %}", {'LANGUAGE_CODE': 'de'}, u'SEITE NICHT GEFUNDEN'), + 'i18n25': ('{% load i18n %}{% trans somevar|upper %}', {'somevar': 'Page not found', 'LANGUAGE_CODE': 'de'}, u'SEITE NICHT GEFUNDEN'), + + # translation of plural form with extra field in singular form (#13568) + 'i18n26': ('{% load i18n %}{% blocktrans with myextra_field as extra_field count number as counter %}singular {{ extra_field }}{% plural %}plural{% endblocktrans %}', {'number': 1, 'myextra_field': 'test'}, "singular test"), + + # translation of singular form in russian (#14126) + 'i18n27': ('{% load i18n %}{% blocktrans count number as counter %}1 result{% plural %}{{ counter }} results{% endblocktrans %}', {'number': 1, 'LANGUAGE_CODE': 'ru'}, u'1 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442'), + + ### HANDLING OF TEMPLATE_STRING_IF_INVALID ################################### + + 'invalidstr01': ('{{ var|default:"Foo" }}', {}, ('Foo','INVALID')), + 'invalidstr02': ('{{ var|default_if_none:"Foo" }}', {}, ('','INVALID')), + 'invalidstr03': ('{% for v in var %}({{ v }}){% endfor %}', {}, ''), + 'invalidstr04': ('{% if var %}Yes{% else %}No{% endif %}', {}, 'No'), + 'invalidstr04': ('{% if var|default:"Foo" %}Yes{% else %}No{% endif %}', {}, 'Yes'), + 'invalidstr05': ('{{ var }}', {}, ('', 'INVALID %s', 'var')), + 'invalidstr06': ('{{ var.prop }}', {'var': {}}, ('', 'INVALID %s', 'var.prop')), + + ### MULTILINE ############################################################# + + 'multiline01': (""" + Hello, + boys. + How + are + you + gentlemen. + """, + {}, + """ + Hello, + boys. + How + are + you + gentlemen. + """), + + ### REGROUP TAG ########################################################### + 'regroup01': ('{% regroup data by bar as grouped %}' + \ + '{% for group in grouped %}' + \ + '{{ group.grouper }}:' + \ + '{% for item in group.list %}' + \ + '{{ item.foo }}' + \ + '{% endfor %},' + \ + '{% endfor %}', + {'data': [ {'foo':'c', 'bar':1}, + {'foo':'d', 'bar':1}, + {'foo':'a', 'bar':2}, + {'foo':'b', 'bar':2}, + {'foo':'x', 'bar':3} ]}, + '1:cd,2:ab,3:x,'), + + # Test for silent failure when target variable isn't found + 'regroup02': ('{% regroup data by bar as grouped %}' + \ + '{% for group in grouped %}' + \ + '{{ group.grouper }}:' + \ + '{% for item in group.list %}' + \ + '{{ item.foo }}' + \ + '{% endfor %},' + \ + '{% endfor %}', + {}, ''), + + ### TEMPLATETAG TAG ####################################################### + 'templatetag01': ('{% templatetag openblock %}', {}, '{%'), + 'templatetag02': ('{% templatetag closeblock %}', {}, '%}'), + 'templatetag03': ('{% templatetag openvariable %}', {}, '{{'), + 'templatetag04': ('{% templatetag closevariable %}', {}, '}}'), + 'templatetag05': ('{% templatetag %}', {}, template.TemplateSyntaxError), + 'templatetag06': ('{% templatetag foo %}', {}, template.TemplateSyntaxError), + 'templatetag07': ('{% templatetag openbrace %}', {}, '{'), + 'templatetag08': ('{% templatetag closebrace %}', {}, '}'), + 'templatetag09': ('{% templatetag openbrace %}{% templatetag openbrace %}', {}, '{{'), + 'templatetag10': ('{% templatetag closebrace %}{% templatetag closebrace %}', {}, '}}'), + 'templatetag11': ('{% templatetag opencomment %}', {}, '{#'), + 'templatetag12': ('{% templatetag closecomment %}', {}, '#}'), + + ### WIDTHRATIO TAG ######################################################## + 'widthratio01': ('{% widthratio a b 0 %}', {'a':50,'b':100}, '0'), + 'widthratio02': ('{% widthratio a b 100 %}', {'a':0,'b':0}, ''), + 'widthratio03': ('{% widthratio a b 100 %}', {'a':0,'b':100}, '0'), + 'widthratio04': ('{% widthratio a b 100 %}', {'a':50,'b':100}, '50'), + 'widthratio05': ('{% widthratio a b 100 %}', {'a':100,'b':100}, '100'), + + # 62.5 should round to 63 + 'widthratio06': ('{% widthratio a b 100 %}', {'a':50,'b':80}, '63'), + + # 71.4 should round to 71 + 'widthratio07': ('{% widthratio a b 100 %}', {'a':50,'b':70}, '71'), + + # Raise exception if we don't have 3 args, last one an integer + 'widthratio08': ('{% widthratio %}', {}, template.TemplateSyntaxError), + 'widthratio09': ('{% widthratio a b %}', {'a':50,'b':100}, template.TemplateSyntaxError), + 'widthratio10': ('{% widthratio a b 100.0 %}', {'a':50,'b':100}, '50'), + + # #10043: widthratio should allow max_width to be a variable + 'widthratio11': ('{% widthratio a b c %}', {'a':50,'b':100, 'c': 100}, '50'), + + ### WITH TAG ######################################################## + 'with01': ('{% with dict.key as key %}{{ key }}{% endwith %}', {'dict': {'key':50}}, '50'), + 'with02': ('{{ key }}{% with dict.key as key %}{{ key }}-{{ dict.key }}-{{ key }}{% endwith %}{{ key }}', {'dict': {'key':50}}, ('50-50-50', 'INVALID50-50-50INVALID')), + + 'with-error01': ('{% with dict.key xx key %}{{ key }}{% endwith %}', {'dict': {'key':50}}, template.TemplateSyntaxError), + 'with-error02': ('{% with dict.key as %}{{ key }}{% endwith %}', {'dict': {'key':50}}, template.TemplateSyntaxError), + + ### NOW TAG ######################################################## + # Simple case + 'now01': ('{% now "j n Y"%}', {}, str(datetime.now().day) + ' ' + str(datetime.now().month) + ' ' + str(datetime.now().year)), + + # Check parsing of escaped and special characters + 'now02': ('{% now "j "n" Y"%}', {}, template.TemplateSyntaxError), + # 'now03': ('{% now "j \"n\" Y"%}', {}, str(datetime.now().day) + '"' + str(datetime.now().month) + '"' + str(datetime.now().year)), + # 'now04': ('{% now "j \nn\n Y"%}', {}, str(datetime.now().day) + '\n' + str(datetime.now().month) + '\n' + str(datetime.now().year)) + + ### URL TAG ######################################################## + # Successes + 'legacyurl02': ('{% url regressiontests.templates.views.client_action id=client.id,action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'legacyurl02a': ('{% url regressiontests.templates.views.client_action client.id,"update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'legacyurl02b': ("{% url regressiontests.templates.views.client_action id=client.id,action='update' %}", {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'legacyurl02c': ("{% url regressiontests.templates.views.client_action client.id,'update' %}", {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'legacyurl10': ('{% url regressiontests.templates.views.client_action id=client.id,action="two words" %}', {'client': {'id': 1}}, '/url_tag/client/1/two%20words/'), + 'legacyurl13': ('{% url regressiontests.templates.views.client_action id=client.id, action=arg|join:"-" %}', {'client': {'id': 1}, 'arg':['a','b']}, '/url_tag/client/1/a-b/'), + 'legacyurl14': ('{% url regressiontests.templates.views.client_action client.id, arg|join:"-" %}', {'client': {'id': 1}, 'arg':['a','b']}, '/url_tag/client/1/a-b/'), + 'legacyurl16': ('{% url regressiontests.templates.views.client_action action="update",id="1" %}', {}, '/url_tag/client/1/update/'), + 'legacyurl16a': ("{% url regressiontests.templates.views.client_action action='update',id='1' %}", {}, '/url_tag/client/1/update/'), + 'legacyurl17': ('{% url regressiontests.templates.views.client_action client_id=client.my_id,action=action %}', {'client': {'my_id': 1}, 'action': 'update'}, '/url_tag/client/1/update/'), + + 'url01': ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'), + 'url02': ('{% url regressiontests.templates.views.client_action id=client.id action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'url02a': ('{% url regressiontests.templates.views.client_action client.id "update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'url02b': ("{% url regressiontests.templates.views.client_action id=client.id action='update' %}", {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'url02c': ("{% url regressiontests.templates.views.client_action client.id 'update' %}", {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'url03': ('{% url regressiontests.templates.views.index %}', {}, '/url_tag/'), + 'url04': ('{% url named.client client.id %}', {'client': {'id': 1}}, '/url_tag/named-client/1/'), + 'url05': (u'{% url метка_оператора v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'url06': (u'{% url метка_оператора_2 tag=v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'url07': (u'{% url regressiontests.templates.views.client2 tag=v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'url08': (u'{% url метка_оператора v %}', {'v': 'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'url09': (u'{% url метка_оператора_2 tag=v %}', {'v': 'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'url10': ('{% url regressiontests.templates.views.client_action id=client.id action="two words" %}', {'client': {'id': 1}}, '/url_tag/client/1/two%20words/'), + 'url11': ('{% url regressiontests.templates.views.client_action id=client.id action="==" %}', {'client': {'id': 1}}, '/url_tag/client/1/==/'), + 'url12': ('{% url regressiontests.templates.views.client_action id=client.id action="," %}', {'client': {'id': 1}}, '/url_tag/client/1/,/'), + 'url13': ('{% url regressiontests.templates.views.client_action id=client.id action=arg|join:"-" %}', {'client': {'id': 1}, 'arg':['a','b']}, '/url_tag/client/1/a-b/'), + 'url14': ('{% url regressiontests.templates.views.client_action client.id arg|join:"-" %}', {'client': {'id': 1}, 'arg':['a','b']}, '/url_tag/client/1/a-b/'), + 'url15': ('{% url regressiontests.templates.views.client_action 12 "test" %}', {}, '/url_tag/client/12/test/'), + 'url18': ('{% url regressiontests.templates.views.client "1,2" %}', {}, '/url_tag/client/1,2/'), + + # Failures + 'url-fail01': ('{% url %}', {}, template.TemplateSyntaxError), + 'url-fail02': ('{% url no_such_view %}', {}, urlresolvers.NoReverseMatch), + 'url-fail03': ('{% url regressiontests.templates.views.client %}', {}, urlresolvers.NoReverseMatch), + 'url-fail04': ('{% url view id, %}', {}, template.TemplateSyntaxError), + 'url-fail05': ('{% url view id= %}', {}, template.TemplateSyntaxError), + 'url-fail06': ('{% url view a.id=id %}', {}, template.TemplateSyntaxError), + 'url-fail07': ('{% url view a.id!id %}', {}, template.TemplateSyntaxError), + 'url-fail08': ('{% url view id="unterminatedstring %}', {}, template.TemplateSyntaxError), + 'url-fail09': ('{% url view id=", %}', {}, template.TemplateSyntaxError), + + # {% url ... as var %} + 'url-asvar01': ('{% url regressiontests.templates.views.index as url %}', {}, ''), + 'url-asvar02': ('{% url regressiontests.templates.views.index as url %}{{ url }}', {}, '/url_tag/'), + 'url-asvar03': ('{% url no_such_view as url %}{{ url }}', {}, ''), + + ### CACHE TAG ###################################################### + 'cache01': ('{% load cache %}{% cache -1 test %}cache01{% endcache %}', {}, 'cache01'), + 'cache02': ('{% load cache %}{% cache -1 test %}cache02{% endcache %}', {}, 'cache02'), + 'cache03': ('{% load cache %}{% cache 2 test %}cache03{% endcache %}', {}, 'cache03'), + 'cache04': ('{% load cache %}{% cache 2 test %}cache04{% endcache %}', {}, 'cache03'), + 'cache05': ('{% load cache %}{% cache 2 test foo %}cache05{% endcache %}', {'foo': 1}, 'cache05'), + 'cache06': ('{% load cache %}{% cache 2 test foo %}cache06{% endcache %}', {'foo': 2}, 'cache06'), + 'cache07': ('{% load cache %}{% cache 2 test foo %}cache07{% endcache %}', {'foo': 1}, 'cache05'), + + # Allow first argument to be a variable. + 'cache08': ('{% load cache %}{% cache time test foo %}cache08{% endcache %}', {'foo': 2, 'time': 2}, 'cache06'), + 'cache09': ('{% load cache %}{% cache time test foo %}cache09{% endcache %}', {'foo': 3, 'time': -1}, 'cache09'), + 'cache10': ('{% load cache %}{% cache time test foo %}cache10{% endcache %}', {'foo': 3, 'time': -1}, 'cache10'), + + # Raise exception if we don't have at least 2 args, first one integer. + 'cache11': ('{% load cache %}{% cache %}{% endcache %}', {}, template.TemplateSyntaxError), + 'cache12': ('{% load cache %}{% cache 1 %}{% endcache %}', {}, template.TemplateSyntaxError), + 'cache13': ('{% load cache %}{% cache foo bar %}{% endcache %}', {}, template.TemplateSyntaxError), + 'cache14': ('{% load cache %}{% cache foo bar %}{% endcache %}', {'foo': 'fail'}, template.TemplateSyntaxError), + 'cache15': ('{% load cache %}{% cache foo bar %}{% endcache %}', {'foo': []}, template.TemplateSyntaxError), + + # Regression test for #7460. + 'cache16': ('{% load cache %}{% cache 1 foo bar %}{% endcache %}', {'foo': 'foo', 'bar': 'with spaces'}, ''), + + # 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>"), + 'autoescape-tag03': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": "<b>hello</b>"}, "<b>hello</b>"), + + # Autoescape disabling and enabling nest in a predictable way. + 'autoescape-tag04': ("{% autoescape off %}{{ first }} {% autoescape on%}{{ first }}{% endautoescape %}{% endautoescape %}", {"first": "<a>"}, "<a> <a>"), + + 'autoescape-tag05': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": "<b>first</b>"}, "<b>first</b>"), + + # Strings (ASCII or unicode) already marked as "safe" are not + # auto-escaped + 'autoescape-tag06': ("{{ first }}", {"first": mark_safe("<b>first</b>")}, "<b>first</b>"), + 'autoescape-tag07': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": mark_safe(u"<b>Apple</b>")}, u"<b>Apple</b>"), + + # Literal string arguments to filters, if used in the result, are + # safe. + 'autoescape-tag08': (r'{% autoescape on %}{{ var|default_if_none:" endquote\" hah" }}{% endautoescape %}', {"var": None}, ' endquote" hah'), + + # Objects which return safe strings as their __unicode__ method + # won't get double-escaped. + 'autoescape-tag09': (r'{{ unsafe }}', {'unsafe': filters.UnsafeClass()}, 'you & me'), + 'autoescape-tag10': (r'{{ safe }}', {'safe': filters.SafeClass()}, 'you > me'), + + # The "safe" and "escape" filters cannot work due to internal + # 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,&,R," ), + + # Escape requirement survives lookup. + 'autoescape-lookup01': ('{{ var.key }}', { "var": {"key": "this & that" }}, "this & that" ), + + } + + +class TemplateTagLoading(unittest.TestCase): + + def setUp(self): + self.old_path = sys.path[:] + self.old_apps = settings.INSTALLED_APPS + self.egg_dir = '%s/eggs' % os.path.dirname(__file__) + self.old_tag_modules = template.templatetags_modules + template.templatetags_modules = [] + + def tearDown(self): + settings.INSTALLED_APPS = self.old_apps + sys.path = self.old_path + template.templatetags_modules = self.old_tag_modules + + def test_load_error(self): + ttext = "{% load broken_tag %}" + self.assertRaises(template.TemplateSyntaxError, template.Template, ttext) + try: + template.Template(ttext) + except template.TemplateSyntaxError, e: + self.assertTrue('ImportError' in e.args[0]) + self.assertTrue('Xtemplate' in e.args[0]) + + def test_load_error_egg(self): + ttext = "{% load broken_egg %}" + egg_name = '%s/tagsegg.egg' % self.egg_dir + sys.path.append(egg_name) + settings.INSTALLED_APPS = ('tagsegg',) + self.assertRaises(template.TemplateSyntaxError, template.Template, ttext) + try: + template.Template(ttext) + except template.TemplateSyntaxError, e: + self.assertTrue('ImportError' in e.args[0]) + self.assertTrue('Xtemplate' in e.args[0]) + + def test_load_working_egg(self): + ttext = "{% load working_egg %}" + egg_name = '%s/tagsegg.egg' % self.egg_dir + sys.path.append(egg_name) + settings.INSTALLED_APPS = ('tagsegg',) + t = template.Template(ttext) + +if __name__ == "__main__": + unittest.main() diff --git a/parts/django/tests/regressiontests/templates/unicode.py b/parts/django/tests/regressiontests/templates/unicode.py new file mode 100644 index 0000000..05b0e22 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/unicode.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from unittest import TestCase + +from django.template import Template, TemplateEncodingError, Context +from django.utils.safestring import SafeData + + +class UnicodeTests(TestCase): + def test_template(self): + # Templates can be created from unicode strings. + t1 = Template(u'ŠĐĆŽćžšđ {{ var }}') + # Templates can also be created from bytestrings. These are assumed to + # be encoded using UTF-8. + s = '\xc5\xa0\xc4\x90\xc4\x86\xc5\xbd\xc4\x87\xc5\xbe\xc5\xa1\xc4\x91 {{ var }}' + t2 = Template(s) + s = '\x80\xc5\xc0' + self.assertRaises(TemplateEncodingError, Template, s) + + # Contexts can be constructed from unicode or UTF-8 bytestrings. + c1 = Context({"var": "foo"}) + c2 = Context({u"var": "foo"}) + c3 = Context({"var": u"Đđ"}) + c4 = Context({u"var": "\xc4\x90\xc4\x91"}) + + # Since both templates and all four contexts represent the same thing, + # they all render the same (and are returned as unicode objects and + # "safe" objects as well, for auto-escaping purposes). + self.assertEqual(t1.render(c3), t2.render(c3)) + self.assertTrue(isinstance(t1.render(c3), unicode)) + self.assertTrue(isinstance(t1.render(c3), SafeData)) diff --git a/parts/django/tests/regressiontests/templates/urls.py b/parts/django/tests/regressiontests/templates/urls.py new file mode 100644 index 0000000..28d4133 --- /dev/null +++ b/parts/django/tests/regressiontests/templates/urls.py @@ -0,0 +1,17 @@ +# coding: utf-8 +from django.conf.urls.defaults import * +from regressiontests.templates import views + +urlpatterns = patterns('', + + # Test urls for testing reverse lookups + (r'^$', views.index), + (r'^client/([\d,]+)/$', views.client), + (r'^client/(?P<id>\d+)/(?P<action>[^/]+)/$', views.client_action), + (r'^client/(?P<client_id>\d+)/(?P<action>[^/]+)/$', views.client_action), + url(r'^named-client/(\d+)/$', views.client2, name="named.client"), + + # Unicode strings are permitted everywhere. + url(ur'^Юникод/(\w+)/$', views.client2, name=u"метка_оператора"), + url(ur'^Юникод/(?P<tag>\S+)/$', 'regressiontests.templates.views.client2', name=u"метка_оператора_2"), +) diff --git a/parts/django/tests/regressiontests/templates/views.py b/parts/django/tests/regressiontests/templates/views.py new file mode 100644 index 0000000..ca3cecd --- /dev/null +++ b/parts/django/tests/regressiontests/templates/views.py @@ -0,0 +1,13 @@ +# Fake views for testing url reverse lookup + +def index(request): + pass + +def client(request, id): + pass + +def client_action(request, id, action): + pass + +def client2(request, tag): + pass diff --git a/parts/django/tests/regressiontests/test_client_regress/__init__.py b/parts/django/tests/regressiontests/test_client_regress/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/test_client_regress/__init__.py diff --git a/parts/django/tests/regressiontests/test_client_regress/bad_templates/404.html b/parts/django/tests/regressiontests/test_client_regress/bad_templates/404.html new file mode 100644 index 0000000..816bcb9 --- /dev/null +++ b/parts/django/tests/regressiontests/test_client_regress/bad_templates/404.html @@ -0,0 +1,3 @@ +{% block foo %} + +This template is deliberately bad - we want it to raise an exception when it is used. diff --git a/parts/django/tests/regressiontests/test_client_regress/fixtures/testdata.json b/parts/django/tests/regressiontests/test_client_regress/fixtures/testdata.json new file mode 100644 index 0000000..0dcf625 --- /dev/null +++ b/parts/django/tests/regressiontests/test_client_regress/fixtures/testdata.json @@ -0,0 +1,56 @@ +[ + { + "pk": "1", + "model": "auth.user", + "fields": { + "username": "testclient", + "first_name": "Test", + "last_name": "Client", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": "2", + "model": "auth.user", + "fields": { + "username": "inactive", + "first_name": "Inactive", + "last_name": "User", + "is_active": false, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": "3", + "model": "auth.user", + "fields": { + "username": "staff", + "first_name": "Staff", + "last_name": "Member", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient@example.com", + "date_joined": "2006-12-17 07:03:31" + } + } +]
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/test_client_regress/models.py b/parts/django/tests/regressiontests/test_client_regress/models.py new file mode 100644 index 0000000..40c7623 --- /dev/null +++ b/parts/django/tests/regressiontests/test_client_regress/models.py @@ -0,0 +1,864 @@ +# -*- coding: utf-8 -*- +""" +Regression tests for the Test Client, especially the customized assertions. +""" +import os + +from django.conf import settings +from django.core.exceptions import SuspiciousOperation +from django.core.urlresolvers import reverse +from django.template import (TemplateDoesNotExist, TemplateSyntaxError, + Context, loader) +from django.test import TestCase, Client +from django.test.client import encode_file +from django.test.utils import ContextList + + +class AssertContainsTests(TestCase): + def setUp(self): + self.old_templates = settings.TEMPLATE_DIRS + settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), 'templates'),) + + def tearDown(self): + settings.TEMPLATE_DIRS = self.old_templates + + def test_contains(self): + "Responses can be inspected for content, including counting repeated substrings" + response = self.client.get('/test_client_regress/no_template_view/') + + self.assertNotContains(response, 'never') + self.assertContains(response, 'never', 0) + self.assertContains(response, 'once') + self.assertContains(response, 'once', 1) + self.assertContains(response, 'twice') + self.assertContains(response, 'twice', 2) + + 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)") + 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)") + + 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)") + 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)") + + try: + self.assertNotContains(response, 'once') + except AssertionError, e: + self.assertEquals(str(e), "Response should not contain 'once'") + try: + self.assertNotContains(response, 'once', msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Response should not contain 'once'") + + try: + self.assertContains(response, 'never', 1) + except AssertionError, e: + self.assertEquals(str(e), "Found 0 instances of 'never' in response (expected 1)") + try: + self.assertContains(response, 'never', 1, msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Found 0 instances of 'never' in response (expected 1)") + + try: + self.assertContains(response, 'once', 0) + except AssertionError, e: + self.assertEquals(str(e), "Found 1 instances of 'once' in response (expected 0)") + try: + self.assertContains(response, 'once', 0, msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Found 1 instances of 'once' in response (expected 0)") + + try: + self.assertContains(response, 'once', 2) + except AssertionError, e: + self.assertEquals(str(e), "Found 1 instances of 'once' in response (expected 2)") + try: + self.assertContains(response, 'once', 2, msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Found 1 instances of 'once' in response (expected 2)") + + try: + self.assertContains(response, 'twice', 1) + except AssertionError, e: + self.assertEquals(str(e), "Found 2 instances of 'twice' in response (expected 1)") + try: + self.assertContains(response, 'twice', 1, msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Found 2 instances of 'twice' in response (expected 1)") + + try: + self.assertContains(response, 'thrice') + except AssertionError, e: + self.assertEquals(str(e), "Couldn't find 'thrice' in response") + try: + self.assertContains(response, 'thrice', msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Couldn't find 'thrice' in response") + + try: + self.assertContains(response, 'thrice', 3) + except AssertionError, e: + self.assertEquals(str(e), "Found 0 instances of 'thrice' in response (expected 3)") + try: + self.assertContains(response, 'thrice', 3, msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Found 0 instances of 'thrice' in response (expected 3)") + + def test_unicode_contains(self): + "Unicode characters can be found in template context" + #Regression test for #10183 + r = self.client.get('/test_client_regress/check_unicode/') + self.assertContains(r, u'さかき') + self.assertContains(r, '\xe5\xb3\xa0'.decode('utf-8')) + + def test_unicode_not_contains(self): + "Unicode characters can be searched for, and not found in template context" + #Regression test for #10183 + r = self.client.get('/test_client_regress/check_unicode/') + self.assertNotContains(r, u'はたけ') + self.assertNotContains(r, '\xe3\x81\xaf\xe3\x81\x9f\xe3\x81\x91'.decode('utf-8')) + + +class AssertTemplateUsedTests(TestCase): + fixtures = ['testdata.json'] + + def test_no_context(self): + "Template usage assertions work then templates aren't in use" + response = self.client.get('/test_client_regress/no_template_view/') + + # Check that the no template case doesn't mess with the template assertions + self.assertTemplateNotUsed(response, 'GET Template') + + try: + self.assertTemplateUsed(response, 'GET Template') + except AssertionError, e: + self.assertEquals(str(e), "No templates used to render the response") + + try: + self.assertTemplateUsed(response, 'GET Template', msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: No templates used to render the response") + + def test_single_context(self): + "Template assertions work when there is a single context" + response = self.client.get('/test_client/post_view/', {}) + + try: + self.assertTemplateNotUsed(response, 'Empty GET Template') + except AssertionError, e: + self.assertEquals(str(e), "Template 'Empty GET Template' was used unexpectedly in rendering the response") + + try: + self.assertTemplateNotUsed(response, 'Empty GET Template', msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Template 'Empty GET Template' was used unexpectedly in rendering the response") + + try: + self.assertTemplateUsed(response, 'Empty POST Template') + except AssertionError, e: + self.assertEquals(str(e), "Template 'Empty POST Template' was not a template used to render the response. Actual template(s) used: Empty GET Template") + + try: + self.assertTemplateUsed(response, 'Empty POST Template', msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Template 'Empty POST Template' was not a template used to render the response. Actual template(s) used: Empty GET Template") + + def test_multiple_context(self): + "Template assertions work when there are multiple contexts" + post_data = { + 'text': 'Hello World', + 'email': 'foo@example.com', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view_with_template/', post_data) + self.assertContains(response, 'POST data OK') + try: + self.assertTemplateNotUsed(response, "form_view.html") + except AssertionError, e: + self.assertEquals(str(e), "Template 'form_view.html' was used unexpectedly in rendering the response") + + try: + self.assertTemplateNotUsed(response, 'base.html') + except AssertionError, e: + self.assertEquals(str(e), "Template 'base.html' was used unexpectedly in rendering the response") + + try: + self.assertTemplateUsed(response, "Valid POST Template") + except AssertionError, e: + self.assertEquals(str(e), "Template 'Valid POST Template' was not a template used to render the response. Actual template(s) used: form_view.html, base.html") + +class AssertRedirectsTests(TestCase): + def test_redirect_page(self): + "An assertion is raised if the original page couldn't be retrieved as expected" + # This page will redirect with code 301, not 302 + response = self.client.get('/test_client/permanent_redirect_view/') + try: + self.assertRedirects(response, '/test_client/get_view/') + except AssertionError, e: + self.assertEquals(str(e), "Response didn't redirect as expected: Response code was 301 (expected 302)") + + try: + self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Response didn't redirect as expected: Response code was 301 (expected 302)") + + def test_lost_query(self): + "An assertion is raised if the redirect location doesn't preserve GET parameters" + response = self.client.get('/test_client/redirect_view/', {'var': 'value'}) + try: + self.assertRedirects(response, '/test_client/get_view/') + except AssertionError, e: + self.assertEquals(str(e), "Response redirected to 'http://testserver/test_client/get_view/?var=value', expected 'http://testserver/test_client/get_view/'") + + try: + self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Response redirected to 'http://testserver/test_client/get_view/?var=value', expected 'http://testserver/test_client/get_view/'") + + def test_incorrect_target(self): + "An assertion is raised if the response redirects to another target" + response = self.client.get('/test_client/permanent_redirect_view/') + try: + # Should redirect to get_view + self.assertRedirects(response, '/test_client/some_view/') + except AssertionError, e: + self.assertEquals(str(e), "Response didn't redirect as expected: Response code was 301 (expected 302)") + + def test_target_page(self): + "An assertion is raised if the response redirect target cannot be retrieved as expected" + response = self.client.get('/test_client/double_redirect_view/') + try: + # The redirect target responds with a 301 code, not 200 + self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/') + except AssertionError, e: + self.assertEquals(str(e), "Couldn't retrieve redirection page '/test_client/permanent_redirect_view/': response code was 301 (expected 200)") + + try: + # The redirect target responds with a 301 code, not 200 + self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/', msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Couldn't retrieve redirection page '/test_client/permanent_redirect_view/': response code was 301 (expected 200)") + + def test_redirect_chain(self): + "You can follow a redirect chain of multiple redirects" + response = self.client.get('/test_client_regress/redirects/further/more/', {}, follow=True) + self.assertRedirects(response, '/test_client_regress/no_template_view/', + status_code=301, target_status_code=200) + + self.assertEquals(len(response.redirect_chain), 1) + self.assertEquals(response.redirect_chain[0], ('http://testserver/test_client_regress/no_template_view/', 301)) + + def test_multiple_redirect_chain(self): + "You can follow a redirect chain of multiple redirects" + response = self.client.get('/test_client_regress/redirects/', {}, follow=True) + self.assertRedirects(response, '/test_client_regress/no_template_view/', + status_code=301, target_status_code=200) + + self.assertEquals(len(response.redirect_chain), 3) + self.assertEquals(response.redirect_chain[0], ('http://testserver/test_client_regress/redirects/further/', 301)) + self.assertEquals(response.redirect_chain[1], ('http://testserver/test_client_regress/redirects/further/more/', 301)) + self.assertEquals(response.redirect_chain[2], ('http://testserver/test_client_regress/no_template_view/', 301)) + + def test_redirect_chain_to_non_existent(self): + "You can follow a chain to a non-existent view" + response = self.client.get('/test_client_regress/redirect_to_non_existent_view2/', {}, follow=True) + self.assertRedirects(response, '/test_client_regress/non_existent_view/', + status_code=301, target_status_code=404) + + def test_redirect_chain_to_self(self): + "Redirections to self are caught and escaped" + response = self.client.get('/test_client_regress/redirect_to_self/', {}, follow=True) + # The chain of redirects stops once the cycle is detected. + self.assertRedirects(response, '/test_client_regress/redirect_to_self/', + status_code=301, target_status_code=301) + self.assertEquals(len(response.redirect_chain), 2) + + def test_circular_redirect(self): + "Circular redirect chains are caught and escaped" + response = self.client.get('/test_client_regress/circular_redirect_1/', {}, follow=True) + # The chain of redirects will get back to the starting point, but stop there. + self.assertRedirects(response, '/test_client_regress/circular_redirect_2/', + status_code=301, target_status_code=301) + self.assertEquals(len(response.redirect_chain), 4) + + def test_redirect_chain_post(self): + "A redirect chain will be followed from an initial POST post" + response = self.client.post('/test_client_regress/redirects/', + {'nothing': 'to_send'}, follow=True) + self.assertRedirects(response, + '/test_client_regress/no_template_view/', 301, 200) + self.assertEquals(len(response.redirect_chain), 3) + + def test_redirect_chain_head(self): + "A redirect chain will be followed from an initial HEAD request" + response = self.client.head('/test_client_regress/redirects/', + {'nothing': 'to_send'}, follow=True) + self.assertRedirects(response, + '/test_client_regress/no_template_view/', 301, 200) + self.assertEquals(len(response.redirect_chain), 3) + + def test_redirect_chain_options(self): + "A redirect chain will be followed from an initial OPTIONS request" + response = self.client.options('/test_client_regress/redirects/', + {'nothing': 'to_send'}, follow=True) + self.assertRedirects(response, + '/test_client_regress/no_template_view/', 301, 200) + self.assertEquals(len(response.redirect_chain), 3) + + def test_redirect_chain_put(self): + "A redirect chain will be followed from an initial PUT request" + response = self.client.put('/test_client_regress/redirects/', + {'nothing': 'to_send'}, follow=True) + self.assertRedirects(response, + '/test_client_regress/no_template_view/', 301, 200) + self.assertEquals(len(response.redirect_chain), 3) + + def test_redirect_chain_delete(self): + "A redirect chain will be followed from an initial DELETE request" + response = self.client.delete('/test_client_regress/redirects/', + {'nothing': 'to_send'}, follow=True) + self.assertRedirects(response, + '/test_client_regress/no_template_view/', 301, 200) + self.assertEquals(len(response.redirect_chain), 3) + + def test_redirect_chain_on_non_redirect_page(self): + "An assertion is raised if the original page couldn't be retrieved as expected" + # This page will redirect with code 301, not 302 + response = self.client.get('/test_client/get_view/', follow=True) + try: + self.assertRedirects(response, '/test_client/get_view/') + except AssertionError, e: + self.assertEquals(str(e), "Response didn't redirect as expected: Response code was 200 (expected 302)") + + try: + self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Response didn't redirect as expected: Response code was 200 (expected 302)") + + def test_redirect_on_non_redirect_page(self): + "An assertion is raised if the original page couldn't be retrieved as expected" + # This page will redirect with code 301, not 302 + response = self.client.get('/test_client/get_view/') + try: + self.assertRedirects(response, '/test_client/get_view/') + except AssertionError, e: + self.assertEquals(str(e), "Response didn't redirect as expected: Response code was 200 (expected 302)") + + try: + self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') + except AssertionError, e: + self.assertEquals(str(e), "abc: Response didn't redirect as expected: Response code was 200 (expected 302)") + + +class AssertFormErrorTests(TestCase): + def test_unknown_form(self): + "An assertion is raised if the form name is unknown" + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + try: + self.assertFormError(response, 'wrong_form', 'some_field', 'Some error.') + except AssertionError, e: + self.assertEqual(str(e), "The form 'wrong_form' was not used to render the response") + try: + self.assertFormError(response, 'wrong_form', 'some_field', 'Some error.', msg_prefix='abc') + except AssertionError, e: + self.assertEqual(str(e), "abc: The form 'wrong_form' was not used to render the response") + + def test_unknown_field(self): + "An assertion is raised if the field name is unknown" + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + try: + self.assertFormError(response, 'form', 'some_field', 'Some error.') + except AssertionError, e: + self.assertEqual(str(e), "The form 'form' in context 0 does not contain the field 'some_field'") + try: + self.assertFormError(response, 'form', 'some_field', 'Some error.', msg_prefix='abc') + except AssertionError, e: + self.assertEqual(str(e), "abc: The form 'form' in context 0 does not contain the field 'some_field'") + + def test_noerror_field(self): + "An assertion is raised if the field doesn't have any errors" + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + try: + self.assertFormError(response, 'form', 'value', 'Some error.') + except AssertionError, e: + self.assertEqual(str(e), "The field 'value' on form 'form' in context 0 contains no errors") + try: + self.assertFormError(response, 'form', 'value', 'Some error.', msg_prefix='abc') + except AssertionError, e: + self.assertEqual(str(e), "abc: The field 'value' on form 'form' in context 0 contains no errors") + + def test_unknown_error(self): + "An assertion is raised if the field doesn't contain the provided error" + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + try: + self.assertFormError(response, 'form', 'email', 'Some error.') + except AssertionError, e: + self.assertEqual(str(e), "The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [u'Enter a valid e-mail address.'])") + try: + self.assertFormError(response, 'form', 'email', 'Some error.', msg_prefix='abc') + except AssertionError, e: + self.assertEqual(str(e), "abc: The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [u'Enter a valid e-mail address.'])") + + def test_unknown_nonfield_error(self): + """ + Checks that an assertion is raised if the form's non field errors + doesn't contain the provided error. + """ + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + try: + self.assertFormError(response, 'form', None, 'Some error.') + except AssertionError, e: + self.assertEqual(str(e), "The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )") + try: + self.assertFormError(response, 'form', None, 'Some error.', msg_prefix='abc') + except AssertionError, e: + self.assertEqual(str(e), "abc: The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )") + +class LoginTests(TestCase): + fixtures = ['testdata'] + + def test_login_different_client(self): + "Check that using a different test client doesn't violate authentication" + + # Create a second client, and log in. + c = Client() + login = c.login(username='testclient', password='password') + self.assertTrue(login, 'Could not log in') + + # Get a redirection page with the second client. + response = c.get("/test_client_regress/login_protected_redirect_view/") + + # At this points, the self.client isn't logged in. + # Check that assertRedirects uses the original client, not the + # default client. + self.assertRedirects(response, "http://testserver/test_client_regress/get_view/") + + +class SessionEngineTests(TestCase): + fixtures = ['testdata'] + + def setUp(self): + self.old_SESSION_ENGINE = settings.SESSION_ENGINE + settings.SESSION_ENGINE = 'regressiontests.test_client_regress.session' + + def tearDown(self): + settings.SESSION_ENGINE = self.old_SESSION_ENGINE + + def test_login(self): + "A session engine that modifies the session key can be used to log in" + login = self.client.login(username='testclient', password='password') + self.assertTrue(login, 'Could not log in') + + # Try to access a login protected page. + response = self.client.get("/test_client/login_protected_view/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + +class URLEscapingTests(TestCase): + def test_simple_argument_get(self): + "Get a view that has a simple string argument" + response = self.client.get(reverse('arg_view', args=['Slartibartfast'])) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'Howdy, Slartibartfast') + + def test_argument_with_space_get(self): + "Get a view that has a string argument that requires escaping" + response = self.client.get(reverse('arg_view', args=['Arthur Dent'])) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'Hi, Arthur') + + def test_simple_argument_post(self): + "Post for a view that has a simple string argument" + response = self.client.post(reverse('arg_view', args=['Slartibartfast'])) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'Howdy, Slartibartfast') + + def test_argument_with_space_post(self): + "Post for a view that has a string argument that requires escaping" + response = self.client.post(reverse('arg_view', args=['Arthur Dent'])) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'Hi, Arthur') + +class ExceptionTests(TestCase): + fixtures = ['testdata.json'] + + def test_exception_cleared(self): + "#5836 - A stale user exception isn't re-raised by the test client." + + login = self.client.login(username='testclient',password='password') + self.assertTrue(login, 'Could not log in') + try: + response = self.client.get("/test_client_regress/staff_only/") + self.fail("General users should not be able to visit this page") + except SuspiciousOperation: + pass + + # At this point, an exception has been raised, and should be cleared. + + # This next operation should be successful; if it isn't we have a problem. + login = self.client.login(username='staff', password='password') + self.assertTrue(login, 'Could not log in') + try: + self.client.get("/test_client_regress/staff_only/") + except SuspiciousOperation: + self.fail("Staff should be able to visit this page") + +class TemplateExceptionTests(TestCase): + def setUp(self): + # Reset the loaders so they don't try to render cached templates. + if loader.template_source_loaders is not None: + for template_loader in loader.template_source_loaders: + if hasattr(template_loader, 'reset'): + template_loader.reset() + self.old_templates = settings.TEMPLATE_DIRS + settings.TEMPLATE_DIRS = () + + def tearDown(self): + settings.TEMPLATE_DIRS = self.old_templates + + def test_no_404_template(self): + "Missing templates are correctly reported by test client" + try: + response = self.client.get("/no_such_view/") + self.fail("Should get error about missing template") + except TemplateDoesNotExist: + pass + + def test_bad_404_template(self): + "Errors found when rendering 404 error templates are re-raised" + settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), 'bad_templates'),) + try: + response = self.client.get("/no_such_view/") + self.fail("Should get error about syntax error in template") + except TemplateSyntaxError: + pass + +# We need two different tests to check URLconf substitution - one to check +# it was changed, and another one (without self.urls) to check it was reverted on +# teardown. This pair of tests relies upon the alphabetical ordering of test execution. +class UrlconfSubstitutionTests(TestCase): + urls = 'regressiontests.test_client_regress.urls' + + def test_urlconf_was_changed(self): + "TestCase can enforce a custom URLconf on a per-test basis" + url = reverse('arg_view', args=['somename']) + self.assertEquals(url, '/arg_view/somename/') + +# This test needs to run *after* UrlconfSubstitutionTests; the zz prefix in the +# name is to ensure alphabetical ordering. +class zzUrlconfSubstitutionTests(TestCase): + def test_urlconf_was_reverted(self): + "URLconf is reverted to original value after modification in a TestCase" + url = reverse('arg_view', args=['somename']) + self.assertEquals(url, '/test_client_regress/arg_view/somename/') + +class ContextTests(TestCase): + fixtures = ['testdata'] + + def test_single_context(self): + "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') + + try: + response.context['does-not-exist'] + self.fail('Should not be able to retrieve non-existent key') + except KeyError, e: + self.assertEquals(e.args[0], 'does-not-exist') + + def test_inherited_context(self): + "Context variables can be retrieved from a list of contexts" + 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') + + try: + response.context['does-not-exist'] + self.fail('Should not be able to retrieve non-existent key') + except KeyError, e: + self.assertEquals(e.args[0], 'does-not-exist') + + +class SessionTests(TestCase): + fixtures = ['testdata.json'] + + def test_session(self): + "The session isn't lost if a user logs in" + # The session doesn't exist to start. + response = self.client.get('/test_client_regress/check_session/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'NO') + + # This request sets a session variable. + response = self.client.get('/test_client_regress/set_session/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'set_session') + + # Check that the session has been modified + response = self.client.get('/test_client_regress/check_session/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'YES') + + # Log in + login = self.client.login(username='testclient',password='password') + self.assertTrue(login, 'Could not log in') + + # Session should still contain the modified value + response = self.client.get('/test_client_regress/check_session/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'YES') + + def test_logout(self): + """Logout should work whether the user is logged in or not (#9978).""" + self.client.logout() + login = self.client.login(username='testclient',password='password') + self.assertTrue(login, 'Could not log in') + self.client.logout() + self.client.logout() + +class RequestMethodTests(TestCase): + def test_get(self): + "Request a view via request method GET" + response = self.client.get('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'request method: GET') + + def test_post(self): + "Request a view via request method POST" + response = self.client.post('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'request method: POST') + + def test_head(self): + "Request a view via request method HEAD" + response = self.client.head('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + # A HEAD request doesn't return any content. + self.assertNotEqual(response.content, 'request method: HEAD') + self.assertEqual(response.content, '') + + def test_options(self): + "Request a view via request method OPTIONS" + response = self.client.options('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'request method: OPTIONS') + + def test_put(self): + "Request a view via request method PUT" + response = self.client.put('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'request method: PUT') + + def test_delete(self): + "Request a view via request method DELETE" + response = self.client.delete('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'request method: DELETE') + +class RequestMethodStringDataTests(TestCase): + def test_post(self): + "Request a view with string data via request method POST" + # Regression test for #11371 + data = u'{"test": "json"}' + response = self.client.post('/test_client_regress/request_methods/', data=data, content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'request method: POST') + + def test_put(self): + "Request a view with string data via request method PUT" + # Regression test for #11371 + data = u'{"test": "json"}' + response = self.client.put('/test_client_regress/request_methods/', data=data, content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'request method: PUT') + +class QueryStringTests(TestCase): + def test_get_like_requests(self): + for method_name in ('get','head','options','put','delete'): + # A GET-like request can pass a query string as data + method = getattr(self.client, method_name) + response = method("/test_client_regress/request_data/", data={'foo':'whiz'}) + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['request-foo'], 'whiz') + + # A GET-like request can pass a query string as part of the URL + response = method("/test_client_regress/request_data/?foo=whiz") + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['request-foo'], 'whiz') + + # Data provided in the URL to a GET-like request is overridden by actual form data + response = method("/test_client_regress/request_data/?foo=whiz", data={'foo':'bang'}) + self.assertEqual(response.context['get-foo'], 'bang') + self.assertEqual(response.context['request-foo'], 'bang') + + response = method("/test_client_regress/request_data/?foo=whiz", data={'bar':'bang'}) + self.assertEqual(response.context['get-foo'], None) + self.assertEqual(response.context['get-bar'], 'bang') + self.assertEqual(response.context['request-foo'], None) + self.assertEqual(response.context['request-bar'], 'bang') + + def test_post_like_requests(self): + # A POST-like request can pass a query string as data + response = self.client.post("/test_client_regress/request_data/", data={'foo':'whiz'}) + self.assertEqual(response.context['get-foo'], None) + self.assertEqual(response.context['post-foo'], 'whiz') + + # A POST-like request can pass a query string as part of the URL + response = self.client.post("/test_client_regress/request_data/?foo=whiz") + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['post-foo'], None) + self.assertEqual(response.context['request-foo'], 'whiz') + + # POST data provided in the URL augments actual form data + response = self.client.post("/test_client_regress/request_data/?foo=whiz", data={'foo':'bang'}) + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['post-foo'], 'bang') + self.assertEqual(response.context['request-foo'], 'bang') + + response = self.client.post("/test_client_regress/request_data/?foo=whiz", data={'bar':'bang'}) + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['get-bar'], None) + self.assertEqual(response.context['post-foo'], None) + self.assertEqual(response.context['post-bar'], 'bang') + self.assertEqual(response.context['request-foo'], 'whiz') + self.assertEqual(response.context['request-bar'], 'bang') + +class UnicodePayloadTests(TestCase): + def test_simple_unicode_payload(self): + "A simple ASCII-only unicode JSON document can be POSTed" + # Regression test for #10571 + json = u'{"english": "mountain pass"}' + response = self.client.post("/test_client_regress/parse_unicode_json/", json, + content_type="application/json") + self.assertEqual(response.content, json) + + def test_unicode_payload_utf8(self): + "A non-ASCII unicode data encoded as UTF-8 can be POSTed" + # Regression test for #10571 + json = u'{"dog": "собака"}' + response = self.client.post("/test_client_regress/parse_unicode_json/", json, + content_type="application/json; charset=utf-8") + self.assertEqual(response.content, json.encode('utf-8')) + + def test_unicode_payload_utf16(self): + "A non-ASCII unicode data encoded as UTF-16 can be POSTed" + # Regression test for #10571 + json = u'{"dog": "собака"}' + response = self.client.post("/test_client_regress/parse_unicode_json/", json, + content_type="application/json; charset=utf-16") + self.assertEqual(response.content, json.encode('utf-16')) + + def test_unicode_payload_non_utf(self): + "A non-ASCII unicode data as a non-UTF based encoding can be POSTed" + #Regression test for #10571 + json = u'{"dog": "собака"}' + 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/parts/django/tests/regressiontests/test_client_regress/session.py b/parts/django/tests/regressiontests/test_client_regress/session.py new file mode 100644 index 0000000..74ae3b6 --- /dev/null +++ b/parts/django/tests/regressiontests/test_client_regress/session.py @@ -0,0 +1,30 @@ +from django.contrib.sessions.backends.base import SessionBase + +class SessionStore(SessionBase): + """ + A simple cookie-based session storage implemenation. + + The session key is actually the session data, pickled and encoded. + This means that saving the session will change the session key. + """ + def __init__(self, session_key=None): + super(SessionStore, self).__init__(session_key) + + def exists(self, session_key): + return False + + def create(self): + self.session_key = self.encode({}) + + def save(self, must_create=False): + self.session_key = self.encode(self._session) + + def delete(self, session_key=None): + self.session_key = self.encode({}) + + def load(self): + try: + return self.decode(self.session_key) + except: + self.modified = True + return {}
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/test_client_regress/templates/unicode.html b/parts/django/tests/regressiontests/test_client_regress/templates/unicode.html new file mode 100644 index 0000000..bdb6c91 --- /dev/null +++ b/parts/django/tests/regressiontests/test_client_regress/templates/unicode.html @@ -0,0 +1,5 @@ +* 峠 (とうげ tōge "mountain pass") +* 榊 (さかき sakaki "tree, genus Cleyera") +* 辻 (つじ tsuji "crossroads, street") +* 働 (どう dō, はたら hatara(ku) "work") +* 腺 (せん sen, "gland") diff --git a/parts/django/tests/regressiontests/test_client_regress/urls.py b/parts/django/tests/regressiontests/test_client_regress/urls.py new file mode 100644 index 0000000..650d80b --- /dev/null +++ b/parts/django/tests/regressiontests/test_client_regress/urls.py @@ -0,0 +1,29 @@ +from django.conf.urls.defaults import * +from django.views.generic.simple import redirect_to +import views + +urlpatterns = patterns('', + (r'^no_template_view/$', views.no_template_view), + (r'^staff_only/$', views.staff_only_view), + (r'^get_view/$', views.get_view), + (r'^request_data/$', views.request_data), + (r'^request_data_extended/$', views.request_data, {'template':'extended.html', 'data':'bacon'}), + url(r'^arg_view/(?P<name>.+)/$', views.view_with_argument, name='arg_view'), + (r'^login_protected_redirect_view/$', views.login_protected_redirect_view), + (r'^redirects/$', redirect_to, {'url': '/test_client_regress/redirects/further/'}), + (r'^redirects/further/$', redirect_to, {'url': '/test_client_regress/redirects/further/more/'}), + (r'^redirects/further/more/$', redirect_to, {'url': '/test_client_regress/no_template_view/'}), + (r'^redirect_to_non_existent_view/$', redirect_to, {'url': '/test_client_regress/non_existent_view/'}), + (r'^redirect_to_non_existent_view2/$', redirect_to, {'url': '/test_client_regress/redirect_to_non_existent_view/'}), + (r'^redirect_to_self/$', redirect_to, {'url': '/test_client_regress/redirect_to_self/'}), + (r'^circular_redirect_1/$', redirect_to, {'url': '/test_client_regress/circular_redirect_2/'}), + (r'^circular_redirect_2/$', redirect_to, {'url': '/test_client_regress/circular_redirect_3/'}), + (r'^circular_redirect_3/$', redirect_to, {'url': '/test_client_regress/circular_redirect_1/'}), + (r'^set_session/$', views.set_session_view), + (r'^check_session/$', views.check_session_view), + (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/parts/django/tests/regressiontests/test_client_regress/views.py b/parts/django/tests/regressiontests/test_client_regress/views.py new file mode 100644 index 0000000..40aa61f --- /dev/null +++ b/parts/django/tests/regressiontests/test_client_regress/views.py @@ -0,0 +1,93 @@ +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse, HttpResponseRedirect +from django.core.exceptions import SuspiciousOperation +from django.shortcuts import render_to_response +from django.utils import simplejson +from django.utils.encoding import smart_str +from django.core.serializers.json import DjangoJSONEncoder +from django.test.client import CONTENT_TYPE_RE + +def no_template_view(request): + "A simple view that expects a GET request, and returns a rendered template" + return HttpResponse("No template used. Sample content: twice once twice. Content ends.") + +def staff_only_view(request): + "A view that can only be visited by staff. Non staff members get an exception" + if request.user.is_staff: + return HttpResponse('') + else: + raise SuspiciousOperation() + +def get_view(request): + "A simple login protected view" + return HttpResponse("Hello world") +get_view = login_required(get_view) + +def request_data(request, template='base.html', data='sausage'): + "A simple view that returns the request data in the context" + return render_to_response(template, { + 'get-foo':request.GET.get('foo',None), + 'get-bar':request.GET.get('bar',None), + 'post-foo':request.POST.get('foo',None), + 'post-bar':request.POST.get('bar',None), + 'request-foo':request.REQUEST.get('foo',None), + 'request-bar':request.REQUEST.get('bar',None), + 'data': data, + }) + +def view_with_argument(request, name): + """A view that takes a string argument + + The purpose of this view is to check that if a space is provided in + the argument, the test framework unescapes the %20 before passing + the value to the view. + """ + if name == 'Arthur Dent': + return HttpResponse('Hi, Arthur') + else: + return HttpResponse('Howdy, %s' % name) + +def login_protected_redirect_view(request): + "A view that redirects all requests to the GET view" + return HttpResponseRedirect('/test_client_regress/get_view/') +login_protected_redirect_view = login_required(login_protected_redirect_view) + +def set_session_view(request): + "A view that sets a session variable" + request.session['session_var'] = 'YES' + return HttpResponse('set_session') + +def check_session_view(request): + "A view that reads a session variable" + return HttpResponse(request.session.get('session_var', 'NO')) + +def request_methods_view(request): + "A view that responds with the request method" + return HttpResponse('request method: %s' % request.method) + +def return_unicode(request): + return render_to_response('unicode.html') + +def return_json_file(request): + "A view that parses and returns a JSON string as a file." + match = CONTENT_TYPE_RE.match(request.META['CONTENT_TYPE']) + if match: + charset = match.group(1) + else: + charset = settings.DEFAULT_CHARSET + + # This just checks that the uploaded data is JSON + obj_dict = simplejson.loads(request.raw_post_data.decode(charset)) + obj_json = simplejson.dumps(obj_dict, encoding=charset, + cls=DjangoJSONEncoder, + ensure_ascii=False) + response = HttpResponse(smart_str(obj_json, encoding=charset), status=200, + 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/parts/django/tests/regressiontests/test_runner/__init__.py b/parts/django/tests/regressiontests/test_runner/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/test_runner/__init__.py diff --git a/parts/django/tests/regressiontests/test_runner/models.py b/parts/django/tests/regressiontests/test_runner/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/test_runner/models.py diff --git a/parts/django/tests/regressiontests/test_runner/tests.py b/parts/django/tests/regressiontests/test_runner/tests.py new file mode 100644 index 0000000..3387d65 --- /dev/null +++ b/parts/django/tests/regressiontests/test_runner/tests.py @@ -0,0 +1,120 @@ +""" +Tests for django test runner +""" +import StringIO +import unittest +import django +from django.core.exceptions import ImproperlyConfigured +from django.test import simple + +class DjangoTestRunnerTests(unittest.TestCase): + + def test_failfast(self): + class MockTestOne(unittest.TestCase): + def runTest(self): + assert False + class MockTestTwo(unittest.TestCase): + def runTest(self): + assert False + + suite = unittest.TestSuite([MockTestOne(), MockTestTwo()]) + mock_stream = StringIO.StringIO() + dtr = simple.DjangoTestRunner(verbosity=0, failfast=False, stream=mock_stream) + result = dtr.run(suite) + self.assertEqual(2, result.testsRun) + self.assertEqual(2, len(result.failures)) + + dtr = simple.DjangoTestRunner(verbosity=0, failfast=True, stream=mock_stream) + result = dtr.run(suite) + self.assertEqual(1, result.testsRun) + self.assertEqual(1, len(result.failures)) + +class DependencyOrderingTests(unittest.TestCase): + + def test_simple_dependencies(self): + raw = [ + ('s1', ['alpha']), + ('s2', ['bravo']), + ('s3', ['charlie']), + ] + dependencies = { + 'alpha': ['charlie'], + 'bravo': ['charlie'], + } + + ordered = simple.dependency_ordered(raw, dependencies=dependencies) + ordered_sigs = [sig for sig,aliases in ordered] + + self.assertTrue('s1' in ordered_sigs) + self.assertTrue('s2' in ordered_sigs) + self.assertTrue('s3' in ordered_sigs) + self.assertTrue(ordered_sigs.index('s3') < ordered_sigs.index('s1')) + self.assertTrue(ordered_sigs.index('s3') < ordered_sigs.index('s2')) + + def test_chained_dependencies(self): + raw = [ + ('s1', ['alpha']), + ('s2', ['bravo']), + ('s3', ['charlie']), + ] + dependencies = { + 'alpha': ['bravo'], + 'bravo': ['charlie'], + } + + ordered = simple.dependency_ordered(raw, dependencies=dependencies) + ordered_sigs = [sig for sig,aliases in ordered] + + self.assertTrue('s1' in ordered_sigs) + self.assertTrue('s2' in ordered_sigs) + self.assertTrue('s3' in ordered_sigs) + + # Explicit dependencies + self.assertTrue(ordered_sigs.index('s2') < ordered_sigs.index('s1')) + self.assertTrue(ordered_sigs.index('s3') < ordered_sigs.index('s2')) + + # Implied dependencies + self.assertTrue(ordered_sigs.index('s3') < ordered_sigs.index('s1')) + + def test_multiple_dependencies(self): + raw = [ + ('s1', ['alpha']), + ('s2', ['bravo']), + ('s3', ['charlie']), + ('s4', ['delta']), + ] + dependencies = { + 'alpha': ['bravo','delta'], + 'bravo': ['charlie'], + 'delta': ['charlie'], + } + + ordered = simple.dependency_ordered(raw, dependencies=dependencies) + ordered_sigs = [sig for sig,aliases in ordered] + + self.assertTrue('s1' in ordered_sigs) + self.assertTrue('s2' in ordered_sigs) + self.assertTrue('s3' in ordered_sigs) + self.assertTrue('s4' in ordered_sigs) + + # Explicit dependencies + self.assertTrue(ordered_sigs.index('s2') < ordered_sigs.index('s1')) + self.assertTrue(ordered_sigs.index('s4') < ordered_sigs.index('s1')) + self.assertTrue(ordered_sigs.index('s3') < ordered_sigs.index('s2')) + self.assertTrue(ordered_sigs.index('s3') < ordered_sigs.index('s4')) + + # Implicit dependencies + self.assertTrue(ordered_sigs.index('s3') < ordered_sigs.index('s1')) + + def test_circular_dependencies(self): + raw = [ + ('s1', ['alpha']), + ('s2', ['bravo']), + ] + dependencies = { + 'bravo': ['alpha'], + 'alpha': ['bravo'], + } + + self.assertRaises(ImproperlyConfigured, simple.dependency_ordered, raw, dependencies=dependencies) + diff --git a/parts/django/tests/regressiontests/test_utils/__init__.py b/parts/django/tests/regressiontests/test_utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/test_utils/__init__.py diff --git a/parts/django/tests/regressiontests/test_utils/models.py b/parts/django/tests/regressiontests/test_utils/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/test_utils/models.py diff --git a/parts/django/tests/regressiontests/test_utils/tests.py b/parts/django/tests/regressiontests/test_utils/tests.py new file mode 100644 index 0000000..a2539bf --- /dev/null +++ b/parts/django/tests/regressiontests/test_utils/tests.py @@ -0,0 +1,72 @@ +r""" +# Some checks of the doctest output normalizer. +# Standard doctests do fairly +>>> from django.utils import simplejson +>>> from django.utils.xmlutils import SimplerXMLGenerator +>>> from StringIO import StringIO + +>>> def produce_long(): +... return 42L + +>>> def produce_int(): +... return 42 + +>>> def produce_json(): +... return simplejson.dumps(['foo', {'bar': ('baz', None, 1.0, 2), 'whiz': 42}]) + +>>> def produce_xml(): +... stream = StringIO() +... xml = SimplerXMLGenerator(stream, encoding='utf-8') +... xml.startDocument() +... xml.startElement("foo", {"aaa" : "1.0", "bbb": "2.0"}) +... xml.startElement("bar", {"ccc" : "3.0"}) +... xml.characters("Hello") +... xml.endElement("bar") +... xml.startElement("whiz", {}) +... xml.characters("Goodbye") +... xml.endElement("whiz") +... xml.endElement("foo") +... xml.endDocument() +... return stream.getvalue() + +>>> def produce_xml_fragment(): +... stream = StringIO() +... xml = SimplerXMLGenerator(stream, encoding='utf-8') +... xml.startElement("foo", {"aaa": "1.0", "bbb": "2.0"}) +... xml.characters("Hello") +... xml.endElement("foo") +... xml.startElement("bar", {"ccc": "3.0", "ddd": "4.0"}) +... xml.endElement("bar") +... return stream.getvalue() + +# Long values are normalized and are comparable to normal integers ... +>>> produce_long() +42 + +# ... and vice versa +>>> produce_int() +42L + +# JSON output is normalized for field order, so it doesn't matter +# which order json dictionary attributes are listed in output +>>> produce_json() +'["foo", {"bar": ["baz", null, 1.0, 2], "whiz": 42}]' + +>>> produce_json() +'["foo", {"whiz": 42, "bar": ["baz", null, 1.0, 2]}]' + +# XML output is normalized for attribute order, so it doesn't matter +# which order XML element attributes are listed in output +>>> produce_xml() +'<?xml version="1.0" encoding="UTF-8"?>\n<foo aaa="1.0" bbb="2.0"><bar ccc="3.0">Hello</bar><whiz>Goodbye</whiz></foo>' + +>>> produce_xml() +'<?xml version="1.0" encoding="UTF-8"?>\n<foo bbb="2.0" aaa="1.0"><bar ccc="3.0">Hello</bar><whiz>Goodbye</whiz></foo>' + +>>> produce_xml_fragment() +'<foo aaa="1.0" bbb="2.0">Hello</foo><bar ccc="3.0" ddd="4.0"></bar>' + +>>> produce_xml_fragment() +'<foo bbb="2.0" aaa="1.0">Hello</foo><bar ddd="4.0" ccc="3.0"></bar>' + +"""
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/text/__init__.py b/parts/django/tests/regressiontests/text/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/text/__init__.py diff --git a/parts/django/tests/regressiontests/text/models.py b/parts/django/tests/regressiontests/text/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/text/models.py diff --git a/parts/django/tests/regressiontests/text/tests.py b/parts/django/tests/regressiontests/text/tests.py new file mode 100644 index 0000000..fd02036 --- /dev/null +++ b/parts/django/tests/regressiontests/text/tests.py @@ -0,0 +1,81 @@ +# coding: utf-8 +from django.test import TestCase + +from django.utils.text import * +from django.utils.http import urlquote, urlquote_plus, cookie_date, http_date +from django.utils.encoding import iri_to_uri + +class TextTests(TestCase): + """ + Tests for stuff in django.utils.text and other text munging util functions. + """ + + def test_smart_split(self): + + self.assertEquals(list(smart_split(r'''This is "a person" test.''')), + [u'This', u'is', u'"a person"', u'test.']) + + self.assertEquals(list(smart_split(r'''This is "a person's" test.'''))[2], + u'"a person\'s"') + + self.assertEquals(list(smart_split(r'''This is "a person\"s" test.'''))[2], + u'"a person\\"s"') + + self.assertEquals(list(smart_split('''"a 'one''')), [u'"a', u"'one"]) + + self.assertEquals(list(smart_split(r'''all friends' tests'''))[1], + "friends'") + + self.assertEquals(list(smart_split(u'url search_page words="something else"')), + [u'url', u'search_page', u'words="something else"']) + + self.assertEquals(list(smart_split(u"url search_page words='something else'")), + [u'url', u'search_page', u"words='something else'"]) + + self.assertEquals(list(smart_split(u'url search_page words "something else"')), + [u'url', u'search_page', u'words', u'"something else"']) + + self.assertEquals(list(smart_split(u'url search_page words-"something else"')), + [u'url', u'search_page', u'words-"something else"']) + + self.assertEquals(list(smart_split(u'url search_page words=hello')), + [u'url', u'search_page', u'words=hello']) + + self.assertEquals(list(smart_split(u'url search_page words="something else')), + [u'url', u'search_page', u'words="something', u'else']) + + self.assertEquals(list(smart_split("cut:','|cut:' '")), + [u"cut:','|cut:' '"]) + + def test_urlquote(self): + + self.assertEquals(urlquote(u'Paris & Orl\xe9ans'), + u'Paris%20%26%20Orl%C3%A9ans') + self.assertEquals(urlquote(u'Paris & Orl\xe9ans', safe="&"), + u'Paris%20&%20Orl%C3%A9ans') + self.assertEquals(urlquote_plus(u'Paris & Orl\xe9ans'), + u'Paris+%26+Orl%C3%A9ans') + self.assertEquals(urlquote_plus(u'Paris & Orl\xe9ans', safe="&"), + u'Paris+&+Orl%C3%A9ans') + + def test_cookie_date(self): + t = 1167616461.0 + self.assertEquals(cookie_date(t), 'Mon, 01-Jan-2007 01:54:21 GMT') + + def test_http_date(self): + t = 1167616461.0 + self.assertEquals(http_date(t), 'Mon, 01 Jan 2007 01:54:21 GMT') + + def test_iri_to_uri(self): + self.assertEquals(iri_to_uri(u'red%09ros\xe9#red'), + 'red%09ros%C3%A9#red') + + self.assertEquals(iri_to_uri(u'/blog/for/J\xfcrgen M\xfcnster/'), + '/blog/for/J%C3%BCrgen%20M%C3%BCnster/') + + self.assertEquals(iri_to_uri(u'locations/%s' % urlquote_plus(u'Paris & Orl\xe9ans')), + 'locations/Paris+%26+Orl%C3%A9ans') + + def test_iri_to_uri_idempotent(self): + self.assertEquals(iri_to_uri(iri_to_uri(u'red%09ros\xe9#red')), + 'red%09ros%C3%A9#red') diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/__init__.py b/parts/django/tests/regressiontests/urlpatterns_reverse/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/__init__.py diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/extra_urls.py b/parts/django/tests/regressiontests/urlpatterns_reverse/extra_urls.py new file mode 100644 index 0000000..c171f6d --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/extra_urls.py @@ -0,0 +1,13 @@ +""" +Some extra URL patterns that are included at the top level. +""" + +from django.conf.urls.defaults import * +from views import empty_view + +urlpatterns = patterns('', + url(r'^e-places/(\d+)/$', empty_view, name='extra-places'), + url(r'^e-people/(?P<name>\w+)/$', empty_view, name="extra-people"), + url('', include('regressiontests.urlpatterns_reverse.included_urls2')), + url(r'^prefix/(?P<prefix>\w+)/', include('regressiontests.urlpatterns_reverse.included_urls2')), +) diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py b/parts/django/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py new file mode 100644 index 0000000..0731906 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py @@ -0,0 +1,13 @@ +from django.conf.urls.defaults import * +from namespace_urls import URLObject + +testobj3 = URLObject('testapp', 'test-ns3') + +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'), + + (r'^test3/', include(testobj3.urls)), + (r'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')), +) + diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/included_urls.py b/parts/django/tests/regressiontests/urlpatterns_reverse/included_urls.py new file mode 100644 index 0000000..f8acf34 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/included_urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import * +from views import empty_view + +urlpatterns = patterns('', + url(r'^$', empty_view, name="inner-nothing"), + url(r'^extra/(?P<extra>\w+)/$', empty_view, name="inner-extra"), + url(r'^(?P<one>\d+)|(?P<two>\d+)/$', empty_view, name="inner-disjunction"), +) diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/included_urls2.py b/parts/django/tests/regressiontests/urlpatterns_reverse/included_urls2.py new file mode 100644 index 0000000..f414ca6 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/included_urls2.py @@ -0,0 +1,14 @@ +""" +These URL patterns are included in two different ways in the main urls.py, with +an extra argument present in one case. Thus, there are two different ways for +each name to resolve and Django must distinguish the possibilities based on the +argument list. +""" + +from django.conf.urls.defaults import * +from views import empty_view + +urlpatterns = patterns('', + url(r'^part/(?P<value>\w+)/$', empty_view, name="part"), + url(r'^part2/(?:(?P<value>\w+)/)?$', empty_view, name="part2"), +) diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/middleware.py b/parts/django/tests/regressiontests/urlpatterns_reverse/middleware.py new file mode 100644 index 0000000..cd3c045 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/middleware.py @@ -0,0 +1,11 @@ +from django.core.urlresolvers import set_urlconf + +import urlconf_inner + +class ChangeURLconfMiddleware(object): + def process_request(self, request): + request.urlconf = urlconf_inner.__name__ + +class NullChangeURLconfMiddleware(object): + def process_request(self, request): + request.urlconf = None diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/models.py b/parts/django/tests/regressiontests/urlpatterns_reverse/models.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/models.py diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/namespace_urls.py b/parts/django/tests/regressiontests/urlpatterns_reverse/namespace_urls.py new file mode 100644 index 0000000..27cc7f7 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/namespace_urls.py @@ -0,0 +1,38 @@ +from django.conf.urls.defaults import * + +class URLObject(object): + def __init__(self, app_name, namespace): + self.app_name = app_name + self.namespace = namespace + + def urls(self): + return patterns('', + url(r'^inner/$', 'empty_view', name='urlobject-view'), + url(r'^inner/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='urlobject-view'), + ), self.app_name, self.namespace + urls = property(urls) + +testobj1 = URLObject('testapp', 'test-ns1') +testobj2 = URLObject('testapp', 'test-ns2') +default_testobj = URLObject('testapp', 'testapp') + +otherobj1 = URLObject('nodefault', 'other-ns1') +otherobj2 = URLObject('nodefault', 'other-ns2') + +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'), + + (r'^test1/', include(testobj1.urls)), + (r'^test2/', include(testobj2.urls)), + (r'^default/', include(default_testobj.urls)), + + (r'^other1/', include(otherobj1.urls)), + (r'^other2/', include(otherobj2.urls)), + + (r'^ns-included1/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns1')), + (r'^ns-included2/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns2')), + + (r'^included/', include('regressiontests.urlpatterns_reverse.included_namespace_urls')), + +) diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/no_urls.py b/parts/django/tests/regressiontests/urlpatterns_reverse/no_urls.py new file mode 100644 index 0000000..c9b9efe --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/no_urls.py @@ -0,0 +1,2 @@ +#from django.conf.urls.defaults import * + diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/tests.py b/parts/django/tests/regressiontests/urlpatterns_reverse/tests.py new file mode 100644 index 0000000..d0b7146 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/tests.py @@ -0,0 +1,330 @@ +""" +Unit tests for reverse URL lookups. +""" +import unittest + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404, RegexURLResolver +from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect +from django.shortcuts import redirect +from django.test import TestCase + +import urlconf_outer +import urlconf_inner +import middleware + +test_data = ( + ('places', '/places/3/', [3], {}), + ('places', '/places/3/', ['3'], {}), + ('places', NoReverseMatch, ['a'], {}), + ('places', NoReverseMatch, [], {}), + ('places?', '/place/', [], {}), + ('places+', '/places/', [], {}), + ('places*', '/place/', [], {}), + ('places2?', '/', [], {}), + ('places2+', '/places/', [], {}), + ('places2*', '/', [], {}), + ('places3', '/places/4/', [4], {}), + ('places3', '/places/harlem/', ['harlem'], {}), + ('places3', NoReverseMatch, ['harlem64'], {}), + ('places4', '/places/3/', [], {'id': 3}), + ('people', NoReverseMatch, [], {}), + ('people', '/people/adrian/', ['adrian'], {}), + ('people', '/people/adrian/', [], {'name': 'adrian'}), + ('people', NoReverseMatch, ['name with spaces'], {}), + ('people', NoReverseMatch, [], {'name': 'name with spaces'}), + ('people2', '/people/name/', [], {}), + ('people2a', '/people/name/fred/', ['fred'], {}), + ('optional', '/optional/fred/', [], {'name': 'fred'}), + ('optional', '/optional/fred/', ['fred'], {}), + ('hardcoded', '/hardcoded/', [], {}), + ('hardcoded2', '/hardcoded/doc.pdf', [], {}), + ('people3', '/people/il/adrian/', [], {'state': 'il', 'name': 'adrian'}), + ('people3', NoReverseMatch, [], {'state': 'il'}), + ('people3', NoReverseMatch, [], {'name': 'adrian'}), + ('people4', NoReverseMatch, [], {'state': 'il', 'name': 'adrian'}), + ('people6', '/people/il/test/adrian/', ['il/test', 'adrian'], {}), + ('people6', '/people//adrian/', ['adrian'], {}), + ('range', '/character_set/a/', [], {}), + ('range2', '/character_set/x/', [], {}), + ('price', '/price/$10/', ['10'], {}), + ('price2', '/price/$10/', ['10'], {}), + ('price3', '/price/$10/', ['10'], {}), + ('product', '/product/chocolate+($2.00)/', [], {'price': '2.00', 'product': 'chocolate'}), + ('headlines', '/headlines/2007.5.21/', [], dict(year=2007, month=5, day=21)), + ('windows', r'/windows_path/C:%5CDocuments%20and%20Settings%5Cspam/', [], dict(drive_name='C', path=r'Documents and Settings\spam')), + ('special', r'/special_chars/+%5C$*/', [r'+\$*'], {}), + ('special', NoReverseMatch, [''], {}), + ('mixed', '/john/0/', [], {'name': 'john'}), + ('repeats', '/repeats/a/', [], {}), + ('repeats2', '/repeats/aa/', [], {}), + ('repeats3', '/repeats/aa/', [], {}), + ('insensitive', '/CaseInsensitive/fred', ['fred'], {}), + ('test', '/test/1', [], {}), + ('test2', '/test/2', [], {}), + ('inner-nothing', '/outer/42/', [], {'outer': '42'}), + ('inner-nothing', '/outer/42/', ['42'], {}), + ('inner-nothing', NoReverseMatch, ['foo'], {}), + ('inner-extra', '/outer/42/extra/inner/', [], {'extra': 'inner', 'outer': '42'}), + ('inner-extra', '/outer/42/extra/inner/', ['42', 'inner'], {}), + ('inner-extra', NoReverseMatch, ['fred', 'inner'], {}), + ('disjunction', NoReverseMatch, ['foo'], {}), + ('inner-disjunction', NoReverseMatch, ['10', '11'], {}), + ('extra-places', '/e-places/10/', ['10'], {}), + ('extra-people', '/e-people/fred/', ['fred'], {}), + ('extra-people', '/e-people/fred/', [], {'name': 'fred'}), + ('part', '/part/one/', [], {'value': 'one'}), + ('part', '/prefix/xx/part/one/', [], {'value': 'one', 'prefix': 'xx'}), + ('part2', '/part2/one/', [], {'value': 'one'}), + ('part2', '/part2/', [], {}), + ('part2', '/prefix/xx/part2/one/', [], {'value': 'one', 'prefix': 'xx'}), + ('part2', '/prefix/xx/part2/', [], {'prefix': 'xx'}), + + # Regression for #9038 + # These views are resolved by method name. Each method is deployed twice - + # once with an explicit argument, and once using the default value on + # the method. This is potentially ambiguous, as you have to pick the + # correct view for the arguments provided. + ('kwargs_view', '/arg_view/', [], {}), + ('kwargs_view', '/arg_view/10/', [], {'arg1':10}), + ('regressiontests.urlpatterns_reverse.views.absolute_kwargs_view', '/absolute_arg_view/', [], {}), + ('regressiontests.urlpatterns_reverse.views.absolute_kwargs_view', '/absolute_arg_view/10/', [], {'arg1':10}), + ('non_path_include', '/includes/non_path_include/', [], {}) + +) + +class NoURLPatternsTests(TestCase): + urls = 'regressiontests.urlpatterns_reverse.no_urls' + + def assertRaisesErrorWithMessage(self, error, message, callable, + *args, **kwargs): + self.assertRaises(error, callable, *args, **kwargs) + try: + callable(*args, **kwargs) + except error, e: + self.assertEqual(message, str(e)) + + def test_no_urls_exception(self): + """ + RegexURLResolver should raise an exception when no urlpatterns exist. + """ + resolver = RegexURLResolver(r'^$', self.urls) + + self.assertRaisesErrorWithMessage(ImproperlyConfigured, + "The included urlconf regressiontests.urlpatterns_reverse.no_urls "\ + "doesn't have any patterns in it", getattr, resolver, 'url_patterns') + +class URLPatternReverse(TestCase): + urls = 'regressiontests.urlpatterns_reverse.urls' + + def test_urlpattern_reverse(self): + for name, expected, args, kwargs in test_data: + try: + got = reverse(name, args=args, kwargs=kwargs) + except NoReverseMatch, e: + self.assertEqual(expected, NoReverseMatch) + 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): + """ + Verifies that we raise a Resolver404 if what we are resolving doesn't + meet the basic requirements of a path to match - i.e., at the very + least, it matches the root pattern '^/'. We must never return None + from resolve, or we will get a TypeError further down the line. + + Regression for #10834. + """ + self.assertRaises(Resolver404, resolve, '') + self.assertRaises(Resolver404, resolve, 'a') + self.assertRaises(Resolver404, resolve, '\\') + self.assertRaises(Resolver404, resolve, '.') + +class ReverseShortcutTests(TestCase): + urls = 'regressiontests.urlpatterns_reverse.urls' + + def test_redirect_to_object(self): + # We don't really need a model; just something with a get_absolute_url + class FakeObj(object): + def get_absolute_url(self): + return "/hi-there/" + + res = redirect(FakeObj()) + self.assert_(isinstance(res, HttpResponseRedirect)) + self.assertEqual(res['Location'], '/hi-there/') + + res = redirect(FakeObj(), permanent=True) + self.assert_(isinstance(res, HttpResponsePermanentRedirect)) + self.assertEqual(res['Location'], '/hi-there/') + + def test_redirect_to_view_name(self): + res = redirect('hardcoded2') + self.assertEqual(res['Location'], '/hardcoded/doc.pdf') + res = redirect('places', 1) + self.assertEqual(res['Location'], '/places/1/') + res = redirect('headlines', year='2008', month='02', day='17') + self.assertEqual(res['Location'], '/headlines/2008.02.17/') + self.assertRaises(NoReverseMatch, redirect, 'not-a-view') + + def test_redirect_to_url(self): + res = redirect('/foo/') + self.assertEqual(res['Location'], '/foo/') + res = redirect('http://example.com/') + self.assertEqual(res['Location'], 'http://example.com/') + + def test_redirect_view_object(self): + from views import absolute_kwargs_view + res = redirect(absolute_kwargs_view) + self.assertEqual(res['Location'], '/absolute_arg_view/') + self.assertRaises(NoReverseMatch, redirect, absolute_kwargs_view, wrong_argument=None) + + +class NamespaceTests(TestCase): + urls = 'regressiontests.urlpatterns_reverse.namespace_urls' + + def test_ambiguous_object(self): + "Names deployed via dynamic URL objects that require namespaces can't be resolved" + self.assertRaises(NoReverseMatch, reverse, 'urlobject-view') + self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', args=[37,42]) + self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', kwargs={'arg1':42, 'arg2':37}) + + def test_ambiguous_urlpattern(self): + "Names deployed via dynamic URL objects that require namespaces can't be resolved" + self.assertRaises(NoReverseMatch, reverse, 'inner-nothing') + self.assertRaises(NoReverseMatch, reverse, 'inner-nothing', args=[37,42]) + self.assertRaises(NoReverseMatch, reverse, 'inner-nothing', kwargs={'arg1':42, 'arg2':37}) + + def test_non_existent_namespace(self): + "Non-existent namespaces raise errors" + self.assertRaises(NoReverseMatch, reverse, 'blahblah:urlobject-view') + self.assertRaises(NoReverseMatch, reverse, 'test-ns1:blahblah:urlobject-view') + + def test_normal_name(self): + "Normal lookups work as expected" + self.assertEquals('/normal/', reverse('normal-view')) + self.assertEquals('/normal/37/42/', reverse('normal-view', args=[37,42])) + self.assertEquals('/normal/42/37/', reverse('normal-view', kwargs={'arg1':42, 'arg2':37})) + + def test_simple_included_name(self): + "Normal lookups work on names included from other patterns" + self.assertEquals('/included/normal/', reverse('inc-normal-view')) + self.assertEquals('/included/normal/37/42/', reverse('inc-normal-view', args=[37,42])) + self.assertEquals('/included/normal/42/37/', reverse('inc-normal-view', kwargs={'arg1':42, 'arg2':37})) + + def test_namespace_object(self): + "Dynamic URL objects can be found using a namespace" + self.assertEquals('/test1/inner/', reverse('test-ns1:urlobject-view')) + self.assertEquals('/test1/inner/37/42/', reverse('test-ns1:urlobject-view', args=[37,42])) + self.assertEquals('/test1/inner/42/37/', reverse('test-ns1:urlobject-view', kwargs={'arg1':42, 'arg2':37})) + + def test_embedded_namespace_object(self): + "Namespaces can be installed anywhere in the URL pattern tree" + self.assertEquals('/included/test3/inner/', reverse('test-ns3:urlobject-view')) + self.assertEquals('/included/test3/inner/37/42/', reverse('test-ns3:urlobject-view', args=[37,42])) + self.assertEquals('/included/test3/inner/42/37/', reverse('test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37})) + + def test_namespace_pattern(self): + "Namespaces can be applied to include()'d urlpatterns" + self.assertEquals('/ns-included1/normal/', reverse('inc-ns1:inc-normal-view')) + self.assertEquals('/ns-included1/normal/37/42/', reverse('inc-ns1:inc-normal-view', args=[37,42])) + self.assertEquals('/ns-included1/normal/42/37/', reverse('inc-ns1:inc-normal-view', kwargs={'arg1':42, 'arg2':37})) + + def test_multiple_namespace_pattern(self): + "Namespaces can be embedded" + self.assertEquals('/ns-included1/test3/inner/', reverse('inc-ns1:test-ns3:urlobject-view')) + 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_app_lookup_object(self): + "A default application namespace can be used for lookup" + self.assertEquals('/default/inner/', reverse('testapp:urlobject-view')) + self.assertEquals('/default/inner/37/42/', reverse('testapp:urlobject-view', args=[37,42])) + self.assertEquals('/default/inner/42/37/', reverse('testapp:urlobject-view', kwargs={'arg1':42, 'arg2':37})) + + def test_app_lookup_object_with_default(self): + "A default application namespace is sensitive to the 'current' app can be used for lookup" + self.assertEquals('/included/test3/inner/', reverse('testapp:urlobject-view', current_app='test-ns3')) + self.assertEquals('/included/test3/inner/37/42/', reverse('testapp:urlobject-view', args=[37,42], current_app='test-ns3')) + self.assertEquals('/included/test3/inner/42/37/', reverse('testapp:urlobject-view', kwargs={'arg1':42, 'arg2':37}, current_app='test-ns3')) + + def test_app_lookup_object_without_default(self): + "An application namespace without a default is sensitive to the 'current' app can be used for lookup" + self.assertEquals('/other2/inner/', reverse('nodefault:urlobject-view')) + self.assertEquals('/other2/inner/37/42/', reverse('nodefault:urlobject-view', args=[37,42])) + self.assertEquals('/other2/inner/42/37/', reverse('nodefault:urlobject-view', kwargs={'arg1':42, 'arg2':37})) + + self.assertEquals('/other1/inner/', reverse('nodefault:urlobject-view', current_app='other-ns1')) + self.assertEquals('/other1/inner/37/42/', reverse('nodefault:urlobject-view', args=[37,42], current_app='other-ns1')) + self.assertEquals('/other1/inner/42/37/', reverse('nodefault:urlobject-view', kwargs={'arg1':42, 'arg2':37}, current_app='other-ns1')) + +class RequestURLconfTests(TestCase): + def setUp(self): + self.root_urlconf = settings.ROOT_URLCONF + self.middleware_classes = settings.MIDDLEWARE_CLASSES + settings.ROOT_URLCONF = urlconf_outer.__name__ + + def tearDown(self): + settings.ROOT_URLCONF = self.root_urlconf + settings.MIDDLEWARE_CLASSES = self.middleware_classes + + def test_urlconf(self): + response = self.client.get('/test/me/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'outer:/test/me/,' + 'inner:/inner_urlconf/second_test/') + response = self.client.get('/inner_urlconf/second_test/') + self.assertEqual(response.status_code, 200) + response = self.client.get('/second_test/') + self.assertEqual(response.status_code, 404) + + def test_urlconf_overridden(self): + settings.MIDDLEWARE_CLASSES += ( + '%s.ChangeURLconfMiddleware' % middleware.__name__, + ) + response = self.client.get('/test/me/') + self.assertEqual(response.status_code, 404) + response = self.client.get('/inner_urlconf/second_test/') + self.assertEqual(response.status_code, 404) + response = self.client.get('/second_test/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, 'outer:,inner:/second_test/') + + def test_urlconf_overridden_with_null(self): + settings.MIDDLEWARE_CLASSES += ( + '%s.NullChangeURLconfMiddleware' % middleware.__name__, + ) + self.assertRaises(ImproperlyConfigured, self.client.get, '/test/me/') + +class ErrorHandlerResolutionTests(TestCase): + """Tests for handler404 and handler500""" + + def setUp(self): + urlconf = 'regressiontests.urlpatterns_reverse.urls_error_handlers' + urlconf_callables = 'regressiontests.urlpatterns_reverse.urls_error_handlers_callables' + self.resolver = RegexURLResolver(r'^$', urlconf) + self.callable_resolver = RegexURLResolver(r'^$', urlconf_callables) + + def test_named_handlers(self): + from views import empty_view + handler = (empty_view, {}) + self.assertEqual(self.resolver.resolve404(), handler) + self.assertEqual(self.resolver.resolve500(), handler) + + def test_callable_handers(self): + from views import empty_view + handler = (empty_view, {}) + self.assertEqual(self.callable_resolver.resolve404(), handler) + self.assertEqual(self.callable_resolver.resolve500(), 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/') diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py b/parts/django/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py new file mode 100644 index 0000000..d188e06 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py @@ -0,0 +1,12 @@ +from django.conf.urls.defaults import * +from django.template import Template, Context +from django.http import HttpResponse + +def inner_view(request): + content = Template('{% url outer as outer_url %}outer:{{ outer_url }},' + '{% url inner as inner_url %}inner:{{ inner_url }}').render(Context()) + return HttpResponse(content) + +urlpatterns = patterns('', + url(r'^second_test/$', inner_view, name='inner'), +)
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/urlconf_outer.py b/parts/django/tests/regressiontests/urlpatterns_reverse/urlconf_outer.py new file mode 100644 index 0000000..506e036 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/urlconf_outer.py @@ -0,0 +1,9 @@ +from django.conf.urls.defaults import * + +import urlconf_inner + + +urlpatterns = patterns('', + url(r'^test/me/$', urlconf_inner.inner_view, name='outer'), + url(r'^inner_urlconf/', include(urlconf_inner.__name__)) +)
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/urls.py b/parts/django/tests/regressiontests/urlpatterns_reverse/urls.py new file mode 100644 index 0000000..c603c02 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/urls.py @@ -0,0 +1,63 @@ +from django.conf.urls.defaults import * +from views import empty_view, absolute_kwargs_view + +other_patterns = patterns('', + url(r'non_path_include/$', empty_view, name='non_path_include'), +) + +urlpatterns = patterns('', + url(r'^places/(\d+)/$', empty_view, name='places'), + url(r'^places?/$', empty_view, name="places?"), + url(r'^places+/$', empty_view, name="places+"), + url(r'^places*/$', empty_view, name="places*"), + url(r'^(?:places/)?$', empty_view, name="places2?"), + url(r'^(?:places/)+$', empty_view, name="places2+"), + url(r'^(?:places/)*$', empty_view, name="places2*"), + url(r'^places/(\d+|[a-z_]+)/', empty_view, name="places3"), + url(r'^places/(?P<id>\d+)/$', empty_view, name="places4"), + url(r'^people/(?P<name>\w+)/$', empty_view, name="people"), + url(r'^people/(?:name/)', empty_view, name="people2"), + url(r'^people/(?:name/(\w+)/)?', empty_view, name="people2a"), + url(r'^optional/(?P<name>.*)/(?:.+/)?', empty_view, name="optional"), + url(r'^hardcoded/$', 'hardcoded/', empty_view, name="hardcoded"), + url(r'^hardcoded/doc\.pdf$', empty_view, name="hardcoded2"), + url(r'^people/(?P<state>\w\w)/(?P<name>\w+)/$', empty_view, name="people3"), + url(r'^people/(?P<state>\w\w)/(?P<name>\d)/$', empty_view, name="people4"), + url(r'^people/((?P<state>\w\w)/test)?/(\w+)/$', empty_view, name="people6"), + url(r'^character_set/[abcdef0-9]/$', empty_view, name="range"), + url(r'^character_set/[\w]/$', empty_view, name="range2"), + url(r'^price/\$(\d+)/$', empty_view, name="price"), + url(r'^price/[$](\d+)/$', empty_view, name="price2"), + url(r'^price/[\$](\d+)/$', empty_view, name="price3"), + url(r'^product/(?P<product>\w+)\+\(\$(?P<price>\d+(\.\d+)?)\)/$', + empty_view, name="product"), + url(r'^headlines/(?P<year>\d+)\.(?P<month>\d+)\.(?P<day>\d+)/$', empty_view, + name="headlines"), + url(r'^windows_path/(?P<drive_name>[A-Z]):\\(?P<path>.+)/$', empty_view, + name="windows"), + url(r'^special_chars/(.+)/$', empty_view, name="special"), + url(r'^(?P<name>.+)/\d+/$', empty_view, name="mixed"), + url(r'^repeats/a{1,2}/$', empty_view, name="repeats"), + url(r'^repeats/a{2,4}/$', empty_view, name="repeats2"), + url(r'^repeats/a{2}/$', empty_view, name="repeats3"), + url(r'^(?i)CaseInsensitive/(\w+)', empty_view, name="insensitive"), + url(r'^test/1/?', empty_view, name="test"), + url(r'^(?i)test/2/?$', empty_view, name="test2"), + url(r'^outer/(?P<outer>\d+)/', + include('regressiontests.urlpatterns_reverse.included_urls')), + url('', include('regressiontests.urlpatterns_reverse.extra_urls')), + + # This is non-reversible, but we shouldn't blow up when parsing it. + url(r'^(?:foo|bar)(\w+)/$', empty_view, name="disjunction"), + + # Regression views for #9038. See tests for more details + url(r'arg_view/$', 'kwargs_view'), + url(r'arg_view/(?P<arg1>\d+)/$', 'kwargs_view'), + url(r'absolute_arg_view/(?P<arg1>\d+)/$', absolute_kwargs_view), + url(r'absolute_arg_view/$', absolute_kwargs_view), + + url('^includes/', include(other_patterns)), + +) + + diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/urls_error_handlers.py b/parts/django/tests/regressiontests/urlpatterns_reverse/urls_error_handlers.py new file mode 100644 index 0000000..c2e0d32 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/urls_error_handlers.py @@ -0,0 +1,8 @@ +# Used by the ErrorHandlerResolutionTests test case. + +from django.conf.urls.defaults import patterns + +urlpatterns = patterns('') + +handler404 = 'regressiontests.urlpatterns_reverse.views.empty_view' +handler500 = 'regressiontests.urlpatterns_reverse.views.empty_view' diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/urls_error_handlers_callables.py b/parts/django/tests/regressiontests/urlpatterns_reverse/urls_error_handlers_callables.py new file mode 100644 index 0000000..00f25a7 --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/urls_error_handlers_callables.py @@ -0,0 +1,9 @@ +# Used by the ErrorHandlerResolutionTests test case. + +from django.conf.urls.defaults import patterns +from views import empty_view + +urlpatterns = patterns('') + +handler404 = empty_view +handler500 = empty_view diff --git a/parts/django/tests/regressiontests/urlpatterns_reverse/views.py b/parts/django/tests/regressiontests/urlpatterns_reverse/views.py new file mode 100644 index 0000000..99c00bd --- /dev/null +++ b/parts/django/tests/regressiontests/urlpatterns_reverse/views.py @@ -0,0 +1,8 @@ +def empty_view(request, *args, **kwargs): + pass + +def kwargs_view(request, arg1=1, arg2=2): + pass + +def absolute_kwargs_view(request, arg1=1, arg2=2): + pass diff --git a/parts/django/tests/regressiontests/utils/__init__.py b/parts/django/tests/regressiontests/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/__init__.py diff --git a/parts/django/tests/regressiontests/utils/checksums.py b/parts/django/tests/regressiontests/utils/checksums.py new file mode 100644 index 0000000..cee6dca --- /dev/null +++ b/parts/django/tests/regressiontests/utils/checksums.py @@ -0,0 +1,29 @@ +import unittest + +from django.utils import checksums + +class TestUtilsChecksums(unittest.TestCase): + + def check_output(self, function, value, output=None): + """ + Check that function(value) equals output. If output is None, + check that function(value) equals value. + """ + if output is None: + output = value + self.assertEqual(function(value), output) + + def test_luhn(self): + f = checksums.luhn + items = ( + (4111111111111111, True), ('4111111111111111', True), + (4222222222222, True), (378734493671000, True), + (5424000000000015, True), (5555555555554444, True), + (1008, True), ('0000001008', True), ('000000001008', True), + (4012888888881881, True), (1234567890123456789012345678909, True), + (4111111111211111, False), (42222222222224, False), + (100, False), ('100', False), ('0000100', False), + ('abc', False), (None, False), (object(), False), + ) + for value, output in items: + self.check_output(f, value, output) diff --git a/parts/django/tests/regressiontests/utils/datastructures.py b/parts/django/tests/regressiontests/utils/datastructures.py new file mode 100644 index 0000000..a41281c --- /dev/null +++ b/parts/django/tests/regressiontests/utils/datastructures.py @@ -0,0 +1,256 @@ +""" +Tests for stuff in django.utils.datastructures. +""" +import pickle +import unittest + +from django.utils.datastructures import * + + +class DatastructuresTestCase(unittest.TestCase): + def assertRaisesErrorWithMessage(self, error, message, callable, + *args, **kwargs): + self.assertRaises(error, callable, *args, **kwargs) + try: + callable(*args, **kwargs) + except error, e: + self.assertEqual(message, str(e)) + + +class SortedDictTests(DatastructuresTestCase): + def setUp(self): + self.d1 = SortedDict() + self.d1[7] = 'seven' + self.d1[1] = 'one' + self.d1[9] = 'nine' + + self.d2 = SortedDict() + self.d2[1] = 'one' + self.d2[9] = 'nine' + self.d2[0] = 'nil' + self.d2[7] = 'seven' + + def test_basic_methods(self): + self.assertEquals(self.d1.keys(), [7, 1, 9]) + self.assertEquals(self.d1.values(), ['seven', 'one', 'nine']) + self.assertEquals(self.d1.items(), [(7, 'seven'), (1, 'one'), (9, 'nine')]) + + def test_overwrite_ordering(self): + """ Overwriting an item keeps it's place. """ + self.d1[1] = 'ONE' + self.assertEquals(self.d1.values(), ['seven', 'ONE', 'nine']) + + def test_append_items(self): + """ New items go to the end. """ + self.d1[0] = 'nil' + self.assertEquals(self.d1.keys(), [7, 1, 9, 0]) + + def test_delete_and_insert(self): + """ + Deleting an item, then inserting the same key again will place it + at the end. + """ + del self.d2[7] + self.assertEquals(self.d2.keys(), [1, 9, 0]) + self.d2[7] = 'lucky number 7' + self.assertEquals(self.d2.keys(), [1, 9, 0, 7]) + + def test_change_keys(self): + """ + Changing the keys won't do anything, it's only a copy of the + keys dict. + """ + k = self.d2.keys() + k.remove(9) + self.assertEquals(self.d2.keys(), [1, 9, 0, 7]) + + def test_init_keys(self): + """ + Initialising a SortedDict with two keys will just take the first one. + + A real dict will actually take the second value so we will too, but + we'll keep the ordering from the first key found. + """ + tuples = ((2, 'two'), (1, 'one'), (2, 'second-two')) + d = SortedDict(tuples) + + self.assertEquals(d.keys(), [2, 1]) + + real_dict = dict(tuples) + self.assertEquals(sorted(real_dict.values()), ['one', 'second-two']) + + # Here the order of SortedDict values *is* what we are testing + self.assertEquals(d.values(), ['second-two', 'one']) + + def test_overwrite(self): + self.d1[1] = 'not one' + self.assertEqual(self.d1[1], 'not one') + self.assertEqual(self.d1.keys(), self.d1.copy().keys()) + + def test_append(self): + self.d1[13] = 'thirteen' + self.assertEquals( + repr(self.d1), + "{7: 'seven', 1: 'one', 9: 'nine', 13: 'thirteen'}" + ) + + def test_pop(self): + self.assertEquals(self.d1.pop(1, 'missing'), 'one') + self.assertEquals(self.d1.pop(1, 'missing'), 'missing') + + # We don't know which item will be popped in popitem(), so we'll + # just check that the number of keys has decreased. + l = len(self.d1) + self.d1.popitem() + self.assertEquals(l - len(self.d1), 1) + + def test_dict_equality(self): + d = SortedDict((i, i) for i in xrange(3)) + self.assertEquals(d, {0: 0, 1: 1, 2: 2}) + + def test_tuple_init(self): + d = SortedDict(((1, "one"), (0, "zero"), (2, "two"))) + self.assertEquals(repr(d), "{1: 'one', 0: 'zero', 2: 'two'}") + + def test_pickle(self): + self.assertEquals( + pickle.loads(pickle.dumps(self.d1, 2)), + {7: 'seven', 1: 'one', 9: 'nine'} + ) + + def test_clear(self): + self.d1.clear() + self.assertEquals(self.d1, {}) + self.assertEquals(self.d1.keyOrder, []) + +class MergeDictTests(DatastructuresTestCase): + + def test_simple_mergedict(self): + d1 = {'chris':'cool', 'camri':'cute', 'cotton':'adorable', + 'tulip':'snuggable', 'twoofme':'firstone'} + + d2 = {'chris2':'cool2', 'camri2':'cute2', 'cotton2':'adorable2', + 'tulip2':'snuggable2'} + + d3 = {'chris3':'cool3', 'camri3':'cute3', 'cotton3':'adorable3', + 'tulip3':'snuggable3'} + + d4 = {'twoofme': 'secondone'} + + md = MergeDict(d1, d2, d3) + + self.assertEquals(md['chris'], 'cool') + self.assertEquals(md['camri'], 'cute') + self.assertEquals(md['twoofme'], 'firstone') + + md2 = md.copy() + self.assertEquals(md2['chris'], 'cool') + + def test_mergedict_merges_multivaluedict(self): + """ MergeDict can merge MultiValueDicts """ + + multi1 = MultiValueDict({'key1': ['value1'], + 'key2': ['value2', 'value3']}) + + multi2 = MultiValueDict({'key2': ['value4'], + 'key4': ['value5', 'value6']}) + + mm = MergeDict(multi1, multi2) + + # Although 'key2' appears in both dictionaries, + # only the first value is used. + self.assertEquals(mm.getlist('key2'), ['value2', 'value3']) + self.assertEquals(mm.getlist('key4'), ['value5', 'value6']) + self.assertEquals(mm.getlist('undefined'), []) + + self.assertEquals(sorted(mm.keys()), ['key1', 'key2', 'key4']) + self.assertEquals(len(mm.values()), 3) + + self.assertTrue('value1' in mm.values()) + + self.assertEquals(sorted(mm.items(), key=lambda k: k[0]), + [('key1', 'value1'), ('key2', 'value3'), + ('key4', 'value6')]) + + self.assertEquals([(k,mm.getlist(k)) for k in sorted(mm)], + [('key1', ['value1']), + ('key2', ['value2', 'value3']), + ('key4', ['value5', 'value6'])]) + +class MultiValueDictTests(DatastructuresTestCase): + + def test_multivaluedict(self): + d = MultiValueDict({'name': ['Adrian', 'Simon'], + 'position': ['Developer']}) + + self.assertEquals(d['name'], 'Simon') + self.assertEquals(d.get('name'), 'Simon') + self.assertEquals(d.getlist('name'), ['Adrian', 'Simon']) + self.assertEquals(list(d.iteritems()), + [('position', 'Developer'), ('name', 'Simon')]) + + self.assertEquals(list(d.iterlists()), + [('position', ['Developer']), + ('name', ['Adrian', 'Simon'])]) + + # MultiValueDictKeyError: "Key 'lastname' not found in + # <MultiValueDict: {'position': ['Developer'], + # 'name': ['Adrian', 'Simon']}>" + self.assertRaisesErrorWithMessage(MultiValueDictKeyError, + '"Key \'lastname\' not found in <MultiValueDict: {\'position\':'\ + ' [\'Developer\'], \'name\': [\'Adrian\', \'Simon\']}>"', + d.__getitem__, 'lastname') + + self.assertEquals(d.get('lastname'), None) + self.assertEquals(d.get('lastname', 'nonexistent'), 'nonexistent') + self.assertEquals(d.getlist('lastname'), []) + + d.setlist('lastname', ['Holovaty', 'Willison']) + self.assertEquals(d.getlist('lastname'), ['Holovaty', 'Willison']) + self.assertEquals(d.values(), ['Developer', 'Simon', 'Willison']) + self.assertEquals(list(d.itervalues()), + ['Developer', 'Simon', 'Willison']) + + +class DotExpandedDictTests(DatastructuresTestCase): + + def test_dotexpandeddict(self): + + d = DotExpandedDict({'person.1.firstname': ['Simon'], + 'person.1.lastname': ['Willison'], + 'person.2.firstname': ['Adrian'], + 'person.2.lastname': ['Holovaty']}) + + self.assertEquals(d['person']['1']['lastname'], ['Willison']) + self.assertEquals(d['person']['2']['lastname'], ['Holovaty']) + self.assertEquals(d['person']['2']['firstname'], ['Adrian']) + + +class ImmutableListTests(DatastructuresTestCase): + + def test_sort(self): + d = ImmutableList(range(10)) + + # AttributeError: ImmutableList object is immutable. + self.assertRaisesErrorWithMessage(AttributeError, + 'ImmutableList object is immutable.', d.sort) + + self.assertEquals(repr(d), '(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)') + + def test_custom_warning(self): + d = ImmutableList(range(10), warning="Object is immutable!") + + self.assertEquals(d[1], 1) + + # AttributeError: Object is immutable! + self.assertRaisesErrorWithMessage(AttributeError, + 'Object is immutable!', d.__setitem__, 1, 'test') + + +class DictWrapperTests(DatastructuresTestCase): + + def test_dictwrapper(self): + f = lambda x: "*%s" % x + d = DictWrapper({'a': 'a'}, f, 'xx_') + self.assertEquals("Normal: %(a)s. Modified: %(xx_a)s" % d, + 'Normal: a. Modified: *a') diff --git a/parts/django/tests/regressiontests/utils/dateformat.py b/parts/django/tests/regressiontests/utils/dateformat.py new file mode 100644 index 0000000..b312c8d --- /dev/null +++ b/parts/django/tests/regressiontests/utils/dateformat.py @@ -0,0 +1,129 @@ +from datetime import datetime, date +import os +import time +import unittest + +from django.utils.dateformat import format +from django.utils import dateformat, translation +from django.utils.tzinfo import FixedOffset, LocalTimezone + +class DateFormatTests(unittest.TestCase): + def setUp(self): + self.old_TZ = os.environ.get('TZ') + os.environ['TZ'] = 'Europe/Copenhagen' + translation.activate('en-us') + + try: + # Check if a timezone has been set + time.tzset() + self.tz_tests = True + except AttributeError: + # No timezone available. Don't run the tests that require a TZ + self.tz_tests = False + + def tearDown(self): + if self.old_TZ is None: + del os.environ['TZ'] + else: + os.environ['TZ'] = self.old_TZ + + # Cleanup - force re-evaluation of TZ environment variable. + if self.tz_tests: + time.tzset() + + def test_date(self): + d = date(2009, 5, 16) + self.assertEquals(date.fromtimestamp(int(format(d, 'U'))), d) + + def test_naive_datetime(self): + dt = datetime(2009, 5, 16, 5, 30, 30) + self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U'))), dt) + + def test_datetime_with_local_tzinfo(self): + ltz = LocalTimezone(datetime.now()) + dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=ltz) + self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U')), ltz), dt) + self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U'))), dt.replace(tzinfo=None)) + + def test_datetime_with_tzinfo(self): + tz = FixedOffset(-510) + ltz = LocalTimezone(datetime.now()) + dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=tz) + self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U')), tz), dt) + self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U')), ltz), dt) + self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U'))), dt.astimezone(ltz).replace(tzinfo=None)) + self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U')), tz).utctimetuple(), dt.utctimetuple()) + self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U')), ltz).utctimetuple(), dt.utctimetuple()) + + def test_epoch(self): + utc = FixedOffset(0) + udt = datetime(1970, 1, 1, tzinfo=utc) + self.assertEquals(format(udt, 'U'), u'0') + + def test_empty_format(self): + my_birthday = datetime(1979, 7, 8, 22, 00) + + self.assertEquals(dateformat.format(my_birthday, ''), u'') + + def test_am_pm(self): + my_birthday = datetime(1979, 7, 8, 22, 00) + + self.assertEquals(dateformat.format(my_birthday, 'a'), u'p.m.') + + def test_date_formats(self): + my_birthday = datetime(1979, 7, 8, 22, 00) + timestamp = datetime(2008, 5, 19, 11, 45, 23, 123456) + + self.assertEquals(dateformat.format(my_birthday, 'A'), u'PM') + self.assertEquals(dateformat.format(timestamp, 'c'), u'2008-05-19T11:45:23.123456') + self.assertEquals(dateformat.format(my_birthday, 'd'), u'08') + self.assertEquals(dateformat.format(my_birthday, 'j'), u'8') + self.assertEquals(dateformat.format(my_birthday, 'l'), u'Sunday') + self.assertEquals(dateformat.format(my_birthday, 'L'), u'False') + self.assertEquals(dateformat.format(my_birthday, 'm'), u'07') + self.assertEquals(dateformat.format(my_birthday, 'M'), u'Jul') + self.assertEquals(dateformat.format(my_birthday, 'b'), u'jul') + self.assertEquals(dateformat.format(my_birthday, 'n'), u'7') + self.assertEquals(dateformat.format(my_birthday, 'N'), u'July') + + def test_time_formats(self): + my_birthday = datetime(1979, 7, 8, 22, 00) + + self.assertEquals(dateformat.format(my_birthday, 'P'), u'10 p.m.') + self.assertEquals(dateformat.format(my_birthday, 's'), u'00') + self.assertEquals(dateformat.format(my_birthday, 'S'), u'th') + self.assertEquals(dateformat.format(my_birthday, 't'), u'31') + self.assertEquals(dateformat.format(my_birthday, 'w'), u'0') + self.assertEquals(dateformat.format(my_birthday, 'W'), u'27') + self.assertEquals(dateformat.format(my_birthday, 'y'), u'79') + self.assertEquals(dateformat.format(my_birthday, 'Y'), u'1979') + self.assertEquals(dateformat.format(my_birthday, 'z'), u'189') + + def test_dateformat(self): + my_birthday = datetime(1979, 7, 8, 22, 00) + + self.assertEquals(dateformat.format(my_birthday, r'Y z \C\E\T'), u'1979 189 CET') + + self.assertEquals(dateformat.format(my_birthday, r'jS o\f F'), u'8th of July') + + def test_futuredates(self): + the_future = datetime(2100, 10, 25, 0, 00) + self.assertEquals(dateformat.format(the_future, r'Y'), u'2100') + + def test_timezones(self): + my_birthday = datetime(1979, 7, 8, 22, 00) + summertime = datetime(2005, 10, 30, 1, 00) + wintertime = datetime(2005, 10, 30, 4, 00) + timestamp = datetime(2008, 5, 19, 11, 45, 23, 123456) + + if self.tz_tests: + self.assertEquals(dateformat.format(my_birthday, 'O'), u'+0100') + self.assertEquals(dateformat.format(my_birthday, 'r'), u'Sun, 8 Jul 1979 22:00:00 +0100') + self.assertEquals(dateformat.format(my_birthday, 'T'), u'CET') + self.assertEquals(dateformat.format(my_birthday, 'U'), u'300315600') + self.assertEquals(dateformat.format(timestamp, 'u'), u'123456') + self.assertEquals(dateformat.format(my_birthday, 'Z'), u'3600') + self.assertEquals(dateformat.format(summertime, 'I'), u'1') + self.assertEquals(dateformat.format(summertime, 'O'), u'+0200') + self.assertEquals(dateformat.format(wintertime, 'I'), u'0') + self.assertEquals(dateformat.format(wintertime, 'O'), u'+0100') diff --git a/parts/django/tests/regressiontests/utils/datetime_safe.py b/parts/django/tests/regressiontests/utils/datetime_safe.py new file mode 100644 index 0000000..458a6b7 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/datetime_safe.py @@ -0,0 +1,42 @@ +import unittest + +from datetime import date as original_date, datetime as original_datetime +from django.utils.datetime_safe import date, datetime + +class DatetimeTests(unittest.TestCase): + + def setUp(self): + self.just_safe = (1900, 1, 1) + self.just_unsafe = (1899, 12, 31, 23, 59, 59) + self.really_old = (20, 1, 1) + self.more_recent = (2006, 1, 1) + + def test_compare_datetimes(self): + self.assertEqual(original_datetime(*self.more_recent), datetime(*self.more_recent)) + self.assertEqual(original_datetime(*self.really_old), datetime(*self.really_old)) + self.assertEqual(original_date(*self.more_recent), date(*self.more_recent)) + self.assertEqual(original_date(*self.really_old), date(*self.really_old)) + + self.assertEqual(original_date(*self.just_safe).strftime('%Y-%m-%d'), date(*self.just_safe).strftime('%Y-%m-%d')) + self.assertEqual(original_datetime(*self.just_safe).strftime('%Y-%m-%d'), datetime(*self.just_safe).strftime('%Y-%m-%d')) + + def test_safe_strftime(self): + self.assertEquals(date(*self.just_unsafe[:3]).strftime('%Y-%m-%d (weekday %w)'), '1899-12-31 (weekday 0)') + self.assertEquals(date(*self.just_safe).strftime('%Y-%m-%d (weekday %w)'), '1900-01-01 (weekday 1)') + + self.assertEquals(datetime(*self.just_unsafe).strftime('%Y-%m-%d %H:%M:%S (weekday %w)'), '1899-12-31 23:59:59 (weekday 0)') + self.assertEquals(datetime(*self.just_safe).strftime('%Y-%m-%d %H:%M:%S (weekday %w)'), '1900-01-01 00:00:00 (weekday 1)') + + # %y will error before this date + self.assertEquals(date(*self.just_safe).strftime('%y'), '00') + self.assertEquals(datetime(*self.just_safe).strftime('%y'), '00') + + self.assertEquals(date(1850, 8, 2).strftime("%Y/%m/%d was a %A"), '1850/08/02 was a Friday') + + def test_zero_padding(self): + """ + Regression for #12524 + + Check that pre-1000AD dates are padded with zeros if necessary + """ + self.assertEquals(date(1, 1, 1).strftime("%Y/%m/%d was a %A"), '0001/01/01 was a Monday') diff --git a/parts/django/tests/regressiontests/utils/decorators.py b/parts/django/tests/regressiontests/utils/decorators.py new file mode 100644 index 0000000..ca9214f --- /dev/null +++ b/parts/django/tests/regressiontests/utils/decorators.py @@ -0,0 +1,19 @@ +from django.test import TestCase + +class DecoratorFromMiddlewareTests(TestCase): + """ + Tests for view decorators created using + ``django.utils.decorators.decorator_from_middleware``. + """ + + def test_process_view_middleware(self): + """ + Test a middleware that implements process_view. + """ + self.client.get('/utils/xview/') + + def test_callable_process_view_middleware(self): + """ + Test a middleware that implements process_view, operating on a callable class. + """ + self.client.get('/utils/class_xview/') diff --git a/parts/django/tests/regressiontests/utils/eggs/test_egg.egg b/parts/django/tests/regressiontests/utils/eggs/test_egg.egg Binary files differnew file mode 100644 index 0000000..9b08cc1 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/eggs/test_egg.egg diff --git a/parts/django/tests/regressiontests/utils/feedgenerator.py b/parts/django/tests/regressiontests/utils/feedgenerator.py new file mode 100644 index 0000000..9085d41 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/feedgenerator.py @@ -0,0 +1,63 @@ +import datetime +import unittest + +from django.utils import feedgenerator, tzinfo + +class FeedgeneratorTest(unittest.TestCase): + """ + Tests for the low-level syndication feed framework. + """ + + def test_get_tag_uri(self): + """ + Test get_tag_uri() correctly generates TagURIs. + """ + self.assertEqual( + feedgenerator.get_tag_uri('http://example.org/foo/bar#headline', datetime.date(2004, 10, 25)), + u'tag:example.org,2004-10-25:/foo/bar/headline') + + def test_get_tag_uri_with_port(self): + """ + Test that get_tag_uri() correctly generates TagURIs from URLs with port + numbers. + """ + self.assertEqual( + feedgenerator.get_tag_uri('http://www.example.org:8000/2008/11/14/django#headline', datetime.datetime(2008, 11, 14, 13, 37, 0)), + u'tag:www.example.org,2008-11-14:/2008/11/14/django/headline') + + def test_rfc2822_date(self): + """ + Test rfc2822_date() correctly formats datetime objects. + """ + self.assertEqual( + feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0)), + "Fri, 14 Nov 2008 13:37:00 -0000" + ) + + def test_rfc2822_date_with_timezone(self): + """ + Test rfc2822_date() correctly formats datetime objects with tzinfo. + """ + self.assertEqual( + feedgenerator.rfc2822_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=60)))), + "Fri, 14 Nov 2008 13:37:00 +0100" + ) + + def test_rfc3339_date(self): + """ + Test rfc3339_date() correctly formats datetime objects. + """ + self.assertEqual( + feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0)), + "2008-11-14T13:37:00Z" + ) + + def test_rfc3339_date_with_timezone(self): + """ + Test rfc3339_date() correctly formats datetime objects with tzinfo. + """ + self.assertEqual( + feedgenerator.rfc3339_date(datetime.datetime(2008, 11, 14, 13, 37, 0, tzinfo=tzinfo.FixedOffset(datetime.timedelta(minutes=120)))), + "2008-11-14T13:37:00+02:00" + ) + diff --git a/parts/django/tests/regressiontests/utils/functional.py b/parts/django/tests/regressiontests/utils/functional.py new file mode 100644 index 0000000..206a583 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/functional.py @@ -0,0 +1,10 @@ +import unittest + +from django.utils.functional import lazy + + +class FunctionalTestCase(unittest.TestCase): + def test_lazy(self): + t = lazy(lambda: tuple(range(3)), list, tuple) + for a, b in zip(t(), range(3)): + self.assertEqual(a, b) diff --git a/parts/django/tests/regressiontests/utils/html.py b/parts/django/tests/regressiontests/utils/html.py new file mode 100644 index 0000000..a9b0d33 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/html.py @@ -0,0 +1,111 @@ +import unittest + +from django.utils import html + +class TestUtilsHtml(unittest.TestCase): + + def check_output(self, function, value, output=None): + """ + Check that function(value) equals output. If output is None, + check that function(value) equals value. + """ + if output is None: + output = value + self.assertEqual(function(value), output) + + def test_escape(self): + f = html.escape + items = ( + ('&','&'), + ('<', '<'), + ('>', '>'), + ('"', '"'), + ("'", '''), + ) + # Substitution patterns for testing the above items. + patterns = ("%s", "asdf%sfdsa", "%s1", "1%sb") + for value, output in items: + for pattern in patterns: + self.check_output(f, pattern % value, pattern % output) + # Check repeated values. + self.check_output(f, value * 2, output * 2) + # Verify it doesn't double replace &. + self.check_output(f, '<&', '<&') + + def test_linebreaks(self): + f = html.linebreaks + items = ( + ("para1\n\npara2\r\rpara3", "<p>para1</p>\n\n<p>para2</p>\n\n<p>para3</p>"), + ("para1\nsub1\rsub2\n\npara2", "<p>para1<br />sub1<br />sub2</p>\n\n<p>para2</p>"), + ("para1\r\n\r\npara2\rsub1\r\rpara4", "<p>para1</p>\n\n<p>para2<br />sub1</p>\n\n<p>para4</p>"), + ("para1\tmore\n\npara2", "<p>para1\tmore</p>\n\n<p>para2</p>"), + ) + for value, output in items: + self.check_output(f, value, output) + + def test_strip_tags(self): + f = html.strip_tags + items = ( + ('<adf>a', 'a'), + ('</adf>a', 'a'), + ('<asdf><asdf>e', 'e'), + ('<f', '<f'), + ('</fe', '</fe'), + ('<x>b<y>', 'b'), + ) + for value, output in items: + self.check_output(f, value, output) + + def test_strip_spaces_between_tags(self): + f = html.strip_spaces_between_tags + # Strings that should come out untouched. + items = (' <adf>', '<adf> ', ' </adf> ', ' <f> x</f>') + for value in items: + self.check_output(f, value) + # Strings that have spaces to strip. + items = ( + ('<d> </d>', '<d></d>'), + ('<p>hello </p>\n<p> world</p>', '<p>hello </p><p> world</p>'), + ('\n<p>\t</p>\n<p> </p>\n', '\n<p></p><p></p>\n'), + ) + for value, output in items: + self.check_output(f, value, output) + + def test_strip_entities(self): + f = html.strip_entities + # Strings that should come out untouched. + values = ("&", "&a", "&a", "a&#a") + for value in values: + self.check_output(f, value) + # Valid entities that should be stripped from the patterns. + entities = ("", "", "&a;", "&fdasdfasdfasdf;") + patterns = ( + ("asdf %(entity)s ", "asdf "), + ("%(entity)s%(entity)s", ""), + ("&%(entity)s%(entity)s", "&"), + ("%(entity)s3", "3"), + ) + for entity in entities: + for in_pattern, output in patterns: + self.check_output(f, in_pattern % {'entity': entity}, output) + + def test_fix_ampersands(self): + f = html.fix_ampersands + # Strings without ampersands or with ampersands already encoded. + values = ("a", "b", "&a;", "& &x; ", "asdf") + patterns = ( + ("%s", "%s"), + ("&%s", "&%s"), + ("&%s&", "&%s&"), + ) + for value in values: + for in_pattern, out_pattern in patterns: + self.check_output(f, in_pattern % value, out_pattern % value) + # Strings with ampersands that need encoding. + items = ( + ("&#;", "&#;"), + ("ͫ ;", "&#875 ;"), + ("abc;", "&#4abc;"), + ) + for value, output in items: + self.check_output(f, value, output) diff --git a/parts/django/tests/regressiontests/utils/models.py b/parts/django/tests/regressiontests/utils/models.py new file mode 100644 index 0000000..97a72ba --- /dev/null +++ b/parts/django/tests/regressiontests/utils/models.py @@ -0,0 +1 @@ +# Test runner needs a models.py file. diff --git a/parts/django/tests/regressiontests/utils/module_loading.py b/parts/django/tests/regressiontests/utils/module_loading.py new file mode 100644 index 0000000..8cbefbb --- /dev/null +++ b/parts/django/tests/regressiontests/utils/module_loading.py @@ -0,0 +1,114 @@ +import os +import sys +import unittest +from zipimport import zipimporter + +from django.utils.importlib import import_module +from django.utils.module_loading import module_has_submodule + +class DefaultLoader(unittest.TestCase): + def test_loader(self): + "Normal module existence can be tested" + test_module = import_module('regressiontests.utils.test_module') + + # An importable child + self.assertTrue(module_has_submodule(test_module, 'good_module')) + mod = import_module('regressiontests.utils.test_module.good_module') + self.assertEqual(mod.content, 'Good Module') + + # A child that exists, but will generate an import error if loaded + self.assertTrue(module_has_submodule(test_module, 'bad_module')) + self.assertRaises(ImportError, import_module, 'regressiontests.utils.test_module.bad_module') + + # A child that doesn't exist + self.assertFalse(module_has_submodule(test_module, 'no_such_module')) + self.assertRaises(ImportError, import_module, 'regressiontests.utils.test_module.no_such_module') + +class EggLoader(unittest.TestCase): + def setUp(self): + self.old_path = sys.path[:] + self.egg_dir = '%s/eggs' % os.path.dirname(__file__) + + def tearDown(self): + sys.path = self.old_path + sys.path_importer_cache.clear() + + sys.modules.pop('egg_module.sub1.sub2.bad_module', None) + sys.modules.pop('egg_module.sub1.sub2.good_module', None) + sys.modules.pop('egg_module.sub1.sub2', None) + sys.modules.pop('egg_module.sub1', None) + sys.modules.pop('egg_module.bad_module', None) + sys.modules.pop('egg_module.good_module', None) + sys.modules.pop('egg_module', None) + + def test_shallow_loader(self): + "Module existence can be tested inside eggs" + egg_name = '%s/test_egg.egg' % self.egg_dir + sys.path.append(egg_name) + egg_module = import_module('egg_module') + + # An importable child + self.assertTrue(module_has_submodule(egg_module, 'good_module')) + mod = import_module('egg_module.good_module') + self.assertEqual(mod.content, 'Good Module') + + # A child that exists, but will generate an import error if loaded + self.assertTrue(module_has_submodule(egg_module, 'bad_module')) + self.assertRaises(ImportError, import_module, 'egg_module.bad_module') + + # A child that doesn't exist + self.assertFalse(module_has_submodule(egg_module, 'no_such_module')) + self.assertRaises(ImportError, import_module, 'egg_module.no_such_module') + + def test_deep_loader(self): + "Modules deep inside an egg can still be tested for existence" + egg_name = '%s/test_egg.egg' % self.egg_dir + sys.path.append(egg_name) + egg_module = import_module('egg_module.sub1.sub2') + + # An importable child + self.assertTrue(module_has_submodule(egg_module, 'good_module')) + mod = import_module('egg_module.sub1.sub2.good_module') + self.assertEqual(mod.content, 'Deep Good Module') + + # A child that exists, but will generate an import error if loaded + self.assertTrue(module_has_submodule(egg_module, 'bad_module')) + self.assertRaises(ImportError, import_module, 'egg_module.sub1.sub2.bad_module') + + # A child that doesn't exist + self.assertFalse(module_has_submodule(egg_module, 'no_such_module')) + self.assertRaises(ImportError, import_module, 'egg_module.sub1.sub2.no_such_module') + +class TestFinder(object): + def __init__(self, *args, **kwargs): + self.importer = zipimporter(*args, **kwargs) + + def find_module(self, path): + importer = self.importer.find_module(path) + if importer is None: + return + return TestLoader(importer) + +class TestLoader(object): + def __init__(self, importer): + self.importer = importer + + def load_module(self, name): + mod = self.importer.load_module(name) + mod.__loader__ = self + return mod + +class CustomLoader(EggLoader): + """The Custom Loader test is exactly the same as the EggLoader, but + it uses a custom defined Loader and Finder that is intentionally + split into two classes. Although the EggLoader combines both functions + into one class, this isn't required. + """ + def setUp(self): + super(CustomLoader, self).setUp() + sys.path_hooks.insert(0, TestFinder) + sys.path_importer_cache.clear() + + def tearDown(self): + super(CustomLoader, self).tearDown() + sys.path_hooks.pop(0) diff --git a/parts/django/tests/regressiontests/utils/simplelazyobject.py b/parts/django/tests/regressiontests/utils/simplelazyobject.py new file mode 100644 index 0000000..4a930dd --- /dev/null +++ b/parts/django/tests/regressiontests/utils/simplelazyobject.py @@ -0,0 +1,77 @@ +import unittest + +import django.utils.copycompat as copy +from django.utils.functional import SimpleLazyObject + +class _ComplexObject(object): + def __init__(self, name): + self.name = name + + def __eq__(self, other): + return self.name == other.name + + def __hash__(self): + return hash(self.name) + + def __str__(self): + return "I am _ComplexObject(%r)" % self.name + + def __unicode__(self): + return unicode(self.name) + + def __repr__(self): + return "_ComplexObject(%r)" % self.name + +complex_object = lambda: _ComplexObject("joe") + +class TestUtilsSimpleLazyObject(unittest.TestCase): + """ + Tests for SimpleLazyObject + """ + # Note that concrete use cases for SimpleLazyObject are also found in the + # auth context processor tests (unless the implementation of that function + # is changed). + + def test_equality(self): + self.assertEqual(complex_object(), SimpleLazyObject(complex_object)) + self.assertEqual(SimpleLazyObject(complex_object), complex_object()) + + def test_hash(self): + # hash() equality would not be true for many objects, but it should be + # for _ComplexObject + self.assertEqual(hash(complex_object()), + hash(SimpleLazyObject(complex_object))) + + def test_repr(self): + # For debugging, it will really confuse things if there is no clue that + # SimpleLazyObject is actually a proxy object. So we don't + # proxy __repr__ + self.assert_("SimpleLazyObject" in repr(SimpleLazyObject(complex_object))) + + def test_str(self): + self.assertEqual("I am _ComplexObject('joe')", str(SimpleLazyObject(complex_object))) + + def test_unicode(self): + self.assertEqual(u"joe", unicode(SimpleLazyObject(complex_object))) + + def test_class(self): + # This is important for classes that use __class__ in things like + # equality tests. + self.assertEqual(_ComplexObject, SimpleLazyObject(complex_object).__class__) + + def test_deepcopy(self): + # Check that we *can* do deep copy, and that it returns the right + # objects. + + # First, for an unevaluated SimpleLazyObject + s = SimpleLazyObject(complex_object) + assert s._wrapped is None + s2 = copy.deepcopy(s) + assert s._wrapped is None # something has gone wrong is s is evaluated + self.assertEqual(s2, complex_object()) + + # Second, for an evaluated SimpleLazyObject + name = s.name # evaluate + assert s._wrapped is not None + s3 = copy.deepcopy(s) + self.assertEqual(s3, complex_object()) diff --git a/parts/django/tests/regressiontests/utils/termcolors.py b/parts/django/tests/regressiontests/utils/termcolors.py new file mode 100644 index 0000000..ccae32c --- /dev/null +++ b/parts/django/tests/regressiontests/utils/termcolors.py @@ -0,0 +1,149 @@ +import unittest + +from django.utils.termcolors import parse_color_setting, PALETTES, DEFAULT_PALETTE, LIGHT_PALETTE, DARK_PALETTE, NOCOLOR_PALETTE + +class TermColorTests(unittest.TestCase): + + def test_empty_string(self): + self.assertEquals(parse_color_setting(''), PALETTES[DEFAULT_PALETTE]) + + def test_simple_palette(self): + self.assertEquals(parse_color_setting('light'), PALETTES[LIGHT_PALETTE]) + self.assertEquals(parse_color_setting('dark'), PALETTES[DARK_PALETTE]) + self.assertEquals(parse_color_setting('nocolor'), None) + + def test_fg(self): + self.assertEquals(parse_color_setting('error=green'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + + def test_fg_bg(self): + self.assertEquals(parse_color_setting('error=green/blue'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg':'blue'})) + + def test_fg_opts(self): + self.assertEquals(parse_color_setting('error=green,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink',)})) + self.assertEquals(parse_color_setting('error=green,bold,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink','bold')})) + + def test_fg_bg_opts(self): + self.assertEquals(parse_color_setting('error=green/blue,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg':'blue', 'opts': ('blink',)})) + self.assertEquals(parse_color_setting('error=green/blue,bold,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg':'blue', 'opts': ('blink','bold')})) + + def test_override_palette(self): + self.assertEquals(parse_color_setting('light;error=green'), + dict(PALETTES[LIGHT_PALETTE], + ERROR={'fg':'green'})) + + def test_override_nocolor(self): + self.assertEquals(parse_color_setting('nocolor;error=green'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg': 'green'})) + + def test_reverse_override(self): + self.assertEquals(parse_color_setting('error=green;light'), PALETTES[LIGHT_PALETTE]) + + def test_multiple_roles(self): + self.assertEquals(parse_color_setting('error=green;sql_field=blue'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'}, + SQL_FIELD={'fg':'blue'})) + + def test_override_with_multiple_roles(self): + self.assertEquals(parse_color_setting('light;error=green;sql_field=blue'), + dict(PALETTES[LIGHT_PALETTE], + ERROR={'fg':'green'}, + SQL_FIELD={'fg':'blue'})) + + def test_empty_definition(self): + self.assertEquals(parse_color_setting(';'), None) + self.assertEquals(parse_color_setting('light;'), PALETTES[LIGHT_PALETTE]) + self.assertEquals(parse_color_setting(';;;'), None) + + def test_empty_options(self): + self.assertEquals(parse_color_setting('error=green,'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=green,,,'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=green,,blink,,'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink',)})) + + def test_bad_palette(self): + self.assertEquals(parse_color_setting('unknown'), None) + + def test_bad_role(self): + self.assertEquals(parse_color_setting('unknown='), None) + self.assertEquals(parse_color_setting('unknown=green'), None) + self.assertEquals(parse_color_setting('unknown=green;sql_field=blue'), + dict(PALETTES[NOCOLOR_PALETTE], + SQL_FIELD={'fg':'blue'})) + + def test_bad_color(self): + self.assertEquals(parse_color_setting('error='), None) + self.assertEquals(parse_color_setting('error=;sql_field=blue'), + dict(PALETTES[NOCOLOR_PALETTE], + SQL_FIELD={'fg':'blue'})) + self.assertEquals(parse_color_setting('error=unknown'), None) + self.assertEquals(parse_color_setting('error=unknown;sql_field=blue'), + dict(PALETTES[NOCOLOR_PALETTE], + SQL_FIELD={'fg':'blue'})) + self.assertEquals(parse_color_setting('error=green/unknown'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=green/blue/something'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg': 'blue'})) + self.assertEquals(parse_color_setting('error=green/blue/something,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg': 'blue', 'opts': ('blink',)})) + + def test_bad_option(self): + self.assertEquals(parse_color_setting('error=green,unknown'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=green,unknown,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink',)})) + + def test_role_case(self): + self.assertEquals(parse_color_setting('ERROR=green'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('eRrOr=green'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + + def test_color_case(self): + self.assertEquals(parse_color_setting('error=GREEN'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=GREEN/BLUE'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg':'blue'})) + + self.assertEquals(parse_color_setting('error=gReEn'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=gReEn/bLuE'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg':'blue'})) + + def test_opts_case(self): + self.assertEquals(parse_color_setting('error=green,BLINK'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink',)})) + + self.assertEquals(parse_color_setting('error=green,bLiNk'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink',)})) diff --git a/parts/django/tests/regressiontests/utils/test_module/__init__.py b/parts/django/tests/regressiontests/utils/test_module/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/test_module/__init__.py diff --git a/parts/django/tests/regressiontests/utils/test_module/bad_module.py b/parts/django/tests/regressiontests/utils/test_module/bad_module.py new file mode 100644 index 0000000..cc0cd16 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/test_module/bad_module.py @@ -0,0 +1,3 @@ +import a_package_name_that_does_not_exist + +content = 'Bad Module'
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/utils/test_module/good_module.py b/parts/django/tests/regressiontests/utils/test_module/good_module.py new file mode 100644 index 0000000..0ca6898 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/test_module/good_module.py @@ -0,0 +1 @@ +content = 'Good Module'
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/utils/tests.py b/parts/django/tests/regressiontests/utils/tests.py new file mode 100644 index 0000000..6d3bbfa --- /dev/null +++ b/parts/django/tests/regressiontests/utils/tests.py @@ -0,0 +1,18 @@ +""" +Tests for django.utils. +""" + +from dateformat import * +from feedgenerator import * +from module_loading import * +from termcolors import * +from html import * +from checksums import * +from text import * +from simplelazyobject import * +from decorators import * +from functional import * +from timesince import * +from datastructures import * +from tzinfo import * +from datetime_safe import * diff --git a/parts/django/tests/regressiontests/utils/text.py b/parts/django/tests/regressiontests/utils/text.py new file mode 100644 index 0000000..e7d2d38 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/text.py @@ -0,0 +1,20 @@ +import unittest + +from django.utils import text + +class TestUtilsText(unittest.TestCase): + def test_truncate_words(self): + self.assertEqual(u'The quick brown fox jumped over the lazy dog.', + text.truncate_words(u'The quick brown fox jumped over the lazy dog.', 10)) + self.assertEqual(u'The quick brown fox ...', + text.truncate_words('The quick brown fox jumped over the lazy dog.', 4)) + self.assertEqual(u'The quick brown fox ....', + text.truncate_words('The quick brown fox jumped over the lazy dog.', 4, '....')) + self.assertEqual(u'<p><strong><em>The quick brown fox jumped over the lazy dog.</em></strong></p>', + text.truncate_html_words('<p><strong><em>The quick brown fox jumped over the lazy dog.</em></strong></p>', 10)) + self.assertEqual(u'<p><strong><em>The quick brown fox ...</em></strong></p>', + text.truncate_html_words('<p><strong><em>The quick brown fox jumped over the lazy dog.</em></strong></p>', 4)) + self.assertEqual(u'<p><strong><em>The quick brown fox ....</em></strong></p>', + text.truncate_html_words('<p><strong><em>The quick brown fox jumped over the lazy dog.</em></strong></p>', 4, '....')) + self.assertEqual(u'<p><strong><em>The quick brown fox</em></strong></p>', + text.truncate_html_words('<p><strong><em>The quick brown fox jumped over the lazy dog.</em></strong></p>', 4, None)) diff --git a/parts/django/tests/regressiontests/utils/timesince.py b/parts/django/tests/regressiontests/utils/timesince.py new file mode 100644 index 0000000..774aa3f --- /dev/null +++ b/parts/django/tests/regressiontests/utils/timesince.py @@ -0,0 +1,107 @@ +import datetime +import unittest + +from django.utils.timesince import timesince, timeuntil +from django.utils.tzinfo import LocalTimezone, FixedOffset + +class TimesinceTests(unittest.TestCase): + + def setUp(self): + self.t = datetime.datetime(2007, 8, 14, 13, 46, 0) + self.onemicrosecond = datetime.timedelta(microseconds=1) + self.onesecond = datetime.timedelta(seconds=1) + self.oneminute = datetime.timedelta(minutes=1) + self.onehour = datetime.timedelta(hours=1) + self.oneday = datetime.timedelta(days=1) + self.oneweek = datetime.timedelta(days=7) + self.onemonth = datetime.timedelta(days=30) + self.oneyear = datetime.timedelta(days=365) + + def test_equal_datetimes(self): + """ equal datetimes. """ + self.assertEquals(timesince(self.t, self.t), u'0 minutes') + + def test_ignore_microseconds_and_seconds(self): + """ Microseconds and seconds are ignored. """ + self.assertEquals(timesince(self.t, self.t+self.onemicrosecond), + u'0 minutes') + self.assertEquals(timesince(self.t, self.t+self.onesecond), + u'0 minutes') + + def test_other_units(self): + """ Test other units. """ + self.assertEquals(timesince(self.t, self.t+self.oneminute), + u'1 minute') + self.assertEquals(timesince(self.t, self.t+self.onehour), u'1 hour') + self.assertEquals(timesince(self.t, self.t+self.oneday), u'1 day') + self.assertEquals(timesince(self.t, self.t+self.oneweek), u'1 week') + self.assertEquals(timesince(self.t, self.t+self.onemonth), + u'1 month') + self.assertEquals(timesince(self.t, self.t+self.oneyear), u'1 year') + + def test_multiple_units(self): + """ Test multiple units. """ + self.assertEquals(timesince(self.t, + self.t+2*self.oneday+6*self.onehour), u'2 days, 6 hours') + self.assertEquals(timesince(self.t, + self.t+2*self.oneweek+2*self.oneday), u'2 weeks, 2 days') + + def test_display_first_unit(self): + """ + If the two differing units aren't adjacent, only the first unit is + displayed. + """ + self.assertEquals(timesince(self.t, + self.t+2*self.oneweek+3*self.onehour+4*self.oneminute), + u'2 weeks') + + self.assertEquals(timesince(self.t, + self.t+4*self.oneday+5*self.oneminute), u'4 days') + + def test_display_second_before_first(self): + """ + When the second date occurs before the first, we should always + get 0 minutes. + """ + self.assertEquals(timesince(self.t, self.t-self.onemicrosecond), + u'0 minutes') + self.assertEquals(timesince(self.t, self.t-self.onesecond), + u'0 minutes') + self.assertEquals(timesince(self.t, self.t-self.oneminute), + u'0 minutes') + self.assertEquals(timesince(self.t, self.t-self.onehour), + u'0 minutes') + self.assertEquals(timesince(self.t, self.t-self.oneday), + u'0 minutes') + self.assertEquals(timesince(self.t, self.t-self.oneweek), + u'0 minutes') + self.assertEquals(timesince(self.t, self.t-self.onemonth), + u'0 minutes') + self.assertEquals(timesince(self.t, self.t-self.oneyear), + u'0 minutes') + self.assertEquals(timesince(self.t, + self.t-2*self.oneday-6*self.onehour), u'0 minutes') + self.assertEquals(timesince(self.t, + self.t-2*self.oneweek-2*self.oneday), u'0 minutes') + self.assertEquals(timesince(self.t, + self.t-2*self.oneweek-3*self.onehour-4*self.oneminute), + u'0 minutes') + self.assertEquals(timesince(self.t, + self.t-4*self.oneday-5*self.oneminute), u'0 minutes') + + def test_different_timezones(self): + """ When using two different timezones. """ + now = datetime.datetime.now() + now_tz = datetime.datetime.now(LocalTimezone(now)) + now_tz_i = datetime.datetime.now(FixedOffset((3 * 60) + 15)) + + self.assertEquals(timesince(now), u'0 minutes') + self.assertEquals(timesince(now_tz), u'0 minutes') + self.assertEquals(timeuntil(now_tz, now_tz_i), u'0 minutes') + + def test_both_date_objects(self): + """ Timesince should work with both date objects (#9672) """ + today = datetime.date.today() + self.assertEquals(timeuntil(today+self.oneday, today), u'1 day') + self.assertEquals(timeuntil(today-self.oneday, today), u'0 minutes') + self.assertEquals(timeuntil(today+self.oneweek, today), u'1 week') diff --git a/parts/django/tests/regressiontests/utils/tzinfo.py b/parts/django/tests/regressiontests/utils/tzinfo.py new file mode 100644 index 0000000..edbb9a7 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/tzinfo.py @@ -0,0 +1,18 @@ +import unittest + +from django.utils.tzinfo import FixedOffset + +class TzinfoTests(unittest.TestCase): + + def test_fixedoffset(self): + self.assertEquals(repr(FixedOffset(0)), '+0000') + self.assertEquals(repr(FixedOffset(60)), '+0100') + self.assertEquals(repr(FixedOffset(-60)), '-0100') + self.assertEquals(repr(FixedOffset(280)), '+0440') + self.assertEquals(repr(FixedOffset(-280)), '-0440') + self.assertEquals(repr(FixedOffset(-78.4)), '-0118') + self.assertEquals(repr(FixedOffset(78.4)), '+0118') + self.assertEquals(repr(FixedOffset(-5.5*60)), '-0530') + self.assertEquals(repr(FixedOffset(5.5*60)), '+0530') + self.assertEquals(repr(FixedOffset(-.5*60)), '-0030') + self.assertEquals(repr(FixedOffset(.5*60)), '+0030') diff --git a/parts/django/tests/regressiontests/utils/urls.py b/parts/django/tests/regressiontests/utils/urls.py new file mode 100644 index 0000000..ba09d14 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import * + +import views + +urlpatterns = patterns('', + (r'^xview/$', views.xview), + (r'^class_xview/$', views.class_xview), +) diff --git a/parts/django/tests/regressiontests/utils/views.py b/parts/django/tests/regressiontests/utils/views.py new file mode 100644 index 0000000..ef97c65 --- /dev/null +++ b/parts/django/tests/regressiontests/utils/views.py @@ -0,0 +1,17 @@ +from django.http import HttpResponse +from django.utils.decorators import decorator_from_middleware +from django.middleware.doc import XViewMiddleware + + +xview_dec = decorator_from_middleware(XViewMiddleware) + +def xview(request): + return HttpResponse() +xview = xview_dec(xview) + + +class ClassXView(object): + def __call__(self, request): + return HttpResponse() + +class_xview = xview_dec(ClassXView()) diff --git a/parts/django/tests/regressiontests/views/__init__.py b/parts/django/tests/regressiontests/views/__init__.py new file mode 100644 index 0000000..d1c6e08 --- /dev/null +++ b/parts/django/tests/regressiontests/views/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf8 -*- + +class BrokenException(Exception): + pass + +except_args = ('Broken!', # plain exception with ASCII text + u'¡Broken!', # non-ASCII unicode data + '¡Broken!', # non-ASCII, utf-8 encoded bytestring + '\xa1Broken!', ) # non-ASCII, latin1 bytestring + diff --git a/parts/django/tests/regressiontests/views/app0/__init__.py b/parts/django/tests/regressiontests/views/app0/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/parts/django/tests/regressiontests/views/app0/__init__.py @@ -0,0 +1 @@ +# diff --git a/parts/django/tests/regressiontests/views/app0/locale/en/LC_MESSAGES/djangojs.mo b/parts/django/tests/regressiontests/views/app0/locale/en/LC_MESSAGES/djangojs.mo Binary files differnew file mode 100644 index 0000000..662204a --- /dev/null +++ b/parts/django/tests/regressiontests/views/app0/locale/en/LC_MESSAGES/djangojs.mo diff --git a/parts/django/tests/regressiontests/views/app0/locale/en/LC_MESSAGES/djangojs.po b/parts/django/tests/regressiontests/views/app0/locale/en/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..a458935 --- /dev/null +++ b/parts/django/tests/regressiontests/views/app0/locale/en/LC_MESSAGES/djangojs.po @@ -0,0 +1,20 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-15 19:15+0200\n" +"PO-Revision-Date: 2010-05-12 12:41-0300\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "il faut traduire cette chaîne de caractères de app0" +msgstr "this app0 string is to be translated" diff --git a/parts/django/tests/regressiontests/views/app1/__init__.py b/parts/django/tests/regressiontests/views/app1/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/parts/django/tests/regressiontests/views/app1/__init__.py @@ -0,0 +1 @@ +# diff --git a/parts/django/tests/regressiontests/views/app1/locale/fr/LC_MESSAGES/djangojs.mo b/parts/django/tests/regressiontests/views/app1/locale/fr/LC_MESSAGES/djangojs.mo Binary files differnew file mode 100644 index 0000000..5d6aecb --- /dev/null +++ b/parts/django/tests/regressiontests/views/app1/locale/fr/LC_MESSAGES/djangojs.mo diff --git a/parts/django/tests/regressiontests/views/app1/locale/fr/LC_MESSAGES/djangojs.po b/parts/django/tests/regressiontests/views/app1/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..a4627db --- /dev/null +++ b/parts/django/tests/regressiontests/views/app1/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,20 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-15 19:15+0200\n" +"PO-Revision-Date: 2010-05-12 12:41-0300\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "this app1 string is to be translated" +msgstr "il faut traduire cette chaîne de caractères de app1" diff --git a/parts/django/tests/regressiontests/views/app2/__init__.py b/parts/django/tests/regressiontests/views/app2/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/parts/django/tests/regressiontests/views/app2/__init__.py @@ -0,0 +1 @@ +# diff --git a/parts/django/tests/regressiontests/views/app2/locale/fr/LC_MESSAGES/djangojs.mo b/parts/django/tests/regressiontests/views/app2/locale/fr/LC_MESSAGES/djangojs.mo Binary files differnew file mode 100644 index 0000000..17e1863 --- /dev/null +++ b/parts/django/tests/regressiontests/views/app2/locale/fr/LC_MESSAGES/djangojs.mo diff --git a/parts/django/tests/regressiontests/views/app2/locale/fr/LC_MESSAGES/djangojs.po b/parts/django/tests/regressiontests/views/app2/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..637b9e6 --- /dev/null +++ b/parts/django/tests/regressiontests/views/app2/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,20 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-15 19:15+0200\n" +"PO-Revision-Date: 2010-05-12 22:05-0300\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "this app2 string is to be translated" +msgstr "il faut traduire cette chaîne de caractères de app2" diff --git a/parts/django/tests/regressiontests/views/app3/__init__.py b/parts/django/tests/regressiontests/views/app3/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/parts/django/tests/regressiontests/views/app3/__init__.py @@ -0,0 +1 @@ +# diff --git a/parts/django/tests/regressiontests/views/app3/locale/es_AR/LC_MESSAGES/djangojs.mo b/parts/django/tests/regressiontests/views/app3/locale/es_AR/LC_MESSAGES/djangojs.mo Binary files differnew file mode 100644 index 0000000..0c485a9 --- /dev/null +++ b/parts/django/tests/regressiontests/views/app3/locale/es_AR/LC_MESSAGES/djangojs.mo diff --git a/parts/django/tests/regressiontests/views/app3/locale/es_AR/LC_MESSAGES/djangojs.po b/parts/django/tests/regressiontests/views/app3/locale/es_AR/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..1e3be0b --- /dev/null +++ b/parts/django/tests/regressiontests/views/app3/locale/es_AR/LC_MESSAGES/djangojs.po @@ -0,0 +1,20 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-15 19:15+0200\n" +"PO-Revision-Date: 2010-05-12 12:41-0300\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "il faut traduire cette chaîne de caractères de app3" +msgstr "este texto de app3 debe ser traducido" diff --git a/parts/django/tests/regressiontests/views/app4/__init__.py b/parts/django/tests/regressiontests/views/app4/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/parts/django/tests/regressiontests/views/app4/__init__.py @@ -0,0 +1 @@ +# diff --git a/parts/django/tests/regressiontests/views/app4/locale/es_AR/LC_MESSAGES/djangojs.mo b/parts/django/tests/regressiontests/views/app4/locale/es_AR/LC_MESSAGES/djangojs.mo Binary files differnew file mode 100644 index 0000000..581fbb0 --- /dev/null +++ b/parts/django/tests/regressiontests/views/app4/locale/es_AR/LC_MESSAGES/djangojs.mo diff --git a/parts/django/tests/regressiontests/views/app4/locale/es_AR/LC_MESSAGES/djangojs.po b/parts/django/tests/regressiontests/views/app4/locale/es_AR/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..27403c0 --- /dev/null +++ b/parts/django/tests/regressiontests/views/app4/locale/es_AR/LC_MESSAGES/djangojs.po @@ -0,0 +1,20 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-15 19:15+0200\n" +"PO-Revision-Date: 2010-05-12 12:41-0300\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "il faut traduire cette chaîne de caractères de app4" +msgstr "este texto de app4 debe ser traducido" diff --git a/parts/django/tests/regressiontests/views/fixtures/testdata.json b/parts/django/tests/regressiontests/views/fixtures/testdata.json new file mode 100644 index 0000000..ab68407 --- /dev/null +++ b/parts/django/tests/regressiontests/views/fixtures/testdata.json @@ -0,0 +1,75 @@ +[ + { + "pk": "1", + "model": "auth.user", + "fields": { + "username": "testclient", + "first_name": "Test", + "last_name": "Client", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": 1, + "model": "views.author", + "fields": { + "name": "Boris" + } + }, + { + "pk": 1, + "model": "views.article", + "fields": { + "author": 1, + "title": "Old Article", + "slug": "old_article", + "date_created": "2001-01-01 21:22:23" + } + }, + { + "pk": 2, + "model": "views.article", + "fields": { + "author": 1, + "title": "Current Article", + "slug": "current_article", + "date_created": "2007-09-17 21:22:23" + } + }, + { + "pk": 3, + "model": "views.article", + "fields": { + "author": 1, + "title": "Future Article", + "slug": "future_article", + "date_created": "3000-01-01 21:22:23" + } + }, + { + "pk": 1, + "model": "views.urlarticle", + "fields": { + "author": 1, + "title": "Old Article", + "slug": "old_article", + "date_created": "2001-01-01 21:22:23" + } + }, + { + "pk": 1, + "model": "sites.site", + "fields": { + "domain": "testserver", + "name": "testserver" + } + } +] diff --git a/parts/django/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.mo b/parts/django/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.mo Binary files differnew file mode 100644 index 0000000..b6b0887 --- /dev/null +++ b/parts/django/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.mo diff --git a/parts/django/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.po b/parts/django/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..669af4b --- /dev/null +++ b/parts/django/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.po @@ -0,0 +1,25 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-15 16:45+0200\n" +"PO-Revision-Date: 2010-05-12 12:57-0300\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: media/js/translate.js:1 +msgid "this is to be translated" +msgstr "esto tiene que ser traducido" + + +msgid "Choose a time" +msgstr "Elige una hora" diff --git a/parts/django/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.mo b/parts/django/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.mo Binary files differnew file mode 100644 index 0000000..356147c --- /dev/null +++ b/parts/django/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.mo diff --git a/parts/django/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.po b/parts/django/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..0d03f95 --- /dev/null +++ b/parts/django/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,24 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-15 19:15+0200\n" +"PO-Revision-Date: 2010-05-12 12:41-0300\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "this is to be translated" +msgstr "il faut le traduire" + + +msgid "Choose a time" +msgstr "Choisir une heure" diff --git a/parts/django/tests/regressiontests/views/locale/ru/LC_MESSAGES/djangojs.mo b/parts/django/tests/regressiontests/views/locale/ru/LC_MESSAGES/djangojs.mo Binary files differnew file mode 100644 index 0000000..21659a9 --- /dev/null +++ b/parts/django/tests/regressiontests/views/locale/ru/LC_MESSAGES/djangojs.mo diff --git a/parts/django/tests/regressiontests/views/locale/ru/LC_MESSAGES/djangojs.po b/parts/django/tests/regressiontests/views/locale/ru/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000..4ea193a --- /dev/null +++ b/parts/django/tests/regressiontests/views/locale/ru/LC_MESSAGES/djangojs.po @@ -0,0 +1,24 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-15 16:45+0200\n" +"PO-Revision-Date: 2010-05-12 12:57-0300\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "this is to be translated" +msgstr "перевод" + + +msgid "Choose a time" +msgstr "Выберите время" diff --git a/parts/django/tests/regressiontests/views/media/file.txt b/parts/django/tests/regressiontests/views/media/file.txt new file mode 100644 index 0000000..f1fc82c --- /dev/null +++ b/parts/django/tests/regressiontests/views/media/file.txt @@ -0,0 +1 @@ +An example media file.
\ No newline at end of file diff --git a/parts/django/tests/regressiontests/views/media/file.txt.gz b/parts/django/tests/regressiontests/views/media/file.txt.gz Binary files differnew file mode 100644 index 0000000..0ee7d18 --- /dev/null +++ b/parts/django/tests/regressiontests/views/media/file.txt.gz diff --git a/parts/django/tests/regressiontests/views/media/file.unknown b/parts/django/tests/regressiontests/views/media/file.unknown new file mode 100644 index 0000000..77dcda8 --- /dev/null +++ b/parts/django/tests/regressiontests/views/media/file.unknown @@ -0,0 +1 @@ +An unknown file extension. diff --git a/parts/django/tests/regressiontests/views/models.py b/parts/django/tests/regressiontests/views/models.py new file mode 100644 index 0000000..54f5c1c --- /dev/null +++ b/parts/django/tests/regressiontests/views/models.py @@ -0,0 +1,49 @@ +""" +Regression tests for Django built-in views. +""" + +from django.db import models + +class Author(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + + def get_absolute_url(self): + return '/views/authors/%s/' % self.id + +class BaseArticle(models.Model): + """ + An abstract article Model so that we can create article models with and + without a get_absolute_url method (for create_update generic views tests). + """ + title = models.CharField(max_length=100) + slug = models.SlugField() + author = models.ForeignKey(Author) + + class Meta: + abstract = True + + def __unicode__(self): + return self.title + +class Article(BaseArticle): + date_created = models.DateTimeField() + +class UrlArticle(BaseArticle): + """ + An Article class with a get_absolute_url defined. + """ + date_created = models.DateTimeField() + + def get_absolute_url(self): + return '/urlarticles/%s/' % self.slug + get_absolute_url.purge = True + +class DateArticle(BaseArticle): + """ + An article Model with a DateField instead of DateTimeField, + for testing #7602 + """ + date_created = models.DateField() diff --git a/parts/django/tests/regressiontests/views/templates/debug/template_exception.html b/parts/django/tests/regressiontests/views/templates/debug/template_exception.html new file mode 100644 index 0000000..c6b34a8 --- /dev/null +++ b/parts/django/tests/regressiontests/views/templates/debug/template_exception.html @@ -0,0 +1,2 @@ +{% load debugtags %} +{% go_boom arg %} diff --git a/parts/django/tests/regressiontests/views/templatetags/__init__.py b/parts/django/tests/regressiontests/views/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/views/templatetags/__init__.py diff --git a/parts/django/tests/regressiontests/views/templatetags/debugtags.py b/parts/django/tests/regressiontests/views/templatetags/debugtags.py new file mode 100644 index 0000000..9b2c661 --- /dev/null +++ b/parts/django/tests/regressiontests/views/templatetags/debugtags.py @@ -0,0 +1,10 @@ +from django import template + +from regressiontests.views import BrokenException + +register = template.Library() + +@register.simple_tag +def go_boom(arg): + raise BrokenException(arg) + diff --git a/parts/django/tests/regressiontests/views/tests/__init__.py b/parts/django/tests/regressiontests/views/tests/__init__.py new file mode 100644 index 0000000..697968e --- /dev/null +++ b/parts/django/tests/regressiontests/views/tests/__init__.py @@ -0,0 +1,7 @@ +from debug import * +from defaults import * +from generic.create_update import * +from generic.date_based import * +from i18n import * +from specials import * +from static import * diff --git a/parts/django/tests/regressiontests/views/tests/debug.py b/parts/django/tests/regressiontests/views/tests/debug.py new file mode 100644 index 0000000..4ebe37f --- /dev/null +++ b/parts/django/tests/regressiontests/views/tests/debug.py @@ -0,0 +1,50 @@ +import inspect + +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase +from django.core.urlresolvers import reverse +from django.template import TemplateSyntaxError + +from regressiontests.views import BrokenException, except_args + +class DebugViewTests(TestCase): + def setUp(self): + self.old_debug = settings.DEBUG + settings.DEBUG = True + self.old_template_debug = settings.TEMPLATE_DEBUG + settings.TEMPLATE_DEBUG = True + + def tearDown(self): + settings.DEBUG = self.old_debug + settings.TEMPLATE_DEBUG = self.old_template_debug + + def test_files(self): + response = self.client.get('/views/raises/') + self.assertEquals(response.status_code, 500) + + data = { + 'file_data.txt': SimpleUploadedFile('file_data.txt', 'haha'), + } + response = self.client.post('/views/raises/', data) + self.assertTrue('file_data.txt' in response.content) + self.assertFalse('haha' in response.content) + + def test_404(self): + response = self.client.get('/views/raises404/') + self.assertEquals(response.status_code, 404) + + def test_view_exceptions(self): + for n in range(len(except_args)): + self.assertRaises(BrokenException, self.client.get, + reverse('view_exception', args=(n,))) + + def test_template_exceptions(self): + for n in range(len(except_args)): + try: + self.client.get(reverse('template_exception', args=(n,))) + except TemplateSyntaxError, e: + raising_loc = inspect.trace()[-1][-2][0].strip() + self.assertFalse(raising_loc.find('raise BrokenException') == -1, + "Failed to find 'raise BrokenException' in last frame of traceback, instead found: %s" % + raising_loc) diff --git a/parts/django/tests/regressiontests/views/tests/defaults.py b/parts/django/tests/regressiontests/views/tests/defaults.py new file mode 100644 index 0000000..ffc3471 --- /dev/null +++ b/parts/django/tests/regressiontests/views/tests/defaults.py @@ -0,0 +1,85 @@ +from os import path + +from django.conf import settings +from django.test import TestCase +from django.contrib.contenttypes.models import ContentType + +from regressiontests.views.models import Author, Article, UrlArticle + +class DefaultsTests(TestCase): + """Test django views in django/views/defaults.py""" + fixtures = ['testdata.json'] + non_existing_urls = ['/views/non_existing_url/', # this is in urls.py + '/views/other_non_existing_url/'] # this NOT in urls.py + + def test_shortcut_with_absolute_url(self): + "Can view a shortcut for an Author object that has a get_absolute_url method" + for obj in Author.objects.all(): + short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, obj.pk) + response = self.client.get(short_url) + self.assertRedirects(response, 'http://testserver%s' % obj.get_absolute_url(), + status_code=302, target_status_code=404) + + def test_shortcut_no_absolute_url(self): + "Shortcuts for an object that has no get_absolute_url method raises 404" + for obj in Article.objects.all(): + short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Article).id, obj.pk) + response = self.client.get(short_url) + self.assertEquals(response.status_code, 404) + + def test_wrong_type_pk(self): + short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, 'nobody/expects') + response = self.client.get(short_url) + self.assertEquals(response.status_code, 404) + + def test_shortcut_bad_pk(self): + short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, '42424242') + response = self.client.get(short_url) + self.assertEquals(response.status_code, 404) + + def test_nonint_content_type(self): + an_author = Author.objects.all()[0] + short_url = '/views/shortcut/%s/%s/' % ('spam', an_author.pk) + response = self.client.get(short_url) + self.assertEquals(response.status_code, 404) + + def test_bad_content_type(self): + an_author = Author.objects.all()[0] + short_url = '/views/shortcut/%s/%s/' % (42424242, an_author.pk) + response = self.client.get(short_url) + self.assertEquals(response.status_code, 404) + + def test_page_not_found(self): + "A 404 status is returned by the page_not_found view" + for url in self.non_existing_urls: + response = self.client.get(url) + self.assertEquals(response.status_code, 404) + + def test_csrf_token_in_404(self): + """ + The 404 page should have the csrf_token available in the context + """ + # See ticket #14565 + old_DEBUG = settings.DEBUG + try: + settings.DEBUG = False # so we get real 404, not technical + for url in self.non_existing_urls: + response = self.client.get(url) + csrf_token = response.context['csrf_token'] + self.assertNotEqual(str(csrf_token), 'NOTPROVIDED') + self.assertNotEqual(str(csrf_token), '') + finally: + settings.DEBUG = old_DEBUG + + def test_server_error(self): + "The server_error view raises a 500 status" + response = self.client.get('/views/server_error/') + self.assertEquals(response.status_code, 500) + + def test_get_absolute_url_attributes(self): + "A model can set attributes on the get_absolute_url method" + self.assertTrue(getattr(UrlArticle.get_absolute_url, 'purge', False), + 'The attributes of the original get_absolute_url must be added.') + article = UrlArticle.objects.get(pk=1) + self.assertTrue(getattr(article.get_absolute_url, 'purge', False), + 'The attributes of the original get_absolute_url must be added.') diff --git a/parts/django/tests/regressiontests/views/tests/generic/__init__.py b/parts/django/tests/regressiontests/views/tests/generic/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/parts/django/tests/regressiontests/views/tests/generic/__init__.py diff --git a/parts/django/tests/regressiontests/views/tests/generic/create_update.py b/parts/django/tests/regressiontests/views/tests/generic/create_update.py new file mode 100644 index 0000000..4ba1c35 --- /dev/null +++ b/parts/django/tests/regressiontests/views/tests/generic/create_update.py @@ -0,0 +1,211 @@ +import datetime + +from django.test import TestCase +from django.core.exceptions import ImproperlyConfigured +from regressiontests.views.models import Article, UrlArticle + +class CreateObjectTest(TestCase): + + fixtures = ['testdata.json'] + + def test_login_required_view(self): + """ + Verifies that an unauthenticated user attempting to access a + login_required view gets redirected to the login page and that + an authenticated user is let through. + """ + view_url = '/views/create_update/member/create/article/' + response = self.client.get(view_url) + self.assertRedirects(response, '/accounts/login/?next=%s' % view_url) + # Now login and try again. + login = self.client.login(username='testclient', password='password') + self.assertTrue(login, 'Could not log in') + response = self.client.get(view_url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'views/article_form.html') + + def test_create_article_display_page(self): + """ + Ensures the generic view returned the page and contains a form. + """ + view_url = '/views/create_update/create/article/' + response = self.client.get(view_url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'views/article_form.html') + if not response.context.get('form'): + self.fail('No form found in the response.') + + def test_create_article_with_errors(self): + """ + POSTs a form that contains validation errors. + """ + view_url = '/views/create_update/create/article/' + num_articles = Article.objects.count() + response = self.client.post(view_url, { + 'title': 'My First Article', + }) + self.assertFormError(response, 'form', 'slug', [u'This field is required.']) + self.assertTemplateUsed(response, 'views/article_form.html') + self.assertEqual(num_articles, Article.objects.count(), + "Number of Articles should not have changed.") + + def test_create_custom_save_article(self): + """ + Creates a new article using a custom form class with a save method + that alters the slug entered. + """ + view_url = '/views/create_update/create_custom/article/' + response = self.client.post(view_url, { + 'title': 'Test Article', + 'slug': 'this-should-get-replaced', + 'author': 1, + 'date_created': datetime.datetime(2007, 6, 25), + }) + self.assertRedirects(response, + '/views/create_update/view/article/some-other-slug/', + target_status_code=404) + +class UpdateDeleteObjectTest(TestCase): + + fixtures = ['testdata.json'] + + def test_update_object_form_display(self): + """ + Verifies that the form was created properly and with initial values. + """ + response = self.client.get('/views/create_update/update/article/old_article/') + self.assertTemplateUsed(response, 'views/article_form.html') + self.assertEquals(unicode(response.context['form']['title']), + u'<input id="id_title" type="text" name="title" value="Old Article" maxlength="100" />') + + def test_update_object(self): + """ + Verifies the updating of an Article. + """ + response = self.client.post('/views/create_update/update/article/old_article/', { + 'title': 'Another Article', + 'slug': 'another-article-slug', + 'author': 1, + 'date_created': datetime.datetime(2007, 6, 25), + }) + article = Article.objects.get(pk=1) + self.assertEquals(article.title, "Another Article") + + def test_delete_object_confirm(self): + """ + Verifies the confirm deletion page is displayed using a GET. + """ + response = self.client.get('/views/create_update/delete/article/old_article/') + self.assertTemplateUsed(response, 'views/article_confirm_delete.html') + + def test_delete_object(self): + """ + Verifies the object actually gets deleted on a POST. + """ + view_url = '/views/create_update/delete/article/old_article/' + response = self.client.post(view_url) + try: + Article.objects.get(slug='old_article') + except Article.DoesNotExist: + pass + else: + self.fail('Object was not deleted.') + +class PostSaveRedirectTests(TestCase): + """ + Verifies that the views redirect to the correct locations depending on + if a post_save_redirect was passed and a get_absolute_url method exists + on the Model. + """ + + fixtures = ['testdata.json'] + article_model = Article + + create_url = '/views/create_update/create/article/' + update_url = '/views/create_update/update/article/old_article/' + delete_url = '/views/create_update/delete/article/old_article/' + + create_redirect = '/views/create_update/view/article/my-first-article/' + update_redirect = '/views/create_update/view/article/another-article-slug/' + delete_redirect = '/views/create_update/' + + def test_create_article(self): + num_articles = self.article_model.objects.count() + response = self.client.post(self.create_url, { + 'title': 'My First Article', + 'slug': 'my-first-article', + 'author': '1', + 'date_created': datetime.datetime(2007, 6, 25), + }) + self.assertRedirects(response, self.create_redirect, + target_status_code=404) + self.assertEqual(num_articles + 1, self.article_model.objects.count(), + "A new Article should have been created.") + + def test_update_article(self): + num_articles = self.article_model.objects.count() + response = self.client.post(self.update_url, { + 'title': 'Another Article', + 'slug': 'another-article-slug', + 'author': 1, + 'date_created': datetime.datetime(2007, 6, 25), + }) + self.assertRedirects(response, self.update_redirect, + target_status_code=404) + self.assertEqual(num_articles, self.article_model.objects.count(), + "A new Article should not have been created.") + + def test_delete_article(self): + num_articles = self.article_model.objects.count() + response = self.client.post(self.delete_url) + self.assertRedirects(response, self.delete_redirect, + target_status_code=404) + self.assertEqual(num_articles - 1, self.article_model.objects.count(), + "An Article should have been deleted.") + +class NoPostSaveNoAbsoluteUrl(PostSaveRedirectTests): + """ + Tests that when no post_save_redirect is passed and no get_absolute_url + method exists on the Model that the view raises an ImproperlyConfigured + error. + """ + + create_url = '/views/create_update/no_redirect/create/article/' + update_url = '/views/create_update/no_redirect/update/article/old_article/' + + def test_create_article(self): + self.assertRaises(ImproperlyConfigured, + super(NoPostSaveNoAbsoluteUrl, self).test_create_article) + + def test_update_article(self): + self.assertRaises(ImproperlyConfigured, + super(NoPostSaveNoAbsoluteUrl, self).test_update_article) + + def test_delete_article(self): + """ + The delete_object view requires a post_delete_redirect, so skip testing + here. + """ + pass + +class AbsoluteUrlNoPostSave(PostSaveRedirectTests): + """ + Tests that the views redirect to the Model's get_absolute_url when no + post_save_redirect is passed. + """ + + # Article model with get_absolute_url method. + article_model = UrlArticle + + create_url = '/views/create_update/no_url/create/article/' + update_url = '/views/create_update/no_url/update/article/old_article/' + + create_redirect = '/urlarticles/my-first-article/' + update_redirect = '/urlarticles/another-article-slug/' + + def test_delete_article(self): + """ + The delete_object view requires a post_delete_redirect, so skip testing + here. + """ + pass diff --git a/parts/django/tests/regressiontests/views/tests/generic/date_based.py b/parts/django/tests/regressiontests/views/tests/generic/date_based.py new file mode 100644 index 0000000..c6ba562 --- /dev/null +++ b/parts/django/tests/regressiontests/views/tests/generic/date_based.py @@ -0,0 +1,140 @@ +# coding: utf-8 +from django.test import TestCase +from datetime import datetime, date +from datetime import timedelta +from regressiontests.views.models import Article, Author, DateArticle + +class ObjectDetailTest(TestCase): + fixtures = ['testdata.json'] + def setUp(self): + # Correct the date for the current article + current_article = Article.objects.get(title="Current Article") + current_article.date_created = datetime.now() + current_article.save() + + def test_finds_past(self): + "date_based.object_detail can view a page in the past" + response = self.client.get('/views/date_based/object_detail/2001/01/01/old_article/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['object'].title, "Old Article") + + def test_object_detail_finds_today(self): + "date_based.object_detail can view a page from today" + today_url = datetime.now().strftime('%Y/%m/%d') + response = self.client.get('/views/date_based/object_detail/%s/current_article/' % today_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['object'].title, "Current Article") + + def test_object_detail_ignores_future(self): + "date_based.object_detail can view a page from the future, but only if allowed." + response = self.client.get('/views/date_based/object_detail/3000/01/01/future_article/') + self.assertEqual(response.status_code, 404) + + def test_object_detail_allowed_future_if_enabled(self): + "date_based.object_detail can view a page from the future if explicitly allowed." + response = self.client.get('/views/date_based/object_detail/3000/01/01/future_article/allow_future/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['object'].title, "Future Article") + +class MonthArchiveTest(TestCase): + def test_archive_month_includes_only_month(self): + "Regression for #3031: Archives around Feburary include only one month" + author = Author(name="John Smith") + author.save() + + # 2004 was a leap year, so it should be weird enough to not cheat + first_second_of_feb = datetime(2004, 2, 1, 0, 0, 1) + first_second_of_mar = datetime(2004, 3, 1, 0, 0, 1) + two_seconds = timedelta(0, 2, 0) + article = Article(title="example", author=author) + + article.date_created = first_second_of_feb + article.save() + response = self.client.get('/views/date_based/archive_month/2004/02/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['next_month'], date(2004, 3, 1)) + self.assertEqual(response.context['previous_month'], date(2004, 1, 1)) + + article.date_created = first_second_of_feb-two_seconds + article.save() + response = self.client.get('/views/date_based/archive_month/2004/02/') + self.assertEqual(response.status_code, 404) + + article.date_created = first_second_of_mar-two_seconds + article.save() + response = self.client.get('/views/date_based/archive_month/2004/02/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['next_month'], date(2004, 3, 1)) + self.assertEqual(response.context['previous_month'], date(2004, 1, 1)) + + article.date_created = first_second_of_mar + article.save() + response = self.client.get('/views/date_based/archive_month/2004/02/') + self.assertEqual(response.status_code, 404) + + article2 = DateArticle(title="example", author=author) + + article2.date_created = first_second_of_feb.date() + article2.save() + response = self.client.get('/views/date_based/datefield/archive_month/2004/02/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['next_month'], date(2004, 3, 1)) + self.assertEqual(response.context['previous_month'], date(2004, 1, 1)) + + article2.date_created = (first_second_of_feb-two_seconds).date() + article2.save() + response = self.client.get('/views/date_based/datefield/archive_month/2004/02/') + self.assertEqual(response.status_code, 404) + + article2.date_created = (first_second_of_mar-two_seconds).date() + article2.save() + response = self.client.get('/views/date_based/datefield/archive_month/2004/02/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['next_month'], date(2004, 3, 1)) + self.assertEqual(response.context['previous_month'], date(2004, 1, 1)) + + article2.date_created = first_second_of_mar.date() + article2.save() + response = self.client.get('/views/date_based/datefield/archive_month/2004/02/') + self.assertEqual(response.status_code, 404) + + now = datetime.now() + prev_month = now.date().replace(day=1) + if prev_month.month == 1: + prev_month = prev_month.replace(year=prev_month.year-1, month=12) + else: + prev_month = prev_month.replace(month=prev_month.month-1) + article2.date_created = now + article2.save() + response = self.client.get('/views/date_based/datefield/archive_month/%s/' % now.strftime('%Y/%m')) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['next_month'], None) + self.assertEqual(response.context['previous_month'], prev_month) + + def test_archive_month_date_list(self): + author = Author(name="John Smith") + author.save() + date1 = datetime(2010, 1, 1, 0, 0, 0) + date2 = datetime(2010, 1, 2, 0, 0, 0) + Article.objects.create(title='example1', author=author, date_created=date1) + Article.objects.create(title='example2', author=author, date_created=date2) + response = self.client.get('/views/date_based/archive_month/2010/1/') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['date_list']), 2) + self.assertEqual(response.context['date_list'][0], date1) + # Checks that the same date is not included more than once in the list + Article.objects.create(title='example2', author=author, date_created=date2) + response = self.client.get('/views/date_based/archive_month/2010/1/') + self.assertEqual(len(response.context['date_list']), 2) + +class DayArchiveTests(TestCase): + + def test_year_month_day_format(self): + """ + Make sure day views don't get confused with numeric month formats (#7944) + """ + author = Author.objects.create(name="John Smith") + article = Article.objects.create(title="example", author=author, date_created=datetime(2004, 1, 21, 0, 0, 1)) + response = self.client.get('/views/date_based/archive_day/2004/1/21/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['object_list'][0], article) diff --git a/parts/django/tests/regressiontests/views/tests/i18n.py b/parts/django/tests/regressiontests/views/tests/i18n.py new file mode 100644 index 0000000..24aa933 --- /dev/null +++ b/parts/django/tests/regressiontests/views/tests/i18n.py @@ -0,0 +1,149 @@ +# -*- coding:utf-8 -*- +import gettext + +from django.conf import settings +from django.test import TestCase +from django.utils.translation import activate, deactivate +from django.utils.text import javascript_quote + +from regressiontests.views.urls import locale_dir + +class I18NTests(TestCase): + """ Tests django views in django/views/i18n.py """ + + def test_setlang(self): + """The set_language view can be used to change the session language""" + for lang_code, lang_name in settings.LANGUAGES: + post_data = dict(language=lang_code, next='/views/') + response = self.client.post('/views/i18n/setlang/', data=post_data) + self.assertRedirects(response, 'http://testserver/views/') + self.assertEquals(self.client.session['django_language'], lang_code) + + def test_jsi18n(self): + """The javascript_catalog can be deployed with language settings""" + for lang_code in ['es', 'fr', 'ru']: + activate(lang_code) + catalog = gettext.translation('djangojs', locale_dir, [lang_code]) + trans_txt = catalog.ugettext('this is to be translated') + response = self.client.get('/views/jsi18n/') + # in response content must to be a line like that: + # catalog['this is to be translated'] = 'same_that_trans_txt' + # javascript_quote is used to be able to check unicode strings + self.assertContains(response, javascript_quote(trans_txt), 1) + + +class JsI18NTests(TestCase): + """ + Tests django views in django/views/i18n.py that need to change + settings.LANGUAGE_CODE. + """ + + def setUp(self): + self.old_language_code = settings.LANGUAGE_CODE + self.old_installed_apps = settings.INSTALLED_APPS + + def tearDown(self): + deactivate() + settings.LANGUAGE_CODE = self.old_language_code + settings.INSTALLED_APPS = self.old_installed_apps + + def test_jsi18n_with_missing_en_files(self): + """ + The javascript_catalog shouldn't load the fallback language in the + case that the current selected language is actually the one translated + from, and hence missing translation files completely. + + This happens easily when you're translating from English to other + languages and you've set settings.LANGUAGE_CODE to some other language + than English. + """ + settings.LANGUAGE_CODE = 'es' + activate('en-us') + response = self.client.get('/views/jsi18n/') + self.assertNotContains(response, 'esto tiene que ser traducido') + + def test_jsi18n_fallback_language(self): + """ + Let's make sure that the fallback language is still working properly + in cases where the selected language cannot be found. + """ + settings.LANGUAGE_CODE = 'fr' + activate('fi') + response = self.client.get('/views/jsi18n/') + self.assertContains(response, 'il faut le traduire') + + def testI18NLanguageNonEnglishDefault(self): + """ + Check if the Javascript i18n view returns an empty language catalog + if the default language is non-English, the selected language + is English and there is not 'en' translation available. See #13388, + #3594 and #13726 for more details. + """ + settings.LANGUAGE_CODE = 'fr' + activate('en-us') + response = self.client.get('/views/jsi18n/') + self.assertNotContains(response, 'Choisir une heure') + + def test_nonenglish_default_english_userpref(self): + """ + Same as above with the difference that there IS an 'en' translation + available. The Javascript i18n view must return a NON empty language catalog + with the proper English translations. See #13726 for more details. + """ + settings.LANGUAGE_CODE = 'fr' + settings.INSTALLED_APPS = list(settings.INSTALLED_APPS) + ['regressiontests.views.app0'] + activate('en-us') + response = self.client.get('/views/jsi18n_english_translation/') + self.assertContains(response, javascript_quote('this app0 string is to be translated')) + + def testI18NLanguageNonEnglishFallback(self): + """ + Makes sure that the fallback language is still working properly + in cases where the selected language cannot be found. + """ + settings.LANGUAGE_CODE = 'fr' + activate('none') + response = self.client.get('/views/jsi18n/') + self.assertContains(response, 'Choisir une heure') + + +class JsI18NTestsMultiPackage(TestCase): + """ + Tests for django views in django/views/i18n.py that need to change + settings.LANGUAGE_CODE and merge JS translation from several packages. + """ + + def setUp(self): + self.old_language_code = settings.LANGUAGE_CODE + self.old_installed_apps = settings.INSTALLED_APPS + + def tearDown(self): + settings.LANGUAGE_CODE = self.old_language_code + settings.INSTALLED_APPS = self.old_installed_apps + + def testI18NLanguageEnglishDefault(self): + """ + Check if the JavaScript i18n view returns a complete language catalog + if the default language is en-us, the selected language has a + translation available and a catalog composed by djangojs domain + translations of multiple Python packages is requested. See #13388, + #3594 and #13514 for more details. + """ + settings.LANGUAGE_CODE = 'en-us' + settings.INSTALLED_APPS = list(settings.INSTALLED_APPS) + ['regressiontests.views.app1', 'regressiontests.views.app2'] + activate('fr') + response = self.client.get('/views/jsi18n_multi_packages1/') + self.assertContains(response, javascript_quote('il faut traduire cette chaîne de caractères de app1')) + deactivate() + + def testI18NDifferentNonEnLangs(self): + """ + Similar to above but with neither default or requested language being + English. + """ + settings.LANGUAGE_CODE = 'fr' + settings.INSTALLED_APPS = list(settings.INSTALLED_APPS) + ['regressiontests.views.app3', 'regressiontests.views.app4'] + activate('es-ar') + response = self.client.get('/views/jsi18n_multi_packages2/') + self.assertContains(response, javascript_quote('este texto de app3 debe ser traducido')) + deactivate() diff --git a/parts/django/tests/regressiontests/views/tests/specials.py b/parts/django/tests/regressiontests/views/tests/specials.py new file mode 100644 index 0000000..bcdffca --- /dev/null +++ b/parts/django/tests/regressiontests/views/tests/specials.py @@ -0,0 +1,35 @@ +# coding: utf-8 +from django.test import TestCase + +class URLHandling(TestCase): + """ + Tests for URL handling in views and responses. + """ + redirect_target = "/views/%E4%B8%AD%E6%96%87/target/" + + def test_combining_redirect(self): + """ + Tests that redirecting to an IRI, requiring encoding before we use it + in an HTTP response, is handled correctly. In this case the arg to + HttpRedirect is ASCII but the current request path contains non-ASCII + characters so this test ensures the creation of the full path with a + base non-ASCII part is handled correctly. + """ + response = self.client.get(u'/views/中文/') + self.assertRedirects(response, self.redirect_target) + + def test_nonascii_redirect(self): + """ + Tests that a non-ASCII argument to HttpRedirect is handled properly. + """ + response = self.client.get('/views/nonascii_redirect/') + self.assertRedirects(response, self.redirect_target) + + def test_permanent_nonascii_redirect(self): + """ + Tests that a non-ASCII argument to HttpPermanentRedirect is handled + properly. + """ + response = self.client.get('/views/permanent_nonascii_redirect/') + self.assertRedirects(response, self.redirect_target, status_code=301) + diff --git a/parts/django/tests/regressiontests/views/tests/static.py b/parts/django/tests/regressiontests/views/tests/static.py new file mode 100644 index 0000000..de0bd51 --- /dev/null +++ b/parts/django/tests/regressiontests/views/tests/static.py @@ -0,0 +1,77 @@ +import mimetypes +from os import path + +from django.test import TestCase +from django.http import HttpResponseNotModified +from regressiontests.views.urls import media_dir + +class StaticTests(TestCase): + """Tests django views in django/views/static.py""" + + def test_serve(self): + "The static view can serve static media" + media_files = ['file.txt', 'file.txt.gz'] + for filename in media_files: + response = self.client.get('/views/site_media/%s' % filename) + file_path = path.join(media_dir, filename) + self.assertEquals(open(file_path).read(), response.content) + self.assertEquals(len(response.content), int(response['Content-Length'])) + self.assertEquals(mimetypes.guess_type(file_path)[1], response.get('Content-Encoding', None)) + + def test_unknown_mime_type(self): + response = self.client.get('/views/site_media/file.unknown') + self.assertEquals('application/octet-stream', response['Content-Type']) + + def test_copes_with_empty_path_component(self): + file_name = 'file.txt' + response = self.client.get('/views/site_media//%s' % file_name) + file = open(path.join(media_dir, file_name)) + self.assertEquals(file.read(), response.content) + + def test_is_modified_since(self): + file_name = 'file.txt' + response = self.client.get( + '/views/site_media/%s' % file_name, + HTTP_IF_MODIFIED_SINCE='Thu, 1 Jan 1970 00:00:00 GMT') + file = open(path.join(media_dir, file_name)) + self.assertEquals(file.read(), response.content) + + def test_not_modified_since(self): + file_name = 'file.txt' + response = self.client.get( + '/views/site_media/%s' % file_name, + HTTP_IF_MODIFIED_SINCE='Mon, 18 Jan 2038 05:14:07 UTC' + # This is 24h before max Unix time. Remember to fix Django and + # update this test well before 2038 :) + ) + self.assertTrue(isinstance(response, HttpResponseNotModified)) + + def test_invalid_if_modified_since(self): + """Handle bogus If-Modified-Since values gracefully + + Assume that a file is modified since an invalid timestamp as per RFC + 2616, section 14.25. + """ + file_name = 'file.txt' + invalid_date = 'Mon, 28 May 999999999999 28:25:26 GMT' + response = self.client.get('/views/site_media/%s' % file_name, + HTTP_IF_MODIFIED_SINCE=invalid_date) + file = open(path.join(media_dir, file_name)) + self.assertEquals(file.read(), response.content) + self.assertEquals(len(response.content), + int(response['Content-Length'])) + + def test_invalid_if_modified_since2(self): + """Handle even more bogus If-Modified-Since values gracefully + + Assume that a file is modified since an invalid timestamp as per RFC + 2616, section 14.25. + """ + file_name = 'file.txt' + invalid_date = ': 1291108438, Wed, 20 Oct 2010 14:05:00 GMT' + response = self.client.get('/views/site_media/%s' % file_name, + HTTP_IF_MODIFIED_SINCE=invalid_date) + file = open(path.join(media_dir, file_name)) + self.assertEquals(file.read(), response.content) + self.assertEquals(len(response.content), + int(response['Content-Length'])) diff --git a/parts/django/tests/regressiontests/views/urls.py b/parts/django/tests/regressiontests/views/urls.py new file mode 100644 index 0000000..0ccb988 --- /dev/null +++ b/parts/django/tests/regressiontests/views/urls.py @@ -0,0 +1,131 @@ +# coding: utf-8 +from os import path + +from django.conf.urls.defaults import * + +from models import * +import views + + +base_dir = path.dirname(path.abspath(__file__)) +media_dir = path.join(base_dir, 'media') +locale_dir = path.join(base_dir, 'locale') + +js_info_dict = { + 'domain': 'djangojs', + 'packages': ('regressiontests.views',), +} + +js_info_dict_english_translation = { + 'domain': 'djangojs', + 'packages': ('regressiontests.views.app0',), +} + +js_info_dict_multi_packages1 = { + 'domain': 'djangojs', + 'packages': ('regressiontests.views.app1', 'regressiontests.views.app2'), +} + +js_info_dict_multi_packages2 = { + 'domain': 'djangojs', + 'packages': ('regressiontests.views.app3', 'regressiontests.views.app4'), +} + +date_based_info_dict = { + 'queryset': Article.objects.all(), + 'date_field': 'date_created', + 'month_format': '%m', +} +numeric_days_info_dict = dict(date_based_info_dict, day_format='%d') + +date_based_datefield_info_dict = dict(date_based_info_dict, queryset=DateArticle.objects.all()) + +urlpatterns = patterns('', + (r'^$', views.index_page), + + # Default views + (r'^shortcut/(\d+)/(.*)/$', 'django.views.defaults.shortcut'), + (r'^non_existing_url/', 'django.views.defaults.page_not_found'), + (r'^server_error/', 'django.views.defaults.server_error'), + + # i18n views + (r'^i18n/', include('django.conf.urls.i18n')), + (r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict), + (r'^jsi18n_english_translation/$', 'django.views.i18n.javascript_catalog', js_info_dict_english_translation), + (r'^jsi18n_multi_packages1/$', 'django.views.i18n.javascript_catalog', js_info_dict_multi_packages1), + (r'^jsi18n_multi_packages2/$', 'django.views.i18n.javascript_catalog', js_info_dict_multi_packages2), + + # Static views + (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': media_dir}), + + # Special URLs for particular regression cases. + url(u'^中文/$', 'regressiontests.views.views.redirect'), + url(u'^中文/target/$', 'regressiontests.views.views.index_page'), +) + +# Date-based generic views. +urlpatterns += patterns('django.views.generic.date_based', + (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$', + 'object_detail', + dict(slug_field='slug', **date_based_info_dict)), + (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/allow_future/$', + 'object_detail', + dict(allow_future=True, slug_field='slug', **date_based_info_dict)), + (r'^date_based/archive_day/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$', + 'archive_day', + numeric_days_info_dict), + (r'^date_based/archive_month/(?P<year>\d{4})/(?P<month>\d{1,2})/$', + 'archive_month', + date_based_info_dict), + (r'^date_based/datefield/archive_month/(?P<year>\d{4})/(?P<month>\d{1,2})/$', + 'archive_month', + date_based_datefield_info_dict), +) + +# crud generic views. + +urlpatterns += patterns('django.views.generic.create_update', + (r'^create_update/member/create/article/$', 'create_object', + dict(login_required=True, model=Article)), + (r'^create_update/create/article/$', 'create_object', + dict(post_save_redirect='/views/create_update/view/article/%(slug)s/', + model=Article)), + (r'^create_update/update/article/(?P<slug>[-\w]+)/$', 'update_object', + dict(post_save_redirect='/views/create_update/view/article/%(slug)s/', + slug_field='slug', model=Article)), + (r'^create_update/create_custom/article/$', views.custom_create), + (r'^create_update/delete/article/(?P<slug>[-\w]+)/$', 'delete_object', + dict(post_delete_redirect='/views/create_update/', slug_field='slug', + model=Article)), + + # No post_save_redirect and no get_absolute_url on model. + (r'^create_update/no_redirect/create/article/$', 'create_object', + dict(model=Article)), + (r'^create_update/no_redirect/update/article/(?P<slug>[-\w]+)/$', + 'update_object', dict(slug_field='slug', model=Article)), + + # get_absolute_url on model, but no passed post_save_redirect. + (r'^create_update/no_url/create/article/$', 'create_object', + dict(model=UrlArticle)), + (r'^create_update/no_url/update/article/(?P<slug>[-\w]+)/$', + 'update_object', dict(slug_field='slug', model=UrlArticle)), +) + +# a view that raises an exception for the debug view +urlpatterns += patterns('', + (r'^raises/$', views.raises), + (r'^raises404/$', views.raises404), +) + +# rediriects, both temporary and permanent, with non-ASCII targets +urlpatterns += patterns('django.views.generic.simple', + ('^nonascii_redirect/$', 'redirect_to', + {'url': u'/views/中文/target/', 'permanent': False}), + ('^permanent_nonascii_redirect/$', 'redirect_to', + {'url': u'/views/中文/target/', 'permanent': True}), +) + +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'), +) diff --git a/parts/django/tests/regressiontests/views/views.py b/parts/django/tests/regressiontests/views/views.py new file mode 100644 index 0000000..445b4ed --- /dev/null +++ b/parts/django/tests/regressiontests/views/views.py @@ -0,0 +1,59 @@ +import sys + +from django.http import HttpResponse, HttpResponseRedirect +from django import forms +from django.views.debug import technical_500_response +from django.views.generic.create_update import create_object +from django.core.urlresolvers import get_resolver +from django.shortcuts import render_to_response + +from regressiontests.views import BrokenException, except_args + +from models import Article + + +def index_page(request): + """Dummy index page""" + return HttpResponse('<html><body>Dummy page</body></html>') + +def custom_create(request): + """ + Calls create_object generic view with a custom form class. + """ + class SlugChangingArticleForm(forms.ModelForm): + """Custom form class to overwrite the slug.""" + + class Meta: + model = Article + + def save(self, *args, **kwargs): + self.instance.slug = 'some-other-slug' + return super(SlugChangingArticleForm, self).save(*args, **kwargs) + + return create_object(request, + post_save_redirect='/views/create_update/view/article/%(slug)s/', + form_class=SlugChangingArticleForm) + +def raises(request): + try: + raise Exception + except Exception: + return technical_500_response(request, *sys.exc_info()) + +def raises404(request): + resolver = get_resolver(None) + resolver.resolve('') + +def redirect(request): + """ + Forces an HTTP redirect. + """ + return HttpResponseRedirect("target/") + +def view_exception(request, n): + raise BrokenException(except_args[int(n)]) + +def template_exception(request, n): + return render_to_response('debug/template_exception.html', + {'arg': except_args[int(n)]}) + |