diff options
| author | Jason Pellerin <jpellerin@gmail.com> | 2006-09-08 16:35:39 +0000 |
|---|---|---|
| committer | Jason Pellerin <jpellerin@gmail.com> | 2006-09-08 16:35:39 +0000 |
| commit | 84f7a2133c4d553a234165bb8cbfaf70681bb028 (patch) | |
| tree | 53d08c1ffc275a4e1d62d7f48ea81172a5919051 | |
| parent | ae3896cb74d4bc42acaf6fade0d2a57e28045b2a (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
34 files changed, 610 insertions, 157 deletions
@@ -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/> {{ frame.lineno }}. {{ frame.context_line|escape }}<br/> {% endif %} {% endfor %}<br/> - {{ exception_type }} at {{ request.path }}<br/> + {{ exception_type }} at {{ request.path|escape }}<br/> {{ 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'), +) |
