summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorBoulder Sprinters <boulder-sprinters@djangoproject.com>2007-03-09 17:43:46 +0000
committerBoulder Sprinters <boulder-sprinters@djangoproject.com>2007-03-09 17:43:46 +0000
commit0b7dd14d1f87e2ecef7aacc39fe4189667ed4fdf (patch)
treeb77497f9de324d38a9c6341e54d2742633e20055 /tests
parente17f75551491f5b864c1fc8a97c21d0b2bbf0bcd (diff)
boulder-oracle-sprint: Merged to trunk [4692].
git-svn-id: http://code.djangoproject.com/svn/django/branches/boulder-oracle-sprint@4695 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'tests')
-rw-r--r--tests/modeltests/basic/models.py10
-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/lookup/models.py24
-rw-r--r--tests/modeltests/model_forms/models.py180
-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.py20
-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.py49
-rw-r--r--tests/modeltests/test_client/urls.py3
-rw-r--r--tests/modeltests/test_client/views.py45
-rw-r--r--tests/modeltests/validation/models.py4
-rw-r--r--tests/regressiontests/bug639/__init__.py0
-rw-r--r--tests/regressiontests/bug639/models.py16
-rw-r--r--tests/regressiontests/bug639/test.jpgbin0 -> 1780 bytes
-rw-r--r--tests/regressiontests/bug639/tests.py42
-rw-r--r--tests/regressiontests/datastructures/__init__.py0
-rw-r--r--tests/regressiontests/datastructures/models.py0
-rw-r--r--tests/regressiontests/datastructures/tests.py65
-rw-r--r--tests/regressiontests/dateformat/tests.py2
-rw-r--r--tests/regressiontests/defaultfilters/tests.py64
-rw-r--r--tests/regressiontests/dispatch/__init__.py2
-rw-r--r--tests/regressiontests/dispatch/models.py0
-rw-r--r--tests/regressiontests/dispatch/tests/__init__.py7
-rw-r--r--tests/regressiontests/dispatch/tests/test_dispatcher.py144
-rw-r--r--tests/regressiontests/dispatch/tests/test_robustapply.py34
-rw-r--r--tests/regressiontests/dispatch/tests/test_saferef.py77
-rw-r--r--tests/regressiontests/forms/tests.py485
-rw-r--r--tests/regressiontests/humanize/__init__.py0
-rw-r--r--tests/regressiontests/humanize/models.py0
-rw-r--r--tests/regressiontests/humanize/tests.py52
-rw-r--r--tests/regressiontests/many_to_one_regress/models.py29
-rw-r--r--tests/regressiontests/templates/tests.py65
-rw-r--r--tests/regressiontests/templates/urls.py10
-rw-r--r--tests/regressiontests/templates/views.py10
-rwxr-xr-xtests/runtests.py23
-rw-r--r--tests/urls.py5
44 files changed, 1760 insertions, 47 deletions
diff --git a/tests/modeltests/basic/models.py b/tests/modeltests/basic/models.py
index 2ebe857e7e..062f8b29ed 100644
--- a/tests/modeltests/basic/models.py
+++ b/tests/modeltests/basic/models.py
@@ -14,7 +14,7 @@ class Article(models.Model):
class Meta:
ordering = ('pub_date','headline')
-
+
def __str__(self):
return self.headline
@@ -321,7 +321,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
@@ -360,4 +359,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/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..d82886a6c4
--- /dev/null
+++ b/tests/modeltests/fixtures/models.py
@@ -0,0 +1,88 @@
+"""
+39. 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
+>>> 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/lookup/models.py b/tests/modeltests/lookup/models.py
index aa903d1a64..106c97d3b4 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
@@ -198,6 +209,8 @@ DoesNotExist: Article matching query does not exist.
[]
>>> 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=[])
@@ -206,4 +219,15 @@ DoesNotExist: Article matching query does not exist.
>>> 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
+
+>>> Article.objects.filter(headline__starts='Article')
+Traceback (most recent call last):
+ ...
+TypeError: Cannot resolve keyword 'headline__starts' into field
+
"""}
diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
index 657d506b33..e64174a23f 100644
--- a/tests/modeltests/model_forms/models.py
+++ b/tests/modeltests/model_forms/models.py
@@ -40,13 +40,27 @@ class Writer(models.Model):
class Article(models.Model):
headline = models.CharField(maxlength=50)
pub_date = models.DateField()
+ created = models.DateField(editable=False)
writer = models.ForeignKey(Writer)
article = models.TextField()
categories = models.ManyToManyField(Category, blank=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, form_for_instance, save_instance, BaseForm, Form, CharField
>>> import datetime
@@ -281,4 +295,170 @@ existing Category instance.
<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 name="article"></textarea></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 name="article"></textarea></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.clean_data
+{'phone': u'312-555-1212', 'description': u'Assistance'}
"""}
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..8a19267870
--- /dev/null
+++ b/tests/modeltests/select_related/models.py
@@ -0,0 +1,152 @@
+"""
+XXX. 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 e24ff537c1..e86546c6fe 100644
--- a/tests/modeltests/serializers/models.py
+++ b/tests/modeltests/serializers/models.py
@@ -139,4 +139,24 @@ __test__ = {'API_TESTS':"""
... 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>
+
"""}
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..a3a9749162 100644
--- a/tests/modeltests/test_client/models.py
+++ b/tests/modeltests/test_client/models.py
@@ -19,10 +19,11 @@ 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
-class ClientTest(unittest.TestCase):
+class ClientTest(TestCase):
+ fixtures = ['testdata.json']
+
def setUp(self):
"Set up test environment"
self.client = Client()
@@ -43,7 +44,7 @@ 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')
def test_empty_post(self):
"POST an empty dictionary to a view"
@@ -53,7 +54,7 @@ class ClientTest(unittest.TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template.name, 'Empty POST Template')
- def test_post_view(self):
+ def test_post(self):
"POST some data to a view"
post_data = {
'value': 37
@@ -66,6 +67,14 @@ class ClientTest(unittest.TestCase):
self.assertEqual(response.template.name, 'POST Template')
self.failUnless('Data received' in response.content)
+ def test_raw_post(self):
+ 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/')
@@ -89,7 +98,7 @@ class ClientTest(unittest.TestCase):
# Request a page that requires a login
response = self.client.login('/test_client/login_protected_view/', 'testclient', 'password')
- self.assertTrue(response)
+ self.failUnless(response)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['user'].username, 'testclient')
self.assertEqual(response.template.name, 'Login Template')
@@ -98,4 +107,30 @@ class ClientTest(unittest.TestCase):
"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)
+ self.failIf(response)
+
+ 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
diff --git a/tests/modeltests/test_client/urls.py b/tests/modeltests/test_client/urls.py
index 09bba5c007..0bef1e9b71 100644
--- a/tests/modeltests/test_client/urls.py
+++ b/tests/modeltests/test_client/urls.py
@@ -4,6 +4,9 @@ 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'^login_protected_view/$', views.login_protected_view),
+ (r'^session_view/$', views.session_view),
+ (r'^broken_view/$', views.broken_view)
)
diff --git a/tests/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py
index 7acfc2db60..653e9a10e9 100644
--- a/tests/modeltests/test_client/views.py
+++ b/tests/modeltests/test_client/views.py
@@ -1,3 +1,4 @@
+from xml.dom.minidom import parseString
from django.template import Context, Template
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required
@@ -13,15 +14,34 @@ 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/')
@@ -32,4 +52,17 @@ def login_protected_view(request):
c = Context({'user': request.user})
return HttpResponse(t.render(c))
-login_protected_view = login_required(login_protected_view) \ No newline at end of file
+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.")
diff --git a/tests/modeltests/validation/models.py b/tests/modeltests/validation/models.py
index a9a3d3f485..1066faca4f 100644
--- a/tests/modeltests/validation/models.py
+++ b/tests/modeltests/validation/models.py
@@ -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.']}
+
"""}
diff --git a/tests/regressiontests/bug639/__init__.py b/tests/regressiontests/bug639/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/bug639/__init__.py
diff --git a/tests/regressiontests/bug639/models.py b/tests/regressiontests/bug639/models.py
new file mode 100644
index 0000000000..7cfdfc82ef
--- /dev/null
+++ b/tests/regressiontests/bug639/models.py
@@ -0,0 +1,16 @@
+import tempfile
+from django.db import models
+
+class Photo(models.Model):
+ title = models.CharField(maxlength=30)
+ image = models.FileField(upload_to=tempfile.gettempdir())
+
+ # Support code for the tests; this keeps track of how many times save() gets
+ # called on each instance.
+ def __init__(self, *args, **kwargs):
+ super(Photo, self).__init__(*args, **kwargs)
+ self._savecount = 0
+
+ def save(self):
+ super(Photo, self).save()
+ self._savecount +=1 \ No newline at end of file
diff --git a/tests/regressiontests/bug639/test.jpg b/tests/regressiontests/bug639/test.jpg
new file mode 100644
index 0000000000..391b57a0f3
--- /dev/null
+++ b/tests/regressiontests/bug639/test.jpg
Binary files differ
diff --git a/tests/regressiontests/bug639/tests.py b/tests/regressiontests/bug639/tests.py
new file mode 100644
index 0000000000..f9596d06cb
--- /dev/null
+++ b/tests/regressiontests/bug639/tests.py
@@ -0,0 +1,42 @@
+"""
+Tests for file field behavior, and specifically #639, in which Model.save() gets
+called *again* for each FileField. This test will fail if calling an
+auto-manipulator's save() method causes Model.save() to be called more than once.
+"""
+
+import os
+import unittest
+from regressiontests.bug639.models import Photo
+from django.http import QueryDict
+from django.utils.datastructures import MultiValueDict
+
+class Bug639Test(unittest.TestCase):
+
+ def testBug639(self):
+ """
+ Simulate a file upload and check how many times Model.save() gets called.
+ """
+ # Grab an image for testing
+ img = open(os.path.join(os.path.dirname(__file__), "test.jpg"), "rb").read()
+
+ # Fake a request query dict with the file
+ qd = QueryDict("title=Testing&image=", mutable=True)
+ qd["image_file"] = {
+ "filename" : "test.jpg",
+ "content-type" : "image/jpeg",
+ "content" : img
+ }
+
+ manip = Photo.AddManipulator()
+ manip.do_html2python(qd)
+ p = manip.save(qd)
+
+ # Check the savecount stored on the object (see the model)
+ self.assertEqual(p._savecount, 1)
+
+ def tearDown(self):
+ """
+ Make sure to delete the "uploaded" file to avoid clogging /tmp.
+ """
+ p = Photo.objects.get()
+ os.unlink(p.get_image_filename()) \ No newline at end of file
diff --git a/tests/regressiontests/datastructures/__init__.py b/tests/regressiontests/datastructures/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/datastructures/__init__.py
diff --git a/tests/regressiontests/datastructures/models.py b/tests/regressiontests/datastructures/models.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/datastructures/models.py
diff --git a/tests/regressiontests/datastructures/tests.py b/tests/regressiontests/datastructures/tests.py
new file mode 100644
index 0000000000..e008c255f1
--- /dev/null
+++ b/tests/regressiontests/datastructures/tests.py
@@ -0,0 +1,65 @@
+"""
+# Tests for stuff in django.utils.datastructures.
+
+>>> from django.utils.datastructures import *
+
+### MergeDict #################################################################
+
+>>> d1 = {'chris':'cool','camri':'cute','cotton':'adorable','tulip':'snuggable', 'twoofme':'firstone'}
+>>> d2 = {'chris2':'cool2','camri2':'cute2','cotton2':'adorable2','tulip2':'snuggable2'}
+>>> d3 = {'chris3':'cool3','camri3':'cute3','cotton3':'adorable3','tulip3':'snuggable3'}
+>>> d4 = {'twoofme':'secondone'}
+>>> md = MergeDict( d1,d2,d3 )
+>>> md['chris']
+'cool'
+>>> md['camri']
+'cute'
+>>> md['twoofme']
+'firstone'
+>>> md2 = md.copy()
+>>> md2['chris']
+'cool'
+
+### MultiValueDict ##########################################################
+
+>>> d = MultiValueDict({'name': ['Adrian', 'Simon'], 'position': ['Developer']})
+>>> d['name']
+'Simon'
+>>> d.getlist('name')
+['Adrian', 'Simon']
+>>> d.get('lastname', 'nonexistent')
+'nonexistent'
+>>> d.setlist('lastname', ['Holovaty', 'Willison'])
+
+### SortedDict #################################################################
+
+>>> d = SortedDict()
+>>> d['one'] = 'one'
+>>> d['two'] = 'two'
+>>> d['three'] = 'three'
+>>> d['one']
+'one'
+>>> d['two']
+'two'
+>>> d['three']
+'three'
+>>> d.keys()
+['one', 'two', 'three']
+>>> d.values()
+['one', 'two', 'three']
+>>> d['one'] = 'not one'
+>>> d['one']
+'not one'
+>>> d.keys() == d.copy().keys()
+True
+
+### DotExpandedDict ############################################################
+
+>>> d = DotExpandedDict({'person.1.firstname': ['Simon'], 'person.1.lastname': ['Willison'], 'person.2.firstname': ['Adrian'], 'person.2.lastname': ['Holovaty']})
+>>> d['person']['1']['lastname']
+['Willison']
+>>> d['person']['2']['lastname']
+['Holovaty']
+>>> d['person']['2']['firstname']
+['Adrian']
+"""
diff --git a/tests/regressiontests/dateformat/tests.py b/tests/regressiontests/dateformat/tests.py
index 6e28759a92..f9f84145c5 100644
--- a/tests/regressiontests/dateformat/tests.py
+++ b/tests/regressiontests/dateformat/tests.py
@@ -17,6 +17,8 @@ r"""
'07'
>>> format(my_birthday, 'M')
'Jul'
+>>> format(my_birthday, 'b')
+'jul'
>>> format(my_birthday, 'n')
'7'
>>> format(my_birthday, 'N')
diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py
index 439a40c31b..c850806052 100644
--- a/tests/regressiontests/defaultfilters/tests.py
+++ b/tests/regressiontests/defaultfilters/tests.py
@@ -87,6 +87,20 @@ u'\xeb'
>>> truncatewords('A sentence with a few words in it', 'not a number')
'A sentence with a few words in it'
+>>> truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>', 0)
+''
+
+>>> truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>', 2)
+'<p>one <a href="#">two ...</a></p>'
+
+>>> truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>', 4)
+'<p>one <a href="#">two - three <br>four ...</a></p>'
+
+>>> truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>', 5)
+'<p>one <a href="#">two - three <br>four</a> five</p>'
+
+>>> truncatewords_html('<p>one <a href="#">two - three <br>four</a> five</p>', 100)
+'<p>one <a href="#">two - three <br>four</a> five</p>'
>>> upper('Mixed case input')
'MIXED CASE INPUT'
@@ -97,6 +111,8 @@ u'\xcb'
>>> urlencode('jack & jill')
'jack%20%26%20jill'
+>>> urlencode(1)
+'1'
>>> urlizetrunc('http://short.com/', 20)
@@ -372,7 +388,53 @@ False
>>> phone2numeric('0800 flowers')
'0800 3569377'
-
+# Filters shouldn't break if passed non-strings
+>>> addslashes(123)
+'123'
+>>> linenumbers(123)
+'1. 123'
+>>> lower(123)
+'123'
+>>> make_list(123)
+['1', '2', '3']
+>>> slugify(123)
+'123'
+>>> title(123)
+'123'
+>>> truncatewords(123, 2)
+'123'
+>>> upper(123)
+'123'
+>>> urlencode(123)
+'123'
+>>> urlize(123)
+'123'
+>>> urlizetrunc(123, 1)
+'123'
+>>> wordcount(123)
+1
+>>> wordwrap(123, 2)
+'123'
+>>> ljust('123', 4)
+'123 '
+>>> rjust('123', 4)
+' 123'
+>>> center('123', 5)
+' 123 '
+>>> center('123', 6)
+' 123 '
+>>> cut(123, '2')
+'13'
+>>> escape(123)
+'123'
+>>> linebreaks(123)
+'<p>123</p>'
+>>> linebreaksbr(123)
+'123'
+>>> removetags(123, 'a')
+'123'
+>>> striptags(123)
+'123'
"""
diff --git a/tests/regressiontests/dispatch/__init__.py b/tests/regressiontests/dispatch/__init__.py
new file mode 100644
index 0000000000..679895bb5c
--- /dev/null
+++ b/tests/regressiontests/dispatch/__init__.py
@@ -0,0 +1,2 @@
+"""Unit-tests for the dispatch project
+"""
diff --git a/tests/regressiontests/dispatch/models.py b/tests/regressiontests/dispatch/models.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/dispatch/models.py
diff --git a/tests/regressiontests/dispatch/tests/__init__.py b/tests/regressiontests/dispatch/tests/__init__.py
new file mode 100644
index 0000000000..0fdefe48a7
--- /dev/null
+++ b/tests/regressiontests/dispatch/tests/__init__.py
@@ -0,0 +1,7 @@
+"""
+Unit-tests for the dispatch project
+"""
+
+from test_dispatcher import *
+from test_robustapply import *
+from test_saferef import *
diff --git a/tests/regressiontests/dispatch/tests/test_dispatcher.py b/tests/regressiontests/dispatch/tests/test_dispatcher.py
new file mode 100644
index 0000000000..0bc99a1505
--- /dev/null
+++ b/tests/regressiontests/dispatch/tests/test_dispatcher.py
@@ -0,0 +1,144 @@
+from django.dispatch.dispatcher import *
+from django.dispatch import dispatcher, robust
+import unittest
+import copy
+
+def x(a):
+ return a
+
+class Dummy(object):
+ pass
+
+class Callable(object):
+ def __call__(self, a):
+ return a
+
+ def a(self, a):
+ return a
+
+class DispatcherTests(unittest.TestCase):
+ """Test suite for dispatcher (barely started)"""
+
+ def setUp(self):
+ # track the initial state, since it's possible that others have bleed receivers in
+ self.sendersBack = copy.copy(dispatcher.sendersBack)
+ self.connections = copy.copy(dispatcher.connections)
+ self.senders = copy.copy(dispatcher.senders)
+
+ def _testIsClean(self):
+ """Assert that everything has been cleaned up automatically"""
+ self.assertEqual(dispatcher.sendersBack, self.sendersBack)
+ self.assertEqual(dispatcher.connections, self.connections)
+ self.assertEqual(dispatcher.senders, self.senders)
+
+ def testExact(self):
+ a = Dummy()
+ signal = 'this'
+ connect(x, signal, a)
+ expected = [(x,a)]
+ result = send('this',a, a=a)
+ self.assertEqual(result, expected)
+ disconnect(x, signal, a)
+ self.assertEqual(list(getAllReceivers(a,signal)), [])
+ self._testIsClean()
+
+ def testAnonymousSend(self):
+ a = Dummy()
+ signal = 'this'
+ connect(x, signal)
+ expected = [(x,a)]
+ result = send(signal,None, a=a)
+ self.assertEqual(result, expected)
+ disconnect(x, signal)
+ self.assertEqual(list(getAllReceivers(None,signal)), [])
+ self._testIsClean()
+
+ def testAnyRegistration(self):
+ a = Dummy()
+ signal = 'this'
+ connect(x, signal, Any)
+ expected = [(x,a)]
+ result = send('this',object(), a=a)
+ self.assertEqual(result, expected)
+ disconnect(x, signal, Any)
+ expected = []
+ result = send('this',object(), a=a)
+ self.assertEqual(result, expected)
+ self.assertEqual(list(getAllReceivers(Any,signal)), [])
+
+ self._testIsClean()
+
+ def testAnyRegistration2(self):
+ a = Dummy()
+ signal = 'this'
+ connect(x, Any, a)
+ expected = [(x,a)]
+ result = send('this',a, a=a)
+ self.assertEqual(result, expected)
+ disconnect(x, Any, a)
+ self.assertEqual(list(getAllReceivers(a,Any)), [])
+ self._testIsClean()
+
+ def testGarbageCollected(self):
+ a = Callable()
+ b = Dummy()
+ signal = 'this'
+ connect(a.a, signal, b)
+ expected = []
+ del a
+ result = send('this',b, a=b)
+ self.assertEqual(result, expected)
+ self.assertEqual(list(getAllReceivers(b,signal)), [])
+ self._testIsClean()
+
+ def testGarbageCollectedObj(self):
+ class x:
+ def __call__(self, a):
+ return a
+ a = Callable()
+ b = Dummy()
+ signal = 'this'
+ connect(a, signal, b)
+ expected = []
+ del a
+ result = send('this',b, a=b)
+ self.assertEqual(result, expected)
+ self.assertEqual(list(getAllReceivers(b,signal)), [])
+ self._testIsClean()
+
+
+ def testMultipleRegistration(self):
+ a = Callable()
+ b = Dummy()
+ signal = 'this'
+ connect(a, signal, b)
+ connect(a, signal, b)
+ connect(a, signal, b)
+ connect(a, signal, b)
+ connect(a, signal, b)
+ connect(a, signal, b)
+ result = send('this',b, a=b)
+ self.assertEqual(len(result), 1)
+ self.assertEqual(len(list(getAllReceivers(b,signal))), 1)
+ del a
+ del b
+ del result
+ self._testIsClean()
+
+ def testRobust(self):
+ """Test the sendRobust function"""
+ def fails():
+ raise ValueError('this')
+ a = object()
+ signal = 'this'
+ connect(fails, Any, a)
+ result = robust.sendRobust('this',a, a=a)
+ err = result[0][1]
+ self.assert_(isinstance(err, ValueError))
+ self.assertEqual(err.args, ('this',))
+
+def getSuite():
+ return unittest.makeSuite(DispatcherTests,'test')
+
+if __name__ == "__main__":
+ unittest.main ()
diff --git a/tests/regressiontests/dispatch/tests/test_robustapply.py b/tests/regressiontests/dispatch/tests/test_robustapply.py
new file mode 100644
index 0000000000..499450eec4
--- /dev/null
+++ b/tests/regressiontests/dispatch/tests/test_robustapply.py
@@ -0,0 +1,34 @@
+from django.dispatch.robustapply import *
+
+import unittest
+
+def noArgument():
+ pass
+
+def oneArgument(blah):
+ pass
+
+def twoArgument(blah, other):
+ pass
+
+class TestCases(unittest.TestCase):
+ def test01(self):
+ robustApply(noArgument)
+
+ def test02(self):
+ self.assertRaises(TypeError, robustApply, noArgument, "this")
+
+ def test03(self):
+ self.assertRaises(TypeError, robustApply, oneArgument)
+
+ def test04(self):
+ """Raise error on duplication of a particular argument"""
+ self.assertRaises(TypeError, robustApply, oneArgument, "this", blah = "that")
+
+def getSuite():
+ return unittest.makeSuite(TestCases,'test')
+
+
+if __name__ == "__main__":
+ unittest.main()
+
diff --git a/tests/regressiontests/dispatch/tests/test_saferef.py b/tests/regressiontests/dispatch/tests/test_saferef.py
new file mode 100644
index 0000000000..233a2023e0
--- /dev/null
+++ b/tests/regressiontests/dispatch/tests/test_saferef.py
@@ -0,0 +1,77 @@
+from django.dispatch.saferef import *
+
+import unittest
+
+class Test1(object):
+ def x(self):
+ pass
+
+def test2(obj):
+ pass
+
+class Test2(object):
+ def __call__(self, obj):
+ pass
+
+class Tester(unittest.TestCase):
+ def setUp(self):
+ ts = []
+ ss = []
+ for x in xrange(5000):
+ t = Test1()
+ ts.append(t)
+ s = safeRef(t.x, self._closure)
+ ss.append(s)
+ ts.append(test2)
+ ss.append(safeRef(test2, self._closure))
+ for x in xrange(30):
+ t = Test2()
+ ts.append(t)
+ s = safeRef(t, self._closure)
+ ss.append(s)
+ self.ts = ts
+ self.ss = ss
+ self.closureCount = 0
+
+ def tearDown(self):
+ del self.ts
+ del self.ss
+
+ def testIn(self):
+ """Test the "in" operator for safe references (cmp)"""
+ for t in self.ts[:50]:
+ self.assert_(safeRef(t.x) in self.ss)
+
+ def testValid(self):
+ """Test that the references are valid (return instance methods)"""
+ for s in self.ss:
+ self.assert_(s())
+
+ def testShortCircuit (self):
+ """Test that creation short-circuits to reuse existing references"""
+ sd = {}
+ for s in self.ss:
+ sd[s] = 1
+ for t in self.ts:
+ if hasattr(t, 'x'):
+ self.assert_(sd.has_key(safeRef(t.x)))
+ else:
+ self.assert_(sd.has_key(safeRef(t)))
+
+ def testRepresentation (self):
+ """Test that the reference object's representation works
+
+ XXX Doesn't currently check the results, just that no error
+ is raised
+ """
+ repr(self.ss[-1])
+
+ def _closure(self, ref):
+ """Dumb utility mechanism to increment deletion counter"""
+ self.closureCount +=1
+
+def getSuite():
+ return unittest.makeSuite(Tester,'test')
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
index 20a1937f56..a9ce8d23b3 100644
--- a/tests/regressiontests/forms/tests.py
+++ b/tests/regressiontests/forms/tests.py
@@ -72,6 +72,22 @@ u'<input type="password" class="special" name="email" />'
>>> w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'})
u'<input type="password" class="fun" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" name="email" />'
+The render_value argument lets you specify whether the widget should render
+its value. You may want to do this for security reasons.
+>>> w = PasswordInput(render_value=True)
+>>> w.render('email', 'secret')
+u'<input type="password" name="email" value="secret" />'
+>>> w = PasswordInput(render_value=False)
+>>> w.render('email', '')
+u'<input type="password" name="email" />'
+>>> w.render('email', None)
+u'<input type="password" name="email" />'
+>>> w.render('email', 'secret')
+u'<input type="password" name="email" />'
+>>> w = PasswordInput(attrs={'class': 'fun'}, render_value=False)
+>>> w.render('email', 'secret')
+u'<input type="password" class="fun" name="email" />'
+
# HiddenInput Widget ############################################################
>>> w = HiddenInput()
@@ -302,6 +318,7 @@ The value is compared to its str():
</select>
The 'choices' argument can be any iterable:
+>>> from itertools import chain
>>> def get_choices():
... for i in range(5):
... yield (i, i)
@@ -313,6 +330,17 @@ The 'choices' argument can be any iterable:
<option value="3">3</option>
<option value="4">4</option>
</select>
+>>> things = ({'id': 1, 'name': 'And Boom'}, {'id': 2, 'name': 'One More Thing!'})
+>>> class SomeForm(Form):
+... somechoice = ChoiceField(choices=chain((('', '-'*9),), [(thing['id'], thing['name']) for thing in things]))
+>>> f = SomeForm()
+>>> f.as_table()
+u'<tr><th><label for="id_somechoice">Somechoice:</label></th><td><select name="somechoice" id="id_somechoice">\n<option value="" selected="selected">---------</option>\n<option value="1">And Boom</option>\n<option value="2">One More Thing!</option>\n</select></td></tr>'
+>>> f.as_table()
+u'<tr><th><label for="id_somechoice">Somechoice:</label></th><td><select name="somechoice" id="id_somechoice">\n<option value="" selected="selected">---------</option>\n<option value="1">And Boom</option>\n<option value="2">One More Thing!</option>\n</select></td></tr>'
+>>> f = SomeForm({'somechoice': 2})
+>>> f.as_table()
+u'<tr><th><label for="id_somechoice">Somechoice:</label></th><td><select name="somechoice" id="id_somechoice">\n<option value="">---------</option>\n<option value="1">And Boom</option>\n<option value="2" selected="selected">One More Thing!</option>\n</select></td></tr>'
You can also pass 'choices' to the constructor:
>>> w = Select(choices=[(1, 1), (2, 2), (3, 3)])
@@ -1493,7 +1521,7 @@ u'1'
>>> f.clean('3')
Traceback (most recent call last):
...
-ValidationError: [u'Select a valid choice. 3 is not one of the available choices.']
+ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
>>> f = ChoiceField(choices=[('1', '1'), ('2', '2')], required=False)
>>> f.clean('')
@@ -1507,7 +1535,7 @@ u'1'
>>> f.clean('3')
Traceback (most recent call last):
...
-ValidationError: [u'Select a valid choice. 3 is not one of the available choices.']
+ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
>>> f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')])
>>> f.clean('J')
@@ -1515,7 +1543,7 @@ u'J'
>>> f.clean('John')
Traceback (most recent call last):
...
-ValidationError: [u'Select a valid choice. John is not one of the available choices.']
+ValidationError: [u'Select a valid choice. That choice is not one of the available choices.']
# NullBooleanField ############################################################
@@ -1983,6 +2011,19 @@ For a form with a <select>, use ChoiceField:
<option value="J">Java</option>
</select>
+A subtlety: If one of the choices' value is the empty string and the form is
+unbound, then the <option> for the empty-string choice will get selected="selected".
+>>> class FrameworkForm(Form):
+... name = CharField()
+... language = ChoiceField(choices=[('', '------'), ('P', 'Python'), ('J', 'Java')])
+>>> f = FrameworkForm(auto_id=False)
+>>> print f['language']
+<select name="language">
+<option value="" selected="selected">------</option>
+<option value="P">Python</option>
+<option value="J">Java</option>
+</select>
+
You can specify widget attributes in the Widget constructor.
>>> class FrameworkForm(Form):
... name = CharField()
@@ -2201,6 +2242,19 @@ returns a list of input.
>>> f.clean_data
{'composers': [u'J', u'P'], 'name': u'Yesterday'}
+Validation errors are HTML-escaped when output as HTML.
+>>> class EscapingForm(Form):
+... special_name = CharField()
+... def clean_special_name(self):
+... raise ValidationError("Something's wrong with '%s'" % self.clean_data['special_name'])
+
+>>> f = EscapingForm({'special_name': "Nothing to escape"}, auto_id=False)
+>>> print f
+<tr><th>Special name:</th><td><ul class="errorlist"><li>Something&#39;s wrong with &#39;Nothing to escape&#39;</li></ul><input type="text" name="special_name" value="Nothing to escape" /></td></tr>
+>>> f = EscapingForm({'special_name': "Should escape < & > and <script>alert('xss')</script>"}, auto_id=False)
+>>> print f
+<tr><th>Special name:</th><td><ul class="errorlist"><li>Something&#39;s wrong with &#39;Should escape &lt; &amp; &gt; and &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;&#39;</li></ul><input type="text" name="special_name" value="Should escape &lt; &amp; &gt; and &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;" /></td></tr>
+
# Validating multiple fields in relation to another ###########################
There are a couple of ways to do multiple-field validation. If you want the
@@ -2334,6 +2388,43 @@ the next.
<tr><th>Field3:</th><td><input type="text" name="field3" /></td></tr>
<tr><th>Field4:</th><td><input type="text" name="field4" /></td></tr>
+Similarly, changes to field attributes do not persist from one Form instance
+to the next.
+>>> class Person(Form):
+... first_name = CharField(required=False)
+... last_name = CharField(required=False)
+... def __init__(self, names_required=False, *args, **kwargs):
+... super(Person, self).__init__(*args, **kwargs)
+... if names_required:
+... self.fields['first_name'].required = True
+... self.fields['last_name'].required = True
+>>> f = Person(names_required=False)
+>>> f['first_name'].field.required, f['last_name'].field.required
+(False, False)
+>>> f = Person(names_required=True)
+>>> f['first_name'].field.required, f['last_name'].field.required
+(True, True)
+>>> f = Person(names_required=False)
+>>> f['first_name'].field.required, f['last_name'].field.required
+(False, False)
+>>> class Person(Form):
+... first_name = CharField(max_length=30)
+... last_name = CharField(max_length=30)
+... def __init__(self, name_max_length=None, *args, **kwargs):
+... super(Person, self).__init__(*args, **kwargs)
+... if name_max_length:
+... self.fields['first_name'].max_length = name_max_length
+... self.fields['last_name'].max_length = name_max_length
+>>> f = Person(name_max_length=None)
+>>> f['first_name'].field.max_length, f['last_name'].field.max_length
+(30, 30)
+>>> f = Person(name_max_length=20)
+>>> f['first_name'].field.max_length, f['last_name'].field.max_length
+(20, 20)
+>>> f = Person(name_max_length=None)
+>>> f['first_name'].field.max_length, f['last_name'].field.max_length
+(30, 30)
+
HiddenInput widgets are displayed differently in the as_table(), as_ul()
and as_p() output of a Form -- their verbose names are not displayed, and a
separate row is not displayed. They're displayed in the last row of the
@@ -2645,6 +2736,54 @@ purposes, though.
<li>Username: <input type="text" name="username" maxlength="10" /> e.g., user@example.com</li>
<li>Password: <input type="password" name="password" /><input type="hidden" name="next" value="/" /></li>
+Help text can include arbitrary Unicode characters.
+>>> class UserRegistration(Form):
+... username = CharField(max_length=10, help_text='ŠĐĆŽćžšđ')
+>>> p = UserRegistration(auto_id=False)
+>>> p.as_ul()
+u'<li>Username: <input type="text" name="username" maxlength="10" /> \u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111</li>'
+
+# Subclassing forms ###########################################################
+
+You can subclass a Form to add fields. The resulting form subclass will have
+all of the fields of the parent Form, plus whichever fields you define in the
+subclass.
+>>> class Person(Form):
+... first_name = CharField()
+... last_name = CharField()
+... birthday = DateField()
+>>> class Musician(Person):
+... instrument = CharField()
+>>> p = Person(auto_id=False)
+>>> print p.as_ul()
+<li>First name: <input type="text" name="first_name" /></li>
+<li>Last name: <input type="text" name="last_name" /></li>
+<li>Birthday: <input type="text" name="birthday" /></li>
+>>> m = Musician(auto_id=False)
+>>> print m.as_ul()
+<li>First name: <input type="text" name="first_name" /></li>
+<li>Last name: <input type="text" name="last_name" /></li>
+<li>Birthday: <input type="text" name="birthday" /></li>
+<li>Instrument: <input type="text" name="instrument" /></li>
+
+Yes, you can subclass multiple forms. The fields are added in the order in
+which the parent classes are listed.
+>>> class Person(Form):
+... first_name = CharField()
+... last_name = CharField()
+... birthday = DateField()
+>>> class Instrument(Form):
+... instrument = CharField()
+>>> class Beatle(Person, Instrument):
+... haircut_type = CharField()
+>>> b = Beatle(auto_id=False)
+>>> print b.as_ul()
+<li>First name: <input type="text" name="first_name" /></li>
+<li>Last name: <input type="text" name="last_name" /></li>
+<li>Birthday: <input type="text" name="birthday" /></li>
+<li>Instrument: <input type="text" name="instrument" /></li>
+<li>Haircut type: <input type="text" name="haircut_type" /></li>
+
# Forms with prefixes #########################################################
Sometimes it's necessary to have multiple forms display on the same HTML page,
@@ -2858,7 +2997,7 @@ VALID: {'username': u'adrian', 'password1': u'secret', 'password2': u'secret'}
# Some ideas for using templates with forms ###################################
>>> class UserRegistration(Form):
-... username = CharField(max_length=10)
+... username = CharField(max_length=10, help_text="Good luck picking a username that doesn't already exist.")
... password1 = CharField(widget=PasswordInput)
... password2 = CharField(widget=PasswordInput)
... def clean(self):
@@ -2935,6 +3074,24 @@ field an "id" attribute.
<input type="submit" />
</form>
+User form.[field].help_text to output a field's help text. If the given field
+does not have help text, nothing will be output.
+>>> t = Template('''<form action="">
+... <p>{{ form.username.label_tag }}: {{ form.username }}<br />{{ form.username.help_text }}</p>
+... <p>{{ form.password1.label_tag }}: {{ form.password1 }}</p>
+... <p>{{ form.password2.label_tag }}: {{ form.password2 }}</p>
+... <input type="submit" />
+... </form>''')
+>>> print t.render(Context({'form': UserRegistration(auto_id=False)}))
+<form action="">
+<p>Username: <input type="text" name="username" maxlength="10" /><br />Good luck picking a username that doesn't already exist.</p>
+<p>Password1: <input type="password" name="password1" /></p>
+<p>Password2: <input type="password" name="password2" /></p>
+<input type="submit" />
+</form>
+>>> Template('{{ form.password1.help_text }}').render(Context({'form': UserRegistration(auto_id=False)}))
+''
+
The label_tag() method takes an optional attrs argument: a dictionary of HTML
attributes to add to the <label> tag.
>>> f = UserRegistration(auto_id='id_%s')
@@ -2977,12 +3134,12 @@ the list of errors is empty). You can also use it in {% if %} statements.
<input type="submit" />
</form>
-#################
-# Extra widgets #
-#################
+###############
+# Extra stuff #
+###############
-The newforms library comes with some extra, higher-level Widget classes that
-demonstrate some of the library's abilities.
+The newforms library comes with some extra, higher-level Field and Widget
+classes that demonstrate some of the library's abilities.
# SelectDateWidget ############################################################
@@ -3111,6 +3268,316 @@ True
<option value="2016">2016</option>
</select>
+# USZipCodeField ##############################################################
+
+USZipCodeField validates that the data is either a five-digit U.S. zip code or
+a zip+4.
+>>> from django.contrib.localflavor.usa.forms import USZipCodeField
+>>> f = USZipCodeField()
+>>> f.clean('60606')
+u'60606'
+>>> f.clean(60606)
+u'60606'
+>>> f.clean('04000')
+u'04000'
+>>> f.clean('4000')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a zip code in the format XXXXX or XXXXX-XXXX.']
+>>> f.clean('60606-1234')
+u'60606-1234'
+>>> f.clean('6060-1234')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a zip code in the format XXXXX or XXXXX-XXXX.']
+>>> f.clean('60606-')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a zip code in the format XXXXX or XXXXX-XXXX.']
+>>> 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 = USZipCodeField(required=False)
+>>> f.clean('60606')
+u'60606'
+>>> f.clean(60606)
+u'60606'
+>>> f.clean('04000')
+u'04000'
+>>> f.clean('4000')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a zip code in the format XXXXX or XXXXX-XXXX.']
+>>> f.clean('60606-1234')
+u'60606-1234'
+>>> f.clean('6060-1234')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a zip code in the format XXXXX or XXXXX-XXXX.']
+>>> f.clean('60606-')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a zip code in the format XXXXX or XXXXX-XXXX.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# USPhoneNumberField ##########################################################
+
+USPhoneNumberField validates that the data is a valid U.S. phone number,
+including the area code. It's normalized to XXX-XXX-XXXX format.
+>>> from django.contrib.localflavor.usa.forms import USPhoneNumberField
+>>> f = USPhoneNumberField()
+>>> f.clean('312-555-1212')
+u'312-555-1212'
+>>> f.clean('3125551212')
+u'312-555-1212'
+>>> f.clean('312 555-1212')
+u'312-555-1212'
+>>> f.clean('(312) 555-1212')
+u'312-555-1212'
+>>> f.clean('312 555 1212')
+u'312-555-1212'
+>>> f.clean('312.555.1212')
+u'312-555-1212'
+>>> f.clean('312.555-1212')
+u'312-555-1212'
+>>> f.clean(' (312) 555.1212 ')
+u'312-555-1212'
+>>> f.clean('555-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.']
+>>> f.clean('312-55-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.']
+>>> 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 = USPhoneNumberField(required=False)
+>>> f.clean('312-555-1212')
+u'312-555-1212'
+>>> f.clean('3125551212')
+u'312-555-1212'
+>>> f.clean('312 555-1212')
+u'312-555-1212'
+>>> f.clean('(312) 555-1212')
+u'312-555-1212'
+>>> f.clean('312 555 1212')
+u'312-555-1212'
+>>> f.clean('312.555.1212')
+u'312-555-1212'
+>>> f.clean('312.555-1212')
+u'312-555-1212'
+>>> f.clean(' (312) 555.1212 ')
+u'312-555-1212'
+>>> f.clean('555-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.']
+>>> f.clean('312-55-1212')
+Traceback (most recent call last):
+...
+ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# USStateField ################################################################
+
+USStateField validates that the data is either an abbreviation or name of a
+U.S. state.
+>>> from django.contrib.localflavor.usa.forms import USStateField
+>>> f = USStateField()
+>>> f.clean('il')
+u'IL'
+>>> f.clean('IL')
+u'IL'
+>>> f.clean('illinois')
+u'IL'
+>>> f.clean(' illinois ')
+u'IL'
+>>> f.clean(60606)
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a U.S. state or territory.']
+>>> 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 = USStateField(required=False)
+>>> f.clean('il')
+u'IL'
+>>> f.clean('IL')
+u'IL'
+>>> f.clean('illinois')
+u'IL'
+>>> f.clean(' illinois ')
+u'IL'
+>>> f.clean(60606)
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a U.S. state or territory.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# USStateSelect ###############################################################
+
+USStateSelect is a Select widget that uses a list of U.S. states/territories
+as its choices.
+>>> from django.contrib.localflavor.usa.forms import USStateSelect
+>>> w = USStateSelect()
+>>> print w.render('state', 'IL')
+<select name="state">
+<option value="AL">Alabama</option>
+<option value="AK">Alaska</option>
+<option value="AS">American Samoa</option>
+<option value="AZ">Arizona</option>
+<option value="AR">Arkansas</option>
+<option value="CA">California</option>
+<option value="CO">Colorado</option>
+<option value="CT">Connecticut</option>
+<option value="DE">Deleware</option>
+<option value="DC">District of Columbia</option>
+<option value="FM">Federated States of Micronesia</option>
+<option value="FL">Florida</option>
+<option value="GA">Georgia</option>
+<option value="GU">Guam</option>
+<option value="HI">Hawaii</option>
+<option value="ID">Idaho</option>
+<option value="IL" selected="selected">Illinois</option>
+<option value="IN">Indiana</option>
+<option value="IA">Iowa</option>
+<option value="KS">Kansas</option>
+<option value="KY">Kentucky</option>
+<option value="LA">Louisiana</option>
+<option value="ME">Maine</option>
+<option value="MH">Marshall Islands</option>
+<option value="MD">Maryland</option>
+<option value="MA">Massachusetts</option>
+<option value="MI">Michigan</option>
+<option value="MN">Minnesota</option>
+<option value="MS">Mississippi</option>
+<option value="MO">Missouri</option>
+<option value="MT">Montana</option>
+<option value="NE">Nebraska</option>
+<option value="NV">Nevada</option>
+<option value="NH">New Hampshire</option>
+<option value="NJ">New Jersey</option>
+<option value="NM">New Mexico</option>
+<option value="NY">New York</option>
+<option value="NC">North Carolina</option>
+<option value="ND">North Dakota</option>
+<option value="MP">Northern Mariana Islands</option>
+<option value="OH">Ohio</option>
+<option value="OK">Oklahoma</option>
+<option value="OR">Oregon</option>
+<option value="PW">Palau</option>
+<option value="PA">Pennsylvania</option>
+<option value="PR">Puerto Rico</option>
+<option value="RI">Rhode Island</option>
+<option value="SC">South Carolina</option>
+<option value="SD">South Dakota</option>
+<option value="TN">Tennessee</option>
+<option value="TX">Texas</option>
+<option value="UT">Utah</option>
+<option value="VT">Vermont</option>
+<option value="VI">Virgin Islands</option>
+<option value="VA">Virginia</option>
+<option value="WA">Washington</option>
+<option value="WV">West Virginia</option>
+<option value="WI">Wisconsin</option>
+<option value="WY">Wyoming</option>
+</select>
+
+# UKPostcodeField #############################################################
+
+UKPostcodeField validates that the data is a valid UK postcode.
+>>> from django.contrib.localflavor.uk.forms import UKPostcodeField
+>>> f = UKPostcodeField()
+>>> f.clean('BT32 4PX')
+u'BT32 4PX'
+>>> f.clean('GIR 0AA')
+u'GIR 0AA'
+>>> f.clean('BT324PX')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postcode. A space is required between the two postcode parts.']
+>>> f.clean('1NV 4L1D')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postcode. A space is required between the two postcode parts.']
+>>> 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 = UKPostcodeField(required=False)
+>>> f.clean('BT32 4PX')
+u'BT32 4PX'
+>>> f.clean('GIR 0AA')
+u'GIR 0AA'
+>>> f.clean('1NV 4L1D')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postcode. A space is required between the two postcode parts.']
+>>> f.clean('BT324PX')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postcode. A space is required between the two postcode parts.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+#################################
+# Tests of underlying functions #
+#################################
+
+# smart_unicode tests
+>>> from django.newforms.util import smart_unicode
+>>> class Test:
+... def __str__(self):
+... return 'ŠĐĆŽćžšđ'
+>>> class TestU:
+... def __str__(self):
+... return 'Foo'
+... def __unicode__(self):
+... return u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111'
+>>> smart_unicode(Test())
+u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111'
+>>> smart_unicode(TestU())
+u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111'
+>>> smart_unicode(1)
+u'1'
+>>> smart_unicode('foo')
+u'foo'
"""
if __name__ == "__main__":
diff --git a/tests/regressiontests/humanize/__init__.py b/tests/regressiontests/humanize/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/humanize/__init__.py
diff --git a/tests/regressiontests/humanize/models.py b/tests/regressiontests/humanize/models.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/humanize/models.py
diff --git a/tests/regressiontests/humanize/tests.py b/tests/regressiontests/humanize/tests.py
new file mode 100644
index 0000000000..e342d7ded8
--- /dev/null
+++ b/tests/regressiontests/humanize/tests.py
@@ -0,0 +1,52 @@
+import unittest
+from django.template import Template, Context, add_to_builtins
+
+add_to_builtins('django.contrib.humanize.templatetags.humanize')
+
+class HumanizeTests(unittest.TestCase):
+
+ def humanize_tester(self, test_list, result_list, method):
+ # Using max below ensures we go through both lists
+ # However, if the lists are not equal length, this raises an exception
+ for index in xrange(len(max(test_list,result_list))):
+ test_content = test_list[index]
+ t = Template('{{ test_content|%s }}' % method)
+ rendered = t.render(Context(locals())).strip()
+ self.assertEqual(rendered, result_list[index],
+ msg="""%s test failed, produced %s,
+should've produced %s""" % (method, rendered, result_list[index]))
+
+ def test_ordinal(self):
+ test_list = ('1','2','3','4','11','12',
+ '13','101','102','103','111',
+ 'something else')
+ result_list = ('1st', '2nd', '3rd', '4th', '11th',
+ '12th', '13th', '101st', '102nd', '103rd',
+ '111th', 'something else')
+
+ self.humanize_tester(test_list, result_list, 'ordinal')
+
+ def test_intcomma(self):
+ test_list = ('100','1000','10123','10311','1000000')
+ result_list = ('100', '1,000', '10,123', '10,311', '1,000,000')
+
+ self.humanize_tester(test_list, result_list, 'intcomma')
+
+ def test_intword(self):
+ test_list = ('100', '1000000', '1200000', '1290000',
+ '1000000000','2000000000','6000000000000')
+ result_list = ('100', '1.0 million', '1.2 million', '1.3 million',
+ '1.0 billion', '2.0 billion', '6.0 trillion')
+
+ self.humanize_tester(test_list, result_list, 'intword')
+
+ def test_apnumber(self):
+ test_list = [str(x) for x in xrange(1,11)]
+ result_list = ('one', 'two', 'three', 'four', 'five', 'six',
+ 'seven', 'eight', 'nine', '10')
+
+ self.humanize_tester(test_list, result_list, 'apnumber')
+
+if __name__ == '__main__':
+ unittest.main()
+
diff --git a/tests/regressiontests/many_to_one_regress/models.py b/tests/regressiontests/many_to_one_regress/models.py
index 6c067446b1..8ddec98da4 100644
--- a/tests/regressiontests/many_to_one_regress/models.py
+++ b/tests/regressiontests/many_to_one_regress/models.py
@@ -1,13 +1,34 @@
from django.db import models
+# If ticket #1578 ever slips back in, these models will not be able to be
+# created (the field names being lower-cased versions of their opposite
+# classes is important here).
+
class First(models.Model):
second = models.IntegerField()
class Second(models.Model):
first = models.ForeignKey(First, related_name = 'the_first')
-# If ticket #1578 ever slips back in, these models will not be able to be
-# created (the field names being lower-cased versions of their opposite
-# classes is important here).
+# Protect against repetition of #1839, #2415 and #2536.
+class Third(models.Model):
+ name = models.CharField(maxlength=20)
+ third = models.ForeignKey('self', null=True, related_name='child_set')
+
+class Parent(models.Model):
+ name = models.CharField(maxlength=20)
+ bestchild = models.ForeignKey('Child', null=True, related_name='favored_by')
+
+class Child(models.Model):
+ name = models.CharField(maxlength=20)
+ parent = models.ForeignKey(Parent)
+
-__test__ = {'API_TESTS':""}
+__test__ = {'API_TESTS':"""
+>>> Third.AddManipulator().save(dict(id='3', name='An example', another=None))
+<Third: Third object>
+>>> parent = Parent(name = 'fred')
+>>> parent.save()
+>>> Child.AddManipulator().save(dict(name='bam-bam', parent=parent.id))
+<Child: Child object>
+"""}
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
index 0a41f5b5b7..375fd36196 100644
--- a/tests/regressiontests/templates/tests.py
+++ b/tests/regressiontests/templates/tests.py
@@ -127,6 +127,29 @@ class Templates(unittest.TestCase):
# Fail silently when accessing a non-simple method
'basic-syntax20': ("{{ var.method2 }}", {"var": SomeClass()}, ("","INVALID")),
+ # List-index syntax allows a template to access a certain item of a subscriptable object.
+ 'list-index01': ("{{ var.1 }}", {"var": ["first item", "second item"]}, "second item"),
+
+ # Fail silently when the list index is out of range.
+ 'list-index02': ("{{ var.5 }}", {"var": ["first item", "second item"]}, ("", "INVALID")),
+
+ # Fail silently when the variable is not a subscriptable object.
+ 'list-index03': ("{{ var.1 }}", {"var": None}, ("", "INVALID")),
+
+ # Fail silently when variable is a dict without the specified key.
+ 'list-index04': ("{{ var.1 }}", {"var": {}}, ("", "INVALID")),
+
+ # Dictionary lookup wins out when dict's key is a string.
+ 'list-index05': ("{{ var.1 }}", {"var": {'1': "hello"}}, "hello"),
+
+ # But list-index lookup wins out when dict's key is an int, which
+ # behind the scenes is really a dictionary lookup (for a dict)
+ # after converting the key to an int.
+ 'list-index06': ("{{ var.1 }}", {"var": {1: "hello"}}, "hello"),
+
+ # Dictionary lookup wins out when there is a string and int version of the key.
+ 'list-index07': ("{{ var.1 }}", {"var": {'1': "hello", 1: "world"}}, "hello"),
+
# Basic filter usage
'basic-syntax21': ("{{ var|upper }}", {"var": "Django is the greatest!"}, "DJANGO IS THE GREATEST!"),
@@ -167,7 +190,7 @@ class Templates(unittest.TestCase):
'basic-syntax33': (r'1{{ var.method3 }}2', {"var": SomeClass()}, ("12", "1INVALID2")),
# In methods that raise an exception without a "silent_variable_attribute" set to True,
- # the exception propogates
+ # the exception propagates
'basic-syntax34': (r'1{{ var.method4 }}2', {"var": SomeClass()}, SomeOtherException),
# Escaped backslash in argument
@@ -378,6 +401,20 @@ class Templates(unittest.TestCase):
'ifequal-split09': (r"{% ifequal a 'slash\man' %}yes{% else %}no{% endifequal %}", {'a': r"slash\man"}, "yes"),
'ifequal-split10': (r"{% ifequal a 'slash\man' %}yes{% else %}no{% endifequal %}", {'a': r"slashman"}, "no"),
+ # NUMERIC RESOLUTION
+ 'ifequal-numeric01': ('{% ifequal x 5 %}yes{% endifequal %}', {'x': '5'}, ''),
+ 'ifequal-numeric02': ('{% ifequal x 5 %}yes{% endifequal %}', {'x': 5}, 'yes'),
+ 'ifequal-numeric03': ('{% ifequal x 5.2 %}yes{% endifequal %}', {'x': 5}, ''),
+ 'ifequal-numeric04': ('{% ifequal x 5.2 %}yes{% endifequal %}', {'x': 5.2}, 'yes'),
+ 'ifequal-numeric05': ('{% ifequal x 0.2 %}yes{% endifequal %}', {'x': .2}, 'yes'),
+ 'ifequal-numeric06': ('{% ifequal x .2 %}yes{% endifequal %}', {'x': .2}, 'yes'),
+ 'ifequal-numeric07': ('{% ifequal x 2. %}yes{% endifequal %}', {'x': 2}, ''),
+ 'ifequal-numeric08': ('{% ifequal x "5" %}yes{% endifequal %}', {'x': 5}, ''),
+ 'ifequal-numeric09': ('{% ifequal x "5" %}yes{% endifequal %}', {'x': '5'}, 'yes'),
+ 'ifequal-numeric10': ('{% ifequal x -5 %}yes{% endifequal %}', {'x': -5}, 'yes'),
+ 'ifequal-numeric11': ('{% ifequal x -5.2 %}yes{% endifequal %}', {'x': -5.2}, 'yes'),
+ 'ifequal-numeric12': ('{% ifequal x +5 %}yes{% endifequal %}', {'x': 5}, 'yes'),
+
### IFNOTEQUAL TAG ########################################################
'ifnotequal01': ("{% ifnotequal a b %}yes{% endifnotequal %}", {"a": 1, "b": 2}, "yes"),
'ifnotequal02': ("{% ifnotequal a b %}yes{% endifnotequal %}", {"a": 1, "b": 1}, ""),
@@ -390,6 +427,21 @@ class Templates(unittest.TestCase):
'include03': ('{% include template_name %}', {'template_name': 'basic-syntax02', 'headline': 'Included'}, "Included"),
'include04': ('a{% include "nonexistent" %}b', {}, "ab"),
+ ### NAMED ENDBLOCKS #######################################################
+
+ # Basic test
+ 'namedendblocks01': ("1{% block first %}_{% block second %}2{% endblock second %}_{% endblock first %}3", {}, '1_2_3'),
+
+ # Unbalanced blocks
+ 'namedendblocks02': ("1{% block first %}_{% block second %}2{% endblock first %}_{% endblock second %}3", {}, template.TemplateSyntaxError),
+ 'namedendblocks03': ("1{% block first %}_{% block second %}2{% endblock %}_{% endblock second %}3", {}, template.TemplateSyntaxError),
+ 'namedendblocks04': ("1{% block first %}_{% block second %}2{% endblock second %}_{% endblock third %}3", {}, template.TemplateSyntaxError),
+ 'namedendblocks05': ("1{% block first %}_{% block second %}2{% endblock first %}", {}, template.TemplateSyntaxError),
+
+ # Mixed named and unnamed endblocks
+ 'namedendblocks06': ("1{% block first %}_{% block second %}2{% endblock %}_{% endblock first %}3", {}, '1_2_3'),
+ 'namedendblocks07': ("1{% block first %}_{% block second %}2{% endblock second %}_{% endblock %}3", {}, '1_2_3'),
+
### INHERITANCE ###########################################################
# Standard template with no inheritance
@@ -630,6 +682,17 @@ class Templates(unittest.TestCase):
# Compare to a given parameter
'timeuntil04' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=1), 'b':NOW - timedelta(days=2)}, '1 day'),
'timeuntil05' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=2), 'b':NOW - timedelta(days=2, minutes=1)}, '1 minute'),
+
+ ### URL TAG ########################################################
+ # Successes
+ 'url01' : ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'),
+ 'url02' : ('{% url regressiontests.templates.views.client_action client.id,action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
+ 'url03' : ('{% url regressiontests.templates.views.index %}', {}, '/url_tag/'),
+
+ # Failures
+ 'url04' : ('{% url %}', {}, template.TemplateSyntaxError),
+ 'url05' : ('{% url no_such_view %}', {}, ''),
+ 'url06' : ('{% url regressiontests.templates.views.client no_such_param="value" %}', {}, ''),
}
# Register our custom template loader.
diff --git a/tests/regressiontests/templates/urls.py b/tests/regressiontests/templates/urls.py
new file mode 100644
index 0000000000..dc5b36b08b
--- /dev/null
+++ b/tests/regressiontests/templates/urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls.defaults import *
+from regressiontests.templates import views
+
+urlpatterns = patterns('',
+
+ # Test urls for testing reverse lookups
+ (r'^$', views.index),
+ (r'^client/(\d+)/$', views.client),
+ (r'^client/(\d+)/(?P<action>[^/]+)/$', views.client_action),
+)
diff --git a/tests/regressiontests/templates/views.py b/tests/regressiontests/templates/views.py
new file mode 100644
index 0000000000..b68809944a
--- /dev/null
+++ b/tests/regressiontests/templates/views.py
@@ -0,0 +1,10 @@
+# Fake views for testing url reverse lookup
+
+def index(request):
+ pass
+
+def client(request, id):
+ pass
+
+def client_action(request, id, action):
+ pass
diff --git a/tests/runtests.py b/tests/runtests.py
index 20189c2d99..a111ef1436 100755
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -77,13 +77,19 @@ def django_tests(verbosity, tests_to_run):
old_root_urlconf = settings.ROOT_URLCONF
old_template_dirs = settings.TEMPLATE_DIRS
old_use_i18n = settings.USE_I18N
+ old_middleware_classes = settings.MIDDLEWARE_CLASSES
- # Redirect some settings for the duration of these tests
+ # Redirect some settings for the duration of these tests.
settings.TEST_DATABASE_NAME = TEST_DATABASE_NAME
settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS
settings.ROOT_URLCONF = 'urls'
settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), TEST_TEMPLATE_DIR),)
settings.USE_I18N = True
+ settings.MIDDLEWARE_CLASSES = (
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ )
# Load all the ALWAYS_INSTALLED_APPS.
# (This import statement is intentionally delayed until after we
@@ -91,7 +97,7 @@ def django_tests(verbosity, tests_to_run):
from django.db.models.loading import get_apps, load_app
get_apps()
- # Load all the test model apps
+ # Load all the test model apps.
test_models = []
for model_dir, model_name in get_test_models():
model_label = '.'.join([model_dir, model_name])
@@ -109,7 +115,7 @@ def django_tests(verbosity, tests_to_run):
sys.stderr.write("Error while importing %s:" % model_name + ''.join(traceback.format_exception(*sys.exc_info())[1:]))
continue
- # Add tests for invalid models
+ # Add tests for invalid models.
extra_tests = []
for model_dir, model_name in get_invalid_models():
model_label = '.'.join([model_dir, model_name])
@@ -118,14 +124,17 @@ def django_tests(verbosity, tests_to_run):
# Run the test suite, including the extra validation tests.
from django.test.simple import run_tests
- run_tests(test_models, verbosity, extra_tests=extra_tests)
+ failures = run_tests(test_models, verbosity, extra_tests=extra_tests)
+ if failures:
+ sys.exit(failures)
- # Restore the old settings
+ # Restore the old settings.
settings.INSTALLED_APPS = old_installed_apps
settings.TESTS_DATABASE_NAME = old_test_database_name
settings.ROOT_URLCONF = old_root_urlconf
settings.TEMPLATE_DIRS = old_template_dirs
settings.USE_I18N = old_use_i18n
+ settings.MIDDLEWARE_CLASSES = old_middleware_classes
if __name__ == "__main__":
from optparse import OptionParser
@@ -139,5 +148,7 @@ if __name__ == "__main__":
options, args = parser.parse_args()
if options.settings:
os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
-
+ elif "DJANGO_SETTINGS_MODULE" not in os.environ:
+ parser.error("DJANGO_SETTINGS_MODULE is not set in the environment. "
+ "Set it or use --settings.")
django_tests(int(options.verbosity), args)
diff --git a/tests/urls.py b/tests/urls.py
index 39d5aaee6b..9dcdca944e 100644
--- a/tests/urls.py
+++ b/tests/urls.py
@@ -6,5 +6,8 @@ urlpatterns = patterns('',
# Always provide the auth system login and logout views
(r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}),
- (r'^accounts/logout/$', 'django.contrib.auth.views.login'),
+ (r'^accounts/logout/$', 'django.contrib.auth.views.logout'),
+
+ # test urlconf for {% url %} template tag
+ (r'^url_tag/', include('regressiontests.templates.urls')),
)