diff --git a/AUTHORS b/AUTHORS
index e3db830b74..4d57503bf8 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -104,7 +104,6 @@ answer newbie questions, and generally made Django that much better:
mattycakes@gmail.com
Jason McBrayer
michael.mcewan@gmail.com
- mir@noris.de
mmarshall
Eric Moritz
Robin Munn
@@ -121,12 +120,14 @@ answer newbie questions, and generally made Django that much better:
plisk
Daniel Poelzleithner
J. Rademaker
+ Michael Radziej
Brian Ray
rhettg@gmail.com
Oliver Rutherfurd
Ivan Sagalaev (Maniac)
David Schein
Pete Shinners
+ SmileyChris
sopel
Thomas Steinacher
Radek Švarz
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=''):
"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 != 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:
return False
-
- # Set up the block of form data required by the login page.
+
+ # 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 = """
- {{ exception_type }} at {{ request.path }}
+ {{ exception_type }} at {{ request.path|escape }}