diff options
| author | Andrew Godwin <andrew@aeracode.org> | 2013-08-19 18:30:48 +0100 |
|---|---|---|
| committer | Andrew Godwin <andrew@aeracode.org> | 2013-08-19 18:30:48 +0100 |
| commit | b6a957f0ba8a2ed1b24d7ee042a9c4beaf51ab03 (patch) | |
| tree | 87d42b9e8d3d4c1516b53eee1d9332735762826a /django | |
| parent | 52edc16086e3c28a78c31975bb4da2f9450590b4 (diff) | |
| parent | 3c0300405009b82b52fd15483371097221662fcd (diff) | |
Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts:
docs/ref/django-admin.txt
Diffstat (limited to 'django')
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): |
