diff options
| author | Christopher Long <indirecthit@gmail.com> | 2007-06-17 22:18:54 +0000 |
|---|---|---|
| committer | Christopher Long <indirecthit@gmail.com> | 2007-06-17 22:18:54 +0000 |
| commit | ae22b6d403dcf25098c77f0dfcf59ae58b186461 (patch) | |
| tree | c37fc631e99a7e4d909d6b6d236f495003731ea7 /tests/modeltests | |
| parent | 0cf7bc439129c66df8d64601e885f83b256b4f25 (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')
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'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'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'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'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'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.']} + """} |
