summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Pellerin <jpellerin@gmail.com>2006-09-08 16:35:39 +0000
committerJason Pellerin <jpellerin@gmail.com>2006-09-08 16:35:39 +0000
commit84f7a2133c4d553a234165bb8cbfaf70681bb028 (patch)
tree53d08c1ffc275a4e1d62d7f48ea81172a5919051
parentae3896cb74d4bc42acaf6fade0d2a57e28045b2a (diff)
[multi-db] Merge trunk to [3737]. Some tests still failing.
git-svn-id: http://code.djangoproject.com/svn/django/branches/multiple-db-support@3739 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--AUTHORS3
-rw-r--r--django/contrib/admin/views/auth.py4
-rw-r--r--django/contrib/sitemaps/__init__.py4
-rw-r--r--django/contrib/sitemaps/views.py2
-rw-r--r--django/core/management.py43
-rw-r--r--django/db/backends/postgresql_psycopg2/base.py20
-rw-r--r--django/db/backends/sqlite3/base.py5
-rw-r--r--django/db/models/related.py3
-rw-r--r--django/template/__init__.py12
-rw-r--r--django/template/defaulttags.py8
-rw-r--r--django/template/loader_tags.py2
-rw-r--r--django/test/client.py48
-rw-r--r--django/test/signals.py1
-rw-r--r--django/test/simple.py4
-rw-r--r--django/test/utils.py52
-rw-r--r--django/utils/functional.py4
-rw-r--r--django/views/debug.py22
-rw-r--r--django/views/static.py2
-rw-r--r--docs/syndication_feeds.txt2
-rw-r--r--docs/templates_python.txt16
-rw-r--r--docs/testing.txt210
-rw-r--r--tests/modeltests/invalid_models/models.py37
-rw-r--r--tests/modeltests/test_client/__init__.py0
-rw-r--r--tests/modeltests/test_client/management.py10
-rw-r--r--tests/modeltests/test_client/models.py101
-rw-r--r--tests/modeltests/test_client/urls.py9
-rw-r--r--tests/modeltests/test_client/views.py35
-rw-r--r--tests/regressiontests/templates/tests.py54
-rw-r--r--tests/regressiontests/thread_isolation/tests.py10
-rwxr-xr-xtests/runtests.py13
-rw-r--r--tests/templates/404.html1
-rw-r--r--tests/templates/500.html1
-rw-r--r--tests/templates/login.html19
-rw-r--r--tests/urls.py10
34 files changed, 610 insertions, 157 deletions
diff --git a/AUTHORS b/AUTHORS
index e3db830b74..4d57503bf8 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -104,7 +104,6 @@ answer newbie questions, and generally made Django that much better:
mattycakes@gmail.com
Jason McBrayer <http://www.carcosa.net/jason/>
michael.mcewan@gmail.com
- mir@noris.de
mmarshall
Eric Moritz <http://eric.themoritzfamily.com/>
Robin Munn <http://www.geekforgod.com/>
@@ -121,12 +120,14 @@ answer newbie questions, and generally made Django that much better:
plisk
Daniel Poelzleithner <http://poelzi.org/>
J. Rademaker
+ Michael Radziej <mir@noris.de>
Brian Ray <http://brianray.chipy.org/>
rhettg@gmail.com
Oliver Rutherfurd <http://rutherfurd.net/>
Ivan Sagalaev (Maniac) <http://www.softwaremaniacs.org/>
David Schein
Pete Shinners <pete@shinners.org>
+ SmileyChris <smileychris@gmail.com>
sopel
Thomas Steinacher <tom@eggdrop.ch>
Radek Švarz <http://www.svarz.cz/translate/>
diff --git a/django/contrib/admin/views/auth.py b/django/contrib/admin/views/auth.py
index d09075c2a1..42230050cc 100644
--- a/django/contrib/admin/views/auth.py
+++ b/django/contrib/admin/views/auth.py
@@ -1,3 +1,4 @@
+from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django import forms, template
@@ -5,6 +6,8 @@ from django.shortcuts import render_to_response
from django.http import HttpResponseRedirect
def user_add_stage(request):
+ if not request.user.has_perm('auth.change_user'):
+ raise PermissionDenied
manipulator = UserCreationForm()
if request.method == 'POST':
new_data = request.POST.copy()
@@ -37,3 +40,4 @@ def user_add_stage(request):
'opts': User._meta,
'username_help_text': User._meta.get_field('username').help_text,
}, context_instance=template.RequestContext(request))
+user_add_stage = staff_member_required(user_add_stage)
diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py
index 50f60b821e..2c76e13c22 100644
--- a/django/contrib/sitemaps/__init__.py
+++ b/django/contrib/sitemaps/__init__.py
@@ -16,11 +16,11 @@ def ping_google(sitemap_url=None, ping_url=PING_URL):
if sitemap_url is None:
try:
# First, try to get the "index" sitemap URL.
- sitemap_url = urlresolvers.reverse('django.contrib.sitemap.views.index')
+ sitemap_url = urlresolvers.reverse('django.contrib.sitemaps.views.index')
except urlresolvers.NoReverseMatch:
try:
# Next, try for the "global" sitemap URL.
- sitemap_url = urlresolvers.reverse('django.contrib.sitemap.views.sitemap')
+ sitemap_url = urlresolvers.reverse('django.contrib.sitemaps.views.sitemap')
except urlresolvers.NoReverseMatch:
pass
diff --git a/django/contrib/sitemaps/views.py b/django/contrib/sitemaps/views.py
index 8a4592c3e4..576e3d0bb8 100644
--- a/django/contrib/sitemaps/views.py
+++ b/django/contrib/sitemaps/views.py
@@ -8,7 +8,7 @@ def index(request, sitemaps):
sites = []
protocol = request.is_secure() and 'https' or 'http'
for section in sitemaps.keys():
- sitemap_url = urlresolvers.reverse('django.contrib.sitemap.views.sitemap', kwargs={'section': section})
+ sitemap_url = urlresolvers.reverse('django.contrib.sitemaps.views.sitemap', kwargs={'section': section})
sites.append('%s://%s%s' % (protocol, current_site.domain, sitemap_url))
xml = loader.render_to_string('sitemap_index.xml', {'sitemaps': sites})
return HttpResponse(xml, mimetype='application/xml')
diff --git a/django/core/management.py b/django/core/management.py
index c1454c00d6..a5d6216279 100644
--- a/django/core/management.py
+++ b/django/core/management.py
@@ -755,27 +755,32 @@ def get_validation_errors(outfile, app=None):
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
rel_query_name = f.related_query_name()
- for r in rel_opts.fields:
- if r.name == rel_name:
- e.add(opts, "Accessor for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
- if r.name == rel_query_name:
- e.add(opts, "Reverse query name for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
- for r in rel_opts.many_to_many:
- if r.name == rel_name:
- e.add(opts, "Accessor for m2m field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
- if r.name == rel_query_name:
- e.add(opts, "Reverse query name for m2m field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
- for r in rel_opts.get_all_related_many_to_many_objects():
- if r.field is not f:
+ # If rel_name is none, there is no reverse accessor.
+ # (This only occurs for symmetrical m2m relations to self).
+ # If this is the case, there are no clashes to check for this field, as
+ # there are no reverse descriptors for this field.
+ if rel_name is not None:
+ for r in rel_opts.fields:
+ if r.name == rel_name:
+ e.add(opts, "Accessor for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
+ if r.name == rel_query_name:
+ e.add(opts, "Reverse query name for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
+ for r in rel_opts.many_to_many:
+ if r.name == rel_name:
+ e.add(opts, "Accessor for m2m field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
+ if r.name == rel_query_name:
+ e.add(opts, "Reverse query name for m2m field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
+ for r in rel_opts.get_all_related_many_to_many_objects():
+ if r.field is not f:
+ if r.get_accessor_name() == rel_name:
+ e.add(opts, "Accessor for m2m field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
+ if r.get_accessor_name() == rel_query_name:
+ e.add(opts, "Reverse query name for m2m field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
+ for r in rel_opts.get_all_related_objects():
if r.get_accessor_name() == rel_name:
- e.add(opts, "Accessor for m2m field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
+ e.add(opts, "Accessor for m2m field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
if r.get_accessor_name() == rel_query_name:
- e.add(opts, "Reverse query name for m2m field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
- for r in rel_opts.get_all_related_objects():
- if r.get_accessor_name() == rel_name:
- e.add(opts, "Accessor for m2m field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
- if r.get_accessor_name() == rel_query_name:
- e.add(opts, "Reverse query name for m2m field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
+ e.add(opts, "Reverse query name for m2m field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
# Check admin attribute.
if opts.admin is not None:
diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
index 10108f08b8..7c0c9558e9 100644
--- a/django/db/backends/postgresql_psycopg2/base.py
+++ b/django/db/backends/postgresql_psycopg2/base.py
@@ -69,23 +69,9 @@ def quote_name(name):
return name # Quoting once is enough.
return '"%s"' % name
-def dictfetchone(cursor):
- "Returns a row from the cursor as a dict"
- # TODO: cursor.dictfetchone() doesn't exist in psycopg2,
- # but no Django code uses this. Safe to remove?
- return cursor.dictfetchone()
-
-def dictfetchmany(cursor, number):
- "Returns a certain number of rows from a cursor as a dict"
- # TODO: cursor.dictfetchmany() doesn't exist in psycopg2,
- # but no Django code uses this. Safe to remove?
- return cursor.dictfetchmany(number)
-
-def dictfetchall(cursor):
- "Returns all rows from a cursor as a dict"
- # TODO: cursor.dictfetchall() doesn't exist in psycopg2,
- # but no Django code uses this. Safe to remove?
- return cursor.dictfetchall()
+dictfetchone = util.dictfetchone
+dictfetchmany = util.dictfetchmany
+dictfetchall = util.dictfetchall
def get_last_insert_id(cursor, table_name, pk_name):
cursor.execute("SELECT CURRVAL('\"%s_%s_seq\"')" % (table_name, pk_name))
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 19c15c05ff..e6aff2d847 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -63,7 +63,10 @@ class DatabaseWrapper(local):
self.connection.rollback()
def close(self):
- if self.connection is not None:
+ from django.conf import settings
+ # If database is in memory, closing the connection destroys the database.
+ # To prevent accidental data loss, ignore close requests on an in-memory db.
+ if self.connection is not None and settings.DATABASE_NAME != ":memory:":
self.connection.close()
self.connection = None
diff --git a/django/db/models/related.py b/django/db/models/related.py
index ee3b916cf4..ac1ec50ca2 100644
--- a/django/db/models/related.py
+++ b/django/db/models/related.py
@@ -131,6 +131,9 @@ class RelatedObject(object):
# many-to-many objects. It uses the lower-cased object_name + "_set",
# but this can be overridden with the "related_name" option.
if self.field.rel.multiple:
+ # If this is a symmetrical m2m relation on self, there is no reverse accessor.
+ if getattr(self.field.rel, 'symmetrical', False) and self.model == self.parent_model:
+ return None
return self.field.rel.related_name or (self.opts.object_name.lower() + '_set')
else:
return self.field.rel.related_name or (self.opts.object_name.lower())
diff --git a/django/template/__init__.py b/django/template/__init__.py
index 4cf3304eb6..fa75f6c2f5 100644
--- a/django/template/__init__.py
+++ b/django/template/__init__.py
@@ -137,13 +137,14 @@ class StringOrigin(Origin):
return self.source
class Template(object):
- def __init__(self, template_string, origin=None):
+ def __init__(self, template_string, origin=None, name='<Unknown Template>'):
"Compilation stage"
if settings.TEMPLATE_DEBUG and origin == None:
origin = StringOrigin(template_string)
# Could do some crazy stack-frame stuff to record where this string
# came from...
self.nodelist = compile_string(template_string, origin)
+ self.name = name
def __iter__(self):
for node in self.nodelist:
@@ -434,7 +435,7 @@ class TokenParser(object):
while i < len(subject) and subject[i] != subject[p]:
i += 1
if i >= len(subject):
- raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % subject
+ raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % (i, subject)
i += 1
res = subject[p:i]
while i < len(subject) and subject[i] in (' ', '\t'):
@@ -548,9 +549,12 @@ class FilterExpression(object):
obj = resolve_variable(self.var, context)
except VariableDoesNotExist:
if ignore_failures:
- return None
+ obj = None
else:
- return settings.TEMPLATE_STRING_IF_INVALID
+ if settings.TEMPLATE_STRING_IF_INVALID:
+ return settings.TEMPLATE_STRING_IF_INVALID
+ else:
+ obj = settings.TEMPLATE_STRING_IF_INVALID
for func, args in self.filters:
arg_vals = []
for lookup, arg in args:
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index 0a4fe33d82..e8a58824dc 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -86,7 +86,7 @@ class ForNode(Node):
parentloop = {}
context.push()
try:
- values = self.sequence.resolve(context)
+ values = self.sequence.resolve(context, True)
except VariableDoesNotExist:
values = []
if values is None:
@@ -212,13 +212,13 @@ class RegroupNode(Node):
self.var_name = var_name
def render(self, context):
- obj_list = self.target.resolve(context)
- if obj_list == '': # target_var wasn't found in context; fail silently
+ obj_list = self.target.resolve(context, True)
+ if obj_list == None: # target_var wasn't found in context; fail silently
context[self.var_name] = []
return ''
output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]}
for obj in obj_list:
- grouper = self.expression.resolve(Context({'var': obj}))
+ grouper = self.expression.resolve(Context({'var': obj}), True)
# TODO: Is this a sensible way to determine equality?
if output and repr(output[-1]['grouper']) == repr(grouper):
output[-1]['list'].append(obj)
diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py
index 7f22f207b6..b609a7c273 100644
--- a/django/template/loader_tags.py
+++ b/django/template/loader_tags.py
@@ -51,7 +51,7 @@ class ExtendsNode(Node):
error_msg += " Got this from the %r variable." % self.parent_name_expr #TODO nice repr.
raise TemplateSyntaxError, error_msg
if hasattr(parent, 'render'):
- return parent
+ return parent # parent is a Template object
try:
source, origin = find_template_source(parent, self.template_dirs)
except TemplateDoesNotExist:
diff --git a/django/test/client.py b/django/test/client.py
index 871f6cfb9b..3dfe764a38 100644
--- a/django/test/client.py
+++ b/django/test/client.py
@@ -1,10 +1,9 @@
from cStringIO import StringIO
-from django.contrib.admin.views.decorators import LOGIN_FORM_KEY, _encode_post_data
from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest
from django.dispatch import dispatcher
from django.http import urlencode, SimpleCookie
-from django.template import signals
+from django.test import signals
from django.utils.functional import curry
class ClientHandler(BaseHandler):
@@ -96,7 +95,7 @@ class Client:
HTML rendered to the end-user.
"""
def __init__(self, **defaults):
- self.handler = TestHandler()
+ self.handler = ClientHandler()
self.defaults = defaults
self.cookie = SimpleCookie()
@@ -126,7 +125,7 @@ class Client:
data = {}
on_template_render = curry(store_rendered_templates, data)
dispatcher.connect(on_template_render, signal=signals.template_rendered)
-
+
response = self.handler(environ)
# Add any rendered template detail to the response
@@ -180,29 +179,38 @@ class Client:
def login(self, path, username, password, **extra):
"""
A specialized sequence of GET and POST to log into a view that
- is protected by @login_required or a similar access decorator.
+ is protected by a @login_required access decorator.
- path should be the URL of the login page, or of any page that
- is login protected.
+ path should be the URL of the page that is login protected.
- Returns True if login was successful; False if otherwise.
+ Returns the response from GETting the requested URL after
+ login is complete. Returns False if login process failed.
"""
- # First, GET the login page.
- # This is required to establish the session.
+ # First, GET the page that is login protected.
+ # This page will redirect to the login page.
response = self.get(path)
- if response.status_code != 200:
+ if response.status_code != 302:
return False
+
+ login_path, data = response['Location'].split('?')
+ next = data.split('=')[1]
- # Set up the block of form data required by the login page.
+ # Second, GET the login page; required to set up cookies
+ response = self.get(login_path, **extra)
+ if response.status_code != 200:
+ return False
+
+ # Last, POST the login data.
form_data = {
'username': username,
'password': password,
- 'this_is_the_login_form': 1,
- 'post_data': _encode_post_data({LOGIN_FORM_KEY: 1})
+ 'next' : next,
}
- response = self.post(path, data=form_data, **extra)
-
- # login page should give response 200 (if you requested the login
- # page specifically), or 302 (if you requested a login
- # protected page, to which the login can redirect).
- return response.status_code in (200,302)
+ response = self.post(login_path, data=form_data, **extra)
+
+ # Login page should 302 redirect to the originally requested page
+ if response.status_code != 302 or response['Location'] != path:
+ return False
+
+ # Since we are logged in, request the actual page again
+ return self.get(path)
diff --git a/django/test/signals.py b/django/test/signals.py
new file mode 100644
index 0000000000..40748ff4fe
--- /dev/null
+++ b/django/test/signals.py
@@ -0,0 +1 @@
+template_rendered = object() \ No newline at end of file
diff --git a/django/test/simple.py b/django/test/simple.py
index 2469f80b3a..043787414e 100644
--- a/django/test/simple.py
+++ b/django/test/simple.py
@@ -1,6 +1,7 @@
import unittest, doctest
from django.conf import settings
from django.core import management
+from django.test.utils import setup_test_environment, teardown_test_environment
from django.test.utils import create_test_db, destroy_test_db
from django.test.testcases import OutputChecker, DocTestRunner
@@ -51,6 +52,7 @@ def run_tests(module_list, verbosity=1, extra_tests=[]):
the module. A list of 'extra' tests may also be provided; these tests
will be added to the test suite.
"""
+ setup_test_environment()
settings.DEBUG = False
suite = unittest.TestSuite()
@@ -66,3 +68,5 @@ def run_tests(module_list, verbosity=1, extra_tests=[]):
management.syncdb(verbosity, interactive=False)
unittest.TextTestRunner(verbosity=verbosity).run(suite)
destroy_test_db(old_name, verbosity)
+
+ teardown_test_environment()
diff --git a/django/test/utils.py b/django/test/utils.py
index bde323fa4e..d7a4a9c963 100644
--- a/django/test/utils.py
+++ b/django/test/utils.py
@@ -1,11 +1,41 @@
import sys, time
from django.conf import settings
+
from django.db import backend, connect, connection, connection_info, connections
+from django.dispatch import dispatcher
+from django.test import signals
+from django.template import Template
# The prefix to put on the default database name when creating
# the test database.
TEST_DATABASE_PREFIX = 'test_'
+def instrumented_test_render(self, context):
+ """An instrumented Template render method, providing a signal
+ that can be intercepted by the test system Client
+
+ """
+ dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context)
+ return self.nodelist.render(context)
+
+def setup_test_environment():
+ """Perform any global pre-test setup. This involves:
+
+ - Installing the instrumented test renderer
+
+ """
+ Template.original_render = Template.render
+ Template.render = instrumented_test_render
+
+def teardown_test_environment():
+ """Perform any global post-test teardown. This involves:
+
+ - Restoring the original test renderer
+
+ """
+ Template.render = Template.original_render
+ del Template.original_render
+
def _set_autocommit(connection):
"Make sure a connection is in autocommit mode."
if hasattr(connection.connection, "autocommit"):
@@ -55,12 +85,13 @@ def create_test_db(verbosity=1, autoclobber=False):
else:
print "Tests cancelled."
sys.exit(1)
- # Close the old connection
- connection.close()
+
+ connection.close()
+ settings.DATABASE_NAME = TEST_DATABASE_NAME
- # Get a cursor (even though we don't need one yet). This has
- # the side effect of initializing the test database.
- cursor = connection.cursor()
+ # Get a cursor (even though we don't need one yet). This has
+ # the side effect of initializing the test database.
+ cursor = connection.cursor()
# Fill OTHER_DATABASES with the TEST_DATABASES settings,
# and connect each named connection to the test database, using
@@ -87,15 +118,14 @@ def destroy_test_db(old_database_name, old_databases, verbosity=1):
# connected to it.
if verbosity >= 1:
print "Destroying test database..."
- if settings.DATABASE_ENGINE != "sqlite3":
- connection.close()
- TEST_DATABASE_NAME = settings.DATABASE_NAME
- settings.DATABASE_NAME = old_database_name
+ connection.close()
+ TEST_DATABASE_NAME = settings.DATABASE_NAME
+ settings.DATABASE_NAME = old_database_name
+
+ if settings.DATABASE_ENGINE != "sqlite3":
settings.OTHER_DATABASES = old_databases
cursor = connection.cursor()
_set_autocommit(connection)
time.sleep(1) # To avoid "database is being accessed by other users" errors.
cursor.execute("DROP DATABASE %s" % backend.quote_name(TEST_DATABASE_NAME))
connection.close()
-
-
diff --git a/django/utils/functional.py b/django/utils/functional.py
index d1514d5728..e3c0a3c76b 100644
--- a/django/utils/functional.py
+++ b/django/utils/functional.py
@@ -1,6 +1,6 @@
-def curry(*args, **kwargs):
+def curry(_curried_func, *args, **kwargs):
def _curried(*moreargs, **morekwargs):
- return args[0](*(args[1:]+moreargs), **dict(kwargs.items() + morekwargs.items()))
+ return _curried_func(*(args+moreargs), **dict(kwargs, **morekwargs))
return _curried
class Promise:
diff --git a/django/views/debug.py b/django/views/debug.py
index 6934360afd..6178bdb83b 100644
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -115,7 +115,7 @@ def technical_500_response(request, exc_type, exc_value, tb):
'function': '?',
'lineno': '?',
}]
- t = Template(TECHNICAL_500_TEMPLATE)
+ t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
c = Context({
'exception_type': exc_type.__name__,
'exception_value': exc_value,
@@ -141,7 +141,7 @@ def technical_404_response(request, exception):
# tried exists but is an empty list. The URLconf must've been empty.
return empty_urlconf(request)
- t = Template(TECHNICAL_404_TEMPLATE)
+ t = Template(TECHNICAL_404_TEMPLATE, name='Technical 404 template')
c = Context({
'root_urlconf': settings.ROOT_URLCONF,
'urlpatterns': tried,
@@ -154,7 +154,7 @@ def technical_404_response(request, exception):
def empty_urlconf(request):
"Create an empty URLconf 404 error response."
- t = Template(EMPTY_URLCONF_TEMPLATE)
+ t = Template(EMPTY_URLCONF_TEMPLATE, name='Empty URLConf template')
c = Context({
'project_name': settings.SETTINGS_MODULE.split('.')[0]
})
@@ -189,7 +189,7 @@ TECHNICAL_500_TEMPLATE = """
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="robots" content="NONE,NOARCHIVE" />
- <title>{{ exception_type }} at {{ request.path }}</title>
+ <title>{{ exception_type }} at {{ request.path|escape }}</title>
<style type="text/css">
html * { padding:0; margin:0; }
body * { padding:10px 20px; }
@@ -292,7 +292,7 @@ TECHNICAL_500_TEMPLATE = """
<body>
<div id="summary">
- <h1>{{ exception_type }} at {{ request.path }}</h1>
+ <h1>{{ exception_type }} at {{ request.path|escape }}</h1>
<h2>{{ exception_value|escape }}</h2>
<table class="meta">
<tr>
@@ -301,7 +301,7 @@ TECHNICAL_500_TEMPLATE = """
</tr>
<tr>
<th>Request URL:</th>
- <td>{{ request_protocol }}://{{ request.META.HTTP_HOST }}{{ request.path }}</td>
+ <td>{{ request_protocol }}://{{ request.META.HTTP_HOST }}{{ request.path|escape }}</td>
</tr>
<tr>
<th>Exception Type:</th>
@@ -309,7 +309,7 @@ TECHNICAL_500_TEMPLATE = """
</tr>
<tr>
<th>Exception Value:</th>
- <td>{{ exception_value }}</td>
+ <td>{{ exception_value|escape }}</td>
</tr>
<tr>
<th>Exception Location:</th>
@@ -412,7 +412,7 @@ Traceback (most recent call last):<br/>
&nbsp;&nbsp;{{ frame.lineno }}. {{ frame.context_line|escape }}<br/>
{% endif %}
{% endfor %}<br/>
-&nbsp;&nbsp;{{ exception_type }} at {{ request.path }}<br/>
+&nbsp;&nbsp;{{ exception_type }} at {{ request.path|escape }}<br/>
&nbsp;&nbsp;{{ exception_value|escape }}</code>
</td>
</tr>
@@ -546,7 +546,7 @@ TECHNICAL_404_TEMPLATE = """
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <title>Page not found at {{ request.path }}</title>
+ <title>Page not found at {{ request.path|escape }}</title>
<meta name="robots" content="NONE,NOARCHIVE" />
<style type="text/css">
html * { padding:0; margin:0; }
@@ -576,7 +576,7 @@ TECHNICAL_404_TEMPLATE = """
</tr>
<tr>
<th>Request URL:</th>
- <td>{{ request_protocol }}://{{ request.META.HTTP_HOST }}{{ request.path }}</td>
+ <td>{{ request_protocol }}://{{ request.META.HTTP_HOST }}{{ request.path|escape }}</td>
</tr>
</table>
</div>
@@ -591,7 +591,7 @@ TECHNICAL_404_TEMPLATE = """
<li>{{ pattern|escape }}</li>
{% endfor %}
</ol>
- <p>The current URL, <code>{{ request.path }}</code>, didn't match any of these.</p>
+ <p>The current URL, <code>{{ request.path|escape }}</code>, didn't match any of these.</p>
{% else %}
<p>{{ reason|escape }}</p>
{% endif %}
diff --git a/django/views/static.py b/django/views/static.py
index ac323944d0..3ec4ca14a1 100644
--- a/django/views/static.py
+++ b/django/views/static.py
@@ -81,7 +81,7 @@ def directory_index(path, fullpath):
try:
t = loader.get_template('static/directory_index')
except TemplateDoesNotExist:
- t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE)
+ t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template')
files = []
for f in os.listdir(fullpath):
if not f.startswith('.'):
diff --git a/docs/syndication_feeds.txt b/docs/syndication_feeds.txt
index b00af200a0..225b67eb02 100644
--- a/docs/syndication_feeds.txt
+++ b/docs/syndication_feeds.txt
@@ -707,7 +707,7 @@ This example creates an Atom 1.0 feed and prints it to standard output::
... title=u"My Weblog",
... link=u"http://www.example.com/",
... description=u"In which I write about what I ate today.",
- ... language=u"en"),
+ ... language=u"en")
>>> f.add_item(title=u"Hot dog today",
... link=u"http://www.example.com/entries/1/",
... description=u"<p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p>")
diff --git a/docs/templates_python.txt b/docs/templates_python.txt
index aa7fa901b0..950b122339 100644
--- a/docs/templates_python.txt
+++ b/docs/templates_python.txt
@@ -198,9 +198,19 @@ some things to keep in mind:
How invalid variables are handled
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-If a variable doesn't exist, the template system inserts the value of the
-``TEMPLATE_STRING_IF_INVALID`` setting, which is set to ``''`` (the empty
-string) by default.
+Generally, if a variable doesn't exist, the template system inserts the
+value of the ``TEMPLATE_STRING_IF_INVALID`` setting, which is set to ``''``
+(the empty string) by default.
+
+Filters that are applied to an invalid variable will only be applied if
+``TEMPLATE_STRING_IF_INVALID`` is set to ``''`` (the empty string). If
+``TEMPLATE_STRING_IF_INVALID`` is set to any other value, variable
+filters will be ignored.
+
+This behavior is slightly different for the ``if``, ``for`` and ``regroup``
+template tags. If an invalid variable is provided to one of these template
+tags, the variable will be interpreted as ``None``. Filters are always
+applied to invalid variables within these template tags.
Playing with Context objects
----------------------------
diff --git a/docs/testing.txt b/docs/testing.txt
index 98ed1e8aec..b1ede3e4cc 100644
--- a/docs/testing.txt
+++ b/docs/testing.txt
@@ -92,7 +92,8 @@ Writing unittests
Like doctests, Django's unit tests use a standard library module: unittest_.
As with doctests, Django's test runner looks for any unit test cases defined
-in ``models.py``, or in a ``tests.py`` file in your application directory.
+in ``models.py``, or in a ``tests.py`` file stored in the application
+directory.
An equivalent unittest test case for the above example would look like::
@@ -110,8 +111,9 @@ An equivalent unittest test case for the above example would look like::
self.assertEquals(self.cat.speak(), 'The cat says "meow"')
When you `run your tests`_, the test utility will find all the test cases
-(that is, subclasses of ``unittest.TestCase``) in ``tests.py``, automatically
-build a test suite out of those test cases, and run that suite.
+(that is, subclasses of ``unittest.TestCase``) in ``models.py`` and
+``tests.py``, automatically build a test suite out of those test cases,
+and run that suite.
For more details about ``unittest``, see the `standard library unittest
documentation`_.
@@ -159,20 +161,164 @@ Again, remember that you can use both systems side-by-side (even in the same
app). In the end, most projects will eventually end up using both; each shines
in different circumstances.
-Testing utilities
-=================
+Testing Tools
+=============
+
+To assist in testing various features of your application, Django provides
+tools that can be used to establish tests and test conditions.
+* `Test Client`_
+* Fixtures_
+
Test Client
-----------
-A dummy browser; instruments the template generation process...
+The Test Client is a simple dummy browser. It allows you to simulate
+GET and POST requests on a URL, and observe the response that is received.
+This allows you to test that the correct view is executed for a given URL,
+and that the view constructs the correct response.
+
+As the response is generated, the Test Client gathers details on the
+Template and Context objects that were used to generate the response. These
+Templates and Contexts are then provided as part of the response, and can be
+used as test conditions.
+
+.. admonition:: Test Client vs Browser Automation?
+
+ The Test Client is not intended as a replacement for Twill_, Selenium_,
+ or other browser automation frameworks - it is intended to allow
+ testing of the contexts and templates produced by a view,
+ rather than the HTML rendered to the end-user.
+
+ A comprehensive test suite should use a combination of both: Test Client
+ tests to establish that the correct view is being called and that
+ the view is collecting the correct context data, and Browser Automation
+ tests to check that user interface behaves as expected.
+
+.. _Twill: http://twill.idyll.org/
+.. _Selenium: http://www.openqa.org/selenium/
+
+The Test Client is stateful; if a cookie is returned as part of a response,
+that cookie is provided as part of the next request issued to that Client
+instance. Expiry policies for these cookies are not followed; if you want
+a cookie to expire, either delete it manually from ``client.cookies``, or
+create a new Client instance (which will effectively delete all cookies).
+
+Making requests
+~~~~~~~~~~~~~~~
+
+Creating an instance of ``Client`` (``django.test.client.Client``) requires
+no arguments at time of construction. Once constructed, the following methods
+can be invoked on the ``Client`` instance.
+
+``get(path, data={})``
+ Make a GET request on the provided ``path``. The key-value pairs in the
+ data dictionary will be used to create a GET data payload. For example::
+
+ c = Client()
+ c.get('/customers/details/', {'name':'fred', 'age':7})
+
+ will result in the evaluation of a GET request equivalent to::
+
+ http://yoursite.com/customers/details/?name='fred'&age=7
+
+``post(path, data={})``
+ Make a POST request on the provided ``path``. The key-value pairs in the
+ data dictionary will be used to create the POST data payload. This payload
+ will be transmitted with the mimetype ``multipart/form-data``.
+
+ However submitting files is a special case. To POST a file, you need only
+ provide the file field name as a key, and a file handle to the file you wish to
+ upload as a value. The Test Client will populate the two POST fields (i.e.,
+ ``field`` and ``field_file``) required by FileField. For example::
+
+ c = Client()
+ f = open('wishlist.doc')
+ c.post('/customers/wishes/', {'name':'fred', 'attachment':f})
+ f.close()
+
+ will result in the evaluation of a POST request on ``/customers/wishes/``,
+ with a POST dictionary that contains `name`, `attachment` (containing the
+ file name), and `attachment_file` (containing the file data). Note that you
+ need to manually close the file after it has been provided to the POST.
+
+``login(path, username, password)``
+ In a production site, it is likely that some views will be protected with
+ the @login_required URL provided by ``django.contrib.auth``. Interacting
+ with a URL that has been login protected is a slightly complex operation,
+ so the Test Client provides a simple URL to automate the login process. A
+ call to ``login()`` stimulates the series of GET and POST calls required
+ to log a user into a @login_required protected URL.
+
+ If login is possible, the final return value of ``login()`` is the response
+ that is generated by issuing a GET request on the protected URL. If login
+ is not possible, ``login()`` returns False.
+
+ Note that since the test suite will be executed using the test database,
+ which contains no users by default. As a result, logins for your production
+ site will not work. You will need to create users as part of the test suite
+ to be able to test logins to your application.
+
+Testing Responses
+~~~~~~~~~~~~~~~~~
+
+The ``get()``, ``post()`` and ``login()`` methods all return a Response
+object. This Response object has the following properties that can be used
+for testing purposes:
+
+ =============== ==========================================================
+ Property Description
+ =============== ==========================================================
+ ``status_code`` The HTTP status of the response. See RFC2616_ for a
+ full list of HTTP status codes.
+
+ ``content`` The body of the response. The is the final page
+ content as rendered by the view, or any error message
+ (such as the URL for a 302 redirect).
+
+ ``template`` The Template instance that was used to render the final
+ content. Testing ``template.name`` can be particularly
+ useful; if the template was loaded from a file,
+ ``template.name`` will be the file name that was loaded.
+
+ If multiple templates were rendered, (e.g., if one
+ template includes another template),``template`` will
+ be a list of Template objects, in the order in which
+ they were rendered.
+
+ ``context`` The Context that was used to render the template that
+ produced the response content.
+
+ As with ``template``, if multiple templates were rendered
+ ``context`` will be a list of Context objects, stored in
+ the order in which they were rendered.
+ =============== ==========================================================
+
+.. _RFC2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+
+The following is a simple unit test using the Test Client::
+
+ import unittest
+ from django.test.client import Client
+
+ class SimpleTest(unittest.TestCase):
+ def setUp(self):
+ # Every test needs a client
+ self.client = Client()
+ def test_details(self):
+ # Issue a GET request
+ response = self.client.get('/customer/details/')
+
+ # Check that the respose is 200 OK
+ self.failUnlessEqual(response.status_code, 200)
+ # Check that the rendered context contains 5 customers
+ self.failUnlessEqual(len(response.context['customers']), 5)
Fixtures
--------
Feature still to come...
-
Running tests
=============
@@ -245,11 +391,13 @@ When you run ``./manage.py test``, Django looks at the ``TEST_RUNNER``
setting to determine what to do. By default, ``TEST_RUNNER`` points to ``django.test.simple.run_tests``. This method defines the default Django
testing behaviour. This behaviour involves:
+#. Performing global pre-test setup
#. Creating the test database
#. Running ``syncdb`` to install models and initial data into the test database
#. Looking for Unit Tests and Doctests in ``models.py`` and ``tests.py`` file for each installed application
#. Running the Unit Tests and Doctests that are found
#. Destroying the test database.
+#. Performing global post-test teardown
If you define your own test runner method and point ``TEST_RUNNER``
at that method, Django will execute your test runner whenever you run
@@ -263,14 +411,12 @@ can call it anything you want. The only requirement is that it accept two
arguments:
``run_tests(module_list, verbosity=1)``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The module list is the list of Python modules that contain the models to be
-tested. This is the same format returned by ``django.db.models.get_apps()``
+ The module list is the list of Python modules that contain the models to be
+ tested. This is the same format returned by ``django.db.models.get_apps()``
-Verbosity determines the amount of notification and debug information that
-will be printed to the console; '0' is no output, '1' is normal output,
-and `2` is verbose output.
+ Verbosity determines the amount of notification and debug information that
+ will be printed to the console; '0' is no output, '1' is normal output,
+ and `2` is verbose output.
Testing utilities
-----------------
@@ -278,26 +424,30 @@ Testing utilities
To assist in the creation of your own test runner, Django provides
a number of utility methods in the ``django.test.utils`` module.
-``create_test_db(verbosity=1, autoclobber=False)``:
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+``setup_test_environment()``
+ Performs any global pre-test setup, such as the installing the
+ instrumentation of the template rendering system.
-Creates a new test database, and run ``syncdb`` against it.
+``teardown_test_environment()``
+ Performs any global post-test teardown, such as removing the instrumentation
+ of the template rendering system.
-``verbosity`` has the same behaviour as in the test runner.
+``create_test_db(verbosity=1, autoclobber=False)``
+ Creates a new test database, and run ``syncdb`` against it.
-``Autoclobber`` describes the behavior that will occur if a database with
-the same name as the test database is discovered. If ``autoclobber`` is False,
-the user will be asked to approve destroying the existing database. ``sys.exit``
-is called if the user does not approve. If autoclobber is ``True``, the database
-will be destroyed without consulting the user.
+ ``verbosity`` has the same behaviour as in the test runner.
-``create_test_db()`` has the side effect of modifying
-``settings.DATABASE_NAME`` to match the name of the test database.
+ ``Autoclobber`` describes the behavior that will occur if a database with
+ the same name as the test database is discovered. If ``autoclobber`` is False,
+ the user will be asked to approve destroying the existing database. ``sys.exit``
+ is called if the user does not approve. If autoclobber is ``True``, the database
+ will be destroyed without consulting the user.
-``destroy_test_db(old_database_name, verbosity=1)``:
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ``create_test_db()`` has the side effect of modifying
+ ``settings.DATABASE_NAME`` to match the name of the test database.
-Destroys the database with the name ``settings.DATABASE_NAME`` matching,
-and restores the value of ``settings.DATABASE_NAME`` to the provided name.
+``destroy_test_db(old_database_name, verbosity=1)``
+ Destroys the database with the name ``settings.DATABASE_NAME`` matching,
+ and restores the value of ``settings.DATABASE_NAME`` to the provided name.
-``verbosity`` has the same behaviour as in the test runner.
+ ``verbosity`` has the same behaviour as in the test runner.
diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py
index 5540c1bd5f..2299cd85e6 100644
--- a/tests/modeltests/invalid_models/models.py
+++ b/tests/modeltests/invalid_models/models.py
@@ -68,15 +68,34 @@ class SelfClashForeign(models.Model):
foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id')
foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe')
+class ValidM2M(models.Model):
+ src_safe = models.CharField(maxlength=10)
+ validm2m = models.CharField(maxlength=10)
+
+ # M2M fields are symmetrical by default. Symmetrical M2M fields
+ # on self don't require a related accessor, so many potential
+ # clashes are avoided.
+ validm2m_set = models.ManyToManyField("ValidM2M")
+
+ m2m_1 = models.ManyToManyField("ValidM2M", related_name='id')
+ m2m_2 = models.ManyToManyField("ValidM2M", related_name='src_safe')
+
+ m2m_3 = models.ManyToManyField('self')
+ m2m_4 = models.ManyToManyField('self')
+
class SelfClashM2M(models.Model):
src_safe = models.CharField(maxlength=10)
selfclashm2m = models.CharField(maxlength=10)
- selfclashm2m_set = models.ManyToManyField("SelfClashM2M")
- m2m_1 = models.ManyToManyField("SelfClashM2M", related_name='id')
- m2m_2 = models.ManyToManyField("SelfClashM2M", related_name='src_safe')
-
+ # Non-symmetrical M2M fields _do_ have related accessors, so
+ # there is potential for clashes.
+ selfclashm2m_set = models.ManyToManyField("SelfClashM2M", symmetrical=False)
+
+ m2m_1 = models.ManyToManyField("SelfClashM2M", related_name='id', symmetrical=False)
+ m2m_2 = models.ManyToManyField("SelfClashM2M", related_name='src_safe', symmetrical=False)
+ m2m_3 = models.ManyToManyField('self', symmetrical=False)
+ m2m_4 = models.ManyToManyField('self', symmetrical=False)
model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "maxlength" attribute.
invalid_models.fielderrors: "floatfield": FloatFields require a "decimal_places" attribute.
@@ -147,9 +166,17 @@ invalid_models.selfclashforeign: Accessor for field 'foreign_2' clashes with fie
invalid_models.selfclashforeign: Reverse query name for field 'foreign_2' clashes with field 'SelfClashForeign.src_safe'. Add a related_name argument to the definition for 'foreign_2'.
invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'selfclashm2m_set'.
invalid_models.selfclashm2m: Reverse query name for m2m field 'selfclashm2m_set' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'selfclashm2m_set'.
+invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'selfclashm2m_set'.
invalid_models.selfclashm2m: Accessor for m2m field 'm2m_1' clashes with field 'SelfClashM2M.id'. Add a related_name argument to the definition for 'm2m_1'.
invalid_models.selfclashm2m: Accessor for m2m field 'm2m_2' clashes with field 'SelfClashM2M.src_safe'. Add a related_name argument to the definition for 'm2m_2'.
invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_1' clashes with field 'SelfClashM2M.id'. Add a related_name argument to the definition for 'm2m_1'.
invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_2' clashes with field 'SelfClashM2M.src_safe'. Add a related_name argument to the definition for 'm2m_2'.
+invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'.
+invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'.
+invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'.
+invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'.
+invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'.
+invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'.
+invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_3'.
+invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_4' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_4'.
"""
-
diff --git a/tests/modeltests/test_client/__init__.py b/tests/modeltests/test_client/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/modeltests/test_client/__init__.py
diff --git a/tests/modeltests/test_client/management.py b/tests/modeltests/test_client/management.py
new file mode 100644
index 0000000000..9b5a5c498e
--- /dev/null
+++ b/tests/modeltests/test_client/management.py
@@ -0,0 +1,10 @@
+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
new file mode 100644
index 0000000000..c5b1a241ca
--- /dev/null
+++ b/tests/modeltests/test_client/models.py
@@ -0,0 +1,101 @@
+"""
+39. Testing using the Test Client
+
+The test client is a class that can act like a simple
+browser for testing purposes.
+
+It allows the user to compose GET and POST requests, and
+obtain the response that the server gave to those requests.
+The server Response objects are annotated with the details
+of the contexts and templates that were rendered during the
+process of serving the request.
+
+Client objects are stateful - they will retain cookie (and
+thus session) details for the lifetime of the Client instance.
+
+This is not intended as a replacement for Twill,Selenium, or
+other browser automation frameworks - it is here to allow
+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
+
+class ClientTest(unittest.TestCase):
+ def setUp(self):
+ "Set up test environment"
+ self.client = Client()
+
+ def test_get_view(self):
+ "GET a view"
+ response = self.client.get('/test_client/get_view/')
+
+ # Check some response details
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['var'], 42)
+ self.assertEqual(response.template.name, 'GET Template')
+ self.failUnless('This is a test.' in response.content)
+
+ def test_get_post_view(self):
+ "GET a view that normally expects POSTs"
+ response = self.client.get('/test_client/post_view/', {})
+
+ # Check some response details
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.template.name, 'Empty POST Template')
+
+ def test_empty_post(self):
+ "POST an empty dictionary to a view"
+ response = self.client.post('/test_client/post_view/', {})
+
+ # Check some response details
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.template.name, 'Empty POST Template')
+
+ def test_post_view(self):
+ "POST some data to a view"
+ post_data = {
+ 'value': 37
+ }
+ response = self.client.post('/test_client/post_view/', post_data)
+
+ # Check some response details
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['data'], '37')
+ self.assertEqual(response.template.name, 'POST Template')
+ self.failUnless('Data received' in response.content)
+
+ def test_redirect(self):
+ "GET a URL that redirects elsewhere"
+ response = self.client.get('/test_client/redirect_view/')
+
+ # Check that the response was a 302 (redirect)
+ self.assertEqual(response.status_code, 302)
+
+ def test_unknown_page(self):
+ "GET an invalid URL"
+ response = self.client.get('/test_client/unknown_view/')
+
+ # Check that the response was a 404
+ self.assertEqual(response.status_code, 404)
+
+ def test_view_with_login(self):
+ "Request a page that is protected with @login_required"
+
+ # Get the page without logging in. Should result in 302.
+ response = self.client.get('/test_client/login_protected_view/')
+ self.assertEqual(response.status_code, 302)
+
+ # Request a page that requires a login
+ response = self.client.login('/test_client/login_protected_view/', 'testclient', 'password')
+ self.assertTrue(response)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['user'].username, 'testclient')
+ self.assertEqual(response.template.name, 'Login Template')
+
+ def test_view_with_bad_login(self):
+ "Request a page that is protected with @login, but use bad credentials"
+
+ response = self.client.login('/test_client/login_protected_view/', 'otheruser', 'nopassword')
+ self.assertFalse(response)
diff --git a/tests/modeltests/test_client/urls.py b/tests/modeltests/test_client/urls.py
new file mode 100644
index 0000000000..09bba5c007
--- /dev/null
+++ b/tests/modeltests/test_client/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import *
+import views
+
+urlpatterns = patterns('',
+ (r'^get_view/$', views.get_view),
+ (r'^post_view/$', views.post_view),
+ (r'^redirect_view/$', views.redirect_view),
+ (r'^login_protected_view/$', views.login_protected_view),
+)
diff --git a/tests/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py
new file mode 100644
index 0000000000..bf131032eb
--- /dev/null
+++ b/tests/modeltests/test_client/views.py
@@ -0,0 +1,35 @@
+from django.template import Context, Template
+from django.http import HttpResponse, HttpResponseRedirect
+from django.contrib.auth.decorators import login_required
+
+def get_view(request):
+ "A simple view that expects a GET request, and returns a rendered template"
+ t = Template('This is a test. {{ var }} is the value.', name='GET Template')
+ c = Context({'var': 42})
+
+ return HttpResponse(t.render(c))
+
+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']})
+ else:
+ t = Template('Viewing POST page.', name='Empty POST 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/')
+
+@login_required
+def login_protected_view(request):
+ "A simple view that is login protected."
+ t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template')
+ c = Context({'user': request.user})
+
+ return HttpResponse(t.render(c))
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
index 2d1ce192ef..5a8dd2d6a2 100644
--- a/tests/regressiontests/templates/tests.py
+++ b/tests/regressiontests/templates/tests.py
@@ -84,7 +84,7 @@ class Templates(unittest.TestCase):
'basic-syntax03': ("{{ first }} --- {{ second }}", {"first" : 1, "second" : 2}, "1 --- 2"),
# Fail silently when a variable is not found in the current context
- 'basic-syntax04': ("as{{ missing }}df", {}, "asINVALIDdf"),
+ 'basic-syntax04': ("as{{ missing }}df", {}, ("asdf","asINVALIDdf")),
# A variable may not contain more than one word
'basic-syntax06': ("{{ multi word variable }}", {}, template.TemplateSyntaxError),
@@ -100,7 +100,7 @@ class Templates(unittest.TestCase):
'basic-syntax10': ("{{ var.otherclass.method }}", {"var": SomeClass()}, "OtherClass.method"),
# Fail silently when a variable's attribute isn't found
- 'basic-syntax11': ("{{ var.blech }}", {"var": SomeClass()}, "INVALID"),
+ 'basic-syntax11': ("{{ var.blech }}", {"var": SomeClass()}, ("","INVALID")),
# Raise TemplateSyntaxError when trying to access a variable beginning with an underscore
'basic-syntax12': ("{{ var.__dict__ }}", {"var": SomeClass()}, template.TemplateSyntaxError),
@@ -116,10 +116,10 @@ class Templates(unittest.TestCase):
'basic-syntax18': ("{{ foo.bar }}", {"foo" : {"bar" : "baz"}}, "baz"),
# Fail silently when a variable's dictionary key isn't found
- 'basic-syntax19': ("{{ foo.spam }}", {"foo" : {"bar" : "baz"}}, "INVALID"),
+ 'basic-syntax19': ("{{ foo.spam }}", {"foo" : {"bar" : "baz"}}, ("","INVALID")),
# Fail silently when accessing a non-simple method
- 'basic-syntax20': ("{{ var.method2 }}", {"var": SomeClass()}, "INVALID"),
+ 'basic-syntax20': ("{{ var.method2 }}", {"var": SomeClass()}, ("","INVALID")),
# Basic filter usage
'basic-syntax21': ("{{ var|upper }}", {"var": "Django is the greatest!"}, "DJANGO IS THE GREATEST!"),
@@ -158,7 +158,7 @@ class Templates(unittest.TestCase):
'basic-syntax32': (r'{{ var|yesno:"yup,nup,mup" }} {{ var|yesno }}', {"var": True}, 'yup yes'),
# Fail silently for methods that raise an exception with a "silent_variable_failure" attribute
- 'basic-syntax33': (r'1{{ var.method3 }}2', {"var": SomeClass()}, "1INVALID2"),
+ '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
@@ -464,6 +464,14 @@ class Templates(unittest.TestCase):
# translation of a constant string
'i18n13': ('{{ _("Page not found") }}', {'LANGUAGE_CODE': 'de'}, 'Seite nicht gefunden'),
+ ### HANDLING OF TEMPLATE_TAG_IF_INVALID ###################################
+
+ 'invalidstr01': ('{{ var|default:"Foo" }}', {}, ('Foo','INVALID')),
+ 'invalidstr02': ('{{ var|default_if_none:"Foo" }}', {}, ('','INVALID')),
+ 'invalidstr03': ('{% for v in var %}({{ v }}){% endfor %}', {}, ''),
+ 'invalidstr04': ('{% if var %}Yes{% else %}No{% endif %}', {}, 'No'),
+ 'invalidstr04': ('{% if var|default:"Foo" %}Yes{% else %}No{% endif %}', {}, 'Yes'),
+
### MULTILINE #############################################################
'multiline01': ("""
@@ -507,7 +515,7 @@ class Templates(unittest.TestCase):
'{{ item.foo }}' + \
'{% endfor %},' + \
'{% endfor %}',
- {}, 'INVALID:INVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALID,'),
+ {}, ''),
### TEMPLATETAG TAG #######################################################
'templatetag01': ('{% templatetag openblock %}', {}, '{%'),
@@ -592,30 +600,44 @@ class Templates(unittest.TestCase):
old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
# Set TEMPLATE_STRING_IF_INVALID to a known string
- old_invalid, settings.TEMPLATE_STRING_IF_INVALID = settings.TEMPLATE_STRING_IF_INVALID, 'INVALID'
+ old_invalid = settings.TEMPLATE_STRING_IF_INVALID
for name, vals in tests:
install()
+
+ if isinstance(vals[2], tuple):
+ normal_string_result = vals[2][0]
+ invalid_string_result = vals[2][1]
+ else:
+ normal_string_result = vals[2]
+ invalid_string_result = vals[2]
+
if 'LANGUAGE_CODE' in vals[1]:
activate(vals[1]['LANGUAGE_CODE'])
else:
activate('en-us')
- try:
- output = loader.get_template(name).render(template.Context(vals[1]))
- except Exception, e:
- if e.__class__ != vals[2]:
- failures.append("Template test: %s -- FAILED. Got %s, exception: %s" % (name, e.__class__, e))
- continue
+
+ for invalid_str, result in [('', normal_string_result),
+ ('INVALID', invalid_string_result)]:
+ settings.TEMPLATE_STRING_IF_INVALID = invalid_str
+ try:
+ output = loader.get_template(name).render(template.Context(vals[1]))
+ except Exception, e:
+ if e.__class__ != result:
+ failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s" % (invalid_str, name, e.__class__, e))
+ continue
+ if output != result:
+ failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (invalid_str, name, result, output))
+
if 'LANGUAGE_CODE' in vals[1]:
deactivate()
- if output != vals[2]:
- failures.append("Template test: %s -- FAILED. Expected %r, got %r" % (name, vals[2], output))
+
loader.template_source_loaders = old_template_loaders
deactivate()
settings.TEMPLATE_DEBUG = old_td
settings.TEMPLATE_STRING_IF_INVALID = old_invalid
- self.assertEqual(failures, [])
+ self.assertEqual(failures, [], '\n'.join(failures))
if __name__ == "__main__":
unittest.main()
diff --git a/tests/regressiontests/thread_isolation/tests.py b/tests/regressiontests/thread_isolation/tests.py
index f677c27bf8..39c141720a 100644
--- a/tests/regressiontests/thread_isolation/tests.py
+++ b/tests/regressiontests/thread_isolation/tests.py
@@ -34,8 +34,9 @@ except ImportError:
# Import copy of _thread_local.py from Python 2.4
from django.utils._threading_local import local
-
# helpers
+EV = threading.Event()
+
class LocalSettings:
"""Settings holder that allows thread-local overrides of defaults.
"""
@@ -69,7 +70,7 @@ def thread_two(func, *arg):
debug("t2 ODB: %s", settings.OTHER_DATABASES)
debug("t2 waiting")
- ev.wait(2.0)
+ EV.wait(2.0)
func(*arg)
debug("t2 complete")
t2 = threading.Thread(target=start)
@@ -94,7 +95,7 @@ def thread_three(func, *arg):
connection.settings.DATABASE_NAME)
debug("t3 waiting")
- ev.wait(2.0)
+ EV.wait(2.0)
func(*arg)
debug("t3 complete")
t3 = threading.Thread(target=start)
@@ -113,7 +114,6 @@ def start_response(code, headers):
class TestThreadIsolation(unittest.TestCase):
# event used to synchronize threads so we can be sure they are running
# together
- ev = threading.Event()
lock = threading.RLock()
errors = []
@@ -235,7 +235,7 @@ class TestThreadIsolation(unittest.TestCase):
t3 = thread_three(MockHandler(self.request_three), env, start_response)
try:
- self.ev.set()
+ EV.set()
MockHandler(self.request_one)(env, start_response)
finally:
t2.join()
diff --git a/tests/runtests.py b/tests/runtests.py
index a8dd08e898..20cb446929 100755
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -6,6 +6,7 @@ import unittest
MODEL_TESTS_DIR_NAME = 'modeltests'
REGRESSION_TESTS_DIR_NAME = 'regressiontests'
TEST_DATABASE_NAME = 'django_test_db'
+TEST_TEMPLATE_DIR = 'templates'
TEST_DATABASES = ('_a', '_b')
@@ -81,20 +82,25 @@ class InvalidModelTestCase(unittest.TestCase):
def django_tests(verbosity, tests_to_run):
from django.conf import settings
from django.db.models.loading import get_apps, load_app
+
old_installed_apps = settings.INSTALLED_APPS
old_test_database_name = settings.TEST_DATABASE_NAME
+ old_root_urlconf = settings.ROOT_URLCONF
+ old_template_dirs = settings.TEMPLATE_DIRS
+ # Redirect some settings for the duration of these tests
settings.TEST_DATABASE_NAME = TEST_DATABASE_NAME
settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS
-
settings.TEST_DATABASES = TEST_DATABASES
settings.TEST_DATABASE_MODELS = TEST_DATABASE_MODELS
+ settings.ROOT_URLCONF = 'urls'
+ settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), TEST_TEMPLATE_DIR),)
# load all the ALWAYS_INSTALLED_APPS
get_apps()
- test_models = []
# Load all the test model apps
+ test_models = []
for model_dir, model_name in get_test_models():
model_label = '.'.join([model_dir, model_name])
try:
@@ -125,6 +131,9 @@ def django_tests(verbosity, tests_to_run):
# 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
+
if __name__ == "__main__":
from optparse import OptionParser
usage = "%prog [options] [model model model ...]"
diff --git a/tests/templates/404.html b/tests/templates/404.html
new file mode 100644
index 0000000000..da627e2222
--- /dev/null
+++ b/tests/templates/404.html
@@ -0,0 +1 @@
+Django Internal Tests: 404 Error \ No newline at end of file
diff --git a/tests/templates/500.html b/tests/templates/500.html
new file mode 100644
index 0000000000..ff028cbeb0
--- /dev/null
+++ b/tests/templates/500.html
@@ -0,0 +1 @@
+Django Internal Tests: 500 Error \ No newline at end of file
diff --git a/tests/templates/login.html b/tests/templates/login.html
new file mode 100644
index 0000000000..8a0974c9a1
--- /dev/null
+++ b/tests/templates/login.html
@@ -0,0 +1,19 @@
+<html>
+<head></head>
+<body>
+<h1>Django Internal Tests: Login</h1>
+{% if form.has_errors %}
+<p>Your username and password didn't match. Please try again.</p>
+{% endif %}
+
+<form method="post" action=".">
+<table>
+<tr><td><label for="id_username">Username:</label></td><td>{{ form.username }}</td></tr>
+<tr><td><label for="id_password">Password:</label></td><td>{{ form.password }}</td></tr>
+</table>
+
+<input type="submit" value="login" />
+<input type="hidden" name="next" value="{{ next }}" />
+</form>
+</body>
+</html> \ No newline at end of file
diff --git a/tests/urls.py b/tests/urls.py
new file mode 100644
index 0000000000..39d5aaee6b
--- /dev/null
+++ b/tests/urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('',
+ # test_client modeltest urls
+ (r'^test_client/', include('modeltests.test_client.urls')),
+
+ # 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'),
+)