summaryrefslogtreecommitdiff
path: root/tests/modeltests
diff options
context:
space:
mode:
authorChristopher Long <indirecthit@gmail.com>2007-06-17 22:18:54 +0000
committerChristopher Long <indirecthit@gmail.com>2007-06-17 22:18:54 +0000
commitae22b6d403dcf25098c77f0dfcf59ae58b186461 (patch)
treec37fc631e99a7e4d909d6b6d236f495003731ea7 /tests/modeltests
parent0cf7bc439129c66df8d64601e885f83b256b4f25 (diff)
per-object-permissions: Merged to trunk [5486] NOTE: Not fully tested, will be working on this over the next few weeks.
git-svn-id: http://code.djangoproject.com/svn/django/branches/per-object-permissions@5488 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'tests/modeltests')
-rw-r--r--tests/modeltests/basic/models.py10
-rw-r--r--tests/modeltests/custom_columns/models.py94
-rw-r--r--tests/modeltests/empty/models.py2
-rw-r--r--tests/modeltests/field_defaults/models.py2
-rw-r--r--tests/modeltests/fixtures/__init__.py2
-rw-r--r--tests/modeltests/fixtures/fixtures/fixture1.json18
-rw-r--r--tests/modeltests/fixtures/fixtures/fixture2.json18
-rw-r--r--tests/modeltests/fixtures/fixtures/fixture2.xml11
-rw-r--r--tests/modeltests/fixtures/fixtures/fixture3.xml11
-rw-r--r--tests/modeltests/fixtures/fixtures/initial_data.json10
-rw-r--r--tests/modeltests/fixtures/models.py88
-rw-r--r--tests/modeltests/generic_relations/models.py43
-rw-r--r--tests/modeltests/get_object_or_404/__init__.py0
-rw-r--r--tests/modeltests/get_object_or_404/models.py86
-rw-r--r--tests/modeltests/get_or_create/models.py2
-rw-r--r--tests/modeltests/invalid_models/models.py20
-rw-r--r--tests/modeltests/lookup/models.py60
-rw-r--r--tests/modeltests/m2m_and_m2o/models.py2
-rw-r--r--tests/modeltests/m2m_recursive/models.py2
-rw-r--r--tests/modeltests/manipulators/models.py2
-rw-r--r--tests/modeltests/many_to_many/models.py14
-rw-r--r--tests/modeltests/many_to_one/models.py4
-rw-r--r--tests/modeltests/model_forms/models.py464
-rw-r--r--tests/modeltests/or_lookups/models.py15
-rw-r--r--tests/modeltests/pagination/models.py2
-rw-r--r--tests/modeltests/reverse_lookup/models.py2
-rw-r--r--tests/modeltests/select_related/__init__.py0
-rw-r--r--tests/modeltests/select_related/models.py152
-rw-r--r--tests/modeltests/serializers/models.py47
-rw-r--r--tests/modeltests/test_client/fixtures/testdata.json20
-rw-r--r--tests/modeltests/test_client/management.py10
-rw-r--r--tests/modeltests/test_client/models.py223
-rw-r--r--tests/modeltests/test_client/urls.py11
-rw-r--r--tests/modeltests/test_client/views.py135
-rw-r--r--tests/modeltests/validation/models.py6
35 files changed, 1478 insertions, 110 deletions
diff --git a/tests/modeltests/basic/models.py b/tests/modeltests/basic/models.py
index 1663068892..9af13c0e3e 100644
--- a/tests/modeltests/basic/models.py
+++ b/tests/modeltests/basic/models.py
@@ -12,7 +12,7 @@ class Article(models.Model):
class Meta:
ordering = ('pub_date','headline')
-
+
def __str__(self):
return self.headline
@@ -319,7 +319,6 @@ AttributeError: Manager isn't accessible via Article instances
>>> Article.objects.filter(id__lte=4).delete()
>>> Article.objects.all()
[<Article: Article 6>, <Article: Default headline>, <Article: Article 7>, <Article: Updated article 8>]
-
"""}
from django.conf import settings
@@ -358,4 +357,11 @@ __test__['API_TESTS'] += """
>>> a10 = Article.objects.create(headline="Article 10", pub_date=datetime(2005, 7, 31, 12, 30, 45))
>>> Article.objects.get(headline="Article 10")
<Article: Article 10>
+
+# Edge-case test: A year lookup should retrieve all objects in the given
+year, including Jan. 1 and Dec. 31.
+>>> a11 = Article.objects.create(headline='Article 11', pub_date=datetime(2008, 1, 1))
+>>> a12 = Article.objects.create(headline='Article 12', pub_date=datetime(2008, 12, 31, 23, 59, 59, 999999))
+>>> Article.objects.filter(pub_date__year=2008)
+[<Article: Article 11>, <Article: Article 12>]
"""
diff --git a/tests/modeltests/custom_columns/models.py b/tests/modeltests/custom_columns/models.py
index e88fa80da2..1283da07cf 100644
--- a/tests/modeltests/custom_columns/models.py
+++ b/tests/modeltests/custom_columns/models.py
@@ -1,53 +1,105 @@
"""
-17. Custom column names
+17. Custom column/table names
If your database column name is different than your model attribute, use the
``db_column`` parameter. Note that you'll use the field's name, not its column
name, in API usage.
+
+If your database table name is different than your model name, use the
+``db_table`` Meta attribute. This has no effect on the API used to
+query the database.
+
+If you need to use a table name for a many-to-many relationship that differs
+from the default generated name, use the ``db_table`` parameter on the
+ManyToMany field. This has no effect on the API for querying the database.
+
"""
from django.db import models
-class Person(models.Model):
+class Author(models.Model):
first_name = models.CharField(maxlength=30, db_column='firstname')
last_name = models.CharField(maxlength=30, db_column='last')
def __str__(self):
return '%s %s' % (self.first_name, self.last_name)
+ class Meta:
+ db_table = 'my_author_table'
+ ordering = ('last_name','first_name')
+
+class Article(models.Model):
+ headline = models.CharField(maxlength=100)
+ authors = models.ManyToManyField(Author, db_table='my_m2m_table')
+
+ def __str__(self):
+ return self.headline
+
+ class Meta:
+ ordering = ('headline',)
+
__test__ = {'API_TESTS':"""
-# Create a Person.
->>> p = Person(first_name='John', last_name='Smith')
->>> p.save()
+# Create a Author.
+>>> a = Author(first_name='John', last_name='Smith')
+>>> a.save()
->>> p.id
+>>> a.id
1
->>> Person.objects.all()
-[<Person: John Smith>]
+# Create another author
+>>> a2 = Author(first_name='Peter', last_name='Jones')
+>>> a2.save()
+
+# Create an article
+>>> art = Article(headline='Django lets you build web apps easily')
+>>> art.save()
+>>> art.authors = [a, a2]
->>> Person.objects.filter(first_name__exact='John')
-[<Person: John Smith>]
+# Although the table and column names on Author have been set to
+# custom values, nothing about using the Author model has changed...
->>> Person.objects.get(first_name__exact='John')
-<Person: John Smith>
+# Query the available authors
+>>> Author.objects.all()
+[<Author: Peter Jones>, <Author: John Smith>]
->>> Person.objects.filter(firstname__exact='John')
+>>> Author.objects.filter(first_name__exact='John')
+[<Author: John Smith>]
+
+>>> Author.objects.get(first_name__exact='John')
+<Author: John Smith>
+
+>>> Author.objects.filter(firstname__exact='John')
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'firstname' into field
+TypeError: Cannot resolve keyword 'firstname' into field. Choices are: article, id, first_name, last_name
->>> p = Person.objects.get(last_name__exact='Smith')
->>> p.first_name
+>>> a = Author.objects.get(last_name__exact='Smith')
+>>> a.first_name
'John'
->>> p.last_name
+>>> a.last_name
'Smith'
->>> p.firstname
+>>> a.firstname
Traceback (most recent call last):
...
-AttributeError: 'Person' object has no attribute 'firstname'
->>> p.last
+AttributeError: 'Author' object has no attribute 'firstname'
+>>> a.last
Traceback (most recent call last):
...
-AttributeError: 'Person' object has no attribute 'last'
+AttributeError: 'Author' object has no attribute 'last'
+
+# Although the Article table uses a custom m2m table,
+# nothing about using the m2m relationship has changed...
+
+# Get all the authors for an article
+>>> art.authors.all()
+[<Author: Peter Jones>, <Author: John Smith>]
+
+# Get the articles for an author
+>>> a.article_set.all()
+[<Article: Django lets you build web apps easily>]
+
+# Query the authors across the m2m relation
+>>> art.authors.filter(last_name='Jones')
+[<Author: Peter Jones>]
+
"""}
diff --git a/tests/modeltests/empty/models.py b/tests/modeltests/empty/models.py
index 0e5d572504..2493b53837 100644
--- a/tests/modeltests/empty/models.py
+++ b/tests/modeltests/empty/models.py
@@ -1,5 +1,5 @@
"""
-Empty model tests
+39. Empty model tests
These test that things behave sensibly for the rare corner-case of a model with
no fields.
diff --git a/tests/modeltests/field_defaults/models.py b/tests/modeltests/field_defaults/models.py
index da4cd38974..8e803d00d8 100644
--- a/tests/modeltests/field_defaults/models.py
+++ b/tests/modeltests/field_defaults/models.py
@@ -1,5 +1,5 @@
"""
-31. Callable defaults
+32. Callable defaults
You can pass callable objects as the ``default`` parameter to a field. When
the object is created without an explicit value passed in, Django will call
diff --git a/tests/modeltests/fixtures/__init__.py b/tests/modeltests/fixtures/__init__.py
new file mode 100644
index 0000000000..139597f9cb
--- /dev/null
+++ b/tests/modeltests/fixtures/__init__.py
@@ -0,0 +1,2 @@
+
+
diff --git a/tests/modeltests/fixtures/fixtures/fixture1.json b/tests/modeltests/fixtures/fixtures/fixture1.json
new file mode 100644
index 0000000000..cc11a3e926
--- /dev/null
+++ b/tests/modeltests/fixtures/fixtures/fixture1.json
@@ -0,0 +1,18 @@
+[
+ {
+ "pk": "2",
+ "model": "fixtures.article",
+ "fields": {
+ "headline": "Poker has no place on ESPN",
+ "pub_date": "2006-06-16 12:00:00"
+ }
+ },
+ {
+ "pk": "3",
+ "model": "fixtures.article",
+ "fields": {
+ "headline": "Time to reform copyright",
+ "pub_date": "2006-06-16 13:00:00"
+ }
+ }
+] \ No newline at end of file
diff --git a/tests/modeltests/fixtures/fixtures/fixture2.json b/tests/modeltests/fixtures/fixtures/fixture2.json
new file mode 100644
index 0000000000..01b40d7535
--- /dev/null
+++ b/tests/modeltests/fixtures/fixtures/fixture2.json
@@ -0,0 +1,18 @@
+[
+ {
+ "pk": "3",
+ "model": "fixtures.article",
+ "fields": {
+ "headline": "Copyright is fine the way it is",
+ "pub_date": "2006-06-16 14:00:00"
+ }
+ },
+ {
+ "pk": "4",
+ "model": "fixtures.article",
+ "fields": {
+ "headline": "Django conquers world!",
+ "pub_date": "2006-06-16 15:00:00"
+ }
+ }
+] \ No newline at end of file
diff --git a/tests/modeltests/fixtures/fixtures/fixture2.xml b/tests/modeltests/fixtures/fixtures/fixture2.xml
new file mode 100644
index 0000000000..9ced78162e
--- /dev/null
+++ b/tests/modeltests/fixtures/fixtures/fixture2.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="2" model="fixtures.article">
+ <field type="CharField" name="headline">Poker on TV is great!</field>
+ <field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field>
+ </object>
+ <object pk="5" model="fixtures.article">
+ <field type="CharField" name="headline">XML identified as leading cause of cancer</field>
+ <field type="DateTimeField" name="pub_date">2006-06-16 16:00:00</field>
+ </object>
+</django-objects> \ No newline at end of file
diff --git a/tests/modeltests/fixtures/fixtures/fixture3.xml b/tests/modeltests/fixtures/fixtures/fixture3.xml
new file mode 100644
index 0000000000..9ced78162e
--- /dev/null
+++ b/tests/modeltests/fixtures/fixtures/fixture3.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="2" model="fixtures.article">
+ <field type="CharField" name="headline">Poker on TV is great!</field>
+ <field type="DateTimeField" name="pub_date">2006-06-16 11:00:00</field>
+ </object>
+ <object pk="5" model="fixtures.article">
+ <field type="CharField" name="headline">XML identified as leading cause of cancer</field>
+ <field type="DateTimeField" name="pub_date">2006-06-16 16:00:00</field>
+ </object>
+</django-objects> \ No newline at end of file
diff --git a/tests/modeltests/fixtures/fixtures/initial_data.json b/tests/modeltests/fixtures/fixtures/initial_data.json
new file mode 100644
index 0000000000..477d781dbc
--- /dev/null
+++ b/tests/modeltests/fixtures/fixtures/initial_data.json
@@ -0,0 +1,10 @@
+[
+ {
+ "pk": "1",
+ "model": "fixtures.article",
+ "fields": {
+ "headline": "Python program becomes self aware",
+ "pub_date": "2006-06-16 11:00:00"
+ }
+ }
+] \ No newline at end of file
diff --git a/tests/modeltests/fixtures/models.py b/tests/modeltests/fixtures/models.py
new file mode 100644
index 0000000000..c75e6723fd
--- /dev/null
+++ b/tests/modeltests/fixtures/models.py
@@ -0,0 +1,88 @@
+"""
+37. Fixtures.
+
+Fixtures are a way of loading data into the database in bulk. Fixure data
+can be stored in any serializable format (including JSON and XML). Fixtures
+are identified by name, and are stored in either a directory named 'fixtures'
+in the application directory, on in one of the directories named in the
+FIXTURE_DIRS setting.
+"""
+
+from django.db import models
+
+class Article(models.Model):
+ headline = models.CharField(maxlength=100, default='Default headline')
+ pub_date = models.DateTimeField()
+
+ def __str__(self):
+ return self.headline
+
+ class Meta:
+ ordering = ('-pub_date', 'headline')
+
+__test__ = {'API_TESTS': """
+>>> from django.core import management
+>>> from django.db.models import get_app
+
+# Reset the database representation of this app.
+# This will return the database to a clean initial state.
+>>> management.flush(verbosity=0, interactive=False)
+
+# Syncdb introduces 1 initial data object from initial_data.json.
+>>> Article.objects.all()
+[<Article: Python program becomes self aware>]
+
+# Load fixture 1. Single JSON file, with two objects.
+>>> management.load_data(['fixture1.json'], verbosity=0)
+>>> Article.objects.all()
+[<Article: Time to reform copyright>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]
+
+# Load fixture 2. JSON file imported by default. Overwrites some existing objects
+>>> management.load_data(['fixture2.json'], verbosity=0)
+>>> Article.objects.all()
+[<Article: Django conquers world!>, <Article: Copyright is fine the way it is>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]
+
+# Load fixture 3, XML format.
+>>> management.load_data(['fixture3.xml'], verbosity=0)
+>>> Article.objects.all()
+[<Article: XML identified as leading cause of cancer>, <Article: Django conquers world!>, <Article: Copyright is fine the way it is>, <Article: Poker on TV is great!>, <Article: Python program becomes self aware>]
+
+# Load a fixture that doesn't exist
+>>> management.load_data(['unknown.json'], verbosity=0)
+
+# object list is unaffected
+>>> Article.objects.all()
+[<Article: XML identified as leading cause of cancer>, <Article: Django conquers world!>, <Article: Copyright is fine the way it is>, <Article: Poker on TV is great!>, <Article: Python program becomes self aware>]
+
+# Reset the database representation of this app. This will delete all data.
+>>> management.flush(verbosity=0, interactive=False)
+>>> Article.objects.all()
+[<Article: Python program becomes self aware>]
+
+# Load fixture 1 again, using format discovery
+>>> management.load_data(['fixture1'], verbosity=0)
+>>> Article.objects.all()
+[<Article: Time to reform copyright>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]
+
+# Try to load fixture 2 using format discovery; this will fail
+# because there are two fixture2's in the fixtures directory
+>>> management.load_data(['fixture2'], verbosity=0) # doctest: +ELLIPSIS
+Multiple fixtures named 'fixture2' in '...fixtures'. Aborting.
+
+>>> Article.objects.all()
+[<Article: Time to reform copyright>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]
+
+# Dump the current contents of the database as a JSON fixture
+>>> print management.dump_data(['fixtures'], format='json')
+[{"pk": "3", "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": "2", "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": "1", "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}]
+"""}
+
+from django.test import TestCase
+
+class SampleTestCase(TestCase):
+ fixtures = ['fixture1.json', 'fixture2.json']
+
+ def testClassFixtures(self):
+ "Check that test case has installed 4 fixture objects"
+ self.assertEqual(Article.objects.count(), 4)
+ self.assertEquals(str(Article.objects.all()), "[<Article: Django conquers world!>, <Article: Copyright is fine the way it is>, <Article: Poker has no place on ESPN>, <Article: Python program becomes self aware>]")
diff --git a/tests/modeltests/generic_relations/models.py b/tests/modeltests/generic_relations/models.py
index eb64d7ec3d..195f67db8f 100644
--- a/tests/modeltests/generic_relations/models.py
+++ b/tests/modeltests/generic_relations/models.py
@@ -1,5 +1,5 @@
"""
-33. Generic relations
+34. Generic relations
Generic relations let an object have a foreign key to any object through a
content-type/object-id field. A generic foreign key can point to any object,
@@ -11,6 +11,7 @@ from complete).
from django.db import models
from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
class TaggedItem(models.Model):
"""A tag on an item."""
@@ -18,7 +19,7 @@ class TaggedItem(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
- content_object = models.GenericForeignKey()
+ content_object = generic.GenericForeignKey()
class Meta:
ordering = ["tag"]
@@ -30,7 +31,7 @@ class Animal(models.Model):
common_name = models.CharField(maxlength=150)
latin_name = models.CharField(maxlength=150)
- tags = models.GenericRelation(TaggedItem)
+ tags = generic.GenericRelation(TaggedItem)
def __str__(self):
return self.common_name
@@ -39,7 +40,7 @@ class Vegetable(models.Model):
name = models.CharField(maxlength=150)
is_yucky = models.BooleanField(default=True)
- tags = models.GenericRelation(TaggedItem)
+ tags = generic.GenericRelation(TaggedItem)
def __str__(self):
return self.name
@@ -65,14 +66,14 @@ __test__ = {'API_TESTS':"""
# Objects with declared GenericRelations can be tagged directly -- the API
# mimics the many-to-many API.
->>> lion.tags.create(tag="yellow")
-<TaggedItem: yellow>
->>> lion.tags.create(tag="hairy")
-<TaggedItem: hairy>
>>> bacon.tags.create(tag="fatty")
<TaggedItem: fatty>
>>> bacon.tags.create(tag="salty")
<TaggedItem: salty>
+>>> lion.tags.create(tag="yellow")
+<TaggedItem: yellow>
+>>> lion.tags.create(tag="hairy")
+<TaggedItem: hairy>
>>> lion.tags.all()
[<TaggedItem: hairy>, <TaggedItem: yellow>]
@@ -105,4 +106,30 @@ __test__ = {'API_TESTS':"""
[<TaggedItem: shiny>]
>>> TaggedItem.objects.filter(content_type__pk=ctype.id, object_id=quartz.id)
[<TaggedItem: clearish>]
+
+# If you delete an object with an explicit Generic relation, the related
+# objects are deleted when the source object is deleted.
+# Original list of tags:
+>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
+[('clearish', <ContentType: mineral>, 1), ('fatty', <ContentType: vegetable>, 2), ('hairy', <ContentType: animal>, 1), ('salty', <ContentType: vegetable>, 2), ('shiny', <ContentType: animal>, 2), ('yellow', <ContentType: animal>, 1)]
+
+>>> lion.delete()
+>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
+[('clearish', <ContentType: mineral>, 1), ('fatty', <ContentType: vegetable>, 2), ('salty', <ContentType: vegetable>, 2), ('shiny', <ContentType: animal>, 2)]
+
+# If Generic Relation is not explicitly defined, any related objects
+# remain after deletion of the source object.
+>>> quartz.delete()
+>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
+[('clearish', <ContentType: mineral>, 1), ('fatty', <ContentType: vegetable>, 2), ('salty', <ContentType: vegetable>, 2), ('shiny', <ContentType: animal>, 2)]
+
+# If you delete a tag, the objects using the tag are unaffected
+# (other than losing a tag)
+>>> tag = TaggedItem.objects.get(id=1)
+>>> tag.delete()
+>>> bacon.tags.all()
+[<TaggedItem: salty>]
+>>> [(t.tag, t.content_type, t.object_id) for t in TaggedItem.objects.all()]
+[('clearish', <ContentType: mineral>, 1), ('salty', <ContentType: vegetable>, 2), ('shiny', <ContentType: animal>, 2)]
+
"""}
diff --git a/tests/modeltests/get_object_or_404/__init__.py b/tests/modeltests/get_object_or_404/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/modeltests/get_object_or_404/__init__.py
diff --git a/tests/modeltests/get_object_or_404/models.py b/tests/modeltests/get_object_or_404/models.py
new file mode 100644
index 0000000000..2ad459669f
--- /dev/null
+++ b/tests/modeltests/get_object_or_404/models.py
@@ -0,0 +1,86 @@
+"""
+35. DB-API Shortcuts
+
+get_object_or_404 is a shortcut function to be used in view functions for
+performing a get() lookup and raising a Http404 exception if a DoesNotExist
+exception was rasied during the get() call.
+
+get_list_or_404 is a shortcut function to be used in view functions for
+performing a filter() lookup and raising a Http404 exception if a DoesNotExist
+exception was rasied during the filter() call.
+"""
+
+from django.db import models
+from django.http import Http404
+from django.shortcuts import get_object_or_404, get_list_or_404
+
+class Author(models.Model):
+ name = models.CharField(maxlength=50)
+
+ def __str__(self):
+ return self.name
+
+class ArticleManager(models.Manager):
+ def get_query_set(self):
+ return super(ArticleManager, self).get_query_set().filter(authors__name__icontains='sir')
+
+class Article(models.Model):
+ authors = models.ManyToManyField(Author)
+ title = models.CharField(maxlength=50)
+ objects = models.Manager()
+ by_a_sir = ArticleManager()
+
+ def __str__(self):
+ return self.title
+
+__test__ = {'API_TESTS':"""
+# Create some Authors.
+>>> a = Author.objects.create(name="Brave Sir Robin")
+>>> a.save()
+>>> a2 = Author.objects.create(name="Patsy")
+>>> a2.save()
+
+# No Articles yet, so we should get a Http404 error.
+>>> get_object_or_404(Article, title="Foo")
+Traceback (most recent call last):
+...
+Http404: No Article matches the given query.
+
+# Create an Article.
+>>> article = Article.objects.create(title="Run away!")
+>>> article.authors = [a, a2]
+>>> article.save()
+
+# get_object_or_404 can be passed a Model to query.
+>>> get_object_or_404(Article, title__contains="Run")
+<Article: Run away!>
+
+# We can also use the the Article manager through an Author object.
+>>> get_object_or_404(a.article_set, title__contains="Run")
+<Article: Run away!>
+
+# No articles containing "Camelot". This should raise a Http404 error.
+>>> get_object_or_404(a.article_set, title__contains="Camelot")
+Traceback (most recent call last):
+...
+Http404: No Article matches the given query.
+
+# Custom managers can be used too.
+>>> get_object_or_404(Article.by_a_sir, title="Run away!")
+<Article: Run away!>
+
+# get_list_or_404 can be used to get lists of objects
+>>> get_list_or_404(a.article_set, title__icontains='Run')
+[<Article: Run away!>]
+
+# Http404 is returned if the list is empty
+>>> get_list_or_404(a.article_set, title__icontains='Shrubbery')
+Traceback (most recent call last):
+...
+Http404: No Article matches the given query.
+
+# Custom managers can be used too.
+>>> get_list_or_404(Article.by_a_sir, title__icontains="Run")
+[<Article: Run away!>]
+
+"""}
diff --git a/tests/modeltests/get_or_create/models.py b/tests/modeltests/get_or_create/models.py
index b4f39ceded..f974a82dee 100644
--- a/tests/modeltests/get_or_create/models.py
+++ b/tests/modeltests/get_or_create/models.py
@@ -1,5 +1,5 @@
"""
-32. get_or_create()
+33. get_or_create()
get_or_create() does what it says: it tries to look up an object with the given
parameters. If an object isn't found, it creates one with the given parameters.
diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py
index 2299cd85e6..90f2f54632 100644
--- a/tests/modeltests/invalid_models/models.py
+++ b/tests/modeltests/invalid_models/models.py
@@ -8,7 +8,7 @@ from django.db import models
class FieldErrors(models.Model):
charfield = models.CharField()
- floatfield = models.FloatField()
+ decimalfield = models.DecimalField()
filefield = models.FileField()
prepopulate = models.CharField(maxlength=10, prepopulate_from='bad')
choices = models.CharField(maxlength=10, choices='bad')
@@ -87,19 +87,29 @@ class SelfClashM2M(models.Model):
src_safe = models.CharField(maxlength=10)
selfclashm2m = models.CharField(maxlength=10)
- # Non-symmetrical M2M fields _do_ have related accessors, so
+ # Non-symmetrical M2M fields _do_ have related accessors, so
# there is potential for clashes.
selfclashm2m_set = models.ManyToManyField("SelfClashM2M", symmetrical=False)
-
+
m2m_1 = models.ManyToManyField("SelfClashM2M", related_name='id', symmetrical=False)
m2m_2 = models.ManyToManyField("SelfClashM2M", related_name='src_safe', symmetrical=False)
m2m_3 = models.ManyToManyField('self', symmetrical=False)
m2m_4 = models.ManyToManyField('self', symmetrical=False)
+class Model(models.Model):
+ "But it's valid to call a model Model."
+ year = models.PositiveIntegerField() #1960
+ make = models.CharField(maxlength=10) #Aston Martin
+ name = models.CharField(maxlength=10) #DB 4 GT
+
+class Car(models.Model):
+ colour = models.CharField(maxlength=5)
+ model = models.ForeignKey(Model)
+
model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "maxlength" attribute.
-invalid_models.fielderrors: "floatfield": FloatFields require a "decimal_places" attribute.
-invalid_models.fielderrors: "floatfield": FloatFields require a "max_digits" attribute.
+invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
+invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute.
invalid_models.fielderrors: "prepopulate": prepopulate_from should be a list or tuple.
invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list).
diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py
index 09c3aa7aa8..6af70f8351 100644
--- a/tests/modeltests/lookup/models.py
+++ b/tests/modeltests/lookup/models.py
@@ -58,6 +58,17 @@ Article 4
>>> Article.objects.filter(headline__startswith='Blah blah').count()
0L
+# count() should respect sliced query sets.
+>>> articles = Article.objects.all()
+>>> articles.count()
+7L
+>>> articles[:4].count()
+4
+>>> articles[1:100].count()
+6L
+>>> articles[10:100].count()
+0
+
# Date and date/time lookups can also be done with strings.
>>> Article.objects.filter(pub_date__exact='2005-07-27 00:00:00').count()
3L
@@ -120,6 +131,27 @@ True
[('headline', 'Article 7'), ('id', 7)]
[('headline', 'Article 1'), ('id', 1)]
+
+# you can use values() even on extra fields
+>>> for d in Article.objects.extra( select={'id_plus_one' : 'id + 1'} ).values('id', 'id_plus_one'):
+... i = d.items()
+... i.sort()
+... i
+[('id', 5), ('id_plus_one', 6)]
+[('id', 6), ('id_plus_one', 7)]
+[('id', 4), ('id_plus_one', 5)]
+[('id', 2), ('id_plus_one', 3)]
+[('id', 3), ('id_plus_one', 4)]
+[('id', 7), ('id_plus_one', 8)]
+[('id', 1), ('id_plus_one', 2)]
+
+# however, an exception FieldDoesNotExist will still be thrown
+# if you try to access non-existent field (field that is neither on the model nor extra)
+>>> Article.objects.extra( select={'id_plus_one' : 'id + 1'} ).values('id', 'id_plus_two')
+Traceback (most recent call last):
+ ...
+FieldDoesNotExist: Article has no field named 'id_plus_two'
+
# if you don't specify which fields, all are returned
>>> list(Article.objects.filter(id=5).values()) == [{'id': 5, 'headline': 'Article 5', 'pub_date': datetime(2005, 8, 1, 9, 0)}]
True
@@ -191,4 +223,32 @@ DoesNotExist: Article matching query does not exist.
>>> Article.objects.filter(headline__contains='\\')
[<Article: Article with \ backslash>]
+# none() returns an EmptyQuerySet that behaves like any other QuerySet object
+>>> Article.objects.none()
+[]
+>>> Article.objects.none().filter(headline__startswith='Article')
+[]
+>>> Article.objects.none().count()
+0
+>>> [article for article in Article.objects.none().iterator()]
+[]
+
+# using __in with an empty list should return an empty query set
+>>> Article.objects.filter(id__in=[])
+[]
+
+>>> Article.objects.exclude(id__in=[])
+[<Article: Article with \ backslash>, <Article: Article% with percent sign>, <Article: Article_ with underscore>, <Article: Article 5>, <Article: Article 6>, <Article: Article 4>, <Article: Article 2>, <Article: Article 3>, <Article: Article 7>, <Article: Article 1>]
+
+# Programming errors are pointed out with nice error messages
+>>> Article.objects.filter(pub_date_year='2005').count()
+Traceback (most recent call last):
+ ...
+TypeError: Cannot resolve keyword 'pub_date_year' into field. Choices are: id, headline, pub_date
+
+>>> Article.objects.filter(headline__starts='Article')
+Traceback (most recent call last):
+ ...
+TypeError: Cannot resolve keyword 'headline__starts' into field. Choices are: id, headline, pub_date
+
"""}
diff --git a/tests/modeltests/m2m_and_m2o/models.py b/tests/modeltests/m2m_and_m2o/models.py
index 7fc66ed5a0..09affb002f 100644
--- a/tests/modeltests/m2m_and_m2o/models.py
+++ b/tests/modeltests/m2m_and_m2o/models.py
@@ -1,5 +1,5 @@
"""
-28. Many-to-many and many-to-one relationships to the same table
+29. Many-to-many and many-to-one relationships to the same table
Make sure to set ``related_name`` if you use relationships to the same table.
"""
diff --git a/tests/modeltests/m2m_recursive/models.py b/tests/modeltests/m2m_recursive/models.py
index 9f31cf92c0..15c713a759 100644
--- a/tests/modeltests/m2m_recursive/models.py
+++ b/tests/modeltests/m2m_recursive/models.py
@@ -1,5 +1,5 @@
"""
-27. Many-to-many relationships between the same two tables
+28. Many-to-many relationships between the same two tables
In this example, A Person can have many friends, who are also people. Friendship is a
symmetrical relationship - if I am your friend, you are my friend.
diff --git a/tests/modeltests/manipulators/models.py b/tests/modeltests/manipulators/models.py
index e5b8be55b5..1a44cfe7f4 100644
--- a/tests/modeltests/manipulators/models.py
+++ b/tests/modeltests/manipulators/models.py
@@ -1,5 +1,5 @@
"""
-26. Default manipulators
+27. Default manipulators
Each model gets an AddManipulator and ChangeManipulator by default.
"""
diff --git a/tests/modeltests/many_to_many/models.py b/tests/modeltests/many_to_many/models.py
index 38f8931ee7..5e46ad428d 100644
--- a/tests/modeltests/many_to_many/models.py
+++ b/tests/modeltests/many_to_many/models.py
@@ -203,7 +203,19 @@ __test__ = {'API_TESTS':"""
>>> p2.article_set.all()
[<Article: Oxygen-free diet works wonders>]
-# Recreate the article and Publication we just deleted.
+# Relation sets can also be set using primary key values
+>>> p2.article_set = [a4.id, a5.id]
+>>> p2.article_set.all()
+[<Article: NASA finds intelligent life on Earth>, <Article: Oxygen-free diet works wonders>]
+>>> a4.publications.all()
+[<Publication: Science News>]
+>>> a4.publications = [p3.id]
+>>> p2.article_set.all()
+[<Article: Oxygen-free diet works wonders>]
+>>> a4.publications.all()
+[<Publication: Science Weekly>]
+
+# Recreate the article and Publication we have deleted.
>>> p1 = Publication(id=None, title='The Python Journal')
>>> p1.save()
>>> a2 = Article(id=None, headline='NASA uses Python')
diff --git a/tests/modeltests/many_to_one/models.py b/tests/modeltests/many_to_one/models.py
index 82eb3257d0..02f7bf1066 100644
--- a/tests/modeltests/many_to_one/models.py
+++ b/tests/modeltests/many_to_one/models.py
@@ -174,13 +174,13 @@ False
>>> Article.objects.filter(reporter_id__exact=1)
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'reporter_id' into field
+TypeError: Cannot resolve keyword 'reporter_id' into field. Choices are: id, headline, pub_date, reporter
# You need to specify a comparison clause
>>> Article.objects.filter(reporter_id=1)
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'reporter_id' into field
+TypeError: Cannot resolve keyword 'reporter_id' into field. Choices are: id, headline, pub_date, reporter
# You can also instantiate an Article by passing
# the Reporter's ID instead of a Reporter object.
diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
index 5ffd6aac9f..6ffd4d1bce 100644
--- a/tests/modeltests/model_forms/models.py
+++ b/tests/modeltests/model_forms/models.py
@@ -1,18 +1,35 @@
"""
-34. Generating HTML forms from models
+36. Generating HTML forms from models
-Django provides shortcuts for creating Form objects from a model class.
+Django provides shortcuts for creating Form objects from a model class and a
+model instance.
The function django.newforms.form_for_model() takes a model class and returns
a Form that is tied to the model. This Form works just like any other Form,
-with one additional method: create(). The create() method creates an instance
+with one additional method: save(). The save() method creates an instance
of the model and returns that newly created instance. It saves the instance to
-the database if create(save=True), which is default. If you pass
-create(save=False), then you'll get the object without saving it.
+the database if save(commit=True), which is default. If you pass
+commit=False, then you'll get the object without committing the changes to the
+database.
+
+The function django.newforms.form_for_instance() takes a model instance and
+returns a Form that is tied to the instance. This form works just like any
+other Form, with one additional method: save(). The save()
+method updates the model instance. It also takes a commit=True parameter.
+
+The function django.newforms.save_instance() takes a bound form instance and a
+model instance and saves the form's cleaned_data into the instance. It also takes
+a commit=True parameter.
"""
from django.db import models
+ARTICLE_STATUS = (
+ (1, 'Draft'),
+ (2, 'Pending'),
+ (3, 'Live'),
+)
+
class Category(models.Model):
name = models.CharField(maxlength=20)
url = models.CharField('The URL', maxlength=40)
@@ -20,16 +37,40 @@ class Category(models.Model):
def __str__(self):
return self.name
+class Writer(models.Model):
+ name = models.CharField(maxlength=50, help_text='Use both first and last names.')
+
+ def __str__(self):
+ return self.name
+
class Article(models.Model):
headline = models.CharField(maxlength=50)
- pub_date = models.DateTimeField()
- categories = models.ManyToManyField(Category)
+ pub_date = models.DateField()
+ created = models.DateField(editable=False)
+ writer = models.ForeignKey(Writer)
+ article = models.TextField()
+ categories = models.ManyToManyField(Category, blank=True)
+ status = models.IntegerField(choices=ARTICLE_STATUS, blank=True, null=True)
+
+ def save(self):
+ import datetime
+ if not self.id:
+ self.created = datetime.date.today()
+ return super(Article, self).save()
def __str__(self):
return self.headline
+class PhoneNumber(models.Model):
+ phone = models.PhoneNumberField()
+ description = models.CharField(maxlength=20)
+
+ def __str__(self):
+ return self.phone
+
__test__ = {'API_TESTS': """
->>> from django.newforms import form_for_model, BaseForm
+>>> from django.newforms import form_for_model, form_for_instance, save_instance, BaseForm, Form, CharField
+>>> import datetime
>>> Category.objects.all()
[]
@@ -51,33 +92,36 @@ __test__ = {'API_TESTS': """
<li>The URL: <input type="text" name="url" maxlength="40" /></li>
>>> f = CategoryForm({'name': 'Entertainment', 'url': 'entertainment'})
->>> f.errors
-{}
->>> f.clean_data
+>>> f.is_valid()
+True
+>>> f.cleaned_data
{'url': u'entertainment', 'name': u'Entertainment'}
->>> obj = f.create()
+>>> obj = f.save()
>>> obj
<Category: Entertainment>
>>> Category.objects.all()
[<Category: Entertainment>]
>>> f = CategoryForm({'name': "It's a test", 'url': 'test'})
->>> f.errors
-{}
->>> f.clean_data
+>>> f.is_valid()
+True
+>>> f.cleaned_data
{'url': u'test', 'name': u"It's a test"}
->>> obj = f.create()
+>>> obj = f.save()
>>> obj
<Category: It's a test>
>>> Category.objects.all()
[<Category: Entertainment>, <Category: It's a test>]
+If you call save() with commit=False, then it will return an object that
+hasn't yet been saved to the database. In this case, it's up to you to call
+save() on the resulting model instance.
>>> f = CategoryForm({'name': 'Third test', 'url': 'third'})
->>> f.errors
-{}
->>> f.clean_data
+>>> f.is_valid()
+True
+>>> f.cleaned_data
{'url': u'third', 'name': u'Third test'}
->>> obj = f.create(save=False)
+>>> obj = f.save(commit=False)
>>> obj
<Category: Third test>
>>> Category.objects.all()
@@ -86,21 +130,67 @@ __test__ = {'API_TESTS': """
>>> Category.objects.all()
[<Category: Entertainment>, <Category: It's a test>, <Category: Third test>]
+If you call save() with invalid data, you'll get a ValueError.
>>> f = CategoryForm({'name': '', 'url': 'foo'})
>>> f.errors
{'name': [u'This field is required.']}
->>> f.clean_data
->>> f.create()
+>>> f.cleaned_data
+Traceback (most recent call last):
+...
+AttributeError: 'CategoryForm' object has no attribute 'cleaned_data'
+>>> f.save()
Traceback (most recent call last):
...
ValueError: The Category could not be created because the data didn't validate.
-
>>> f = CategoryForm({'name': '', 'url': 'foo'})
->>> f.create()
+>>> f.save()
Traceback (most recent call last):
...
ValueError: The Category could not be created because the data didn't validate.
+Create a couple of Writers.
+>>> w = Writer(name='Mike Royko')
+>>> w.save()
+>>> w = Writer(name='Bob Woodward')
+>>> w.save()
+
+ManyToManyFields are represented by a MultipleChoiceField, ForeignKeys and any
+fields with the 'choices' attribute are represented by a ChoiceField.
+>>> ArticleForm = form_for_model(Article)
+>>> f = ArticleForm(auto_id=False)
+>>> print f
+<tr><th>Headline:</th><td><input type="text" name="headline" maxlength="50" /></td></tr>
+<tr><th>Pub date:</th><td><input type="text" name="pub_date" /></td></tr>
+<tr><th>Writer:</th><td><select name="writer">
+<option value="" selected="selected">---------</option>
+<option value="1">Mike Royko</option>
+<option value="2">Bob Woodward</option>
+</select></td></tr>
+<tr><th>Article:</th><td><textarea rows="10" cols="40" name="article"></textarea></td></tr>
+<tr><th>Status:</th><td><select name="status">
+<option value="" selected="selected">---------</option>
+<option value="1">Draft</option>
+<option value="2">Pending</option>
+<option value="3">Live</option>
+</select></td></tr>
+<tr><th>Categories:</th><td><select multiple="multiple" name="categories">
+<option value="1">Entertainment</option>
+<option value="2">It&#39;s a test</option>
+<option value="3">Third test</option>
+</select><br /> Hold down "Control", or "Command" on a Mac, to select more than one.</td></tr>
+
+You can restrict a form to a subset of the complete list of fields
+by providing a 'fields' argument. If you try to save a
+model created with such a form, you need to ensure that the fields
+that are _not_ on the form have default values, or are allowed to have
+a value of None. If a field isn't specified on a form, the object created
+from the form can't provide a value for that field!
+>>> PartialArticleForm = form_for_model(Article, fields=('headline','pub_date'))
+>>> f = PartialArticleForm(auto_id=False)
+>>> print f
+<tr><th>Headline:</th><td><input type="text" name="headline" maxlength="50" /></td></tr>
+<tr><th>Pub date:</th><td><input type="text" name="pub_date" /></td></tr>
+
You can pass a custom Form class to form_for_model. Make sure it's a
subclass of BaseForm, not Form.
>>> class CustomForm(BaseForm):
@@ -110,4 +200,330 @@ subclass of BaseForm, not Form.
>>> f = CategoryForm()
>>> f.say_hello()
hello
+
+Use form_for_instance to create a Form from a model instance. The difference
+between this Form and one created via form_for_model is that the object's
+current values are inserted as 'initial' data in each Field.
+>>> w = Writer.objects.get(name='Mike Royko')
+>>> RoykoForm = form_for_instance(w)
+>>> f = RoykoForm(auto_id=False)
+>>> print f
+<tr><th>Name:</th><td><input type="text" name="name" value="Mike Royko" maxlength="50" /><br />Use both first and last names.</td></tr>
+
+>>> art = Article(headline='Test article', pub_date=datetime.date(1988, 1, 4), writer=w, article='Hello.')
+>>> art.save()
+>>> art.id
+1
+>>> TestArticleForm = form_for_instance(art)
+>>> f = TestArticleForm(auto_id=False)
+>>> print f.as_ul()
+<li>Headline: <input type="text" name="headline" value="Test article" maxlength="50" /></li>
+<li>Pub date: <input type="text" name="pub_date" value="1988-01-04" /></li>
+<li>Writer: <select name="writer">
+<option value="">---------</option>
+<option value="1" selected="selected">Mike Royko</option>
+<option value="2">Bob Woodward</option>
+</select></li>
+<li>Article: <textarea rows="10" cols="40" name="article">Hello.</textarea></li>
+<li>Status: <select name="status">
+<option value="" selected="selected">---------</option>
+<option value="1">Draft</option>
+<option value="2">Pending</option>
+<option value="3">Live</option>
+</select></li>
+<li>Categories: <select multiple="multiple" name="categories">
+<option value="1">Entertainment</option>
+<option value="2">It&#39;s a test</option>
+<option value="3">Third test</option>
+</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>
+>>> f = TestArticleForm({'headline': u'Test headline', 'pub_date': u'1984-02-06', 'writer': u'1', 'article': 'Hello.'})
+>>> f.is_valid()
+True
+>>> test_art = f.save()
+>>> test_art.id
+1
+>>> test_art = Article.objects.get(id=1)
+>>> test_art.headline
+'Test headline'
+
+You can create a form over a subset of the available fields
+by specifying a 'fields' argument to form_for_instance.
+>>> PartialArticleForm = form_for_instance(art, fields=('headline','pub_date'))
+>>> f = PartialArticleForm({'headline': u'New headline', 'pub_date': u'1988-01-04'}, auto_id=False)
+>>> print f.as_ul()
+<li>Headline: <input type="text" name="headline" value="New headline" maxlength="50" /></li>
+<li>Pub date: <input type="text" name="pub_date" value="1988-01-04" /></li>
+>>> f.is_valid()
+True
+>>> new_art = f.save()
+>>> new_art.id
+1
+>>> new_art = Article.objects.get(id=1)
+>>> new_art.headline
+'New headline'
+
+Add some categories and test the many-to-many form output.
+>>> new_art.categories.all()
+[]
+>>> new_art.categories.add(Category.objects.get(name='Entertainment'))
+>>> new_art.categories.all()
+[<Category: Entertainment>]
+>>> TestArticleForm = form_for_instance(new_art)
+>>> f = TestArticleForm(auto_id=False)
+>>> print f.as_ul()
+<li>Headline: <input type="text" name="headline" value="New headline" maxlength="50" /></li>
+<li>Pub date: <input type="text" name="pub_date" value="1988-01-04" /></li>
+<li>Writer: <select name="writer">
+<option value="">---------</option>
+<option value="1" selected="selected">Mike Royko</option>
+<option value="2">Bob Woodward</option>
+</select></li>
+<li>Article: <textarea rows="10" cols="40" name="article">Hello.</textarea></li>
+<li>Status: <select name="status">
+<option value="" selected="selected">---------</option>
+<option value="1">Draft</option>
+<option value="2">Pending</option>
+<option value="3">Live</option>
+</select></li>
+<li>Categories: <select multiple="multiple" name="categories">
+<option value="1" selected="selected">Entertainment</option>
+<option value="2">It&#39;s a test</option>
+<option value="3">Third test</option>
+</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>
+
+>>> f = TestArticleForm({'headline': u'New headline', 'pub_date': u'1988-01-04',
+... 'writer': u'1', 'article': u'Hello.', 'categories': [u'1', u'2']})
+>>> new_art = f.save()
+>>> new_art.id
+1
+>>> new_art = Article.objects.get(id=1)
+>>> new_art.categories.all()
+[<Category: Entertainment>, <Category: It's a test>]
+
+Now, submit form data with no categories. This deletes the existing categories.
+>>> f = TestArticleForm({'headline': u'New headline', 'pub_date': u'1988-01-04',
+... 'writer': u'1', 'article': u'Hello.'})
+>>> new_art = f.save()
+>>> new_art.id
+1
+>>> new_art = Article.objects.get(id=1)
+>>> new_art.categories.all()
+[]
+
+Create a new article, with categories, via the form.
+>>> ArticleForm = form_for_model(Article)
+>>> f = ArticleForm({'headline': u'The walrus was Paul', 'pub_date': u'1967-11-01',
+... 'writer': u'1', 'article': u'Test.', 'categories': [u'1', u'2']})
+>>> new_art = f.save()
+>>> new_art.id
+2
+>>> new_art = Article.objects.get(id=2)
+>>> new_art.categories.all()
+[<Category: Entertainment>, <Category: It's a test>]
+
+Create a new article, with no categories, via the form.
+>>> ArticleForm = form_for_model(Article)
+>>> f = ArticleForm({'headline': u'The walrus was Paul', 'pub_date': u'1967-11-01',
+... 'writer': u'1', 'article': u'Test.'})
+>>> new_art = f.save()
+>>> new_art.id
+3
+>>> new_art = Article.objects.get(id=3)
+>>> new_art.categories.all()
+[]
+
+Here, we define a custom Form. Because it happens to have the same fields as
+the Category model, we can use save_instance() to apply its changes to an
+existing Category instance.
+>>> class ShortCategory(Form):
+... name = CharField(max_length=5)
+... url = CharField(max_length=3)
+>>> cat = Category.objects.get(name='Third test')
+>>> cat
+<Category: Third test>
+>>> cat.id
+3
+>>> sc = ShortCategory({'name': 'Third', 'url': '3rd'})
+>>> save_instance(sc, cat)
+<Category: Third>
+>>> Category.objects.get(id=3)
+<Category: Third>
+
+Here, we demonstrate that choices for a ForeignKey ChoiceField are determined
+at runtime, based on the data in the database when the form is displayed, not
+the data in the database when the form is instantiated.
+>>> ArticleForm = form_for_model(Article)
+>>> f = ArticleForm(auto_id=False)
+>>> print f.as_ul()
+<li>Headline: <input type="text" name="headline" maxlength="50" /></li>
+<li>Pub date: <input type="text" name="pub_date" /></li>
+<li>Writer: <select name="writer">
+<option value="" selected="selected">---------</option>
+<option value="1">Mike Royko</option>
+<option value="2">Bob Woodward</option>
+</select></li>
+<li>Article: <textarea rows="10" cols="40" name="article"></textarea></li>
+<li>Status: <select name="status">
+<option value="" selected="selected">---------</option>
+<option value="1">Draft</option>
+<option value="2">Pending</option>
+<option value="3">Live</option>
+</select></li>
+<li>Categories: <select multiple="multiple" name="categories">
+<option value="1">Entertainment</option>
+<option value="2">It&#39;s a test</option>
+<option value="3">Third</option>
+</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>
+>>> Category.objects.create(name='Fourth', url='4th')
+<Category: Fourth>
+>>> Writer.objects.create(name='Carl Bernstein')
+<Writer: Carl Bernstein>
+>>> print f.as_ul()
+<li>Headline: <input type="text" name="headline" maxlength="50" /></li>
+<li>Pub date: <input type="text" name="pub_date" /></li>
+<li>Writer: <select name="writer">
+<option value="" selected="selected">---------</option>
+<option value="1">Mike Royko</option>
+<option value="2">Bob Woodward</option>
+<option value="3">Carl Bernstein</option>
+</select></li>
+<li>Article: <textarea rows="10" cols="40" name="article"></textarea></li>
+<li>Status: <select name="status">
+<option value="" selected="selected">---------</option>
+<option value="1">Draft</option>
+<option value="2">Pending</option>
+<option value="3">Live</option>
+</select></li>
+<li>Categories: <select multiple="multiple" name="categories">
+<option value="1">Entertainment</option>
+<option value="2">It&#39;s a test</option>
+<option value="3">Third</option>
+<option value="4">Fourth</option>
+</select> Hold down "Control", or "Command" on a Mac, to select more than one.</li>
+
+# ModelChoiceField ############################################################
+
+>>> from django.newforms import ModelChoiceField, ModelMultipleChoiceField
+
+>>> f = ModelChoiceField(Category.objects.all())
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean(0)
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
+>>> f.clean(3)
+<Category: Third>
+>>> f.clean(2)
+<Category: It's a test>
+
+# Add a Category object *after* the ModelChoiceField has already been
+# instantiated. This proves clean() checks the database during clean() rather
+# than caching it at time of instantiation.
+>>> Category.objects.create(name='Fifth', url='5th')
+<Category: Fifth>
+>>> f.clean(5)
+<Category: Fifth>
+
+# Delete a Category object *after* the ModelChoiceField has already been
+# instantiated. This proves clean() checks the database during clean() rather
+# than caching it at time of instantiation.
+>>> Category.objects.get(url='5th').delete()
+>>> f.clean(5)
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
+
+>>> f = ModelChoiceField(Category.objects.filter(pk=1), required=False)
+>>> print f.clean('')
+None
+>>> f.clean('')
+>>> f.clean('1')
+<Category: Entertainment>
+>>> f.clean('100')
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
+
+# ModelMultipleChoiceField ####################################################
+
+>>> f = ModelMultipleChoiceField(Category.objects.all())
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean([])
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean([1])
+[<Category: Entertainment>]
+>>> f.clean([2])
+[<Category: It's a test>]
+>>> f.clean(['1'])
+[<Category: Entertainment>]
+>>> f.clean(['1', '2'])
+[<Category: Entertainment>, <Category: It's a test>]
+>>> f.clean([1, '2'])
+[<Category: Entertainment>, <Category: It's a test>]
+>>> f.clean((1, '2'))
+[<Category: Entertainment>, <Category: It's a test>]
+>>> f.clean(['100'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 100 is not one of the available choices.']
+>>> f.clean('hello')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a list of values.']
+
+# Add a Category object *after* the ModelChoiceField has already been
+# instantiated. This proves clean() checks the database during clean() rather
+# than caching it at time of instantiation.
+>>> Category.objects.create(id=6, name='Sixth', url='6th')
+<Category: Sixth>
+>>> f.clean([6])
+[<Category: Sixth>]
+
+# Delete a Category object *after* the ModelChoiceField has already been
+# instantiated. This proves clean() checks the database during clean() rather
+# than caching it at time of instantiation.
+>>> Category.objects.get(url='6th').delete()
+>>> f.clean([6])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 6 is not one of the available choices.']
+
+>>> f = ModelMultipleChoiceField(Category.objects.all(), required=False)
+>>> f.clean([])
+[]
+>>> f.clean(())
+[]
+>>> f.clean(['10'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
+>>> f.clean(['3', '10'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
+>>> f.clean(['1', '10'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Select a valid choice. 10 is not one of the available choices.']
+
+# PhoneNumberField ############################################################
+
+>>> PhoneNumberForm = form_for_model(PhoneNumber)
+>>> f = PhoneNumberForm({'phone': '(312) 555-1212', 'description': 'Assistance'})
+>>> f.is_valid()
+True
+>>> f.cleaned_data
+{'phone': u'312-555-1212', 'description': u'Assistance'}
"""}
diff --git a/tests/modeltests/or_lookups/models.py b/tests/modeltests/or_lookups/models.py
index 2de18edc1f..9f926a7373 100644
--- a/tests/modeltests/or_lookups/models.py
+++ b/tests/modeltests/or_lookups/models.py
@@ -69,6 +69,21 @@ __test__ = {'API_TESTS':"""
>>> Article.objects.filter(Q(pk=1) | Q(pk=2) | Q(pk=3))
[<Article: Hello>, <Article: Goodbye>, <Article: Hello and goodbye>]
+# You could also use "in" to accomplish the same as above.
+>>> Article.objects.filter(pk__in=[1,2,3])
+[<Article: Hello>, <Article: Goodbye>, <Article: Hello and goodbye>]
+
+>>> Article.objects.filter(pk__in=[1,2,3,4])
+[<Article: Hello>, <Article: Goodbye>, <Article: Hello and goodbye>]
+
+# Passing "in" an empty list returns no results ...
+>>> Article.objects.filter(pk__in=[])
+[]
+
+# ... but can return results if we OR it with another query.
+>>> Article.objects.filter(Q(pk__in=[]) | Q(headline__icontains='goodbye'))
+[<Article: Goodbye>, <Article: Hello and goodbye>]
+
# Q arg objects are ANDed
>>> Article.objects.filter(Q(headline__startswith='Hello'), Q(headline__contains='bye'))
[<Article: Hello and goodbye>]
diff --git a/tests/modeltests/pagination/models.py b/tests/modeltests/pagination/models.py
index 3319b5cafa..94deb885f5 100644
--- a/tests/modeltests/pagination/models.py
+++ b/tests/modeltests/pagination/models.py
@@ -1,5 +1,5 @@
"""
-29. Object pagination
+30. Object pagination
Django provides a framework for paginating a list of objects in a few lines
of code. This is often useful for dividing search results or long lists of
diff --git a/tests/modeltests/reverse_lookup/models.py b/tests/modeltests/reverse_lookup/models.py
index 7e6712676f..4d6591551a 100644
--- a/tests/modeltests/reverse_lookup/models.py
+++ b/tests/modeltests/reverse_lookup/models.py
@@ -55,5 +55,5 @@ __test__ = {'API_TESTS':"""
>>> Poll.objects.get(choice__name__exact="This is the answer")
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'choice' into field
+TypeError: Cannot resolve keyword 'choice' into field. Choices are: poll_choice, related_choice, id, question, creator
"""}
diff --git a/tests/modeltests/select_related/__init__.py b/tests/modeltests/select_related/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/modeltests/select_related/__init__.py
diff --git a/tests/modeltests/select_related/models.py b/tests/modeltests/select_related/models.py
new file mode 100644
index 0000000000..cd34bd1d84
--- /dev/null
+++ b/tests/modeltests/select_related/models.py
@@ -0,0 +1,152 @@
+"""
+40. Tests for select_related()
+
+``select_related()`` follows all relationships and pre-caches any foreign key
+values so that complex trees can be fetched in a single query. However, this
+isn't always a good idea, so the ``depth`` argument control how many "levels"
+the select-related behavior will traverse.
+"""
+
+from django.db import models
+
+# Who remembers high school biology?
+
+class Domain(models.Model):
+ name = models.CharField(maxlength=50)
+ def __str__(self):
+ return self.name
+
+class Kingdom(models.Model):
+ name = models.CharField(maxlength=50)
+ domain = models.ForeignKey(Domain)
+ def __str__(self):
+ return self.name
+
+class Phylum(models.Model):
+ name = models.CharField(maxlength=50)
+ kingdom = models.ForeignKey(Kingdom)
+ def __str__(self):
+ return self.name
+
+class Klass(models.Model):
+ name = models.CharField(maxlength=50)
+ phylum = models.ForeignKey(Phylum)
+ def __str__(self):
+ return self.name
+
+class Order(models.Model):
+ name = models.CharField(maxlength=50)
+ klass = models.ForeignKey(Klass)
+ def __str__(self):
+ return self.name
+
+class Family(models.Model):
+ name = models.CharField(maxlength=50)
+ order = models.ForeignKey(Order)
+ def __str__(self):
+ return self.name
+
+class Genus(models.Model):
+ name = models.CharField(maxlength=50)
+ family = models.ForeignKey(Family)
+ def __str__(self):
+ return self.name
+
+class Species(models.Model):
+ name = models.CharField(maxlength=50)
+ genus = models.ForeignKey(Genus)
+ def __str__(self):
+ return self.name
+
+def create_tree(stringtree):
+ """Helper to create a complete tree"""
+ names = stringtree.split()
+ models = [Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species]
+ assert len(names) == len(models), (names, models)
+
+ parent = None
+ for name, model in zip(names, models):
+ try:
+ obj = model.objects.get(name=name)
+ except model.DoesNotExist:
+ obj = model(name=name)
+ if parent:
+ setattr(obj, parent.__class__.__name__.lower(), parent)
+ obj.save()
+ parent = obj
+
+__test__ = {'API_TESTS':"""
+
+# Set up.
+# The test runner sets settings.DEBUG to False, but we want to gather queries
+# so we'll set it to True here and reset it at the end of the test suite.
+>>> from django.conf import settings
+>>> settings.DEBUG = True
+
+>>> create_tree("Eukaryota Animalia Anthropoda Insecta Diptera Drosophilidae Drosophila melanogaster")
+>>> create_tree("Eukaryota Animalia Chordata Mammalia Primates Hominidae Homo sapiens")
+>>> create_tree("Eukaryota Plantae Magnoliophyta Magnoliopsida Fabales Fabaceae Pisum sativum")
+>>> create_tree("Eukaryota Fungi Basidiomycota Homobasidiomycatae Agaricales Amanitacae Amanita muscaria")
+
+>>> from django import db
+
+# Normally, accessing FKs doesn't fill in related objects:
+>>> db.reset_queries()
+>>> fly = Species.objects.get(name="melanogaster")
+>>> fly.genus.family.order.klass.phylum.kingdom.domain
+<Domain: Eukaryota>
+>>> len(db.connection.queries)
+8
+
+# However, a select_related() call will fill in those related objects without any extra queries:
+>>> db.reset_queries()
+>>> person = Species.objects.select_related().get(name="sapiens")
+>>> person.genus.family.order.klass.phylum.kingdom.domain
+<Domain: Eukaryota>
+>>> len(db.connection.queries)
+1
+
+# select_related() also of course applies to entire lists, not just items.
+# Without select_related()
+>>> db.reset_queries()
+>>> world = Species.objects.all()
+>>> [o.genus.family for o in world]
+[<Family: Drosophilidae>, <Family: Hominidae>, <Family: Fabaceae>, <Family: Amanitacae>]
+>>> len(db.connection.queries)
+9
+
+# With select_related():
+>>> db.reset_queries()
+>>> world = Species.objects.all().select_related()
+>>> [o.genus.family for o in world]
+[<Family: Drosophilidae>, <Family: Hominidae>, <Family: Fabaceae>, <Family: Amanitacae>]
+>>> len(db.connection.queries)
+1
+
+# The "depth" argument to select_related() will stop the descent at a particular level:
+>>> db.reset_queries()
+>>> pea = Species.objects.select_related(depth=1).get(name="sativum")
+>>> pea.genus.family.order.klass.phylum.kingdom.domain
+<Domain: Eukaryota>
+
+# Notice: one few query than above because of depth=1
+>>> len(db.connection.queries)
+7
+
+>>> db.reset_queries()
+>>> pea = Species.objects.select_related(depth=5).get(name="sativum")
+>>> pea.genus.family.order.klass.phylum.kingdom.domain
+<Domain: Eukaryota>
+>>> len(db.connection.queries)
+3
+
+>>> db.reset_queries()
+>>> world = Species.objects.all().select_related(depth=2)
+>>> [o.genus.family.order for o in world]
+[<Order: Diptera>, <Order: Primates>, <Order: Fabales>, <Order: Agaricales>]
+>>> len(db.connection.queries)
+5
+
+# Reset DEBUG to where we found it.
+>>> settings.DEBUG = False
+"""}
diff --git a/tests/modeltests/serializers/models.py b/tests/modeltests/serializers/models.py
index d1d10b43c0..8d44d5eae7 100644
--- a/tests/modeltests/serializers/models.py
+++ b/tests/modeltests/serializers/models.py
@@ -1,5 +1,5 @@
"""
-XXX. Serialization
+41. Serialization
``django.core.serializers`` provides interfaces to converting Django querysets
to and from "flat" data (i.e. strings).
@@ -37,6 +37,13 @@ class Article(models.Model):
def __str__(self):
return self.headline
+class AuthorProfile(models.Model):
+ author = models.OneToOneField(Author)
+ date_of_birth = models.DateField()
+
+ def __str__(self):
+ return "Profile of %s" % self.author
+
__test__ = {'API_TESTS':"""
# Create some data:
>>> from datetime import datetime
@@ -118,4 +125,42 @@ __test__ = {'API_TESTS':"""
>>> Article.objects.all()
[<Article: Just kidding; I love TV poker>, <Article: Time to reform copyright>]
+# If you use your own primary key field (such as a OneToOneField),
+# it doesn't appear in the serialized field list - it replaces the
+# pk identifier.
+>>> profile = AuthorProfile(author=joe, date_of_birth=datetime(1970,1,1))
+>>> profile.save()
+
+>>> json = serializers.serialize("json", AuthorProfile.objects.all())
+>>> json
+'[{"pk": "1", "model": "serializers.authorprofile", "fields": {"date_of_birth": "1970-01-01"}}]'
+
+>>> for obj in serializers.deserialize("json", json):
+... print obj
+<DeserializedObject: Profile of Joe>
+
+# Objects ids can be referenced before they are defined in the serialization data
+# However, the deserialization process will need to be contained within a transaction
+>>> json = '[{"pk": "3", "model": "serializers.article", "fields": {"headline": "Forward references pose no problem", "pub_date": "2006-06-16 15:00:00", "categories": [4, 1], "author": 4}}, {"pk": "4", "model": "serializers.category", "fields": {"name": "Reference"}}, {"pk": "4", "model": "serializers.author", "fields": {"name": "Agnes"}}]'
+>>> from django.db import transaction
+>>> transaction.enter_transaction_management()
+>>> transaction.managed(True)
+>>> for obj in serializers.deserialize("json", json):
+... obj.save()
+
+>>> transaction.commit()
+>>> transaction.leave_transaction_management()
+
+>>> article = Article.objects.get(pk=3)
+>>> article
+<Article: Forward references pose no problem>
+>>> article.categories.all()
+[<Category: Reference>, <Category: Sports>]
+>>> article.author
+<Author: Agnes>
+
+# Serializer output can be restricted to a subset of fields
+>>> print serializers.serialize("json", Article.objects.all(), fields=('headline','pub_date'))
+[{"pk": "1", "model": "serializers.article", "fields": {"headline": "Just kidding; I love TV poker", "pub_date": "2006-06-16 11:00:00"}}, {"pk": "2", "model": "serializers.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": "3", "model": "serializers.article", "fields": {"headline": "Forward references pose no problem", "pub_date": "2006-06-16 15:00:00"}}]
+
"""}
diff --git a/tests/modeltests/test_client/fixtures/testdata.json b/tests/modeltests/test_client/fixtures/testdata.json
new file mode 100644
index 0000000000..5c9e415240
--- /dev/null
+++ b/tests/modeltests/test_client/fixtures/testdata.json
@@ -0,0 +1,20 @@
+[
+ {
+ "pk": "1",
+ "model": "auth.user",
+ "fields": {
+ "username": "testclient",
+ "first_name": "Test",
+ "last_name": "Client",
+ "is_active": true,
+ "is_superuser": false,
+ "is_staff": false,
+ "last_login": "2006-12-17 07:03:31",
+ "groups": [],
+ "user_permissions": [],
+ "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161",
+ "email": "testclient@example.com",
+ "date_joined": "2006-12-17 07:03:31"
+ }
+ }
+] \ No newline at end of file
diff --git a/tests/modeltests/test_client/management.py b/tests/modeltests/test_client/management.py
deleted file mode 100644
index 9b5a5c498e..0000000000
--- a/tests/modeltests/test_client/management.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from django.dispatch import dispatcher
-from django.db.models import signals
-import models as test_client_app
-from django.contrib.auth.models import User
-
-def setup_test(app, created_models, verbosity):
- # Create a user account for the login-based tests
- User.objects.create_user('testclient','testclient@example.com', 'password')
-
-dispatcher.connect(setup_test, sender=test_client_app, signal=signals.post_syncdb)
diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py
index c5b1a241ca..5ebf29678c 100644
--- a/tests/modeltests/test_client/models.py
+++ b/tests/modeltests/test_client/models.py
@@ -1,5 +1,5 @@
"""
-39. Testing using the Test Client
+38. Testing using the Test Client
The test client is a class that can act like a simple
browser for testing purposes.
@@ -19,23 +19,20 @@ testing against the contexts and templates produced by a view,
rather than the HTML rendered to the end-user.
"""
-from django.test.client import Client
-import unittest
+from django.test import Client, TestCase
+from django.core import mail
-class ClientTest(unittest.TestCase):
- def setUp(self):
- "Set up test environment"
- self.client = Client()
-
+class ClientTest(TestCase):
+ fixtures = ['testdata.json']
+
def test_get_view(self):
"GET a view"
response = self.client.get('/test_client/get_view/')
# Check some response details
- self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'This is a test')
self.assertEqual(response.context['var'], 42)
self.assertEqual(response.template.name, 'GET Template')
- self.failUnless('This is a test.' in response.content)
def test_get_post_view(self):
"GET a view that normally expects POSTs"
@@ -43,7 +40,9 @@ class ClientTest(unittest.TestCase):
# Check some response details
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.template.name, 'Empty POST Template')
+ self.assertEqual(response.template.name, 'Empty GET Template')
+ self.assertTemplateUsed(response, 'Empty GET Template')
+ self.assertTemplateNotUsed(response, 'Empty POST Template')
def test_empty_post(self):
"POST an empty dictionary to a view"
@@ -52,8 +51,10 @@ class ClientTest(unittest.TestCase):
# Check some response details
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template.name, 'Empty POST Template')
+ self.assertTemplateNotUsed(response, 'Empty GET Template')
+ self.assertTemplateUsed(response, 'Empty POST Template')
- def test_post_view(self):
+ def test_post(self):
"POST some data to a view"
post_data = {
'value': 37
@@ -66,13 +67,135 @@ class ClientTest(unittest.TestCase):
self.assertEqual(response.template.name, 'POST Template')
self.failUnless('Data received' in response.content)
+ def test_raw_post(self):
+ "POST raw data (with a content type) to a view"
+ test_doc = """<?xml version="1.0" encoding="utf-8"?><library><book><title>Blink</title><author>Malcolm Gladwell</author></book></library>"""
+ response = self.client.post("/test_client/raw_post_view/", test_doc,
+ content_type="text/xml")
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.template.name, "Book template")
+ self.assertEqual(response.content, "Blink - Malcolm Gladwell")
+
def test_redirect(self):
"GET a URL that redirects elsewhere"
response = self.client.get('/test_client/redirect_view/')
# Check that the response was a 302 (redirect)
- self.assertEqual(response.status_code, 302)
-
+ self.assertRedirects(response, '/test_client/get_view/')
+
+ def test_permanent_redirect(self):
+ "GET a URL that redirects permanently elsewhere"
+ response = self.client.get('/test_client/permanent_redirect_view/')
+
+ # Check that the response was a 301 (permanent redirect)
+ self.assertRedirects(response, '/test_client/get_view/', status_code=301)
+
+ def test_redirect_to_strange_location(self):
+ "GET a URL that redirects to a non-200 page"
+ response = self.client.get('/test_client/double_redirect_view/')
+
+ # Check that the response was a 302, and that
+ # the attempt to get the redirection location returned 301 when retrieved
+ self.assertRedirects(response, '/test_client/permanent_redirect_view/', target_status_code=301)
+
+ def test_notfound_response(self):
+ "GET a URL that responds as '404:Not Found'"
+ response = self.client.get('/test_client/bad_view/')
+
+ # Check that the response was a 404, and that the content contains MAGIC
+ self.assertContains(response, 'MAGIC', status_code=404)
+
+ def test_valid_form(self):
+ "POST valid data to a form"
+ post_data = {
+ 'text': 'Hello World',
+ 'email': 'foo@example.com',
+ 'value': 37,
+ 'single': 'b',
+ 'multi': ('b','c','e')
+ }
+ response = self.client.post('/test_client/form_view/', post_data)
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, "Valid POST Template")
+
+ def test_incomplete_data_form(self):
+ "POST incomplete data to a form"
+ post_data = {
+ 'text': 'Hello World',
+ 'value': 37
+ }
+ response = self.client.post('/test_client/form_view/', post_data)
+ self.assertContains(response, 'This field is required.', 3)
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, "Invalid POST Template")
+
+ self.assertFormError(response, 'form', 'email', 'This field is required.')
+ self.assertFormError(response, 'form', 'single', 'This field is required.')
+ self.assertFormError(response, 'form', 'multi', 'This field is required.')
+
+ def test_form_error(self):
+ "POST erroneous data to a form"
+ post_data = {
+ 'text': 'Hello World',
+ 'email': 'not an email address',
+ 'value': 37,
+ 'single': 'b',
+ 'multi': ('b','c','e')
+ }
+ response = self.client.post('/test_client/form_view/', post_data)
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, "Invalid POST Template")
+
+ self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.')
+
+ def test_valid_form_with_template(self):
+ "POST valid data to a form using multiple templates"
+ post_data = {
+ 'text': 'Hello World',
+ 'email': 'foo@example.com',
+ 'value': 37,
+ 'single': 'b',
+ 'multi': ('b','c','e')
+ }
+ response = self.client.post('/test_client/form_view_with_template/', post_data)
+ self.assertContains(response, 'POST data OK')
+ self.assertTemplateUsed(response, "form_view.html")
+ self.assertTemplateUsed(response, 'base.html')
+ self.assertTemplateNotUsed(response, "Valid POST Template")
+
+ def test_incomplete_data_form_with_template(self):
+ "POST incomplete data to a form using multiple templates"
+ post_data = {
+ 'text': 'Hello World',
+ 'value': 37
+ }
+ response = self.client.post('/test_client/form_view_with_template/', post_data)
+ self.assertContains(response, 'POST data has errors')
+ self.assertTemplateUsed(response, 'form_view.html')
+ self.assertTemplateUsed(response, 'base.html')
+ self.assertTemplateNotUsed(response, "Invalid POST Template")
+
+ self.assertFormError(response, 'form', 'email', 'This field is required.')
+ self.assertFormError(response, 'form', 'single', 'This field is required.')
+ self.assertFormError(response, 'form', 'multi', 'This field is required.')
+
+ def test_form_error_with_template(self):
+ "POST erroneous data to a form using multiple templates"
+ post_data = {
+ 'text': 'Hello World',
+ 'email': 'not an email address',
+ 'value': 37,
+ 'single': 'b',
+ 'multi': ('b','c','e')
+ }
+ response = self.client.post('/test_client/form_view_with_template/', post_data)
+ self.assertContains(response, 'POST data has errors')
+ self.assertTemplateUsed(response, "form_view.html")
+ self.assertTemplateUsed(response, 'base.html')
+ self.assertTemplateNotUsed(response, "Invalid POST Template")
+
+ self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.')
+
def test_unknown_page(self):
"GET an invalid URL"
response = self.client.get('/test_client/unknown_view/')
@@ -85,17 +208,77 @@ class ClientTest(unittest.TestCase):
# Get the page without logging in. Should result in 302.
response = self.client.get('/test_client/login_protected_view/')
- self.assertEqual(response.status_code, 302)
+ self.assertRedirects(response, '/accounts/login/')
+ # Log in
+ self.client.login(username='testclient', password='password')
+
# Request a page that requires a login
- response = self.client.login('/test_client/login_protected_view/', 'testclient', 'password')
- self.assertTrue(response)
+ response = self.client.get('/test_client/login_protected_view/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['user'].username, 'testclient')
- self.assertEqual(response.template.name, 'Login Template')
def test_view_with_bad_login(self):
"Request a page that is protected with @login, but use bad credentials"
- response = self.client.login('/test_client/login_protected_view/', 'otheruser', 'nopassword')
- self.assertFalse(response)
+ login = self.client.login(username='otheruser', password='nopassword')
+ self.failIf(login)
+
+ def test_session_modifying_view(self):
+ "Request a page that modifies the session"
+ # Session value isn't set initially
+ try:
+ self.client.session['tobacconist']
+ self.fail("Shouldn't have a session value")
+ except KeyError:
+ pass
+
+ from django.contrib.sessions.models import Session
+ response = self.client.post('/test_client/session_view/')
+
+ # Check that the session was modified
+ self.assertEquals(self.client.session['tobacconist'], 'hovercraft')
+
+ def test_view_with_exception(self):
+ "Request a page that is known to throw an error"
+ self.assertRaises(KeyError, self.client.get, "/test_client/broken_view/")
+
+ #Try the same assertion, a different way
+ try:
+ self.client.get('/test_client/broken_view/')
+ self.fail('Should raise an error')
+ except KeyError:
+ pass
+
+ def test_mail_sending(self):
+ "Test that mail is redirected to a dummy outbox during test setup"
+
+ response = self.client.get('/test_client/mail_sending_view/')
+ self.assertEqual(response.status_code, 200)
+
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].subject, 'Test message')
+ self.assertEqual(mail.outbox[0].body, 'This is a test email')
+ self.assertEqual(mail.outbox[0].from_email, 'from@example.com')
+ self.assertEqual(mail.outbox[0].to[0], 'first@example.com')
+ self.assertEqual(mail.outbox[0].to[1], 'second@example.com')
+
+ def test_mass_mail_sending(self):
+ "Test that mass mail is redirected to a dummy outbox during test setup"
+
+ response = self.client.get('/test_client/mass_mail_sending_view/')
+ self.assertEqual(response.status_code, 200)
+
+ self.assertEqual(len(mail.outbox), 2)
+ self.assertEqual(mail.outbox[0].subject, 'First Test message')
+ self.assertEqual(mail.outbox[0].body, 'This is the first test email')
+ self.assertEqual(mail.outbox[0].from_email, 'from@example.com')
+ self.assertEqual(mail.outbox[0].to[0], 'first@example.com')
+ self.assertEqual(mail.outbox[0].to[1], 'second@example.com')
+
+ self.assertEqual(mail.outbox[1].subject, 'Second Test message')
+ self.assertEqual(mail.outbox[1].body, 'This is the second test email')
+ self.assertEqual(mail.outbox[1].from_email, 'from@example.com')
+ self.assertEqual(mail.outbox[1].to[0], 'second@example.com')
+ self.assertEqual(mail.outbox[1].to[1], 'third@example.com')
+ \ No newline at end of file
diff --git a/tests/modeltests/test_client/urls.py b/tests/modeltests/test_client/urls.py
index 09bba5c007..538c0e4b43 100644
--- a/tests/modeltests/test_client/urls.py
+++ b/tests/modeltests/test_client/urls.py
@@ -1,9 +1,20 @@
from django.conf.urls.defaults import *
+from django.views.generic.simple import redirect_to
import views
urlpatterns = patterns('',
(r'^get_view/$', views.get_view),
(r'^post_view/$', views.post_view),
+ (r'^raw_post_view/$', views.raw_post_view),
(r'^redirect_view/$', views.redirect_view),
+ (r'^permanent_redirect_view/$', redirect_to, { 'url': '/test_client/get_view/' }),
+ (r'^double_redirect_view/$', views.double_redirect_view),
+ (r'^bad_view/$', views.bad_view),
+ (r'^form_view/$', views.form_view),
+ (r'^form_view_with_template/$', views.form_view_with_template),
(r'^login_protected_view/$', views.login_protected_view),
+ (r'^session_view/$', views.session_view),
+ (r'^broken_view/$', views.broken_view),
+ (r'^mail_sending_view/$', views.mail_sending_view),
+ (r'^mass_mail_sending_view/$', views.mass_mail_sending_view)
)
diff --git a/tests/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py
index bf131032eb..9bdf213b35 100644
--- a/tests/modeltests/test_client/views.py
+++ b/tests/modeltests/test_client/views.py
@@ -1,6 +1,11 @@
+from xml.dom.minidom import parseString
+from django.core.mail import EmailMessage, SMTPConnection
from django.template import Context, Template
-from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound
from django.contrib.auth.decorators import login_required
+from django.newforms.forms import Form
+from django.newforms import fields
+from django.shortcuts import render_to_response
def get_view(request):
"A simple view that expects a GET request, and returns a rendered template"
@@ -13,23 +18,139 @@ def post_view(request):
"""A view that expects a POST, and returns a different template depending
on whether any POST data is available
"""
- if request.POST:
- t = Template('Data received: {{ data }} is the value.', name='POST Template')
- c = Context({'data': request.POST['value']})
+ if request.method == 'POST':
+ if request.POST:
+ t = Template('Data received: {{ data }} is the value.', name='POST Template')
+ c = Context({'data': request.POST['value']})
+ else:
+ t = Template('Viewing POST page.', name='Empty POST Template')
+ c = Context()
else:
- t = Template('Viewing POST page.', name='Empty POST Template')
+ t = Template('Viewing GET page.', name='Empty GET Template')
c = Context()
-
+
return HttpResponse(t.render(c))
+def raw_post_view(request):
+ """A view which expects raw XML to be posted and returns content extracted
+ from the XML"""
+ if request.method == 'POST':
+ root = parseString(request.raw_post_data)
+ first_book = root.firstChild.firstChild
+ title, author = [n.firstChild.nodeValue for n in first_book.childNodes]
+ t = Template("{{ title }} - {{ author }}", name="Book template")
+ c = Context({"title": title, "author": author})
+ else:
+ t = Template("GET request.", name="Book GET template")
+ c = Context()
+
+ return HttpResponse(t.render(c))
+
def redirect_view(request):
"A view that redirects all requests to the GET view"
return HttpResponseRedirect('/test_client/get_view/')
+
+def double_redirect_view(request):
+ "A view that redirects all requests to a redirection view"
+ return HttpResponseRedirect('/test_client/permanent_redirect_view/')
+
+def bad_view(request):
+ "A view that returns a 404 with some error content"
+ return HttpResponseNotFound('Not found!. This page contains some MAGIC content')
+
+TestChoices = (
+ ('a', 'First Choice'),
+ ('b', 'Second Choice'),
+ ('c', 'Third Choice'),
+ ('d', 'Fourth Choice'),
+ ('e', 'Fifth Choice')
+)
+
+class TestForm(Form):
+ text = fields.CharField()
+ email = fields.EmailField()
+ value = fields.IntegerField()
+ single = fields.ChoiceField(choices=TestChoices)
+ multi = fields.MultipleChoiceField(choices=TestChoices)
+
+def form_view(request):
+ "A view that tests a simple form"
+ if request.method == 'POST':
+ form = TestForm(request.POST)
+ if form.is_valid():
+ t = Template('Valid POST data.', name='Valid POST Template')
+ c = Context()
+ else:
+ t = Template('Invalid POST data. {{ form.errors }}', name='Invalid POST Template')
+ c = Context({'form': form})
+ else:
+ form = TestForm()
+ t = Template('Viewing base form. {{ form }}.', name='Form GET Template')
+ c = Context({'form': form})
-@login_required
+ return HttpResponse(t.render(c))
+
+def form_view_with_template(request):
+ "A view that tests a simple form"
+ if request.method == 'POST':
+ form = TestForm(request.POST)
+ if form.is_valid():
+ message = 'POST data OK'
+ else:
+ message = 'POST data has errors'
+ else:
+ form = TestForm()
+ message = 'GET form page'
+ return render_to_response('form_view.html',
+ {
+ 'form': form,
+ 'message': message
+ }
+ )
+
+
def login_protected_view(request):
"A simple view that is login protected."
t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template')
c = Context({'user': request.user})
return HttpResponse(t.render(c))
+login_protected_view = login_required(login_protected_view)
+
+def session_view(request):
+ "A view that modifies the session"
+ request.session['tobacconist'] = 'hovercraft'
+
+ t = Template('This is a view that modifies the session.',
+ name='Session Modifying View Template')
+ c = Context()
+ return HttpResponse(t.render(c))
+
+def broken_view(request):
+ """A view which just raises an exception, simulating a broken view."""
+ raise KeyError("Oops! Looks like you wrote some bad code.")
+
+def mail_sending_view(request):
+ EmailMessage(
+ "Test message",
+ "This is a test email",
+ "from@example.com",
+ ['first@example.com', 'second@example.com']).send()
+ return HttpResponse("Mail sent")
+
+def mass_mail_sending_view(request):
+ m1 = EmailMessage(
+ 'First Test message',
+ 'This is the first test email',
+ 'from@example.com',
+ ['first@example.com', 'second@example.com'])
+ m2 = EmailMessage(
+ 'Second Test message',
+ 'This is the second test email',
+ 'from@example.com',
+ ['second@example.com', 'third@example.com'])
+
+ c = SMTPConnection()
+ c.send_messages([m1,m2])
+
+ return HttpResponse("Mail sent")
diff --git a/tests/modeltests/validation/models.py b/tests/modeltests/validation/models.py
index a9a3d3f485..b31f981aac 100644
--- a/tests/modeltests/validation/models.py
+++ b/tests/modeltests/validation/models.py
@@ -1,5 +1,5 @@
"""
-30. Validation
+31. Validation
This is an experimental feature!
@@ -146,4 +146,8 @@ u'john@example.com'
>>> p.validate()
{'email': ['Enter a valid e-mail address.']}
+# Make sure that Date and DateTime return validation errors and don't raise Python errors.
+>>> Person(name='John Doe', is_child=True, email='abc@def.com').validate()
+{'favorite_moment': ['This field is required.'], 'birthdate': ['This field is required.']}
+
"""}