mirror of
https://github.com/django/django.git
synced 2025-07-04 09:49:12 +00:00
[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
This commit is contained in:
parent
ae3896cb74
commit
84f7a2133c
3
AUTHORS
3
AUTHORS
@ -104,7 +104,6 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
mattycakes@gmail.com
|
mattycakes@gmail.com
|
||||||
Jason McBrayer <http://www.carcosa.net/jason/>
|
Jason McBrayer <http://www.carcosa.net/jason/>
|
||||||
michael.mcewan@gmail.com
|
michael.mcewan@gmail.com
|
||||||
mir@noris.de
|
|
||||||
mmarshall
|
mmarshall
|
||||||
Eric Moritz <http://eric.themoritzfamily.com/>
|
Eric Moritz <http://eric.themoritzfamily.com/>
|
||||||
Robin Munn <http://www.geekforgod.com/>
|
Robin Munn <http://www.geekforgod.com/>
|
||||||
@ -121,12 +120,14 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
plisk
|
plisk
|
||||||
Daniel Poelzleithner <http://poelzi.org/>
|
Daniel Poelzleithner <http://poelzi.org/>
|
||||||
J. Rademaker
|
J. Rademaker
|
||||||
|
Michael Radziej <mir@noris.de>
|
||||||
Brian Ray <http://brianray.chipy.org/>
|
Brian Ray <http://brianray.chipy.org/>
|
||||||
rhettg@gmail.com
|
rhettg@gmail.com
|
||||||
Oliver Rutherfurd <http://rutherfurd.net/>
|
Oliver Rutherfurd <http://rutherfurd.net/>
|
||||||
Ivan Sagalaev (Maniac) <http://www.softwaremaniacs.org/>
|
Ivan Sagalaev (Maniac) <http://www.softwaremaniacs.org/>
|
||||||
David Schein
|
David Schein
|
||||||
Pete Shinners <pete@shinners.org>
|
Pete Shinners <pete@shinners.org>
|
||||||
|
SmileyChris <smileychris@gmail.com>
|
||||||
sopel
|
sopel
|
||||||
Thomas Steinacher <tom@eggdrop.ch>
|
Thomas Steinacher <tom@eggdrop.ch>
|
||||||
Radek Švarz <http://www.svarz.cz/translate/>
|
Radek Švarz <http://www.svarz.cz/translate/>
|
||||||
|
@ -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.forms import UserCreationForm
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django import forms, template
|
from django import forms, template
|
||||||
@ -5,6 +6,8 @@ from django.shortcuts import render_to_response
|
|||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
|
|
||||||
def user_add_stage(request):
|
def user_add_stage(request):
|
||||||
|
if not request.user.has_perm('auth.change_user'):
|
||||||
|
raise PermissionDenied
|
||||||
manipulator = UserCreationForm()
|
manipulator = UserCreationForm()
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
new_data = request.POST.copy()
|
new_data = request.POST.copy()
|
||||||
@ -37,3 +40,4 @@ def user_add_stage(request):
|
|||||||
'opts': User._meta,
|
'opts': User._meta,
|
||||||
'username_help_text': User._meta.get_field('username').help_text,
|
'username_help_text': User._meta.get_field('username').help_text,
|
||||||
}, context_instance=template.RequestContext(request))
|
}, context_instance=template.RequestContext(request))
|
||||||
|
user_add_stage = staff_member_required(user_add_stage)
|
||||||
|
@ -16,11 +16,11 @@ def ping_google(sitemap_url=None, ping_url=PING_URL):
|
|||||||
if sitemap_url is None:
|
if sitemap_url is None:
|
||||||
try:
|
try:
|
||||||
# First, try to get the "index" sitemap URL.
|
# 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:
|
except urlresolvers.NoReverseMatch:
|
||||||
try:
|
try:
|
||||||
# Next, try for the "global" sitemap URL.
|
# 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:
|
except urlresolvers.NoReverseMatch:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ def index(request, sitemaps):
|
|||||||
sites = []
|
sites = []
|
||||||
protocol = request.is_secure() and 'https' or 'http'
|
protocol = request.is_secure() and 'https' or 'http'
|
||||||
for section in sitemaps.keys():
|
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))
|
sites.append('%s://%s%s' % (protocol, current_site.domain, sitemap_url))
|
||||||
xml = loader.render_to_string('sitemap_index.xml', {'sitemaps': sites})
|
xml = loader.render_to_string('sitemap_index.xml', {'sitemaps': sites})
|
||||||
return HttpResponse(xml, mimetype='application/xml')
|
return HttpResponse(xml, mimetype='application/xml')
|
||||||
|
@ -755,27 +755,32 @@ def get_validation_errors(outfile, app=None):
|
|||||||
|
|
||||||
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
|
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
|
||||||
rel_query_name = f.related_query_name()
|
rel_query_name = f.related_query_name()
|
||||||
for r in rel_opts.fields:
|
# If rel_name is none, there is no reverse accessor.
|
||||||
if r.name == rel_name:
|
# (This only occurs for symmetrical m2m relations to self).
|
||||||
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 this is the case, there are no clashes to check for this field, as
|
||||||
if r.name == rel_query_name:
|
# there are no reverse descriptors for this field.
|
||||||
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))
|
if rel_name is not None:
|
||||||
for r in rel_opts.many_to_many:
|
for r in rel_opts.fields:
|
||||||
if r.name == rel_name:
|
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))
|
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:
|
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))
|
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.get_all_related_many_to_many_objects():
|
for r in rel_opts.many_to_many:
|
||||||
if r.field is not f:
|
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:
|
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:
|
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))
|
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))
|
||||||
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))
|
|
||||||
|
|
||||||
# Check admin attribute.
|
# Check admin attribute.
|
||||||
if opts.admin is not None:
|
if opts.admin is not None:
|
||||||
|
@ -69,23 +69,9 @@ def quote_name(name):
|
|||||||
return name # Quoting once is enough.
|
return name # Quoting once is enough.
|
||||||
return '"%s"' % name
|
return '"%s"' % name
|
||||||
|
|
||||||
def dictfetchone(cursor):
|
dictfetchone = util.dictfetchone
|
||||||
"Returns a row from the cursor as a dict"
|
dictfetchmany = util.dictfetchmany
|
||||||
# TODO: cursor.dictfetchone() doesn't exist in psycopg2,
|
dictfetchall = util.dictfetchall
|
||||||
# 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()
|
|
||||||
|
|
||||||
def get_last_insert_id(cursor, table_name, pk_name):
|
def get_last_insert_id(cursor, table_name, pk_name):
|
||||||
cursor.execute("SELECT CURRVAL('\"%s_%s_seq\"')" % (table_name, pk_name))
|
cursor.execute("SELECT CURRVAL('\"%s_%s_seq\"')" % (table_name, pk_name))
|
||||||
|
@ -63,7 +63,10 @@ class DatabaseWrapper(local):
|
|||||||
self.connection.rollback()
|
self.connection.rollback()
|
||||||
|
|
||||||
def close(self):
|
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.close()
|
||||||
self.connection = None
|
self.connection = None
|
||||||
|
|
||||||
|
@ -131,6 +131,9 @@ class RelatedObject(object):
|
|||||||
# many-to-many objects. It uses the lower-cased object_name + "_set",
|
# many-to-many objects. It uses the lower-cased object_name + "_set",
|
||||||
# but this can be overridden with the "related_name" option.
|
# but this can be overridden with the "related_name" option.
|
||||||
if self.field.rel.multiple:
|
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')
|
return self.field.rel.related_name or (self.opts.object_name.lower() + '_set')
|
||||||
else:
|
else:
|
||||||
return self.field.rel.related_name or (self.opts.object_name.lower())
|
return self.field.rel.related_name or (self.opts.object_name.lower())
|
||||||
|
@ -137,13 +137,14 @@ class StringOrigin(Origin):
|
|||||||
return self.source
|
return self.source
|
||||||
|
|
||||||
class Template(object):
|
class Template(object):
|
||||||
def __init__(self, template_string, origin=None):
|
def __init__(self, template_string, origin=None, name='<Unknown Template>'):
|
||||||
"Compilation stage"
|
"Compilation stage"
|
||||||
if settings.TEMPLATE_DEBUG and origin == None:
|
if settings.TEMPLATE_DEBUG and origin == None:
|
||||||
origin = StringOrigin(template_string)
|
origin = StringOrigin(template_string)
|
||||||
# Could do some crazy stack-frame stuff to record where this string
|
# Could do some crazy stack-frame stuff to record where this string
|
||||||
# came from...
|
# came from...
|
||||||
self.nodelist = compile_string(template_string, origin)
|
self.nodelist = compile_string(template_string, origin)
|
||||||
|
self.name = name
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for node in self.nodelist:
|
for node in self.nodelist:
|
||||||
@ -434,7 +435,7 @@ class TokenParser(object):
|
|||||||
while i < len(subject) and subject[i] != subject[p]:
|
while i < len(subject) and subject[i] != subject[p]:
|
||||||
i += 1
|
i += 1
|
||||||
if i >= len(subject):
|
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
|
i += 1
|
||||||
res = subject[p:i]
|
res = subject[p:i]
|
||||||
while i < len(subject) and subject[i] in (' ', '\t'):
|
while i < len(subject) and subject[i] in (' ', '\t'):
|
||||||
@ -548,9 +549,12 @@ class FilterExpression(object):
|
|||||||
obj = resolve_variable(self.var, context)
|
obj = resolve_variable(self.var, context)
|
||||||
except VariableDoesNotExist:
|
except VariableDoesNotExist:
|
||||||
if ignore_failures:
|
if ignore_failures:
|
||||||
return None
|
obj = None
|
||||||
else:
|
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:
|
for func, args in self.filters:
|
||||||
arg_vals = []
|
arg_vals = []
|
||||||
for lookup, arg in args:
|
for lookup, arg in args:
|
||||||
|
@ -86,7 +86,7 @@ class ForNode(Node):
|
|||||||
parentloop = {}
|
parentloop = {}
|
||||||
context.push()
|
context.push()
|
||||||
try:
|
try:
|
||||||
values = self.sequence.resolve(context)
|
values = self.sequence.resolve(context, True)
|
||||||
except VariableDoesNotExist:
|
except VariableDoesNotExist:
|
||||||
values = []
|
values = []
|
||||||
if values is None:
|
if values is None:
|
||||||
@ -212,13 +212,13 @@ class RegroupNode(Node):
|
|||||||
self.var_name = var_name
|
self.var_name = var_name
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
obj_list = self.target.resolve(context)
|
obj_list = self.target.resolve(context, True)
|
||||||
if obj_list == '': # target_var wasn't found in context; fail silently
|
if obj_list == None: # target_var wasn't found in context; fail silently
|
||||||
context[self.var_name] = []
|
context[self.var_name] = []
|
||||||
return ''
|
return ''
|
||||||
output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]}
|
output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]}
|
||||||
for obj in obj_list:
|
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?
|
# TODO: Is this a sensible way to determine equality?
|
||||||
if output and repr(output[-1]['grouper']) == repr(grouper):
|
if output and repr(output[-1]['grouper']) == repr(grouper):
|
||||||
output[-1]['list'].append(obj)
|
output[-1]['list'].append(obj)
|
||||||
|
@ -51,7 +51,7 @@ class ExtendsNode(Node):
|
|||||||
error_msg += " Got this from the %r variable." % self.parent_name_expr #TODO nice repr.
|
error_msg += " Got this from the %r variable." % self.parent_name_expr #TODO nice repr.
|
||||||
raise TemplateSyntaxError, error_msg
|
raise TemplateSyntaxError, error_msg
|
||||||
if hasattr(parent, 'render'):
|
if hasattr(parent, 'render'):
|
||||||
return parent
|
return parent # parent is a Template object
|
||||||
try:
|
try:
|
||||||
source, origin = find_template_source(parent, self.template_dirs)
|
source, origin = find_template_source(parent, self.template_dirs)
|
||||||
except TemplateDoesNotExist:
|
except TemplateDoesNotExist:
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
from cStringIO import StringIO
|
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.base import BaseHandler
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.dispatch import dispatcher
|
from django.dispatch import dispatcher
|
||||||
from django.http import urlencode, SimpleCookie
|
from django.http import urlencode, SimpleCookie
|
||||||
from django.template import signals
|
from django.test import signals
|
||||||
from django.utils.functional import curry
|
from django.utils.functional import curry
|
||||||
|
|
||||||
class ClientHandler(BaseHandler):
|
class ClientHandler(BaseHandler):
|
||||||
@ -96,7 +95,7 @@ class Client:
|
|||||||
HTML rendered to the end-user.
|
HTML rendered to the end-user.
|
||||||
"""
|
"""
|
||||||
def __init__(self, **defaults):
|
def __init__(self, **defaults):
|
||||||
self.handler = TestHandler()
|
self.handler = ClientHandler()
|
||||||
self.defaults = defaults
|
self.defaults = defaults
|
||||||
self.cookie = SimpleCookie()
|
self.cookie = SimpleCookie()
|
||||||
|
|
||||||
@ -126,7 +125,7 @@ class Client:
|
|||||||
data = {}
|
data = {}
|
||||||
on_template_render = curry(store_rendered_templates, data)
|
on_template_render = curry(store_rendered_templates, data)
|
||||||
dispatcher.connect(on_template_render, signal=signals.template_rendered)
|
dispatcher.connect(on_template_render, signal=signals.template_rendered)
|
||||||
|
|
||||||
response = self.handler(environ)
|
response = self.handler(environ)
|
||||||
|
|
||||||
# Add any rendered template detail to the response
|
# Add any rendered template detail to the response
|
||||||
@ -180,29 +179,38 @@ class Client:
|
|||||||
def login(self, path, username, password, **extra):
|
def login(self, path, username, password, **extra):
|
||||||
"""
|
"""
|
||||||
A specialized sequence of GET and POST to log into a view that
|
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
|
path should be the URL of the page that is login protected.
|
||||||
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.
|
# First, GET the page that is login protected.
|
||||||
# This is required to establish the session.
|
# This page will redirect to the login page.
|
||||||
response = self.get(path)
|
response = self.get(path)
|
||||||
|
if response.status_code != 302:
|
||||||
|
return False
|
||||||
|
|
||||||
|
login_path, data = response['Location'].split('?')
|
||||||
|
next = data.split('=')[1]
|
||||||
|
|
||||||
|
# Second, GET the login page; required to set up cookies
|
||||||
|
response = self.get(login_path, **extra)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Set up the block of form data required by the login page.
|
# Last, POST the login data.
|
||||||
form_data = {
|
form_data = {
|
||||||
'username': username,
|
'username': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
'this_is_the_login_form': 1,
|
'next' : next,
|
||||||
'post_data': _encode_post_data({LOGIN_FORM_KEY: 1})
|
|
||||||
}
|
}
|
||||||
response = self.post(path, data=form_data, **extra)
|
response = self.post(login_path, data=form_data, **extra)
|
||||||
|
|
||||||
# login page should give response 200 (if you requested the login
|
# Login page should 302 redirect to the originally requested page
|
||||||
# page specifically), or 302 (if you requested a login
|
if response.status_code != 302 or response['Location'] != path:
|
||||||
# protected page, to which the login can redirect).
|
return False
|
||||||
return response.status_code in (200,302)
|
|
||||||
|
# Since we are logged in, request the actual page again
|
||||||
|
return self.get(path)
|
||||||
|
1
django/test/signals.py
Normal file
1
django/test/signals.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
template_rendered = object()
|
@ -1,6 +1,7 @@
|
|||||||
import unittest, doctest
|
import unittest, doctest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import management
|
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.utils import create_test_db, destroy_test_db
|
||||||
from django.test.testcases import OutputChecker, DocTestRunner
|
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
|
the module. A list of 'extra' tests may also be provided; these tests
|
||||||
will be added to the test suite.
|
will be added to the test suite.
|
||||||
"""
|
"""
|
||||||
|
setup_test_environment()
|
||||||
|
|
||||||
settings.DEBUG = False
|
settings.DEBUG = False
|
||||||
suite = unittest.TestSuite()
|
suite = unittest.TestSuite()
|
||||||
@ -66,3 +68,5 @@ def run_tests(module_list, verbosity=1, extra_tests=[]):
|
|||||||
management.syncdb(verbosity, interactive=False)
|
management.syncdb(verbosity, interactive=False)
|
||||||
unittest.TextTestRunner(verbosity=verbosity).run(suite)
|
unittest.TextTestRunner(verbosity=verbosity).run(suite)
|
||||||
destroy_test_db(old_name, verbosity)
|
destroy_test_db(old_name, verbosity)
|
||||||
|
|
||||||
|
teardown_test_environment()
|
||||||
|
@ -1,11 +1,41 @@
|
|||||||
import sys, time
|
import sys, time
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from django.db import backend, connect, connection, connection_info, connections
|
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 prefix to put on the default database name when creating
|
||||||
# the test database.
|
# the test database.
|
||||||
TEST_DATABASE_PREFIX = 'test_'
|
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):
|
def _set_autocommit(connection):
|
||||||
"Make sure a connection is in autocommit mode."
|
"Make sure a connection is in autocommit mode."
|
||||||
if hasattr(connection.connection, "autocommit"):
|
if hasattr(connection.connection, "autocommit"):
|
||||||
@ -55,12 +85,13 @@ def create_test_db(verbosity=1, autoclobber=False):
|
|||||||
else:
|
else:
|
||||||
print "Tests cancelled."
|
print "Tests cancelled."
|
||||||
sys.exit(1)
|
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
|
# Get a cursor (even though we don't need one yet). This has
|
||||||
# the side effect of initializing the test database.
|
# the side effect of initializing the test database.
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
# Fill OTHER_DATABASES with the TEST_DATABASES settings,
|
# Fill OTHER_DATABASES with the TEST_DATABASES settings,
|
||||||
# and connect each named connection to the test database, using
|
# 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.
|
# connected to it.
|
||||||
if verbosity >= 1:
|
if verbosity >= 1:
|
||||||
print "Destroying test database..."
|
print "Destroying test database..."
|
||||||
if settings.DATABASE_ENGINE != "sqlite3":
|
connection.close()
|
||||||
connection.close()
|
TEST_DATABASE_NAME = settings.DATABASE_NAME
|
||||||
TEST_DATABASE_NAME = settings.DATABASE_NAME
|
settings.DATABASE_NAME = old_database_name
|
||||||
settings.DATABASE_NAME = old_database_name
|
|
||||||
|
if settings.DATABASE_ENGINE != "sqlite3":
|
||||||
settings.OTHER_DATABASES = old_databases
|
settings.OTHER_DATABASES = old_databases
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
_set_autocommit(connection)
|
_set_autocommit(connection)
|
||||||
time.sleep(1) # To avoid "database is being accessed by other users" errors.
|
time.sleep(1) # To avoid "database is being accessed by other users" errors.
|
||||||
cursor.execute("DROP DATABASE %s" % backend.quote_name(TEST_DATABASE_NAME))
|
cursor.execute("DROP DATABASE %s" % backend.quote_name(TEST_DATABASE_NAME))
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
def curry(*args, **kwargs):
|
def curry(_curried_func, *args, **kwargs):
|
||||||
def _curried(*moreargs, **morekwargs):
|
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
|
return _curried
|
||||||
|
|
||||||
class Promise:
|
class Promise:
|
||||||
|
@ -115,7 +115,7 @@ def technical_500_response(request, exc_type, exc_value, tb):
|
|||||||
'function': '?',
|
'function': '?',
|
||||||
'lineno': '?',
|
'lineno': '?',
|
||||||
}]
|
}]
|
||||||
t = Template(TECHNICAL_500_TEMPLATE)
|
t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
|
||||||
c = Context({
|
c = Context({
|
||||||
'exception_type': exc_type.__name__,
|
'exception_type': exc_type.__name__,
|
||||||
'exception_value': exc_value,
|
'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.
|
# tried exists but is an empty list. The URLconf must've been empty.
|
||||||
return empty_urlconf(request)
|
return empty_urlconf(request)
|
||||||
|
|
||||||
t = Template(TECHNICAL_404_TEMPLATE)
|
t = Template(TECHNICAL_404_TEMPLATE, name='Technical 404 template')
|
||||||
c = Context({
|
c = Context({
|
||||||
'root_urlconf': settings.ROOT_URLCONF,
|
'root_urlconf': settings.ROOT_URLCONF,
|
||||||
'urlpatterns': tried,
|
'urlpatterns': tried,
|
||||||
@ -154,7 +154,7 @@ def technical_404_response(request, exception):
|
|||||||
|
|
||||||
def empty_urlconf(request):
|
def empty_urlconf(request):
|
||||||
"Create an empty URLconf 404 error response."
|
"Create an empty URLconf 404 error response."
|
||||||
t = Template(EMPTY_URLCONF_TEMPLATE)
|
t = Template(EMPTY_URLCONF_TEMPLATE, name='Empty URLConf template')
|
||||||
c = Context({
|
c = Context({
|
||||||
'project_name': settings.SETTINGS_MODULE.split('.')[0]
|
'project_name': settings.SETTINGS_MODULE.split('.')[0]
|
||||||
})
|
})
|
||||||
@ -189,7 +189,7 @@ TECHNICAL_500_TEMPLATE = """
|
|||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
<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">
|
<style type="text/css">
|
||||||
html * { padding:0; margin:0; }
|
html * { padding:0; margin:0; }
|
||||||
body * { padding:10px 20px; }
|
body * { padding:10px 20px; }
|
||||||
@ -292,7 +292,7 @@ TECHNICAL_500_TEMPLATE = """
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id="summary">
|
<div id="summary">
|
||||||
<h1>{{ exception_type }} at {{ request.path }}</h1>
|
<h1>{{ exception_type }} at {{ request.path|escape }}</h1>
|
||||||
<h2>{{ exception_value|escape }}</h2>
|
<h2>{{ exception_value|escape }}</h2>
|
||||||
<table class="meta">
|
<table class="meta">
|
||||||
<tr>
|
<tr>
|
||||||
@ -301,7 +301,7 @@ TECHNICAL_500_TEMPLATE = """
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Request URL:</th>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Exception Type:</th>
|
<th>Exception Type:</th>
|
||||||
@ -309,7 +309,7 @@ TECHNICAL_500_TEMPLATE = """
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Exception Value:</th>
|
<th>Exception Value:</th>
|
||||||
<td>{{ exception_value }}</td>
|
<td>{{ exception_value|escape }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Exception Location:</th>
|
<th>Exception Location:</th>
|
||||||
@ -412,7 +412,7 @@ Traceback (most recent call last):<br/>
|
|||||||
{{ frame.lineno }}. {{ frame.context_line|escape }}<br/>
|
{{ frame.lineno }}. {{ frame.context_line|escape }}<br/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}<br/>
|
{% endfor %}<br/>
|
||||||
{{ exception_type }} at {{ request.path }}<br/>
|
{{ exception_type }} at {{ request.path|escape }}<br/>
|
||||||
{{ exception_value|escape }}</code>
|
{{ exception_value|escape }}</code>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -546,7 +546,7 @@ TECHNICAL_404_TEMPLATE = """
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
<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" />
|
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
html * { padding:0; margin:0; }
|
html * { padding:0; margin:0; }
|
||||||
@ -576,7 +576,7 @@ TECHNICAL_404_TEMPLATE = """
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Request URL:</th>
|
<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>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -591,7 +591,7 @@ TECHNICAL_404_TEMPLATE = """
|
|||||||
<li>{{ pattern|escape }}</li>
|
<li>{{ pattern|escape }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</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 %}
|
{% else %}
|
||||||
<p>{{ reason|escape }}</p>
|
<p>{{ reason|escape }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -81,7 +81,7 @@ def directory_index(path, fullpath):
|
|||||||
try:
|
try:
|
||||||
t = loader.get_template('static/directory_index')
|
t = loader.get_template('static/directory_index')
|
||||||
except TemplateDoesNotExist:
|
except TemplateDoesNotExist:
|
||||||
t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE)
|
t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template')
|
||||||
files = []
|
files = []
|
||||||
for f in os.listdir(fullpath):
|
for f in os.listdir(fullpath):
|
||||||
if not f.startswith('.'):
|
if not f.startswith('.'):
|
||||||
|
@ -707,7 +707,7 @@ This example creates an Atom 1.0 feed and prints it to standard output::
|
|||||||
... title=u"My Weblog",
|
... title=u"My Weblog",
|
||||||
... link=u"http://www.example.com/",
|
... link=u"http://www.example.com/",
|
||||||
... description=u"In which I write about what I ate today.",
|
... 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",
|
>>> f.add_item(title=u"Hot dog today",
|
||||||
... link=u"http://www.example.com/entries/1/",
|
... 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>")
|
... description=u"<p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p>")
|
||||||
|
@ -198,9 +198,19 @@ some things to keep in mind:
|
|||||||
How invalid variables are handled
|
How invalid variables are handled
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
If a variable doesn't exist, the template system inserts the value of the
|
Generally, if a variable doesn't exist, the template system inserts the
|
||||||
``TEMPLATE_STRING_IF_INVALID`` setting, which is set to ``''`` (the empty
|
value of the ``TEMPLATE_STRING_IF_INVALID`` setting, which is set to ``''``
|
||||||
string) by default.
|
(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
|
Playing with Context objects
|
||||||
----------------------------
|
----------------------------
|
||||||
|
210
docs/testing.txt
210
docs/testing.txt
@ -92,7 +92,8 @@ Writing unittests
|
|||||||
|
|
||||||
Like doctests, Django's unit tests use a standard library module: unittest_.
|
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
|
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::
|
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"')
|
self.assertEquals(self.cat.speak(), 'The cat says "meow"')
|
||||||
|
|
||||||
When you `run your tests`_, the test utility will find all the test cases
|
When you `run your tests`_, the test utility will find all the test cases
|
||||||
(that is, subclasses of ``unittest.TestCase``) in ``tests.py``, automatically
|
(that is, subclasses of ``unittest.TestCase``) in ``models.py`` and
|
||||||
build a test suite out of those test cases, and run that suite.
|
``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
|
For more details about ``unittest``, see the `standard library unittest
|
||||||
documentation`_.
|
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
|
app). In the end, most projects will eventually end up using both; each shines
|
||||||
in different circumstances.
|
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
|
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
|
Fixtures
|
||||||
--------
|
--------
|
||||||
|
|
||||||
Feature still to come...
|
Feature still to come...
|
||||||
|
|
||||||
|
|
||||||
Running tests
|
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
|
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:
|
testing behaviour. This behaviour involves:
|
||||||
|
|
||||||
|
#. Performing global pre-test setup
|
||||||
#. Creating the test database
|
#. Creating the test database
|
||||||
#. Running ``syncdb`` to install models and initial data into 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
|
#. 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
|
#. Running the Unit Tests and Doctests that are found
|
||||||
#. Destroying the test database.
|
#. Destroying the test database.
|
||||||
|
#. Performing global post-test teardown
|
||||||
|
|
||||||
If you define your own test runner method and point ``TEST_RUNNER``
|
If you define your own test runner method and point ``TEST_RUNNER``
|
||||||
at that method, Django will execute your test runner whenever you run
|
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:
|
arguments:
|
||||||
|
|
||||||
``run_tests(module_list, verbosity=1)``
|
``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
|
Verbosity determines the amount of notification and debug information that
|
||||||
tested. This is the same format returned by ``django.db.models.get_apps()``
|
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
|
Testing utilities
|
||||||
-----------------
|
-----------------
|
||||||
@ -278,26 +424,30 @@ Testing utilities
|
|||||||
To assist in the creation of your own test runner, Django provides
|
To assist in the creation of your own test runner, Django provides
|
||||||
a number of utility methods in the ``django.test.utils`` module.
|
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
|
``verbosity`` has the same behaviour as in the test runner.
|
||||||
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.
|
|
||||||
|
|
||||||
``create_test_db()`` has the side effect of modifying
|
``Autoclobber`` describes the behavior that will occur if a database with
|
||||||
``settings.DATABASE_NAME`` to match the name of the test database.
|
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,
|
``destroy_test_db(old_database_name, verbosity=1)``
|
||||||
and restores the value of ``settings.DATABASE_NAME`` to the provided name.
|
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.
|
||||||
|
@ -68,15 +68,34 @@ class SelfClashForeign(models.Model):
|
|||||||
foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id')
|
foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id')
|
||||||
foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe')
|
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):
|
class SelfClashM2M(models.Model):
|
||||||
src_safe = models.CharField(maxlength=10)
|
src_safe = models.CharField(maxlength=10)
|
||||||
selfclashm2m = models.CharField(maxlength=10)
|
selfclashm2m = models.CharField(maxlength=10)
|
||||||
|
|
||||||
selfclashm2m_set = models.ManyToManyField("SelfClashM2M")
|
# Non-symmetrical M2M fields _do_ have related accessors, so
|
||||||
m2m_1 = models.ManyToManyField("SelfClashM2M", related_name='id')
|
# there is potential for clashes.
|
||||||
m2m_2 = models.ManyToManyField("SelfClashM2M", related_name='src_safe')
|
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.
|
model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "maxlength" attribute.
|
||||||
invalid_models.fielderrors: "floatfield": FloatFields require a "decimal_places" attribute.
|
invalid_models.fielderrors: "floatfield": FloatFields require a "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.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: 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: 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_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: 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_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: 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'.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
0
tests/modeltests/test_client/__init__.py
Normal file
0
tests/modeltests/test_client/__init__.py
Normal file
10
tests/modeltests/test_client/management.py
Normal file
10
tests/modeltests/test_client/management.py
Normal file
@ -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)
|
101
tests/modeltests/test_client/models.py
Normal file
101
tests/modeltests/test_client/models.py
Normal file
@ -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)
|
9
tests/modeltests/test_client/urls.py
Normal file
9
tests/modeltests/test_client/urls.py
Normal file
@ -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),
|
||||||
|
)
|
35
tests/modeltests/test_client/views.py
Normal file
35
tests/modeltests/test_client/views.py
Normal file
@ -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))
|
@ -84,7 +84,7 @@ class Templates(unittest.TestCase):
|
|||||||
'basic-syntax03': ("{{ first }} --- {{ second }}", {"first" : 1, "second" : 2}, "1 --- 2"),
|
'basic-syntax03': ("{{ first }} --- {{ second }}", {"first" : 1, "second" : 2}, "1 --- 2"),
|
||||||
|
|
||||||
# Fail silently when a variable is not found in the current context
|
# 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
|
# A variable may not contain more than one word
|
||||||
'basic-syntax06': ("{{ multi word variable }}", {}, template.TemplateSyntaxError),
|
'basic-syntax06': ("{{ multi word variable }}", {}, template.TemplateSyntaxError),
|
||||||
@ -100,7 +100,7 @@ class Templates(unittest.TestCase):
|
|||||||
'basic-syntax10': ("{{ var.otherclass.method }}", {"var": SomeClass()}, "OtherClass.method"),
|
'basic-syntax10': ("{{ var.otherclass.method }}", {"var": SomeClass()}, "OtherClass.method"),
|
||||||
|
|
||||||
# Fail silently when a variable's attribute isn't found
|
# 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
|
# Raise TemplateSyntaxError when trying to access a variable beginning with an underscore
|
||||||
'basic-syntax12': ("{{ var.__dict__ }}", {"var": SomeClass()}, template.TemplateSyntaxError),
|
'basic-syntax12': ("{{ var.__dict__ }}", {"var": SomeClass()}, template.TemplateSyntaxError),
|
||||||
@ -116,10 +116,10 @@ class Templates(unittest.TestCase):
|
|||||||
'basic-syntax18': ("{{ foo.bar }}", {"foo" : {"bar" : "baz"}}, "baz"),
|
'basic-syntax18': ("{{ foo.bar }}", {"foo" : {"bar" : "baz"}}, "baz"),
|
||||||
|
|
||||||
# Fail silently when a variable's dictionary key isn't found
|
# 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
|
# 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 filter usage
|
||||||
'basic-syntax21': ("{{ var|upper }}", {"var": "Django is the greatest!"}, "DJANGO IS THE GREATEST!"),
|
'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'),
|
'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
|
# 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,
|
# In methods that raise an exception without a "silent_variable_attribute" set to True,
|
||||||
# the exception propogates
|
# the exception propogates
|
||||||
@ -464,6 +464,14 @@ class Templates(unittest.TestCase):
|
|||||||
# translation of a constant string
|
# translation of a constant string
|
||||||
'i18n13': ('{{ _("Page not found") }}', {'LANGUAGE_CODE': 'de'}, 'Seite nicht gefunden'),
|
'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 #############################################################
|
### MULTILINE #############################################################
|
||||||
|
|
||||||
'multiline01': ("""
|
'multiline01': ("""
|
||||||
@ -507,7 +515,7 @@ class Templates(unittest.TestCase):
|
|||||||
'{{ item.foo }}' + \
|
'{{ item.foo }}' + \
|
||||||
'{% endfor %},' + \
|
'{% endfor %},' + \
|
||||||
'{% endfor %}',
|
'{% endfor %}',
|
||||||
{}, 'INVALID:INVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALID,'),
|
{}, ''),
|
||||||
|
|
||||||
### TEMPLATETAG TAG #######################################################
|
### TEMPLATETAG TAG #######################################################
|
||||||
'templatetag01': ('{% templatetag openblock %}', {}, '{%'),
|
'templatetag01': ('{% templatetag openblock %}', {}, '{%'),
|
||||||
@ -592,30 +600,44 @@ class Templates(unittest.TestCase):
|
|||||||
old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
|
old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
|
||||||
|
|
||||||
# Set TEMPLATE_STRING_IF_INVALID to a known string
|
# 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:
|
for name, vals in tests:
|
||||||
install()
|
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]:
|
if 'LANGUAGE_CODE' in vals[1]:
|
||||||
activate(vals[1]['LANGUAGE_CODE'])
|
activate(vals[1]['LANGUAGE_CODE'])
|
||||||
else:
|
else:
|
||||||
activate('en-us')
|
activate('en-us')
|
||||||
try:
|
|
||||||
output = loader.get_template(name).render(template.Context(vals[1]))
|
for invalid_str, result in [('', normal_string_result),
|
||||||
except Exception, e:
|
('INVALID', invalid_string_result)]:
|
||||||
if e.__class__ != vals[2]:
|
settings.TEMPLATE_STRING_IF_INVALID = invalid_str
|
||||||
failures.append("Template test: %s -- FAILED. Got %s, exception: %s" % (name, e.__class__, e))
|
try:
|
||||||
continue
|
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]:
|
if 'LANGUAGE_CODE' in vals[1]:
|
||||||
deactivate()
|
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
|
loader.template_source_loaders = old_template_loaders
|
||||||
deactivate()
|
deactivate()
|
||||||
settings.TEMPLATE_DEBUG = old_td
|
settings.TEMPLATE_DEBUG = old_td
|
||||||
settings.TEMPLATE_STRING_IF_INVALID = old_invalid
|
settings.TEMPLATE_STRING_IF_INVALID = old_invalid
|
||||||
|
|
||||||
self.assertEqual(failures, [])
|
self.assertEqual(failures, [], '\n'.join(failures))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -34,8 +34,9 @@ except ImportError:
|
|||||||
# Import copy of _thread_local.py from Python 2.4
|
# Import copy of _thread_local.py from Python 2.4
|
||||||
from django.utils._threading_local import local
|
from django.utils._threading_local import local
|
||||||
|
|
||||||
|
|
||||||
# helpers
|
# helpers
|
||||||
|
EV = threading.Event()
|
||||||
|
|
||||||
class LocalSettings:
|
class LocalSettings:
|
||||||
"""Settings holder that allows thread-local overrides of defaults.
|
"""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 ODB: %s", settings.OTHER_DATABASES)
|
||||||
debug("t2 waiting")
|
debug("t2 waiting")
|
||||||
ev.wait(2.0)
|
EV.wait(2.0)
|
||||||
func(*arg)
|
func(*arg)
|
||||||
debug("t2 complete")
|
debug("t2 complete")
|
||||||
t2 = threading.Thread(target=start)
|
t2 = threading.Thread(target=start)
|
||||||
@ -94,7 +95,7 @@ def thread_three(func, *arg):
|
|||||||
connection.settings.DATABASE_NAME)
|
connection.settings.DATABASE_NAME)
|
||||||
|
|
||||||
debug("t3 waiting")
|
debug("t3 waiting")
|
||||||
ev.wait(2.0)
|
EV.wait(2.0)
|
||||||
func(*arg)
|
func(*arg)
|
||||||
debug("t3 complete")
|
debug("t3 complete")
|
||||||
t3 = threading.Thread(target=start)
|
t3 = threading.Thread(target=start)
|
||||||
@ -113,7 +114,6 @@ def start_response(code, headers):
|
|||||||
class TestThreadIsolation(unittest.TestCase):
|
class TestThreadIsolation(unittest.TestCase):
|
||||||
# event used to synchronize threads so we can be sure they are running
|
# event used to synchronize threads so we can be sure they are running
|
||||||
# together
|
# together
|
||||||
ev = threading.Event()
|
|
||||||
lock = threading.RLock()
|
lock = threading.RLock()
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
@ -235,7 +235,7 @@ class TestThreadIsolation(unittest.TestCase):
|
|||||||
t3 = thread_three(MockHandler(self.request_three), env, start_response)
|
t3 = thread_three(MockHandler(self.request_three), env, start_response)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.ev.set()
|
EV.set()
|
||||||
MockHandler(self.request_one)(env, start_response)
|
MockHandler(self.request_one)(env, start_response)
|
||||||
finally:
|
finally:
|
||||||
t2.join()
|
t2.join()
|
||||||
|
@ -6,6 +6,7 @@ import unittest
|
|||||||
MODEL_TESTS_DIR_NAME = 'modeltests'
|
MODEL_TESTS_DIR_NAME = 'modeltests'
|
||||||
REGRESSION_TESTS_DIR_NAME = 'regressiontests'
|
REGRESSION_TESTS_DIR_NAME = 'regressiontests'
|
||||||
TEST_DATABASE_NAME = 'django_test_db'
|
TEST_DATABASE_NAME = 'django_test_db'
|
||||||
|
TEST_TEMPLATE_DIR = 'templates'
|
||||||
|
|
||||||
TEST_DATABASES = ('_a', '_b')
|
TEST_DATABASES = ('_a', '_b')
|
||||||
|
|
||||||
@ -81,20 +82,25 @@ class InvalidModelTestCase(unittest.TestCase):
|
|||||||
def django_tests(verbosity, tests_to_run):
|
def django_tests(verbosity, tests_to_run):
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models.loading import get_apps, load_app
|
from django.db.models.loading import get_apps, load_app
|
||||||
|
|
||||||
old_installed_apps = settings.INSTALLED_APPS
|
old_installed_apps = settings.INSTALLED_APPS
|
||||||
old_test_database_name = settings.TEST_DATABASE_NAME
|
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.TEST_DATABASE_NAME = TEST_DATABASE_NAME
|
||||||
settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS
|
settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS
|
||||||
|
|
||||||
settings.TEST_DATABASES = TEST_DATABASES
|
settings.TEST_DATABASES = TEST_DATABASES
|
||||||
settings.TEST_DATABASE_MODELS = TEST_DATABASE_MODELS
|
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
|
# load all the ALWAYS_INSTALLED_APPS
|
||||||
get_apps()
|
get_apps()
|
||||||
|
|
||||||
test_models = []
|
|
||||||
# Load all the test model apps
|
# Load all the test model apps
|
||||||
|
test_models = []
|
||||||
for model_dir, model_name in get_test_models():
|
for model_dir, model_name in get_test_models():
|
||||||
model_label = '.'.join([model_dir, model_name])
|
model_label = '.'.join([model_dir, model_name])
|
||||||
try:
|
try:
|
||||||
@ -125,6 +131,9 @@ def django_tests(verbosity, tests_to_run):
|
|||||||
# Restore the old settings
|
# Restore the old settings
|
||||||
settings.INSTALLED_APPS = old_installed_apps
|
settings.INSTALLED_APPS = old_installed_apps
|
||||||
settings.TESTS_DATABASE_NAME = old_test_database_name
|
settings.TESTS_DATABASE_NAME = old_test_database_name
|
||||||
|
settings.ROOT_URLCONF = old_root_urlconf
|
||||||
|
settings.TEMPLATE_DIRS = old_template_dirs
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
usage = "%prog [options] [model model model ...]"
|
usage = "%prog [options] [model model model ...]"
|
||||||
|
1
tests/templates/404.html
Normal file
1
tests/templates/404.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
Django Internal Tests: 404 Error
|
1
tests/templates/500.html
Normal file
1
tests/templates/500.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
Django Internal Tests: 500 Error
|
19
tests/templates/login.html
Normal file
19
tests/templates/login.html
Normal file
@ -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>
|
10
tests/urls.py
Normal file
10
tests/urls.py
Normal file
@ -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'),
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user