summaryrefslogtreecommitdiff
path: root/tests/regressiontests
diff options
context:
space:
mode:
Diffstat (limited to 'tests/regressiontests')
-rw-r--r--tests/regressiontests/admin_ordering/__init__.py (renamed from tests/regressiontests/invalid_admin_options/__init__.py)0
-rw-r--r--tests/regressiontests/admin_ordering/models.py46
-rw-r--r--tests/regressiontests/admin_views/__init__.py0
-rw-r--r--tests/regressiontests/admin_views/fixtures/admin-views-users.xml81
-rw-r--r--tests/regressiontests/admin_views/fixtures/string-primary-key.xml6
-rw-r--r--tests/regressiontests/admin_views/models.py61
-rw-r--r--tests/regressiontests/admin_views/tests.py362
-rw-r--r--tests/regressiontests/admin_views/urls.py7
-rw-r--r--tests/regressiontests/admin_widgets/__init__.py0
-rw-r--r--tests/regressiontests/admin_widgets/models.py85
-rw-r--r--tests/regressiontests/forms/forms.py72
-rw-r--r--tests/regressiontests/forms/formsets.py575
-rw-r--r--tests/regressiontests/forms/media.py359
-rw-r--r--tests/regressiontests/forms/tests.py4
-rw-r--r--tests/regressiontests/forms/widgets.py91
-rw-r--r--tests/regressiontests/inline_formsets/__init__.py0
-rw-r--r--tests/regressiontests/inline_formsets/models.py55
-rw-r--r--tests/regressiontests/invalid_admin_options/models.py337
-rw-r--r--tests/regressiontests/modeladmin/__init__.py0
-rw-r--r--tests/regressiontests/modeladmin/models.py876
20 files changed, 2680 insertions, 337 deletions
diff --git a/tests/regressiontests/invalid_admin_options/__init__.py b/tests/regressiontests/admin_ordering/__init__.py
index e69de29bb2..e69de29bb2 100644
--- a/tests/regressiontests/invalid_admin_options/__init__.py
+++ b/tests/regressiontests/admin_ordering/__init__.py
diff --git a/tests/regressiontests/admin_ordering/models.py b/tests/regressiontests/admin_ordering/models.py
new file mode 100644
index 0000000000..601f06bb0a
--- /dev/null
+++ b/tests/regressiontests/admin_ordering/models.py
@@ -0,0 +1,46 @@
+# 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',)
+
+__test__ = {'API_TESTS': """
+
+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.
+
+>>> from django.contrib.admin.options import ModelAdmin
+
+>>> 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()
+
+The default ordering should be by name, as specified in the inner Meta class.
+
+>>> ma = ModelAdmin(Band, None)
+>>> [b.name for b in ma.queryset(None)]
+[u'Aerosmith', u'Radiohead', u'Van Halen']
+
+
+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)
+>>> [b.name for b in ma.queryset(None)]
+[u'Radiohead', u'Van Halen', u'Aerosmith']
+
+"""
+}
diff --git a/tests/regressiontests/admin_views/__init__.py b/tests/regressiontests/admin_views/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/admin_views/__init__.py
diff --git a/tests/regressiontests/admin_views/fixtures/admin-views-users.xml b/tests/regressiontests/admin_views/fixtures/admin-views-users.xml
new file mode 100644
index 0000000000..8d6c62b58f
--- /dev/null
+++ b/tests/regressiontests/admin_views/fixtures/admin-views-users.xml
@@ -0,0 +1,81 @@
+<?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">&lt;p&gt;test content&lt;/p&gt;</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>
+</django-objects> \ No newline at end of file
diff --git a/tests/regressiontests/admin_views/fixtures/string-primary-key.xml b/tests/regressiontests/admin_views/fixtures/string-primary-key.xml
new file mode 100644
index 0000000000..8e1dbf047f
--- /dev/null
+++ b/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/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
new file mode 100644
index 0000000000..2062107397
--- /dev/null
+++ b/tests/regressiontests/admin_views/models.py
@@ -0,0 +1,61 @@
+from django.db import models
+from django.contrib import admin
+
+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.
+ """
+ content = models.TextField()
+ date = models.DateTimeField()
+ section = models.ForeignKey(Section)
+
+class ArticleAdmin(admin.ModelAdmin):
+ list_display = ('content', 'date')
+ list_filter = ('date',)
+
+ def changelist_view(self, request):
+ "Test that extra_context works"
+ return super(ArticleAdmin, self).changelist_view(
+ request, extra_context={
+ 'extra_var': 'Hello!'
+ }
+ )
+
+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'
+ object_history_template = 'custom_admin/object_history.html'
+ delete_confirmation_template = 'custom_admin/delete_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
+
+admin.site.register(Article, ArticleAdmin)
+admin.site.register(CustomArticle, CustomArticleAdmin)
+admin.site.register(Section)
+admin.site.register(ModelWithStringPrimaryKey)
diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
new file mode 100644
index 0000000000..ad50928aa9
--- /dev/null
+++ b/tests/regressiontests/admin_views/tests.py
@@ -0,0 +1,362 @@
+
+from django.test import TestCase
+from django.contrib.auth.models import User, Permission
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.admin.models import LogEntry
+from django.contrib.admin.sites import LOGIN_FORM_KEY, _encode_post_data
+from django.contrib.admin.util import quote
+from django.utils.html import escape
+
+# local test models
+from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey
+
+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 = {'post_data': _encode_post_data({}),
+ LOGIN_FORM_KEY: 1,
+ 'username': 'super',
+ 'password': 'secret'}
+ self.super_email_login = {'post_data': _encode_post_data({}),
+ LOGIN_FORM_KEY: 1,
+ 'username': 'super@example.com',
+ 'password': 'secret'}
+ self.super_email_bad_login = {'post_data': _encode_post_data({}),
+ LOGIN_FORM_KEY: 1,
+ 'username': 'super@example.com',
+ 'password': 'notsecret'}
+ self.adduser_login = {'post_data': _encode_post_data({}),
+ LOGIN_FORM_KEY: 1,
+ 'username': 'adduser',
+ 'password': 'secret'}
+ self.changeuser_login = {'post_data': _encode_post_data({}),
+ LOGIN_FORM_KEY: 1,
+ 'username': 'changeuser',
+ 'password': 'secret'}
+ self.deleteuser_login = {'post_data': _encode_post_data({}),
+ LOGIN_FORM_KEY: 1,
+ 'username': 'deleteuser',
+ 'password': 'secret'}
+ self.joepublic_login = {'post_data': _encode_post_data({}),
+ LOGIN_FORM_KEY: 1,
+ 'username': 'joepublic',
+ 'password': 'secret'}
+
+ def testTrailingSlashRequired(self):
+ """
+ If you leave off the trailing slash, app should redirect and add it.
+ """
+ self.client.post('/test_admin/admin/', self.super_login)
+
+ request = self.client.get(
+ '/test_admin/admin/admin_views/article/add'
+ )
+ self.assertRedirects(request,
+ '/test_admin/admin/admin_views/article/add/'
+ )
+
+ 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.failUnlessEqual(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.failUnlessEqual(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, "Usernames cannot contain the &#39;@&#39; character")
+ 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, "Usernames cannot contain the &#39;@&#39; character")
+
+ # Add User
+ request = self.client.get('/test_admin/admin/')
+ self.failUnlessEqual(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.failUnlessEqual(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.failUnlessEqual(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.failUnlessEqual(request.status_code, 200)
+ login = self.client.post('/test_admin/admin/', self.joepublic_login)
+ self.failUnlessEqual(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'))
+
+ def testAddView(self):
+ """Test add view restricts access and actually adds items."""
+
+ add_dict = {'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)
+ request = self.client.get('/test_admin/admin/admin_views/article/add/')
+ self.failUnlessEqual(request.status_code, 403)
+ # Try POST just to make sure
+ post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
+ self.failUnlessEqual(post.status_code, 403)
+ self.failUnlessEqual(Article.objects.all().count(), 1)
+ 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)
+ post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
+ self.assertRedirects(post, '/test_admin/admin/')
+ self.failUnlessEqual(Article.objects.all().count(), 2)
+ 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)
+ post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
+ self.assertRedirects(post, '/test_admin/admin/admin_views/article/')
+ self.failUnlessEqual(Article.objects.all().count(), 3)
+ self.client.get('/test_admin/admin/logout/')
+
+ # Check and make sure that if user expires, data still persists
+ post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
+ self.assertContains(post, 'Please log in again, because your session has expired.')
+ self.super_login['post_data'] = _encode_post_data(add_dict)
+ post = self.client.post('/test_admin/admin/admin_views/article/add/', self.super_login)
+ self.assertRedirects(post, '/test_admin/admin/admin_views/article/')
+ self.failUnlessEqual(Article.objects.all().count(), 4)
+ self.client.get('/test_admin/admin/logout/')
+
+ def testChangeView(self):
+ """Change view should restrict access and allow users to edit items."""
+
+ change_dict = {'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.failUnlessEqual(request.status_code, 403)
+ request = self.client.get('/test_admin/admin/admin_views/article/1/')
+ self.failUnlessEqual(request.status_code, 403)
+ post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
+ self.failUnlessEqual(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.failUnlessEqual(request.status_code, 200)
+ request = self.client.get('/test_admin/admin/admin_views/article/1/')
+ self.failUnlessEqual(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.failUnlessEqual(Article.objects.get(pk=1).content, '<p>edited article</p>')
+ 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.failUnlessEqual(request.status_code, 200)
+ self.assert_("var hello = 'Hello!';" in request.content)
+ self.assertTemplateUsed(request, 'custom_admin/change_list.html')
+
+ # Test custom change form template
+ request = self.client.get('/test_admin/admin/admin_views/customarticle/add/')
+ self.assertTemplateUsed(request, 'custom_admin/change_form.html')
+
+ # Add an article so we can test delete 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.failUnlessEqual(CustomArticle.objects.all().count(), 1)
+
+ # Test custom delete and object history templates
+ request = self.client.get('/test_admin/admin/admin_views/customarticle/1/delete/')
+ self.assertTemplateUsed(request, 'custom_admin/delete_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 testCustomAdminSiteTemplates(self):
+ from django.contrib import admin
+ self.assertEqual(admin.site.index_template, None)
+ self.assertEqual(admin.site.login_template, None)
+
+ self.client.get('/test_admin/admin/logout/')
+ request = self.client.get('/test_admin/admin/')
+ self.assertTemplateUsed(request, 'admin/login.html')
+ self.client.post('/test_admin/admin/', self.changeuser_login)
+ request = self.client.get('/test_admin/admin/')
+ self.assertTemplateUsed(request, 'admin/index.html')
+
+ self.client.get('/test_admin/admin/logout/')
+ admin.site.login_template = 'custom_admin/login.html'
+ admin.site.index_template = 'custom_admin/index.html'
+ request = self.client.get('/test_admin/admin/')
+ self.assertTemplateUsed(request, 'custom_admin/login.html')
+ self.assert_('Hello from a custom login template' in request.content)
+ self.client.post('/test_admin/admin/', self.changeuser_login)
+ request = self.client.get('/test_admin/admin/')
+ self.assertTemplateUsed(request, 'custom_admin/index.html')
+ self.assert_('Hello from a custom index template' in request.content)
+
+ # Finally, using monkey patching check we can inject custom_context arguments in to index
+ original_index = admin.site.index
+ def index(*args, **kwargs):
+ kwargs['extra_context'] = {'foo': '*bar*'}
+ return original_index(*args, **kwargs)
+ admin.site.index = index
+ request = self.client.get('/test_admin/admin/')
+ self.assertTemplateUsed(request, 'custom_admin/index.html')
+ self.assert_('Hello from a custom index template *bar*' in request.content)
+
+ self.client.get('/test_admin/admin/logout/')
+ del admin.site.index # Resets to using the original
+ admin.site.login_template = None
+ admin.site.index_template = None
+
+ 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.failUnlessEqual(request.status_code, 403)
+ post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict)
+ self.failUnlessEqual(post.status_code, 403)
+ self.failUnlessEqual(Article.objects.all().count(), 1)
+ 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.failUnlessEqual(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.failUnlessEqual(Article.objects.all().count(), 0)
+ self.client.get('/test_admin/admin/logout/')
+
+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_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.failUnlessEqual(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 = """<tr class="row1"><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_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))
+ should_contain = """<a href="../../%s/">%s</a>""" % (quote(self.pk), escape(self.pk))
+ self.assertContains(response, should_contain)
diff --git a/tests/regressiontests/admin_views/urls.py b/tests/regressiontests/admin_views/urls.py
new file mode 100644
index 0000000000..e556812a45
--- /dev/null
+++ b/tests/regressiontests/admin_views/urls.py
@@ -0,0 +1,7 @@
+from django.conf.urls.defaults import *
+from django.contrib import admin
+
+urlpatterns = patterns('',
+ (r'^admin/doc/', include('django.contrib.admindocs.urls')),
+ (r'^admin/(.*)', admin.site.root),
+)
diff --git a/tests/regressiontests/admin_widgets/__init__.py b/tests/regressiontests/admin_widgets/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/admin_widgets/__init__.py
diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
new file mode 100644
index 0000000000..584d973c83
--- /dev/null
+++ b/tests/regressiontests/admin_widgets/models.py
@@ -0,0 +1,85 @@
+
+from django.conf import settings
+from django.db import models
+
+class Member(models.Model):
+ name = models.CharField(max_length=100)
+
+ 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)
+
+ def __unicode__(self):
+ return self.name
+
+__test__ = {'WIDGETS_TESTS': """
+>>> from datetime import datetime
+>>> from django.utils.html import escape, conditional_escape
+>>> from django.contrib.admin.widgets import FilteredSelectMultiple, AdminSplitDateTime
+>>> from django.contrib.admin.widgets import AdminFileWidget, ForeignKeyRawIdWidget, ManyToManyRawIdWidget
+>>> from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
+
+Calling conditional_escape on the output of widget.render will simulate what
+happens in the template. This is easier than setting up a template and context
+for each test.
+
+Make sure that the Admin widgets render properly, that is, without their extra
+HTML escaped.
+
+>>> w = FilteredSelectMultiple('test', False)
+>>> print conditional_escape(w.render('test', 'test'))
+<select multiple="multiple" name="test">
+</select><script type="text/javascript">addEvent(window, "load", function(e) {SelectFilter.init("id_test", "test", 0, "%(ADMIN_MEDIA_PREFIX)s"); });</script>
+<BLANKLINE>
+
+>>> w = AdminSplitDateTime()
+>>> print 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>
+
+>>> w = AdminFileWidget()
+>>> print conditional_escape(w.render('test', 'test'))
+Currently: <a target="_blank" href="%(MEDIA_URL)stest">test</a> <br />Change: <input type="file" name="test" />
+
+>>> band = Band.objects.create(pk=1, name='Linkin Park')
+>>> album = band.album_set.create(name='Hybrid Theory')
+
+>>> rel = Album._meta.get_field('band').rel
+>>> w = ForeignKeyRawIdWidget(rel)
+>>> print conditional_escape(w.render('test', band.pk, attrs={}))
+<input type="text" name="test" value="1" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/band/" 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>&nbsp;<strong>Linkin Park</strong>
+
+>>> m1 = Member.objects.create(pk=1, name='Chester')
+>>> m2 = Member.objects.create(pk=2, name='Mike')
+>>> band.members.add(m1, m2)
+
+>>> rel = Band._meta.get_field('members').rel
+>>> w = ManyToManyRawIdWidget(rel)
+>>> print conditional_escape(w.render('test', [m1.pk, m2.pk], attrs={}))
+<input type="text" name="test" value="1,2" 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>
+>>> w._has_changed(None, None)
+False
+>>> w._has_changed([], None)
+False
+>>> w._has_changed(None, [u'1'])
+True
+>>> w._has_changed([1, 2], [u'1', u'2'])
+False
+>>> w._has_changed([1, 2], [u'1'])
+True
+>>> w._has_changed([1, 2], [u'1', u'3'])
+True
+
+""" % {
+ 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
+ 'MEDIA_URL': settings.MEDIA_URL,
+}}
diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py
index 041fa4054c..9add15163a 100644
--- a/tests/regressiontests/forms/forms.py
+++ b/tests/regressiontests/forms/forms.py
@@ -1667,4 +1667,76 @@ the list of errors is empty). You can also use it in {% if %} statements.
<p><label>Password (again): <input type="password" name="password2" value="bar" /></label></p>
<input type="submit" />
</form>
+
+
+# The empty_permitted attribute ##############################################
+
+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)
+>>> form.is_valid()
+False
+>>> form.errors
+{'name': [u'This field is required.'], 'artist': [u'This field is required.']}
+>>> form.cleaned_data
+Traceback (most recent call last):
+...
+AttributeError: 'SongForm' object has no attribute 'cleaned_data'
+
+
+Now let's show what happens when empty_permitted=True and the form is empty.
+
+>>> form = SongForm(data, empty_permitted=True)
+>>> form.is_valid()
+True
+>>> form.errors
+{}
+>>> 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)
+>>> form.is_valid()
+False
+>>> form.errors
+{'name': [u'This field is required.']}
+>>> form.cleaned_data
+Traceback (most recent call last):
+...
+AttributeError: 'SongForm' object has no attribute 'cleaned_data'
+
+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)
+>>> form.is_valid()
+True
+
+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)
+>>> form.is_valid()
+True
+
"""
diff --git a/tests/regressiontests/forms/formsets.py b/tests/regressiontests/forms/formsets.py
new file mode 100644
index 0000000000..dedc0a8e52
--- /dev/null
+++ b/tests/regressiontests/forms/formsets.py
@@ -0,0 +1,575 @@
+# -*- coding: utf-8 -*-
+tests = """
+# Basic FormSet creation and usage ############################################
+
+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.
+
+>>> from django.newforms import Form, CharField, IntegerField, ValidationError
+>>> from django.newforms.formsets import formset_factory, BaseFormSet
+
+>>> class Choice(Form):
+... choice = CharField()
+... votes = IntegerField()
+
+>>> ChoiceFormSet = formset_factory(Choice)
+
+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')
+>>> print 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_FORMS" value="0" />
+<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_FORMS': '0', # the 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')
+>>> formset.is_valid()
+True
+>>> [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()
+>>> formset.is_valid()
+False
+
+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_FORMS': '0', # the max number of forms
+... 'choices-0-choice': 'Calexico',
+... 'choices-0-votes': '',
+... }
+
+>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset.is_valid()
+False
+>>> formset.errors
+[{'votes': [u'This field is required.']}]
+
+
+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')
+>>> for form in formset.forms:
+... print form.as_ul()
+<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_FORMS': '0', # the 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')
+>>> formset.is_valid()
+True
+>>> [form.cleaned_data for form in formset.forms]
+[{'votes': 100, 'choice': u'Calexico'}, {}]
+
+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_FORMS': '0', # the 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')
+>>> formset.is_valid()
+False
+>>> formset.errors
+[{}, {'votes': [u'This field is required.']}]
+
+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_FORMS': '0', # the 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')
+>>> formset.is_valid()
+False
+>>> formset.errors
+[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}]
+
+
+# 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')
+>>> for form in formset.forms:
+... print form.as_ul()
+<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_FORMS': '0', # the 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')
+>>> formset.is_valid()
+True
+>>> [form.cleaned_data for form in formset.forms]
+[{}, {}, {}]
+
+
+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_FORMS': '0', # the max number of forms
+... 'choices-0-choice': 'Calexico',
+... 'choices-0-votes': '100',
+... 'choices-1-choice': '',
+... 'choices-1-votes': '',
+... 'choices-2-choice': '',
+... 'choices-2-votes': '',
+... }
+
+>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset.is_valid()
+True
+>>> [form.cleaned_data for form in formset.forms]
+[{'votes': 100, 'choice': u'Calexico'}, {}, {}]
+
+
+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_FORMS': '0', # the 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': '',
+... }
+
+>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset.is_valid()
+False
+>>> formset.errors
+[{}, {'votes': [u'This field is required.']}, {}]
+
+
+The extra argument also works when the formset is pre-filled with initial
+data.
+
+>>> initial = [{'choice': u'Calexico', 'votes': 100}]
+>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
+>>> for form in formset.forms:
+... print form.as_ul()
+<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>
+
+
+# FormSets with deletion ######################################################
+
+We can easily add deletion ability to a FormSet with an agrument 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')
+>>> for form in formset.forms:
+... print form.as_ul()
+<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_FORMS': '0', # the 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')
+>>> formset.is_valid()
+True
+>>> [form.cleaned_data for form in formset.forms]
+[{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}, {'votes': 900, 'DELETE': True, 'choice': u'Fergie'}, {}]
+>>> [form.cleaned_data for form in formset.deleted_forms]
+[{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}]
+
+
+# FormSets with ordering ######################################################
+
+We can also add ordering ability to a FormSet with an agrument 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')
+>>> for form in formset.forms:
+... print form.as_ul()
+<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_FORMS': '0', # the 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')
+>>> formset.is_valid()
+True
+>>> for form in formset.ordered_forms:
+... print form.cleaned_data
+{'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'}
+{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'}
+{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'}
+
+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_FORMS': '0', # the 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': '',
+... }
+
+>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+>>> formset.is_valid()
+True
+>>> for form in formset.ordered_forms:
+... print form.cleaned_data
+{'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'}
+
+
+# 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')
+>>> for form in formset.forms:
+... print form.as_ul()
+<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_FORMS': '0', # the 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')
+>>> formset.is_valid()
+True
+>>> for form in formset.ordered_forms:
+... print form.cleaned_data
+{'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'}
+{'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': u'Calexico'}
+>>> [form.cleaned_data for form in formset.deleted_forms]
+[{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}]
+
+
+# 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.
+
+Let's define a FormSet that takes a list of favorite drinks, but raises am
+error if there are any duplicates.
+
+>>> 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'])
+...
+
+>>> FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm,
+... formset=BaseFavoriteDrinksFormSet, extra=3)
+
+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_FORMS': '0', # the max number of forms
+... 'drinks-0-name': 'Gin and Tonic',
+... 'drinks-1-name': 'Gin and Tonic',
+... }
+
+>>> formset = FavoriteDrinksFormSet(data, prefix='drinks')
+>>> formset.is_valid()
+False
+
+Any errors raised by formset.clean() are available via the
+formset.non_form_errors() method.
+
+>>> for error in formset.non_form_errors():
+... print 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_FORMS': '0', # the max number of forms
+... 'drinks-0-name': 'Gin and Tonic',
+... 'drinks-1-name': 'Bloody Mary',
+... }
+
+>>> formset = FavoriteDrinksFormSet(data, prefix='drinks')
+>>> formset.is_valid()
+True
+>>> for error in formset.non_form_errors():
+... print error
+
+# Limiting the maximum number of forms ########################################
+
+# Base case for max_num.
+
+>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=5, max_num=2)
+>>> formset = LimitedFavoriteDrinkFormSet()
+>>> for form in formset.forms:
+... print form
+<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 the that max_num has no affect when extra is less than max_forms.
+
+>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2)
+>>> formset = LimitedFavoriteDrinkFormSet()
+>>> for form in formset.forms:
+... print form
+<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>
+
+# max_num with initial data
+
+# 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)
+>>> for form in formset.forms:
+... print form
+<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)
+>>> for form in formset.forms:
+... print form
+<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>
+
+
+# Regression test for #6926 ##################################################
+
+Make sure the management form has the correct prefix.
+
+>>> formset = FavoriteDrinksFormSet()
+>>> formset.management_form.prefix
+'form'
+
+>>> formset = FavoriteDrinksFormSet(data={})
+>>> formset.management_form.prefix
+'form'
+
+>>> formset = FavoriteDrinksFormSet(initial={})
+>>> formset.management_form.prefix
+'form'
+
+"""
diff --git a/tests/regressiontests/forms/media.py b/tests/regressiontests/forms/media.py
new file mode 100644
index 0000000000..3ea48876f5
--- /dev/null
+++ b/tests/regressiontests/forms/media.py
@@ -0,0 +1,359 @@
+# -*- coding: utf-8 -*-
+# Tests for the media handling on widgets and forms
+
+media_tests = r"""
+>>> from django.newforms import TextInput, Media, TextInput, CharField, Form, MultiWidget
+>>> from django.conf import settings
+>>> ORIGINAL_MEDIA_URL = settings.MEDIA_URL
+>>> settings.MEDIA_URL = 'http://media.example.com/media/'
+
+# 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'))
+>>> print 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)
+>>> print 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>
+
+>>> m3 = Media(Foo)
+>>> print 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()
+>>> print w.media
+<BLANKLINE>
+
+###############################################################
+# 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()
+>>> print 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
+>>> print 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" />
+
+>>> print 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>
+
+# Media objects can be combined. Any given media resource will appear only
+# once. Duplicated media definitions are ignored.
+>>> 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')
+
+>>> w2 = MyWidget2()
+>>> w3 = MyWidget3()
+>>> print 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
+>>> print 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>
+
+###############################################################
+# 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()
+>>> print 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()
+>>> print 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>
+
+# Media properties can reference the media of their parents,
+# even if the parent media was defined using a class
+>>> class MyWidget6(MyWidget1):
+... def _media(self):
+... return super(MyWidget6, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',))
+... media = property(_media)
+
+>>> w6 = MyWidget6()
+>>> print 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>
+
+###############################################################
+# Inheritance of media
+###############################################################
+
+# If a widget extends another but provides no media definition, it inherits the parent widget's media
+>>> class MyWidget7(MyWidget1):
+... pass
+
+>>> w7 = MyWidget7()
+>>> print 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()
+>>> print 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>
+
+# 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 MyWidget9(MyWidget4):
+... class Media:
+... css = {
+... 'all': ('/other/path',)
+... }
+... js = ('/other/js',)
+
+>>> w9 = MyWidget9()
+>>> print 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()
+>>> print 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>
+
+# A widget can explicitly enable full media inheritance by specifying 'extend=True'
+>>> class MyWidget11(MyWidget1):
+... class Media:
+... extend = True
+... css = {
+... 'all': ('/path/to/css3','path/to/css1')
+... }
+... js = ('/path/to/js1','/path/to/js4')
+
+>>> w11 = MyWidget11()
+>>> print 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>
+
+# A widget can enable inheritance of one media type by specifying extend as a tuple
+>>> class MyWidget12(MyWidget1):
+... class Media:
+... extend = ('css',)
+... css = {
+... 'all': ('/path/to/css3','path/to/css1')
+... }
+... js = ('/path/to/js1','/path/to/js4')
+
+>>> w12 = MyWidget12()
+>>> print 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>
+
+###############################################################
+# 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()
+>>> print 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>
+
+###############################################################
+# Multiwidget media handling
+###############################################################
+
+# 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()
+>>> print 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>
+
+###############################################################
+# Media processing for forms
+###############################################################
+
+# 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()
+>>> print 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()
+>>> print 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()
+>>> print 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>
+
+>>> settings.MEDIA_URL = ORIGINAL_MEDIA_URL
+""" \ No newline at end of file
diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
index bb0e30b874..ff8213c8d9 100644
--- a/tests/regressiontests/forms/tests.py
+++ b/tests/regressiontests/forms/tests.py
@@ -26,6 +26,8 @@ from localflavor.za import tests as localflavor_za_tests
from regressions import tests as regression_tests
from util import tests as util_tests
from widgets import tests as widgets_tests
+from formsets import tests as formset_tests
+from media import media_tests
__test__ = {
'extra_tests': extra_tests,
@@ -53,6 +55,8 @@ __test__ = {
'localflavor_us_tests': localflavor_us_tests,
'localflavor_za_tests': localflavor_za_tests,
'regression_tests': regression_tests,
+ 'formset_tests': formset_tests,
+ 'media_tests': media_tests,
'util_tests': util_tests,
'widgets_tests': widgets_tests,
}
diff --git a/tests/regressiontests/forms/widgets.py b/tests/regressiontests/forms/widgets.py
index 2c6b51a8ec..e0837ab5b3 100644
--- a/tests/regressiontests/forms/widgets.py
+++ b/tests/regressiontests/forms/widgets.py
@@ -202,6 +202,30 @@ u'<input type="file" class="fun" name="email" />'
>>> 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.
+>>> w._has_changed(u'', None)
+False
+
+# A file was uploaded and no initial data.
+>>> w._has_changed(u'', {'filename': 'resume.txt', 'content': 'My resume'})
+True
+
+# A file was not uploaded, but there is initial data
+>>> w._has_changed(u'resume.txt', None)
+False
+
+# A file was uploaded and there is initial data (file identity is not dealt
+# with here)
+>>> w._has_changed('resume.txt', {'filename': 'resume.txt', 'content': 'My resume'})
+True
+
# Textarea Widget #############################################################
>>> w = Textarea()
@@ -292,6 +316,21 @@ checkboxes).
>>> w.value_from_datadict({}, {}, 'testing')
False
+>>> w._has_changed(None, None)
+False
+>>> w._has_changed(None, u'')
+False
+>>> w._has_changed(u'', None)
+False
+>>> w._has_changed(u'', u'')
+False
+>>> w._has_changed(False, u'on')
+True
+>>> w._has_changed(True, u'on')
+False
+>>> w._has_changed(True, u'')
+True
+
# Select Widget ###############################################################
>>> w = Select()
@@ -573,6 +612,20 @@ If 'choices' is passed to both the constructor and render(), then they'll both b
>>> 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
+>>> w._has_changed(None, None)
+False
+>>> w._has_changed([], None)
+False
+>>> w._has_changed(None, [u'1'])
+True
+>>> w._has_changed([1, 2], [u'1', u'2'])
+False
+>>> w._has_changed([1, 2], [u'1'])
+True
+>>> w._has_changed([1, 2], [u'1', u'3'])
+True
+
# RadioSelect Widget ##########################################################
>>> w = RadioSelect()
@@ -871,6 +924,20 @@ If 'choices' is passed to both the constructor and render(), then they'll both b
<li><label><input type="checkbox" name="escape" value="good" /> you &gt; me</label></li>
</ul>
+# Test the usage of _has_changed
+>>> w._has_changed(None, None)
+False
+>>> w._has_changed([], None)
+False
+>>> w._has_changed(None, [u'1'])
+True
+>>> w._has_changed([1, 2], [u'1', u'2'])
+False
+>>> w._has_changed([1, 2], [u'1'])
+True
+>>> w._has_changed([1, 2], [u'1', u'3'])
+True
+
# Unicode choices are correctly rendered as HTML
>>> 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>'
@@ -895,6 +962,25 @@ u'<input id="foo_0" type="text" class="big" value="john" name="name_0" /><br /><
>>> 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
+>>> w._has_changed(None, [u'john', u'lennon'])
+True
+
+# test when the data is the same as initial
+>>> w._has_changed(u'john__lennon', [u'john', u'lennon'])
+False
+
+# test when the first widget's data has changed
+>>> w._has_changed(u'john__lennon', [u'alfred', u'lennon'])
+True
+
+# test when the last widget's data has changed. this ensures that it is not
+# short circuiting while testing the widgets.
+>>> w._has_changed(u'john__lennon', [u'john', u'denver'])
+True
+
# SplitDateTimeWidget #########################################################
>>> w = SplitDateTimeWidget()
@@ -913,6 +999,11 @@ included on both widgets.
>>> 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" />'
+>>> w._has_changed(datetime.datetime(2008, 5, 5, 12, 40, 00), [u'2008-05-05', u'12:40:00'])
+False
+>>> w._has_changed(datetime.datetime(2008, 5, 5, 12, 40, 00), [u'2008-05-05', u'12:41:00'])
+True
+
# DateTimeInput ###############################################################
>>> w = DateTimeInput()
diff --git a/tests/regressiontests/inline_formsets/__init__.py b/tests/regressiontests/inline_formsets/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/inline_formsets/__init__.py
diff --git a/tests/regressiontests/inline_formsets/models.py b/tests/regressiontests/inline_formsets/models.py
new file mode 100644
index 0000000000..b22f5e297d
--- /dev/null
+++ b/tests/regressiontests/inline_formsets/models.py
@@ -0,0 +1,55 @@
+# 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)
+
+__test__ = {'API_TESTS': """
+
+>>> from django.newforms.models import inlineformset_factory
+
+
+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.
+
+>>> ifs = inlineformset_factory(Parent, Child)
+Traceback (most recent call last):
+ ...
+Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
+
+
+These two should both work without a problem.
+
+>>> ifs = inlineformset_factory(Parent, Child, fk_name='mother')
+>>> ifs = inlineformset_factory(Parent, Child, fk_name='father')
+
+
+If we specify fk_name, but it isn't a ForeignKey from the child model to the
+parent model, we should get an exception.
+
+>>> ifs = inlineformset_factory(Parent, Child, fk_name='school')
+Traceback (most recent call last):
+ ...
+Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
+
+
+If the field specified in fk_name is not a ForeignKey, we should get an
+exception.
+
+>>> ifs = inlineformset_factory(Parent, Child, fk_name='test')
+Traceback (most recent call last):
+ ...
+Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'
+
+
+"""
+}
diff --git a/tests/regressiontests/invalid_admin_options/models.py b/tests/regressiontests/invalid_admin_options/models.py
deleted file mode 100644
index 14db463735..0000000000
--- a/tests/regressiontests/invalid_admin_options/models.py
+++ /dev/null
@@ -1,337 +0,0 @@
-"""
-Admin options
-
-Test invalid and valid admin options to make sure that
-model validation is working properly.
-"""
-
-from django.db import models
-model_errors = ""
-
-# TODO: Invalid admin options should not cause a metaclass error
-##This should fail gracefully but is causing a metaclass error
-#class BadAdminOption(models.Model):
-# "Test nonexistent admin option"
-# name = models.CharField(max_length=30)
-#
-# class Admin:
-# nonexistent = 'option'
-#
-#model_errors += """invalid_admin_options.badadminoption: "admin" attribute, if given, must be set to a models.AdminOptions() instance.
-#"""
-
-class ListDisplayBadOne(models.Model):
- "Test list_display, list_display must be a list or tuple"
- first_name = models.CharField(max_length=30)
-
- class Admin:
- list_display = 'first_name'
-
-model_errors += """invalid_admin_options.listdisplaybadone: "admin.list_display", if given, must be set to a list or tuple.
-"""
-
-class ListDisplayBadTwo(models.Model):
- "Test list_display, list_display items must be attributes, methods or properties."
- first_name = models.CharField(max_length=30)
-
- class Admin:
- list_display = ['first_name','nonexistent']
-
-model_errors += """invalid_admin_options.listdisplaybadtwo: "admin.list_display" refers to 'nonexistent', which isn't an attribute, method or property.
-"""
-class ListDisplayBadThree(models.Model):
- "Test list_display, list_display items can not be a ManyToManyField."
- first_name = models.CharField(max_length=30)
- nick_names = models.ManyToManyField('ListDisplayGood')
-
- class Admin:
- list_display = ['first_name','nick_names']
-
-model_errors += """invalid_admin_options.listdisplaybadthree: "admin.list_display" doesn't support ManyToManyFields ('nick_names').
-"""
-
-class ListDisplayGood(models.Model):
- "Test list_display, Admin list_display can be a attribute, method or property."
- first_name = models.CharField(max_length=30)
-
- def _last_name(self):
- return self.first_name
- last_name = property(_last_name)
-
- def full_name(self):
- return "%s %s" % (self.first_name, self.last_name)
-
- class Admin:
- list_display = ['first_name','last_name','full_name']
-
-class ListDisplayLinksBadOne(models.Model):
- "Test list_display_links, item must be included in list_display."
- first_name = models.CharField(max_length=30)
- last_name = models.CharField(max_length=30)
-
- class Admin:
- list_display = ['last_name']
- list_display_links = ['first_name']
-
-model_errors += """invalid_admin_options.listdisplaylinksbadone: "admin.list_display_links" refers to 'first_name', which is not defined in "admin.list_display".
-"""
-
-class ListDisplayLinksBadTwo(models.Model):
- "Test list_display_links, must be a list or tuple."
- first_name = models.CharField(max_length=30)
- last_name = models.CharField(max_length=30)
-
- class Admin:
- list_display = ['first_name','last_name']
- list_display_links = 'last_name'
-
-model_errors += """invalid_admin_options.listdisplaylinksbadtwo: "admin.list_display_links", if given, must be set to a list or tuple.
-"""
-
-# TODO: Fix list_display_links validation or remove the check for list_display
-## This is failing but the validation which should fail is not.
-#class ListDisplayLinksBadThree(models.Model):
-# "Test list_display_links, must define list_display to use list_display_links."
-# first_name = models.CharField(max_length=30)
-# last_name = models.CharField(max_length=30)
-#
-# class Admin:
-# list_display_links = ('first_name',)
-#
-#model_errors += """invalid_admin_options.listdisplaylinksbadthree: "admin.list_display" must be defined for "admin.list_display_links" to be used.
-#"""
-
-class ListDisplayLinksGood(models.Model):
- "Test list_display_links, Admin list_display_list can be a attribute, method or property."
- first_name = models.CharField(max_length=30)
-
- def _last_name(self):
- return self.first_name
- last_name = property(_last_name)
-
- def full_name(self):
- return "%s %s" % (self.first_name, self.last_name)
-
- class Admin:
- list_display = ['first_name','last_name','full_name']
- list_display_links = ['first_name','last_name','full_name']
-
-class ListFilterBadOne(models.Model):
- "Test list_filter, must be a list or tuple."
- first_name = models.CharField(max_length=30)
-
- class Admin:
- list_filter = 'first_name'
-
-model_errors += """invalid_admin_options.listfilterbadone: "admin.list_filter", if given, must be set to a list or tuple.
-"""
-
-class ListFilterBadTwo(models.Model):
- "Test list_filter, must be a field not a property or method."
- first_name = models.CharField(max_length=30)
-
- def _last_name(self):
- return self.first_name
- last_name = property(_last_name)
-
- def full_name(self):
- return "%s %s" % (self.first_name, self.last_name)
-
- class Admin:
- list_filter = ['first_name','last_name','full_name']
-
-model_errors += """invalid_admin_options.listfilterbadtwo: "admin.list_filter" refers to 'last_name', which isn't a field.
-invalid_admin_options.listfilterbadtwo: "admin.list_filter" refers to 'full_name', which isn't a field.
-"""
-
-class DateHierarchyBadOne(models.Model):
- "Test date_hierarchy, must be a date or datetime field."
- first_name = models.CharField(max_length=30)
- birth_day = models.DateField()
-
- class Admin:
- date_hierarchy = 'first_name'
-
-# TODO: Date Hierarchy needs to check if field is a date/datetime field.
-#model_errors += """invalid_admin_options.datehierarchybadone: "admin.date_hierarchy" refers to 'first_name', which isn't a date field or datetime field.
-#"""
-
-class DateHierarchyBadTwo(models.Model):
- "Test date_hieracrhy, must be a field."
- first_name = models.CharField(max_length=30)
- birth_day = models.DateField()
-
- class Admin:
- date_hierarchy = 'nonexistent'
-
-model_errors += """invalid_admin_options.datehierarchybadtwo: "admin.date_hierarchy" refers to 'nonexistent', which isn't a field.
-"""
-
-class DateHierarchyGood(models.Model):
- "Test date_hieracrhy, must be a field."
- first_name = models.CharField(max_length=30)
- birth_day = models.DateField()
-
- class Admin:
- date_hierarchy = 'birth_day'
-
-class SearchFieldsBadOne(models.Model):
- "Test search_fields, must be a list or tuple."
- first_name = models.CharField(max_length=30)
-
- class Admin:
- search_fields = ('nonexistent')
-
-# TODO: Add search_fields validation
-#model_errors += """invalid_admin_options.seacrhfieldsbadone: "admin.search_fields", if given, must be set to a list or tuple.
-#"""
-
-class SearchFieldsBadTwo(models.Model):
- "Test search_fields, must be a field."
- first_name = models.CharField(max_length=30)
-
- def _last_name(self):
- return self.first_name
- last_name = property(_last_name)
-
- class Admin:
- search_fields = ['first_name','last_name']
-
-# TODO: Add search_fields validation
-#model_errors += """invalid_admin_options.seacrhfieldsbadone: "admin.search_fields" refers to 'last_name', which isn't a field.
-#"""
-
-class SearchFieldsGood(models.Model):
- "Test search_fields, must be a list or tuple."
- first_name = models.CharField(max_length=30)
- last_name = models.CharField(max_length=30)
-
- class Admin:
- search_fields = ['first_name','last_name']
-
-
-class JsBadOne(models.Model):
- "Test js, must be a list or tuple"
- name = models.CharField(max_length=30)
-
- class Admin:
- js = 'test.js'
-
-# TODO: Add a js validator
-#model_errors += """invalid_admin_options.jsbadone: "admin.js", if given, must be set to a list or tuple.
-#"""
-
-class SaveAsBad(models.Model):
- "Test save_as, should be True or False"
- name = models.CharField(max_length=30)
-
- class Admin:
- save_as = 'not True or False'
-
-# TODO: Add a save_as validator.
-#model_errors += """invalid_admin_options.saveasbad: "admin.save_as", if given, must be set to True or False.
-#"""
-
-class SaveOnTopBad(models.Model):
- "Test save_on_top, should be True or False"
- name = models.CharField(max_length=30)
-
- class Admin:
- save_on_top = 'not True or False'
-
-# TODO: Add a save_on_top validator.
-#model_errors += """invalid_admin_options.saveontopbad: "admin.save_on_top", if given, must be set to True or False.
-#"""
-
-class ListSelectRelatedBad(models.Model):
- "Test list_select_related, should be True or False"
- name = models.CharField(max_length=30)
-
- class Admin:
- list_select_related = 'not True or False'
-
-# TODO: Add a list_select_related validator.
-#model_errors += """invalid_admin_options.listselectrelatebad: "admin.list_select_related", if given, must be set to True or False.
-#"""
-
-class ListPerPageBad(models.Model):
- "Test list_per_page, should be a positive integer value."
- name = models.CharField(max_length=30)
-
- class Admin:
- list_per_page = 89.3
-
-# TODO: Add a list_per_page validator.
-#model_errors += """invalid_admin_options.listperpagebad: "admin.list_per_page", if given, must be a positive integer.
-#"""
-
-class FieldsBadOne(models.Model):
- "Test fields, should be a tuple"
- first_name = models.CharField(max_length=30)
- last_name = models.CharField(max_length=30)
-
- class Admin:
- fields = 'not a tuple'
-
-# TODO: Add a fields validator.
-#model_errors += """invalid_admin_options.fieldsbadone: "admin.fields", if given, must be a tuple.
-#"""
-
-class FieldsBadTwo(models.Model):
- """Test fields, 'fields' dict option is required."""
- first_name = models.CharField(max_length=30)
- last_name = models.CharField(max_length=30)
-
- class Admin:
- fields = ('Name', {'description': 'this fieldset needs fields'})
-
-# TODO: Add a fields validator.
-#model_errors += """invalid_admin_options.fieldsbadtwo: "admin.fields" each fieldset must include a 'fields' dict.
-#"""
-
-class FieldsBadThree(models.Model):
- """Test fields, 'classes' and 'description' are the only allowable extra dict options."""
- first_name = models.CharField(max_length=30)
- last_name = models.CharField(max_length=30)
-
- class Admin:
- fields = ('Name', {'fields': ('first_name','last_name'),'badoption': 'verybadoption'})
-
-# TODO: Add a fields validator.
-#model_errors += """invalid_admin_options.fieldsbadthree: "admin.fields" fieldset options must be either 'classes' or 'description'.
-#"""
-
-class FieldsGood(models.Model):
- "Test fields, working example"
- first_name = models.CharField(max_length=30)
- last_name = models.CharField(max_length=30)
- birth_day = models.DateField()
-
- class Admin:
- fields = (
- ('Name', {'fields': ('first_name','last_name'),'classes': 'collapse'}),
- (None, {'fields': ('birth_day',),'description': 'enter your b-day'})
- )
-
-class OrderingBad(models.Model):
- "Test ordering, must be a field."
- first_name = models.CharField(max_length=30)
- last_name = models.CharField(max_length=30)
-
- class Admin:
- ordering = 'nonexistent'
-
-# TODO: Add a ordering validator.
-#model_errors += """invalid_admin_options.orderingbad: "admin.ordering" refers to 'nonexistent', which isn't a field.
-#"""
-
-## TODO: Add a manager validator, this should fail gracefully.
-#class ManagerBad(models.Model):
-# "Test manager, must be a manager object."
-# first_name = models.CharField(max_length=30)
-#
-# class Admin:
-# manager = 'nonexistent'
-#
-#model_errors += """invalid_admin_options.managerbad: "admin.manager" refers to 'nonexistent', which isn't a Manager().
-#""" \ No newline at end of file
diff --git a/tests/regressiontests/modeladmin/__init__.py b/tests/regressiontests/modeladmin/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/modeladmin/__init__.py
diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py
new file mode 100644
index 0000000000..17e3974e1c
--- /dev/null
+++ b/tests/regressiontests/modeladmin/models.py
@@ -0,0 +1,876 @@
+# 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)
+
+__test__ = {'API_TESTS': """
+
+>>> from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
+>>> from django.contrib.admin.sites import AdminSite
+
+None of the following tests really depend on the content of the request, so
+we'll just pass in None.
+
+>>> request = None
+
+# the sign_date is not 100 percent accurate ;)
+>>> band = Band(name='The Doors', bio='', sign_date=date(1965, 1, 1))
+>>> band.save()
+
+Under the covers, the admin system will initialize ModelAdmin with a Model
+class and an AdminSite instance, so let's just go ahead and do that manually
+for testing.
+
+>>> site = AdminSite()
+>>> ma = ModelAdmin(Band, site)
+
+>>> ma.get_form(request).base_fields.keys()
+['name', 'bio', 'sign_date']
+
+
+# form/fields/fieldsets interaction ##########################################
+
+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 arugment, 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, site)
+>>> ma.get_fieldsets(request)
+[(None, {'fields': ['name', 'bio', 'sign_date']})]
+>>> ma.get_fieldsets(request, band)
+[(None, {'fields': ['name', 'bio', 'sign_date']})]
+
+
+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, site)
+>>> ma.get_fieldsets(request)
+[(None, {'fields': ['name']})]
+>>> ma.get_fieldsets(request, band)
+[(None, {'fields': ['name']})]
+
+
+
+
+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/fielsets
+
+>>> class BandAdmin(ModelAdmin):
+... fields = ['name']
+
+>>> ma = BandAdmin(Band, site)
+>>> ma.get_form(request).base_fields.keys()
+['name']
+>>> ma.get_form(request, band).base_fields.keys()
+['name']
+
+>>> class BandAdmin(ModelAdmin):
+... fieldsets = [(None, {'fields': ['name']})]
+
+>>> ma = BandAdmin(Band, site)
+>>> ma.get_form(request).base_fields.keys()
+['name']
+>>> ma.get_form(request, band).base_fields.keys()
+['name']
+
+
+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.
+
+>>> from django import newforms as forms
+>>> class AdminBandForm(forms.ModelForm):
+... delete = forms.BooleanField()
+...
+... class Meta:
+... model = Band
+
+>>> class BandAdmin(ModelAdmin):
+... form = AdminBandForm
+
+>>> ma = BandAdmin(Band, site)
+>>> ma.get_form(request).base_fields.keys()
+['name', 'bio', 'sign_date', 'delete']
+>>> type(ma.get_form(request).base_fields['sign_date'].widget)
+<class 'django.contrib.admin.widgets.AdminDateWidget'>
+
+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 Beetles', bio='', sign_date=date(1962, 1, 1))
+>>> band2.save()
+
+>>> 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, site)
+>>> form = ma.get_form(request)()
+>>> print form["main_band"]
+<select name="main_band" id="id_main_band">
+<option value="" selected="selected">---------</option>
+<option value="1">The Doors</option>
+</select>
+
+>>> band2.delete()
+
+# radio_fields behavior ################################################
+
+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, site)
+>>> cmafa = cma.get_form(request)
+
+>>> type(cmafa.base_fields['main_band'].widget.widget)
+<class 'django.newforms.widgets.Select'>
+>>> list(cmafa.base_fields['main_band'].widget.choices)
+[(u'', u'---------'), (1, u'The Doors')]
+
+>>> type(cmafa.base_fields['opening_band'].widget.widget)
+<class 'django.newforms.widgets.Select'>
+>>> list(cmafa.base_fields['opening_band'].widget.choices)
+[(u'', u'---------'), (1, u'The Doors')]
+
+>>> type(cmafa.base_fields['day'].widget)
+<class 'django.newforms.widgets.Select'>
+>>> list(cmafa.base_fields['day'].widget.choices)
+[('', '---------'), (1, 'Fri'), (2, 'Sat')]
+
+>>> type(cmafa.base_fields['transport'].widget)
+<class 'django.newforms.widgets.Select'>
+>>> list(cmafa.base_fields['transport'].widget.choices)
+[('', '---------'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')]
+
+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, site)
+>>> cmafa = cma.get_form(request)
+
+>>> type(cmafa.base_fields['main_band'].widget.widget)
+<class 'django.contrib.admin.widgets.AdminRadioSelect'>
+>>> cmafa.base_fields['main_band'].widget.attrs
+{'class': 'radiolist inline'}
+>>> list(cmafa.base_fields['main_band'].widget.choices)
+[(1, u'The Doors')]
+
+>>> type(cmafa.base_fields['opening_band'].widget.widget)
+<class 'django.contrib.admin.widgets.AdminRadioSelect'>
+>>> cmafa.base_fields['opening_band'].widget.attrs
+{'class': 'radiolist'}
+>>> list(cmafa.base_fields['opening_band'].widget.choices)
+[(u'', u'None'), (1, u'The Doors')]
+
+>>> type(cmafa.base_fields['day'].widget)
+<class 'django.contrib.admin.widgets.AdminRadioSelect'>
+>>> cmafa.base_fields['day'].widget.attrs
+{'class': 'radiolist'}
+>>> list(cmafa.base_fields['day'].widget.choices)
+[(1, 'Fri'), (2, 'Sat')]
+
+>>> type(cmafa.base_fields['transport'].widget)
+<class 'django.contrib.admin.widgets.AdminRadioSelect'>
+>>> cmafa.base_fields['transport'].widget.attrs
+{'class': 'radiolist inline'}
+>>> list(cmafa.base_fields['transport'].widget.choices)
+[('', u'None'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')]
+
+>>> band.delete()
+
+# ModelAdmin Option Validation ################################################
+
+>>> from django.contrib.admin.validation import validate
+>>> from django.conf import settings
+
+# Ensure validation only runs when DEBUG = True
+
+>>> settings.DEBUG = True
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... raw_id_fields = 10
+>>> site = AdminSite()
+>>> site.register(ValidationTestModel, ValidationTestModelAdmin)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.raw_id_fields` must be a list or tuple.
+
+>>> settings.DEBUG = False
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... raw_id_fields = 10
+>>> site = AdminSite()
+>>> site.register(ValidationTestModel, ValidationTestModelAdmin)
+
+# raw_id_fields
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... raw_id_fields = 10
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.raw_id_fields` must be a list or tuple.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... raw_id_fields = ('non_existent_field',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.raw_id_fields` refers to field `non_existent_field` that is missing from model `ValidationTestModel`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... raw_id_fields = ('name',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.raw_id_fields[0]`, `name` must be either a ForeignKey or ManyToManyField.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... raw_id_fields = ('users',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# fieldsets
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... fieldsets = 10
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.fieldsets` must be a list or tuple.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... fieldsets = ({},)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.fieldsets[0]` must be a list or tuple.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... fieldsets = ((),)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.fieldsets[0]` does not have exactly two elements.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... fieldsets = (("General", ()),)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.fieldsets[0][1]` must be a dictionary.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... fieldsets = (("General", {}),)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `fields` key is required in ValidationTestModelAdmin.fieldsets[0][1] field options dict.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... fieldsets = (("General", {"fields": ("non_existent_field",)}),)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.fieldsets[0][1]['fields']` refers to field `non_existent_field` that is missing from the form.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... fieldsets = (("General", {"fields": ("name",)}),)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... fieldsets = (("General", {"fields": ("name",)}),)
+... fields = ["name",]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: Both fieldsets and fields are specified in ValidationTestModelAdmin.
+
+# form
+
+>>> class FakeForm(object):
+... pass
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... form = FakeForm
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: ValidationTestModelAdmin.form does not inherit from BaseModelForm.
+
+# fielsets with custom form
+
+>>> class BandAdmin(ModelAdmin):
+... fieldsets = (
+... ('Band', {
+... 'fields': ('non_existent_field',)
+... }),
+... )
+>>> validate(BandAdmin, Band)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `BandAdmin.fieldsets[0][1]['fields']` refers to field `non_existent_field` that is missing from the form.
+
+>>> 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',)
+... }),
+... )
+>>> validate(BandAdmin, Band)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `BandAdmin.fieldsets[0][1]['fields']` refers to field `non_existent_field` that is missing from the form.
+
+>>> 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)
+
+# filter_vertical
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... filter_vertical = 10
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.filter_vertical` must be a list or tuple.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... filter_vertical = ("non_existent_field",)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.filter_vertical` refers to field `non_existent_field` that is missing from model `ValidationTestModel`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... filter_vertical = ("name",)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.filter_vertical[0]` must be a ManyToManyField.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... filter_vertical = ("users",)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# filter_horizontal
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... filter_horizontal = 10
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.filter_horizontal` must be a list or tuple.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... filter_horizontal = ("non_existent_field",)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.filter_horizontal` refers to field `non_existent_field` that is missing from model `ValidationTestModel`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... filter_horizontal = ("name",)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.filter_horizontal[0]` must be a ManyToManyField.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... filter_horizontal = ("users",)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# radio_fields
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... radio_fields = ()
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.radio_fields` must be a dictionary.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... radio_fields = {"non_existent_field": None}
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.radio_fields` refers to field `non_existent_field` that is missing from model `ValidationTestModel`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... radio_fields = {"name": None}
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.radio_fields['name']` is neither an instance of ForeignKey nor does have choices set.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... radio_fields = {"state": None}
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.radio_fields['state']` is neither admin.HORIZONTAL nor admin.VERTICAL.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... radio_fields = {"state": VERTICAL}
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# prepopulated_fields
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... prepopulated_fields = ()
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.prepopulated_fields` must be a dictionary.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... prepopulated_fields = {"non_existent_field": None}
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.prepopulated_fields` refers to field `non_existent_field` that is missing from model `ValidationTestModel`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... prepopulated_fields = {"slug": ("non_existent_field",)}
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.prepopulated_fields['non_existent_field'][0]` refers to field `non_existent_field` that is missing from model `ValidationTestModel`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... prepopulated_fields = {"users": ("name",)}
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.prepopulated_fields['users']` is either a DateTimeField, ForeignKey or ManyToManyField. This isn't allowed.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... prepopulated_fields = {"slug": ("name",)}
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# list_display
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_display = 10
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.list_display` must be a list or tuple.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_display = ('non_existent_field',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.list_display[0]` refers to `non_existent_field` that is neither a field, method or property of model `ValidationTestModel`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_display = ('users',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.list_display[0]`, `users` is a ManyToManyField which is not supported.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_display = ('name',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# list_display_links
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_display_links = 10
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.list_display_links` must be a list or tuple.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_display_links = ('non_existent_field',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.list_display_links[0]` refers to `non_existent_field` that is neither a field, method or property of model `ValidationTestModel`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_display_links = ('name',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.list_display_links[0]`refers to `name` which is not defined in `list_display`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_display = ('name',)
+... list_display_links = ('name',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# list_filter
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_filter = 10
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.list_filter` must be a list or tuple.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_filter = ('non_existent_field',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.list_filter[0]` refers to field `non_existent_field` that is missing from model `ValidationTestModel`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_filter = ('is_active',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# list_per_page
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_per_page = 'hello'
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.list_per_page` should be a integer.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_per_page = 100
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# search_fields
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... search_fields = 10
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.search_fields` must be a list or tuple.
+
+# date_hierarchy
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... date_hierarchy = 'non_existent_field'
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.date_hierarchy` refers to field `non_existent_field` that is missing from model `ValidationTestModel`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... date_hierarchy = 'name'
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.date_hierarchy is neither an instance of DateField nor DateTimeField.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... date_hierarchy = 'pub_date'
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# ordering
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... ordering = 10
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.ordering` must be a list or tuple.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... ordering = ('non_existent_field',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.ordering[0]` refers to field `non_existent_field` that is missing from model `ValidationTestModel`.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... ordering = ('?', 'name')
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.ordering` has the random ordering marker `?`, but contains other fields as well. Please either remove `?` or the other fields.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... ordering = ('?',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... ordering = ('band__name',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... ordering = ('name',)
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# list_select_related
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_select_related = 1
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.list_select_related` should be a boolean.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... list_select_related = False
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# save_as
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... save_as = 1
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.save_as` should be a boolean.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... save_as = True
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# save_on_top
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... save_on_top = 1
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.save_on_top` should be a boolean.
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... save_on_top = True
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# inlines
+
+>>> from django.contrib.admin.options import TabularInline
+
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = 10
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.inlines` must be a list or tuple.
+
+>>> class ValidationTestInline(object):
+... pass
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.inlines[0]` does not inherit from BaseModelAdmin.
+
+>>> class ValidationTestInline(TabularInline):
+... pass
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `model` is a required attribute of `ValidationTestModelAdmin.inlines[0]`.
+
+>>> class SomethingBad(object):
+... pass
+>>> class ValidationTestInline(TabularInline):
+... model = SomethingBad
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestModelAdmin.inlines[0].model` does not inherit from models.Model.
+
+>>> class ValidationTestInline(TabularInline):
+... model = ValidationTestInlineModel
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# fields
+
+>>> class ValidationTestInline(TabularInline):
+... model = ValidationTestInlineModel
+... fields = 10
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestInline.fields` must be a list or tuple.
+
+>>> class ValidationTestInline(TabularInline):
+... model = ValidationTestInlineModel
+... fields = ("non_existent_field",)
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestInline.fields` refers to field `non_existent_field` that is missing from the form.
+
+# fk_name
+
+>>> class ValidationTestInline(TabularInline):
+... model = ValidationTestInlineModel
+... fk_name = "non_existent_field"
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestInline.fk_name` refers to field `non_existent_field` that is missing from model `ValidationTestInlineModel`.
+
+>>> class ValidationTestInline(TabularInline):
+... model = ValidationTestInlineModel
+... fk_name = "parent"
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# extra
+
+>>> class ValidationTestInline(TabularInline):
+... model = ValidationTestInlineModel
+... extra = "hello"
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestInline.extra` should be a integer.
+
+>>> class ValidationTestInline(TabularInline):
+... model = ValidationTestInlineModel
+... extra = 2
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# max_num
+
+>>> class ValidationTestInline(TabularInline):
+... model = ValidationTestInlineModel
+... max_num = "hello"
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestInline.max_num` should be a integer.
+
+>>> class ValidationTestInline(TabularInline):
+... model = ValidationTestInlineModel
+... max_num = 2
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+# formset
+
+>>> from django.newforms.models import BaseModelFormSet
+
+>>> class FakeFormSet(object):
+... pass
+>>> class ValidationTestInline(TabularInline):
+... model = ValidationTestInlineModel
+... formset = FakeFormSet
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+Traceback (most recent call last):
+...
+ImproperlyConfigured: `ValidationTestInline.formset` does not inherit from BaseModelFormSet.
+
+>>> class RealModelFormSet(BaseModelFormSet):
+... pass
+>>> class ValidationTestInline(TabularInline):
+... model = ValidationTestInlineModel
+... formset = RealModelFormSet
+>>> class ValidationTestModelAdmin(ModelAdmin):
+... inlines = [ValidationTestInline]
+>>> validate(ValidationTestModelAdmin, ValidationTestModel)
+
+"""
+}