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 }}