diff options
| author | Boulder Sprinters <boulder-sprinters@djangoproject.com> | 2007-03-09 17:43:46 +0000 |
|---|---|---|
| committer | Boulder Sprinters <boulder-sprinters@djangoproject.com> | 2007-03-09 17:43:46 +0000 |
| commit | 0b7dd14d1f87e2ecef7aacc39fe4189667ed4fdf (patch) | |
| tree | b77497f9de324d38a9c6341e54d2742633e20055 /tests | |
| parent | e17f75551491f5b864c1fc8a97c21d0b2bbf0bcd (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')
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'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'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 Binary files differnew file mode 100644 index 0000000000..391b57a0f3 --- /dev/null +++ b/tests/regressiontests/bug639/test.jpg 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's wrong with 'Nothing to escape'</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's wrong with 'Should escape < & > and <script>alert('xss')</script>'</li></ul><input type="text" name="special_name" value="Should escape < & > and <script>alert('xss')</script>" /></td></tr> + # 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')), ) |
