summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorAndrew Godwin <andrew@aeracode.org>2013-08-19 18:30:48 +0100
committerAndrew Godwin <andrew@aeracode.org>2013-08-19 18:30:48 +0100
commitb6a957f0ba8a2ed1b24d7ee042a9c4beaf51ab03 (patch)
tree87d42b9e8d3d4c1516b53eee1d9332735762826a /django
parent52edc16086e3c28a78c31975bb4da2f9450590b4 (diff)
parent3c0300405009b82b52fd15483371097221662fcd (diff)
Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts: docs/ref/django-admin.txt
Diffstat (limited to 'django')
-rw-r--r--django/conf/global_settings.py5
-rw-r--r--django/contrib/admin/static/admin/css/dashboard.css4
-rw-r--r--django/contrib/admin/util.py26
-rw-r--r--django/contrib/admin/widgets.py4
-rw-r--r--django/contrib/auth/decorators.py6
-rw-r--r--django/contrib/auth/models.py4
-rw-r--r--django/contrib/auth/tests/test_decorators.py58
-rw-r--r--django/contrib/auth/tests/test_forms.py21
-rw-r--r--django/contrib/auth/tests/test_management.py11
-rw-r--r--django/contrib/auth/tests/test_models.py26
-rw-r--r--django/contrib/auth/tests/test_views.py8
-rw-r--r--django/contrib/gis/db/backends/postgis/introspection.py8
-rw-r--r--django/contrib/gis/db/models/sql/query.py4
-rw-r--r--django/contrib/gis/tests/geoapp/models.py2
-rw-r--r--django/contrib/humanize/tests.py29
-rw-r--r--django/core/checks/compatibility/django_1_6_0.py29
-rw-r--r--django/core/files/storage.py11
-rw-r--r--django/db/backends/postgresql_psycopg2/introspection.py4
-rw-r--r--django/db/models/base.py25
-rw-r--r--django/db/models/query.py5
-rw-r--r--django/db/models/sql/compiler.py5
-rw-r--r--django/db/models/sql/query.py160
-rw-r--r--django/forms/forms.py4
-rw-r--r--django/forms/widgets.py15
-rw-r--r--django/template/base.py5
-rw-r--r--django/template/defaulttags.py35
-rw-r--r--django/test/client.py68
-rw-r--r--django/utils/functional.py15
-rw-r--r--django/utils/http.py10
-rw-r--r--django/views/debug.py6
30 files changed, 400 insertions, 213 deletions
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 364aa10320..4e8430aceb 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -313,6 +313,11 @@ FILE_UPLOAD_TEMP_DIR = None
# you'd pass directly to os.chmod; see http://docs.python.org/lib/os-file-dir.html.
FILE_UPLOAD_PERMISSIONS = None
+# The numeric mode to assign to newly-created directories, when uploading files.
+# The value should be a mode as you'd pass to os.chmod;
+# see http://docs.python.org/lib/os-file-dir.html.
+FILE_UPLOAD_DIRECTORY_PERMISSIONS = None
+
# Python module path where user will place custom format definition.
# The directory where this setting is pointing should contain subdirectories
# named as the locales, containing a formats.py file
diff --git a/django/contrib/admin/static/admin/css/dashboard.css b/django/contrib/admin/static/admin/css/dashboard.css
index ceefe1525f..05808bcb0a 100644
--- a/django/contrib/admin/static/admin/css/dashboard.css
+++ b/django/contrib/admin/static/admin/css/dashboard.css
@@ -23,8 +23,8 @@ ul.actionlist li {
list-style-type: none;
}
-ul.actionlist li.changelink {
+ul.actionlist li {
overflow: hidden;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
-} \ No newline at end of file
+}
diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
index dd9047c428..a36c8c13c8 100644
--- a/django/contrib/admin/util.py
+++ b/django/contrib/admin/util.py
@@ -16,7 +16,7 @@ from django.utils import timezone
from django.utils.encoding import force_str, force_text, smart_text
from django.utils import six
from django.utils.translation import ungettext
-from django.core.urlresolvers import reverse
+from django.core.urlresolvers import reverse, NoReverseMatch
def lookup_needs_distinct(opts, lookup_path):
"""
@@ -113,12 +113,20 @@ def get_deleted_objects(objs, opts, user, admin_site, using):
has_admin = obj.__class__ in admin_site._registry
opts = obj._meta
+ no_edit_link = '%s: %s' % (capfirst(opts.verbose_name),
+ force_text(obj))
+
if has_admin:
- admin_url = reverse('%s:%s_%s_change'
- % (admin_site.name,
- opts.app_label,
- opts.model_name),
- None, (quote(obj._get_pk_val()),))
+ try:
+ admin_url = reverse('%s:%s_%s_change'
+ % (admin_site.name,
+ opts.app_label,
+ opts.model_name),
+ None, (quote(obj._get_pk_val()),))
+ except NoReverseMatch:
+ # Change url doesn't exist -- don't display link to edit
+ return no_edit_link
+
p = '%s.%s' % (opts.app_label,
get_permission_codename('delete', opts))
if not user.has_perm(p):
@@ -131,8 +139,7 @@ def get_deleted_objects(objs, opts, user, admin_site, using):
else:
# Don't display link to edit, because it either has no
# admin or is edited inline.
- return '%s: %s' % (capfirst(opts.verbose_name),
- force_text(obj))
+ return no_edit_link
to_delete = collector.nested(format_callback)
@@ -155,9 +162,6 @@ class NestedObjects(Collector):
if source_attr:
self.add_edge(getattr(obj, source_attr), obj)
else:
- if obj._meta.proxy:
- # Take concrete model's instance to avoid mismatch in edges
- obj = obj._meta.concrete_model(pk=obj.pk)
self.add_edge(None, obj)
try:
return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs)
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index c4b15cdd6a..5773db6394 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -305,9 +305,9 @@ class AdminURLFieldWidget(forms.URLInput):
html = super(AdminURLFieldWidget, self).render(name, value, attrs)
if value:
value = force_text(self._format_value(value))
- final_attrs = {'href': mark_safe(smart_urlquote(value))}
+ final_attrs = {'href': smart_urlquote(value)}
html = format_html(
- '<p class="url">{0} <a {1}>{2}</a><br />{3} {4}</p>',
+ '<p class="url">{0} <a{1}>{2}</a><br />{3} {4}</p>',
_('Currently:'), flatatt(final_attrs), value,
_('Change:'), html
)
diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py
index 11518193e7..24e31144b1 100644
--- a/django/contrib/auth/decorators.py
+++ b/django/contrib/auth/decorators.py
@@ -64,8 +64,12 @@ def permission_required(perm, login_url=None, raise_exception=False):
is raised.
"""
def check_perms(user):
+ if not isinstance(perm, (list, tuple)):
+ perms = (perm, )
+ else:
+ perms = perm
# First check if the user has the permission (even anon users)
- if user.has_perm(perm):
+ if user.has_perms(perms):
return True
# In case the 403 handler should be called raise the exception
if raise_exception:
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
index cf3d37e5c3..cf1ca8ca1f 100644
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -400,11 +400,11 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin):
"Returns the short name for the user."
return self.first_name
- def email_user(self, subject, message, from_email=None):
+ def email_user(self, subject, message, from_email=None, **kwargs):
"""
Sends an email to this User.
"""
- send_mail(subject, message, from_email, [self.email])
+ send_mail(subject, message, from_email, [self.email], **kwargs)
class User(AbstractUser):
diff --git a/django/contrib/auth/tests/test_decorators.py b/django/contrib/auth/tests/test_decorators.py
index 6d6d335354..22ad933644 100644
--- a/django/contrib/auth/tests/test_decorators.py
+++ b/django/contrib/auth/tests/test_decorators.py
@@ -1,7 +1,12 @@
from django.conf import settings
-from django.contrib.auth.decorators import login_required
+from django.contrib.auth import models
+from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.tests.test_views import AuthViewsTestCase
from django.contrib.auth.tests.utils import skipIfCustomUser
+from django.core.exceptions import PermissionDenied
+from django.http import HttpResponse
+from django.test import TestCase
+from django.test.client import RequestFactory
@skipIfCustomUser
@@ -49,3 +54,54 @@ class LoginRequiredTestCase(AuthViewsTestCase):
"""
self.testLoginRequired(view_url='/login_required_login_url/',
login_url='/somewhere/')
+
+
+class PermissionsRequiredDecoratorTest(TestCase):
+ """
+ Tests for the permission_required decorator
+ """
+ def setUp(self):
+ self.user = models.User.objects.create(username='joe', password='qwerty')
+ self.factory = RequestFactory()
+ # Add permissions auth.add_customuser and auth.change_customuser
+ perms = models.Permission.objects.filter(codename__in=('add_customuser', 'change_customuser'))
+ self.user.user_permissions.add(*perms)
+
+ def test_many_permissions_pass(self):
+
+ @permission_required(['auth.add_customuser', 'auth.change_customuser'])
+ def a_view(request):
+ return HttpResponse()
+ request = self.factory.get('/rand')
+ request.user = self.user
+ resp = a_view(request)
+ self.assertEqual(resp.status_code, 200)
+
+ def test_single_permission_pass(self):
+
+ @permission_required('auth.add_customuser')
+ def a_view(request):
+ return HttpResponse()
+ request = self.factory.get('/rand')
+ request.user = self.user
+ resp = a_view(request)
+ self.assertEqual(resp.status_code, 200)
+
+ def test_permissioned_denied_redirect(self):
+
+ @permission_required(['auth.add_customuser', 'auth.change_customuser', 'non-existant-permission'])
+ def a_view(request):
+ return HttpResponse()
+ request = self.factory.get('/rand')
+ request.user = self.user
+ resp = a_view(request)
+ self.assertEqual(resp.status_code, 302)
+
+ def test_permissioned_denied_exception_raised(self):
+
+ @permission_required(['auth.add_customuser', 'auth.change_customuser', 'non-existant-permission'], raise_exception=True)
+ def a_view(request):
+ return HttpResponse()
+ request = self.factory.get('/rand')
+ request.user = self.user
+ self.assertRaises(PermissionDenied, a_view, request)
diff --git a/django/contrib/auth/tests/test_forms.py b/django/contrib/auth/tests/test_forms.py
index eef366f184..fa21d2f917 100644
--- a/django/contrib/auth/tests/test_forms.py
+++ b/django/contrib/auth/tests/test_forms.py
@@ -121,17 +121,16 @@ class AuthenticationFormTest(TestCase):
[force_text(form.error_messages['inactive'])])
def test_inactive_user_i18n(self):
- with self.settings(USE_I18N=True):
- with translation.override('pt-br', deactivate=True):
- # The user is inactive.
- data = {
- 'username': 'inactive',
- 'password': 'password',
- }
- form = AuthenticationForm(None, data)
- self.assertFalse(form.is_valid())
- self.assertEqual(form.non_field_errors(),
- [force_text(form.error_messages['inactive'])])
+ with self.settings(USE_I18N=True), translation.override('pt-br', deactivate=True):
+ # The user is inactive.
+ data = {
+ 'username': 'inactive',
+ 'password': 'password',
+ }
+ form = AuthenticationForm(None, data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.non_field_errors(),
+ [force_text(form.error_messages['inactive'])])
def test_custom_login_allowed_policy(self):
# The user is inactive, but our custom form policy allows him to log in.
diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py
index 3711f52dea..91a6a589c1 100644
--- a/django/contrib/auth/tests/test_management.py
+++ b/django/contrib/auth/tests/test_management.py
@@ -239,21 +239,22 @@ class PermissionTestCase(TestCase):
create_permissions(models, [], verbosity=0)
def test_default_permissions(self):
+ permission_content_type = ContentType.objects.get_by_natural_key('auth', 'permission')
models.Permission._meta.permissions = [
('my_custom_permission', 'Some permission'),
]
create_permissions(models, [], verbosity=0)
# add/change/delete permission by default + custom permission
- self.assertEqual(models.Permission.objects.filter(content_type=
- ContentType.objects.get_by_natural_key('auth', 'permission')
+ self.assertEqual(models.Permission.objects.filter(
+ content_type=permission_content_type,
).count(), 4)
- models.Permission.objects.all().delete()
+ models.Permission.objects.filter(content_type=permission_content_type).delete()
models.Permission._meta.default_permissions = []
create_permissions(models, [], verbosity=0)
# custom permission only since default permissions is empty
- self.assertEqual(models.Permission.objects.filter(content_type=
- ContentType.objects.get_by_natural_key('auth', 'permission')
+ self.assertEqual(models.Permission.objects.filter(
+ content_type=permission_content_type,
).count(), 1)
diff --git a/django/contrib/auth/tests/test_models.py b/django/contrib/auth/tests/test_models.py
index fa20775a8d..1373a3c1c1 100644
--- a/django/contrib/auth/tests/test_models.py
+++ b/django/contrib/auth/tests/test_models.py
@@ -1,6 +1,7 @@
from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group, User, UserManager
+from django.contrib.auth.models import AbstractUser, Group, User, UserManager
from django.contrib.auth.tests.utils import skipIfCustomUser
+from django.core import mail
from django.db.models.signals import post_save
from django.test import TestCase
from django.test.utils import override_settings
@@ -73,6 +74,29 @@ class UserManagerTestCase(TestCase):
User.objects.create_user, username='')
+class AbstractUserTestCase(TestCase):
+ def test_email_user(self):
+ # valid send_mail parameters
+ kwargs = {
+ "fail_silently": False,
+ "auth_user": None,
+ "auth_password": None,
+ "connection": None,
+ "html_message": None,
+ }
+ abstract_user = AbstractUser(email='foo@bar.com')
+ abstract_user.email_user(subject="Subject here",
+ message="This is a message", from_email="from@domain.com", **kwargs)
+ # Test that one message has been sent.
+ self.assertEqual(len(mail.outbox), 1)
+ # Verify that test email contains the correct attributes:
+ message = mail.outbox[0]
+ self.assertEqual(message.subject, "Subject here")
+ self.assertEqual(message.body, "This is a message")
+ self.assertEqual(message.from_email, "from@domain.com")
+ self.assertEqual(message.to, [abstract_user.email])
+
+
class IsActiveTestCase(TestCase):
"""
Tests the behavior of the guaranteed is_active attribute
diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py
index 22ccbfd225..7839b0b9f9 100644
--- a/django/contrib/auth/tests/test_views.py
+++ b/django/contrib/auth/tests/test_views.py
@@ -446,7 +446,8 @@ class LoginTest(AuthViewsTestCase):
for bad_url in ('http://example.com',
'https://example.com',
'ftp://exampel.com',
- '//example.com'):
+ '//example.com',
+ 'javascript:alert("XSS")'):
nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
'url': login_url,
@@ -467,6 +468,7 @@ class LoginTest(AuthViewsTestCase):
'/view?param=ftp://exampel.com',
'view/?param=//example.com',
'https:///',
+ 'HTTPS:///',
'//testserver/',
'/url%20with%20spaces/'): # see ticket #12534
safe_url = '%(url)s?%(next)s=%(good_url)s' % {
@@ -661,7 +663,8 @@ class LogoutTest(AuthViewsTestCase):
for bad_url in ('http://example.com',
'https://example.com',
'ftp://exampel.com',
- '//example.com'):
+ '//example.com',
+ 'javascript:alert("XSS")'):
nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
'url': logout_url,
'next': REDIRECT_FIELD_NAME,
@@ -680,6 +683,7 @@ class LogoutTest(AuthViewsTestCase):
'/view?param=ftp://exampel.com',
'view/?param=//example.com',
'https:///',
+ 'HTTPS:///',
'//testserver/',
'/url%20with%20spaces/'): # see ticket #12534
safe_url = '%(url)s?%(next)s=%(good_url)s' % {
diff --git a/django/contrib/gis/db/backends/postgis/introspection.py b/django/contrib/gis/db/backends/postgis/introspection.py
index 7962d19ff9..7df09d0937 100644
--- a/django/contrib/gis/db/backends/postgis/introspection.py
+++ b/django/contrib/gis/db/backends/postgis/introspection.py
@@ -9,6 +9,14 @@ class PostGISIntrospection(DatabaseIntrospection):
# introspection is actually performed.
postgis_types_reverse = {}
+ ignored_tables = DatabaseIntrospection.ignored_tables + [
+ 'geography_columns',
+ 'geometry_columns',
+ 'raster_columns',
+ 'spatial_ref_sys',
+ 'raster_overviews',
+ ]
+
def get_postgis_types(self):
"""
Returns a dictionary with keys that are the PostgreSQL object
diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py
index 5877f2975a..93a7642bbb 100644
--- a/django/contrib/gis/db/models/sql/query.py
+++ b/django/contrib/gis/db/models/sql/query.py
@@ -76,7 +76,7 @@ class GeoQuery(sql.Query):
return super(GeoQuery, self).convert_values(value, field, connection)
return value
- def get_aggregation(self, using):
+ def get_aggregation(self, using, force_subq=False):
# Remove any aggregates marked for reduction from the subquery
# and move them to the outer AggregateQuery.
connection = connections[using]
@@ -84,7 +84,7 @@ class GeoQuery(sql.Query):
if isinstance(aggregate, gis_aggregates.GeoAggregate):
if not getattr(aggregate, 'is_extent', False) or connection.ops.oracle:
self.extra_select_fields[alias] = GeomField()
- return super(GeoQuery, self).get_aggregation(using)
+ return super(GeoQuery, self).get_aggregation(using, force_subq)
def resolve_aggregate(self, value, aggregate, connection):
"""
diff --git a/django/contrib/gis/tests/geoapp/models.py b/django/contrib/gis/tests/geoapp/models.py
index abde509c8b..fa83859063 100644
--- a/django/contrib/gis/tests/geoapp/models.py
+++ b/django/contrib/gis/tests/geoapp/models.py
@@ -40,7 +40,7 @@ class Track(models.Model):
def __str__(self): return self.name
class Truth(models.Model):
- val = models.BooleanField()
+ val = models.BooleanField(default=False)
objects = models.GeoManager()
if not spatialite:
diff --git a/django/contrib/humanize/tests.py b/django/contrib/humanize/tests.py
index f1fcbf2bb3..02f73afadb 100644
--- a/django/contrib/humanize/tests.py
+++ b/django/contrib/humanize/tests.py
@@ -77,15 +77,14 @@ class HumanizeTests(TransRealMixin, TestCase):
'100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.1234567', '1,234,567.1234567',
None)
- with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=False):
- with translation.override('en'):
- self.humanize_tester(test_list, result_list, 'intcomma')
+ with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=False), \
+ translation.override('en'):
+ self.humanize_tester(test_list, result_list, 'intcomma')
def test_intcomma_without_number_grouping(self):
# Regression for #17414
- with translation.override('ja'):
- with self.settings(USE_L10N=True):
- self.humanize_tester([100], ['100'], 'intcomma')
+ with translation.override('ja'), self.settings(USE_L10N=True):
+ self.humanize_tester([100], ['100'], 'intcomma')
def test_intword(self):
test_list = ('100', '1000000', '1200000', '1290000',
@@ -104,18 +103,18 @@ class HumanizeTests(TransRealMixin, TestCase):
'100', '1000', '10123', '10311', '1000000', None)
result_list = ('100', '1.000', '10.123', '10.311', '1.000.000', '1.234.567,25',
'100', '1.000', '10.123', '10.311', '1.000.000', None)
- with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True):
- with translation.override('de'):
- self.humanize_tester(test_list, result_list, 'intcomma')
+ with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True), \
+ translation.override('de'):
+ self.humanize_tester(test_list, result_list, 'intcomma')
def test_i18n_intword(self):
test_list = ('100', '1000000', '1200000', '1290000',
'1000000000', '2000000000', '6000000000000')
result_list = ('100', '1,0 Million', '1,2 Millionen', '1,3 Millionen',
'1,0 Milliarde', '2,0 Milliarden', '6,0 Billionen')
- with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True):
- with translation.override('de'):
- self.humanize_tester(test_list, result_list, 'intword')
+ with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True), \
+ translation.override('de'):
+ self.humanize_tester(test_list, result_list, 'intword')
def test_apnumber(self):
test_list = [str(x) for x in range(1, 11)]
@@ -162,9 +161,9 @@ class HumanizeTests(TransRealMixin, TestCase):
orig_humanize_datetime, humanize.datetime = humanize.datetime, MockDateTime
try:
- with override_settings(TIME_ZONE="America/Chicago", USE_TZ=True):
- with translation.override('en'):
- self.humanize_tester([dt], ['yesterday'], 'naturalday')
+ with override_settings(TIME_ZONE="America/Chicago", USE_TZ=True), \
+ translation.override('en'):
+ self.humanize_tester([dt], ['yesterday'], 'naturalday')
finally:
humanize.datetime = orig_humanize_datetime
diff --git a/django/core/checks/compatibility/django_1_6_0.py b/django/core/checks/compatibility/django_1_6_0.py
index 1998c5ba77..e38b2d32ec 100644
--- a/django/core/checks/compatibility/django_1_6_0.py
+++ b/django/core/checks/compatibility/django_1_6_0.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
+from django.db import models
def check_test_runner():
"""
@@ -24,6 +25,31 @@ def check_test_runner():
]
return ' '.join(message)
+def check_boolean_field_default_value():
+ """
+ Checks if there are any BooleanFields without a default value, &
+ warns the user that the default has changed from False to Null.
+ """
+ fields = []
+ for cls in models.get_models():
+ opts = cls._meta
+ for f in opts.local_fields:
+ if isinstance(f, models.BooleanField) and not f.has_default():
+ fields.append(
+ '%s.%s: "%s"' % (opts.app_label, opts.object_name, f.name)
+ )
+ if fields:
+ fieldnames = ", ".join(fields)
+ message = [
+ "You have not set a default value for one or more BooleanFields:",
+ "%s." % fieldnames,
+ "In Django 1.6 the default value of BooleanField was changed from",
+ "False to Null when Field.default isn't defined. See",
+ "https://docs.djangoproject.com/en/1.6/ref/models/fields/#booleanfield"
+ "for more information."
+ ]
+ return ' '.join(message)
+
def run_checks():
"""
@@ -31,7 +57,8 @@ def run_checks():
messages from all the relevant check functions for this version of Django.
"""
checks = [
- check_test_runner()
+ check_test_runner(),
+ check_boolean_field_default_value(),
]
# Filter out the ``None`` or empty strings.
return [output for output in checks if output]
diff --git a/django/core/files/storage.py b/django/core/files/storage.py
index 5d301a317c..5e587da2da 100644
--- a/django/core/files/storage.py
+++ b/django/core/files/storage.py
@@ -172,7 +172,16 @@ class FileSystemStorage(Storage):
directory = os.path.dirname(full_path)
if not os.path.exists(directory):
try:
- os.makedirs(directory)
+ if settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS is not None:
+ # os.makedirs applies the global umask, so we reset it,
+ # for consistency with FILE_UPLOAD_PERMISSIONS behavior.
+ old_umask = os.umask(0)
+ try:
+ os.makedirs(directory, settings.FILE_UPLOAD_DIRECTORY_PERMISSIONS)
+ finally:
+ os.umask(old_umask)
+ else:
+ os.makedirs(directory)
except OSError as e:
if e.errno != errno.EEXIST:
raise
diff --git a/django/db/backends/postgresql_psycopg2/introspection.py b/django/db/backends/postgresql_psycopg2/introspection.py
index 3e2574b0c1..57d9a67abf 100644
--- a/django/db/backends/postgresql_psycopg2/introspection.py
+++ b/django/db/backends/postgresql_psycopg2/introspection.py
@@ -26,6 +26,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
1700: 'DecimalField',
}
+ ignored_tables = []
+
def get_table_list(self, cursor):
"Returns a list of table names in the current database."
cursor.execute("""
@@ -35,7 +37,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
WHERE c.relkind IN ('r', 'v', '')
AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
AND pg_catalog.pg_table_is_visible(c.oid)""")
- return [row[0] for row in cursor.fetchall()]
+ return [row[0] for row in cursor.fetchall() if row[0] not in self.ignored_tables]
def get_table_description(self, cursor, table_name):
"Returns a description of the table, with the DB-API cursor.description interface."
diff --git a/django/db/models/base.py b/django/db/models/base.py
index a2937f12c6..6a21544baf 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -184,10 +184,21 @@ class ModelBase(type):
else:
new_class._meta.concrete_model = new_class
- # Do the appropriate setup for any model parents.
- o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields
- if isinstance(f, OneToOneField)])
+ # Collect the parent links for multi-table inheritance.
+ parent_links = {}
+ for base in reversed([new_class] + parents):
+ # Conceptually equivalent to `if base is Model`.
+ if not hasattr(base, '_meta'):
+ continue
+ # Skip concrete parent classes.
+ if base != new_class and not base._meta.abstract:
+ continue
+ # Locate OneToOneField instances.
+ for field in base._meta.local_fields:
+ if isinstance(field, OneToOneField):
+ parent_links[field.rel.to] = field
+ # Do the appropriate setup for any model parents.
for base in parents:
original_base = base
if not hasattr(base, '_meta'):
@@ -208,8 +219,8 @@ class ModelBase(type):
if not base._meta.abstract:
# Concrete classes...
base = base._meta.concrete_model
- if base in o2o_map:
- field = o2o_map[base]
+ if base in parent_links:
+ field = parent_links[base]
elif not is_proxy:
attr_name = '%s_ptr' % base._meta.model_name
field = OneToOneField(base, name=attr_name,
@@ -448,7 +459,9 @@ class Model(six.with_metaclass(ModelBase)):
return '%s object' % self.__class__.__name__
def __eq__(self, other):
- return isinstance(other, self.__class__) and self._get_pk_val() == other._get_pk_val()
+ return (isinstance(other, Model) and
+ self._meta.concrete_model == other._meta.concrete_model and
+ self._get_pk_val() == other._get_pk_val())
def __ne__(self, other):
return not self.__eq__(other)
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 4069c04a14..836d394e9b 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -313,14 +313,13 @@ class QuerySet(object):
kwargs[arg.default_alias] = arg
query = self.query.clone()
-
+ force_subq = query.low_mark != 0 or query.high_mark is not None
aggregate_names = []
for (alias, aggregate_expr) in kwargs.items():
query.add_aggregate(aggregate_expr, self.model, alias,
is_summary=True)
aggregate_names.append(alias)
-
- return query.get_aggregation(using=self.db)
+ return query.get_aggregation(using=self.db, force_subq=force_subq)
def count(self):
"""
diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
index e17cb3f616..54b4e86245 100644
--- a/django/db/models/sql/compiler.py
+++ b/django/db/models/sql/compiler.py
@@ -167,7 +167,6 @@ class SQLCompiler(object):
if obj.low_mark == 0 and obj.high_mark is None:
# If there is no slicing in use, then we can safely drop all ordering
obj.clear_ordering(True)
- obj.bump_prefix()
return obj.get_compiler(connection=self.connection).as_sql()
def get_columns(self, with_aliases=False):
@@ -808,13 +807,14 @@ class SQLCompiler(object):
return result
def as_subquery_condition(self, alias, columns, qn):
+ inner_qn = self.quote_name_unless_alias
qn2 = self.connection.ops.quote_name
if len(columns) == 1:
sql, params = self.as_sql()
return '%s.%s IN (%s)' % (qn(alias), qn2(columns[0]), sql), params
for index, select_col in enumerate(self.query.select):
- lhs = '%s.%s' % (qn(select_col.col[0]), qn2(select_col.col[1]))
+ lhs = '%s.%s' % (inner_qn(select_col.col[0]), qn2(select_col.col[1]))
rhs = '%s.%s' % (qn(alias), qn2(columns[index]))
self.query.where.add(
QueryWrapper('%s = %s' % (lhs, rhs), []), 'AND')
@@ -1010,7 +1010,6 @@ class SQLUpdateCompiler(SQLCompiler):
# We need to use a sub-select in the where clause to filter on things
# from other tables.
query = self.query.clone(klass=Query)
- query.bump_prefix()
query.extra = {}
query.select = []
query.add_fields([query.get_meta().pk.name])
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index 15f2643495..1d19c5b9a8 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -97,6 +97,7 @@ class Query(object):
LOUTER = 'LEFT OUTER JOIN'
alias_prefix = 'T'
+ subq_aliases = frozenset([alias_prefix])
query_terms = QUERY_TERMS
aggregates_module = base_aggregates_module
@@ -273,6 +274,10 @@ class Query(object):
else:
obj.used_aliases = set()
obj.filter_is_sticky = False
+ if 'alias_prefix' in self.__dict__:
+ obj.alias_prefix = self.alias_prefix
+ if 'subq_aliases' in self.__dict__:
+ obj.subq_aliases = self.subq_aliases.copy()
obj.__dict__.update(kwargs)
if hasattr(obj, '_setup_query'):
@@ -310,7 +315,7 @@ class Query(object):
# Return value depends on the type of the field being processed.
return self.convert_values(value, aggregate.field, connection)
- def get_aggregation(self, using):
+ def get_aggregation(self, using, force_subq=False):
"""
Returns the dictionary with the values of the existing aggregations.
"""
@@ -320,18 +325,26 @@ class Query(object):
# If there is a group by clause, aggregating does not add useful
# information but retrieves only the first row. Aggregate
# over the subquery instead.
- if self.group_by is not None:
+ if self.group_by is not None or force_subq:
from django.db.models.sql.subqueries import AggregateQuery
query = AggregateQuery(self.model)
-
obj = self.clone()
+ if not force_subq:
+ # In forced subq case the ordering and limits will likely
+ # affect the results.
+ obj.clear_ordering(True)
+ obj.clear_limits()
+ obj.select_for_update = False
+ obj.select_related = False
+ obj.related_select_cols = []
+ relabels = dict((t, 'subquery') for t in self.tables)
# Remove any aggregates marked for reduction from the subquery
# and move them to the outer AggregateQuery.
for alias, aggregate in self.aggregate_select.items():
if aggregate.is_summary:
- query.aggregate_select[alias] = aggregate
+ query.aggregate_select[alias] = aggregate.relabeled_clone(relabels)
del obj.aggregate_select[alias]
try:
@@ -780,28 +793,22 @@ class Query(object):
data = data._replace(lhs_alias=change_map[lhs])
self.alias_map[alias] = data
- def bump_prefix(self, exceptions=()):
+ def bump_prefix(self, outer_query):
"""
- Changes the alias prefix to the next letter in the alphabet and
- relabels all the aliases. Even tables that previously had no alias will
- get an alias after this call (it's mostly used for nested queries and
- the outer query will already be using the non-aliased table name).
-
- Subclasses who create their own prefix should override this method to
- produce a similar result (a new prefix and relabelled aliases).
-
- The 'exceptions' parameter is a container that holds alias names which
- should not be changed.
+ Changes the alias prefix to the next letter in the alphabet in a way
+ that the outer query's aliases and this query's aliases will not
+ conflict. Even tables that previously had no alias will get an alias
+ after this call.
"""
- current = ord(self.alias_prefix)
- assert current < ord('Z')
- prefix = chr(current + 1)
- self.alias_prefix = prefix
+ self.alias_prefix = chr(ord(self.alias_prefix) + 1)
+ while self.alias_prefix in self.subq_aliases:
+ self.alias_prefix = chr(ord(self.alias_prefix) + 1)
+ assert self.alias_prefix < 'Z'
+ self.subq_aliases = self.subq_aliases.union([self.alias_prefix])
+ outer_query.subq_aliases = outer_query.subq_aliases.union(self.subq_aliases)
change_map = OrderedDict()
for pos, alias in enumerate(self.tables):
- if alias in exceptions:
- continue
- new_alias = '%s%d' % (prefix, pos)
+ new_alias = '%s%d' % (self.alias_prefix, pos)
change_map[alias] = new_alias
self.tables[pos] = new_alias
self.change_aliases(change_map)
@@ -1005,6 +1012,65 @@ class Query(object):
# Add the aggregate to the query
aggregate.add_to_query(self, alias, col=col, source=source, is_summary=is_summary)
+ def prepare_lookup_value(self, value, lookup_type, can_reuse):
+ # Interpret '__exact=None' as the sql 'is NULL'; otherwise, reject all
+ # uses of None as a query value.
+ if value is None:
+ if lookup_type != 'exact':
+ raise ValueError("Cannot use None as a query value")
+ lookup_type = 'isnull'
+ value = True
+ elif callable(value):
+ value = value()
+ elif isinstance(value, ExpressionNode):
+ # If value is a query expression, evaluate it
+ value = SQLEvaluator(value, self, reuse=can_reuse)
+ if hasattr(value, 'query') and hasattr(value.query, 'bump_prefix'):
+ value = value._clone()
+ value.query.bump_prefix(self)
+ if hasattr(value, 'bump_prefix'):
+ value = value.clone()
+ value.bump_prefix(self)
+ # For Oracle '' is equivalent to null. The check needs to be done
+ # at this stage because join promotion can't be done at compiler
+ # stage. Using DEFAULT_DB_ALIAS isn't nice, but it is the best we
+ # can do here. Similar thing is done in is_nullable(), too.
+ if (connections[DEFAULT_DB_ALIAS].features.interprets_empty_strings_as_nulls and
+ lookup_type == 'exact' and value == ''):
+ value = True
+ lookup_type = 'isnull'
+ return value, lookup_type
+
+ def solve_lookup_type(self, lookup):
+ """
+ Solve the lookup type from the lookup (eg: 'foobar__id__icontains')
+ """
+ lookup_type = 'exact' # Default lookup type
+ lookup_parts = lookup.split(LOOKUP_SEP)
+ num_parts = len(lookup_parts)
+ if (len(lookup_parts) > 1 and lookup_parts[-1] in self.query_terms
+ and lookup not in self.aggregates):
+ # Traverse the lookup query to distinguish related fields from
+ # lookup types.
+ lookup_model = self.model
+ for counter, field_name in enumerate(lookup_parts):
+ try:
+ lookup_field = lookup_model._meta.get_field(field_name)
+ except FieldDoesNotExist:
+ # Not a field. Bail out.
+ lookup_type = lookup_parts.pop()
+ break
+ # Unless we're at the end of the list of lookups, let's attempt
+ # to continue traversing relations.
+ if (counter + 1) < num_parts:
+ try:
+ lookup_model = lookup_field.rel.to
+ except AttributeError:
+ # Not a related field. Bail out.
+ lookup_type = lookup_parts.pop()
+ break
+ return lookup_type, lookup_parts
+
def build_filter(self, filter_expr, branch_negated=False, current_negated=False,
can_reuse=None):
"""
@@ -1033,58 +1099,15 @@ class Query(object):
is responsible for unreffing the joins used.
"""
arg, value = filter_expr
- parts = arg.split(LOOKUP_SEP)
+ lookup_type, parts = self.solve_lookup_type(arg)
if not parts:
raise FieldError("Cannot parse keyword query %r" % arg)
# Work out the lookup type and remove it from the end of 'parts',
# if necessary.
- lookup_type = 'exact' # Default lookup type
- num_parts = len(parts)
- if (len(parts) > 1 and parts[-1] in self.query_terms
- and arg not in self.aggregates):
- # Traverse the lookup query to distinguish related fields from
- # lookup types.
- lookup_model = self.model
- for counter, field_name in enumerate(parts):
- try:
- lookup_field = lookup_model._meta.get_field(field_name)
- except FieldDoesNotExist:
- # Not a field. Bail out.
- lookup_type = parts.pop()
- break
- # Unless we're at the end of the list of lookups, let's attempt
- # to continue traversing relations.
- if (counter + 1) < num_parts:
- try:
- lookup_model = lookup_field.rel.to
- except AttributeError:
- # Not a related field. Bail out.
- lookup_type = parts.pop()
- break
+ value, lookup_type = self.prepare_lookup_value(value, lookup_type, can_reuse)
clause = self.where_class()
- # Interpret '__exact=None' as the sql 'is NULL'; otherwise, reject all
- # uses of None as a query value.
- if value is None:
- if lookup_type != 'exact':
- raise ValueError("Cannot use None as a query value")
- lookup_type = 'isnull'
- value = True
- elif callable(value):
- value = value()
- elif isinstance(value, ExpressionNode):
- # If value is a query expression, evaluate it
- value = SQLEvaluator(value, self, reuse=can_reuse)
- # For Oracle '' is equivalent to null. The check needs to be done
- # at this stage because join promotion can't be done at compiler
- # stage. Using DEFAULT_DB_ALIAS isn't nice, but it is the best we
- # can do here. Similar thing is done in is_nullable(), too.
- if (connections[DEFAULT_DB_ALIAS].features.interprets_empty_strings_as_nulls and
- lookup_type == 'exact' and value == ''):
- value = True
- lookup_type = 'isnull'
-
for alias, aggregate in self.aggregates.items():
if alias in (parts[0], LOOKUP_SEP.join(parts)):
clause.add((aggregate, lookup_type, value), AND)
@@ -1096,7 +1119,7 @@ class Query(object):
try:
field, sources, opts, join_list, path = self.setup_joins(
- parts, opts, alias, can_reuse, allow_many,)
+ parts, opts, alias, can_reuse, allow_many,)
if can_reuse is not None:
can_reuse.update(join_list)
except MultiJoin as e:
@@ -1404,7 +1427,6 @@ class Query(object):
# Generate the inner query.
query = Query(self.model)
query.where.add(query.build_filter(filter_expr), AND)
- query.bump_prefix()
query.clear_ordering(True)
# Try to have as simple as possible subquery -> trim leading joins from
# the subquery.
diff --git a/django/forms/forms.py b/django/forms/forms.py
index c2b700ce77..ec51507981 100644
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -434,7 +434,9 @@ class BoundField(object):
This really is only useful for RadioSelect widgets, so that you can
iterate over individual radio buttons in a template.
"""
- for subwidget in self.field.widget.subwidgets(self.html_name, self.value()):
+ id_ = self.field.widget.attrs.get('id') or self.auto_id
+ attrs = {'id': id_} if id_ else {}
+ for subwidget in self.field.widget.subwidgets(self.html_name, self.value(), attrs):
yield subwidget
def __len__(self):
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 0a5059a9c2..98d47d0b00 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -601,16 +601,15 @@ class ChoiceInput(SubWidget):
self.choice_value = force_text(choice[0])
self.choice_label = force_text(choice[1])
self.index = index
+ if 'id' in self.attrs:
+ self.attrs['id'] += "_%d" % self.index
def __str__(self):
return self.render()
def render(self, name=None, value=None, attrs=None, choices=()):
- name = name or self.name
- value = value or self.value
- attrs = attrs or self.attrs
- if 'id' in self.attrs:
- label_for = format_html(' for="{0}_{1}"', self.attrs['id'], self.index)
+ if self.id_for_label:
+ label_for = format_html(' for="{0}"', self.id_for_label)
else:
label_for = ''
return format_html('<label{0}>{1} {2}</label>', label_for, self.tag(), self.choice_label)
@@ -619,13 +618,15 @@ class ChoiceInput(SubWidget):
return self.value == self.choice_value
def tag(self):
- if 'id' in self.attrs:
- self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
final_attrs = dict(self.attrs, type=self.input_type, name=self.name, value=self.choice_value)
if self.is_checked():
final_attrs['checked'] = 'checked'
return format_html('<input{0} />', flatatt(final_attrs))
+ @property
+ def id_for_label(self):
+ return self.attrs.get('id', '')
+
class RadioChoiceInput(ChoiceInput):
input_type = 'radio'
diff --git a/django/template/base.py b/django/template/base.py
index ed4196012a..382b85aefd 100644
--- a/django/template/base.py
+++ b/django/template/base.py
@@ -6,7 +6,7 @@ from importlib import import_module
from inspect import getargspec
from django.conf import settings
-from django.template.context import (Context, RequestContext,
+from django.template.context import (BaseContext, Context, RequestContext,
ContextPopException)
from django.utils.itercompat import is_iterable
from django.utils.text import (smart_split, unescape_string_literal,
@@ -765,6 +765,9 @@ class Variable(object):
current = current[bit]
except (TypeError, AttributeError, KeyError, ValueError):
try: # attribute lookup
+ # Don't return class attributes if the class is the context:
+ if isinstance(current, BaseContext) and getattr(type(current), bit):
+ raise AttributeError
current = getattr(current, bit)
except (TypeError, AttributeError):
try: # list-index lookup
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index 5c9490f749..921a3594cc 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -458,10 +458,11 @@ class VerbatimNode(Node):
return self.content
class WidthRatioNode(Node):
- def __init__(self, val_expr, max_expr, max_width):
+ def __init__(self, val_expr, max_expr, max_width, asvar=None):
self.val_expr = val_expr
self.max_expr = max_expr
self.max_width = max_width
+ self.asvar = asvar
def render(self, context):
try:
@@ -480,7 +481,13 @@ class WidthRatioNode(Node):
return '0'
except (ValueError, TypeError):
return ''
- return str(int(round(ratio)))
+ result = str(int(round(ratio)))
+
+ if self.asvar:
+ context[self.asvar] = result
+ return ''
+ else:
+ return result
class WithNode(Node):
def __init__(self, var, name, nodelist, extra_context=None):
@@ -1353,20 +1360,34 @@ def widthratio(parser, token):
For example::
- <img src='bar.gif' height='10' width='{% widthratio this_value max_value max_width %}' />
+ <img src="bar.png" alt="Bar"
+ height="10" width="{% widthratio this_value max_value max_width %}" />
If ``this_value`` is 175, ``max_value`` is 200, and ``max_width`` is 100,
the image in the above example will be 88 pixels wide
(because 175/200 = .875; .875 * 100 = 87.5 which is rounded up to 88).
+
+ In some cases you might want to capture the result of widthratio in a
+ variable. It can be useful for instance in a blocktrans like this::
+
+ {% widthratio this_value max_value max_width as width %}
+ {% blocktrans %}The width is: {{ width }}{% endblocktrans %}
"""
bits = token.split_contents()
- if len(bits) != 4:
- raise TemplateSyntaxError("widthratio takes three arguments")
- tag, this_value_expr, max_value_expr, max_width = bits
+ if len(bits) == 4:
+ tag, this_value_expr, max_value_expr, max_width = bits
+ asvar = None
+ elif len(bits) == 6:
+ tag, this_value_expr, max_value_expr, max_width, as_, asvar = bits
+ if as_ != 'as':
+ raise TemplateSyntaxError("Invalid syntax in widthratio tag. Expecting 'as' keyword")
+ else:
+ raise TemplateSyntaxError("widthratio takes at least three arguments")
return WidthRatioNode(parser.compile_filter(this_value_expr),
parser.compile_filter(max_value_expr),
- parser.compile_filter(max_width))
+ parser.compile_filter(max_width),
+ asvar=asvar)
@register.tag('with')
def do_with(parser, token):
diff --git a/django/test/client.py b/django/test/client.py
index 754d4d73f8..3c58eae4b5 100644
--- a/django/test/client.py
+++ b/django/test/client.py
@@ -37,6 +37,7 @@ BOUNDARY = 'BoUnDaRyStRiNg'
MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY
CONTENT_TYPE_RE = re.compile('.*; charset=([\w\d-]+);?')
+
class FakePayload(object):
"""
A wrapper around BytesIO that restricts what can be read since data from
@@ -123,6 +124,7 @@ class ClientHandler(BaseHandler):
return response
+
def store_rendered_templates(store, signal, sender, template, context, **kwargs):
"""
Stores templates and contexts that are rendered.
@@ -133,6 +135,7 @@ def store_rendered_templates(store, signal, sender, template, context, **kwargs)
store.setdefault('templates', []).append(template)
store.setdefault('context', ContextList()).append(copy(context))
+
def encode_multipart(boundary, data):
"""
Encodes multipart POST data from a dictionary of form values.
@@ -178,6 +181,7 @@ def encode_multipart(boundary, data):
])
return b'\r\n'.join(lines)
+
def encode_file(boundary, key, file):
to_bytes = lambda s: force_bytes(s, settings.DEFAULT_CHARSET)
if hasattr(file, 'content_type'):
@@ -189,8 +193,8 @@ def encode_file(boundary, key, file):
content_type = 'application/octet-stream'
return [
to_bytes('--%s' % boundary),
- to_bytes('Content-Disposition: form-data; name="%s"; filename="%s"' \
- % (key, os.path.basename(file.name))),
+ to_bytes('Content-Disposition: form-data; name="%s"; filename="%s"'
+ % (key, os.path.basename(file.name))),
to_bytes('Content-Type: %s' % content_type),
b'',
file.read()
@@ -274,14 +278,11 @@ class RequestFactory(object):
def get(self, path, data={}, **extra):
"Construct a GET request."
- parsed = urlparse(path)
r = {
- 'PATH_INFO': self._get_path(parsed),
- 'QUERY_STRING': urlencode(data, doseq=True) or force_str(parsed[4]),
- 'REQUEST_METHOD': str('GET'),
+ 'QUERY_STRING': urlencode(data, doseq=True),
}
r.update(extra)
- return self.request(**r)
+ return self.generic('GET', path, **r)
def post(self, path, data={}, content_type=MULTIPART_CONTENT,
**extra):
@@ -289,32 +290,19 @@ class RequestFactory(object):
post_data = self._encode_data(data, content_type)
- parsed = urlparse(path)
- r = {
- 'CONTENT_LENGTH': len(post_data),
- 'CONTENT_TYPE': content_type,
- 'PATH_INFO': self._get_path(parsed),
- 'QUERY_STRING': force_str(parsed[4]),
- 'REQUEST_METHOD': str('POST'),
- 'wsgi.input': FakePayload(post_data),
- }
- r.update(extra)
- return self.request(**r)
+ return self.generic('POST', path, post_data, content_type, **extra)
def head(self, path, data={}, **extra):
"Construct a HEAD request."
- parsed = urlparse(path)
r = {
- 'PATH_INFO': self._get_path(parsed),
- 'QUERY_STRING': urlencode(data, doseq=True) or force_str(parsed[4]),
- 'REQUEST_METHOD': str('HEAD'),
+ 'QUERY_STRING': urlencode(data, doseq=True),
}
r.update(extra)
- return self.request(**r)
+ return self.generic('HEAD', path, **r)
def options(self, path, data='', content_type='application/octet-stream',
- **extra):
+ **extra):
"Construct an OPTIONS request."
return self.generic('OPTIONS', path, data, content_type, **extra)
@@ -324,22 +312,22 @@ class RequestFactory(object):
return self.generic('PUT', path, data, content_type, **extra)
def patch(self, path, data='', content_type='application/octet-stream',
- **extra):
+ **extra):
"Construct a PATCH request."
return self.generic('PATCH', path, data, content_type, **extra)
def delete(self, path, data='', content_type='application/octet-stream',
- **extra):
+ **extra):
"Construct a DELETE request."
return self.generic('DELETE', path, data, content_type, **extra)
def generic(self, method, path,
data='', content_type='application/octet-stream', **extra):
+ """Constructs an arbitrary HTTP request."""
parsed = urlparse(path)
data = force_bytes(data, settings.DEFAULT_CHARSET)
r = {
'PATH_INFO': self._get_path(parsed),
- 'QUERY_STRING': force_str(parsed[4]),
'REQUEST_METHOD': str(method),
}
if data:
@@ -349,8 +337,12 @@ class RequestFactory(object):
'wsgi.input': FakePayload(data),
})
r.update(extra)
+ # If QUERY_STRING is absent or empty, we want to extract it from the URL.
+ if not r.get('QUERY_STRING'):
+ r['QUERY_STRING'] = force_str(parsed[4])
return self.request(**r)
+
class Client(RequestFactory):
"""
A class that can act as a client for testing purposes.
@@ -392,7 +384,6 @@ class Client(RequestFactory):
return {}
session = property(_session)
-
def request(self, **request):
"""
The master request method. Composes the environment dictionary
@@ -406,7 +397,8 @@ class Client(RequestFactory):
# callback function.
data = {}
on_template_render = curry(store_rendered_templates, data)
- signals.template_rendered.connect(on_template_render, dispatch_uid="template-render")
+ signal_uid = "template-render-%s" % id(request)
+ signals.template_rendered.connect(on_template_render, dispatch_uid=signal_uid)
# Capture exceptions created by the handler.
got_request_exception.connect(self.store_exc_info, dispatch_uid="request-exception")
try:
@@ -452,7 +444,7 @@ class Client(RequestFactory):
return response
finally:
- signals.template_rendered.disconnect(dispatch_uid="template-render")
+ signals.template_rendered.disconnect(dispatch_uid=signal_uid)
got_request_exception.disconnect(dispatch_uid="request-exception")
def get(self, path, data={}, follow=False, **extra):
@@ -484,12 +476,11 @@ class Client(RequestFactory):
return response
def options(self, path, data='', content_type='application/octet-stream',
- follow=False, **extra):
+ follow=False, **extra):
"""
Request a response from the server using OPTIONS.
"""
- response = super(Client, self).options(path,
- data=data, content_type=content_type, **extra)
+ response = super(Client, self).options(path, data=data, content_type=content_type, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response
@@ -499,14 +490,13 @@ class Client(RequestFactory):
"""
Send a resource to the server using PUT.
"""
- response = super(Client, self).put(path,
- data=data, content_type=content_type, **extra)
+ response = super(Client, self).put(path, data=data, content_type=content_type, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response
def patch(self, path, data='', content_type='application/octet-stream',
- follow=False, **extra):
+ follow=False, **extra):
"""
Send a resource to the server using PATCH.
"""
@@ -517,12 +507,12 @@ class Client(RequestFactory):
return response
def delete(self, path, data='', content_type='application/octet-stream',
- follow=False, **extra):
+ follow=False, **extra):
"""
Send a DELETE request to the server.
"""
- response = super(Client, self).delete(path,
- data=data, content_type=content_type, **extra)
+ response = super(Client, self).delete(
+ path, data=data, content_type=content_type, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response
diff --git a/django/utils/functional.py b/django/utils/functional.py
index e23bd3ff80..9cc703fe84 100644
--- a/django/utils/functional.py
+++ b/django/utils/functional.py
@@ -263,17 +263,12 @@ class LazyObject(object):
__dir__ = new_method_proxy(dir)
# Dictionary methods support
- @new_method_proxy
- def __getitem__(self, key):
- return self[key]
+ __getitem__ = new_method_proxy(operator.getitem)
+ __setitem__ = new_method_proxy(operator.setitem)
+ __delitem__ = new_method_proxy(operator.delitem)
- @new_method_proxy
- def __setitem__(self, key, value):
- self[key] = value
-
- @new_method_proxy
- def __delitem__(self, key):
- del self[key]
+ __len__ = new_method_proxy(len)
+ __contains__ = new_method_proxy(operator.contains)
# Workaround for http://bugs.python.org/issue12370
diff --git a/django/utils/http.py b/django/utils/http.py
index 4647d89847..9b36ab91d7 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -109,8 +109,7 @@ def http_date(epoch_seconds=None):
Outputs a string in the format 'Wdy, DD Mon YYYY HH:MM:SS GMT'.
"""
- rfcdate = formatdate(epoch_seconds)
- return '%s GMT' % rfcdate[:25]
+ return formatdate(epoch_seconds, usegmt=True)
def parse_http_date(date):
"""
@@ -253,11 +252,12 @@ def same_origin(url1, url2):
def is_safe_url(url, host=None):
"""
Return ``True`` if the url is a safe redirection (i.e. it doesn't point to
- a different host).
+ a different host and uses a safe scheme).
Always returns ``False`` on an empty url.
"""
if not url:
return False
- netloc = urllib_parse.urlparse(url)[1]
- return not netloc or netloc == host
+ url_info = urllib_parse.urlparse(url)
+ return (not url_info.netloc or url_info.netloc == host) and \
+ (not url_info.scheme or url_info.scheme in ['http', 'https'])
diff --git a/django/views/debug.py b/django/views/debug.py
index 2129a83d67..16f75df8c3 100644
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -227,7 +227,7 @@ class ExceptionReporter(object):
return "File exists"
def get_traceback_data(self):
- "Return a Context instance containing traceback information."
+ """Return a dictionary containing traceback information."""
if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist):
from django.template.loader import template_source_loaders
@@ -295,13 +295,13 @@ class ExceptionReporter(object):
def get_traceback_html(self):
"Return HTML version of debug 500 HTTP error page."
t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
- c = Context(self.get_traceback_data())
+ c = Context(self.get_traceback_data(), use_l10n=False)
return t.render(c)
def get_traceback_text(self):
"Return plain text version of debug 500 HTTP error page."
t = Template(TECHNICAL_500_TEXT_TEMPLATE, name='Technical 500 template')
- c = Context(self.get_traceback_data(), autoescape=False)
+ c = Context(self.get_traceback_data(), autoescape=False, use_l10n=False)
return t.render(c)
def get_template_exception_info(self):