diff --git a/AUTHORS b/AUTHORS index 15f3e8dbf4..059310d5c6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -317,6 +317,7 @@ answer newbie questions, and generally made Django that much better: Michael Josephson jpellerin@gmail.com junzhang.jn@gmail.com + Krzysztof Jurewicz Xia Kai Antti Kaihola Peter van Kampen @@ -418,6 +419,7 @@ answer newbie questions, and generally made Django that much better: Christian Metts michal@plovarna.cz Justin Michalicek + Bojan Mihelac Slawek Mikula Katie Miller Shawn Milochik @@ -440,6 +442,7 @@ answer newbie questions, and generally made Django that much better: Gopal Narayanan Fraser Nevett Sam Newman + Alasdair Nicol Ryan Niemeyer Filip Noetzel Afonso Fernández Nogueira @@ -537,6 +540,7 @@ answer newbie questions, and generally made Django that much better: Brenton Simpson Jozko Skrablin Ben Slavin + Jonathan Slenders sloonz Paul Smith Steven L. Smith (fvox13) @@ -573,6 +577,7 @@ answer newbie questions, and generally made Django that much better: Aaron Swartz Ville Säävuori Mart Sõmermaa + Susan Tan Christian Tanzer Tyler Tarabula Tyson Tate 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( - '

{0} {2}
{3} {4}

', + '

{0} {2}
{3} {4}

', _('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('{1} {2}', 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('', 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:: - + Bar 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): diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index af652de78c..6493c18f99 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -295,9 +295,9 @@ validation process will break. Therefore, you must ensure that the form field used to represent your custom field performs whatever input validation and data cleaning is necessary to convert user-provided form input into a -`to_python()`-compatible model field value. This may require writing a +``to_python()``-compatible model field value. This may require writing a custom form field, and/or implementing the :meth:`.formfield` method on -your field to return a form field class whose `to_python()` returns the +your field to return a form field class whose ``to_python()`` returns the correct datatype. Documenting your custom field diff --git a/docs/howto/deployment/index.txt b/docs/howto/deployment/index.txt index 8b0368ac67..688bae2397 100644 --- a/docs/howto/deployment/index.txt +++ b/docs/howto/deployment/index.txt @@ -28,6 +28,7 @@ the easiest, fastest, and most stable deployment choice. * `Chapter 12 of the Django Book (second edition)`_ discusses deployment and especially scaling in more detail. However, note that this edition was written against Django version 1.1 and has not been updated since - `mod_python` was first deprecated, then completely removed in Django 1.5. + ``mod_python`` was first deprecated, then completely removed in + Django 1.5. .. _chapter 12 of the django book (second edition): http://djangobook.com/en/2.0/chapter12/ diff --git a/docs/howto/deployment/wsgi/apache-auth.txt b/docs/howto/deployment/wsgi/apache-auth.txt index d06e89cd0e..80219ffdf4 100644 --- a/docs/howto/deployment/wsgi/apache-auth.txt +++ b/docs/howto/deployment/wsgi/apache-auth.txt @@ -16,7 +16,7 @@ version >= 2.2 and mod_wsgi >= 2.0. For example, you could: .. note:: If you have installed a :ref:`custom User model ` and - want to use this default auth handler, it must support an `is_active` + want to use this default auth handler, it must support an ``is_active`` attribute. If you want to use group based authorization, your custom user must have a relation named 'groups', referring to a related object that has a 'name' field. You can also specify your own custom mod_wsgi diff --git a/docs/howto/deployment/wsgi/modwsgi.txt b/docs/howto/deployment/wsgi/modwsgi.txt index 2cbcd8ce7e..1d11e94685 100644 --- a/docs/howto/deployment/wsgi/modwsgi.txt +++ b/docs/howto/deployment/wsgi/modwsgi.txt @@ -193,7 +193,7 @@ other approaches: configuration). 2. Use an ``Alias`` directive, as demonstrated above, to alias the appropriate - URL (probably :setting:`STATIC_URL` + `admin/`) to the actual location of + URL (probably :setting:`STATIC_URL` + ``admin/``) to the actual location of the admin files. 3. Copy the admin static files so that they live within your Apache diff --git a/docs/internals/contributing/writing-code/working-with-git.txt b/docs/internals/contributing/writing-code/working-with-git.txt index dcfdd9e85b..32fc459e70 100644 --- a/docs/internals/contributing/writing-code/working-with-git.txt +++ b/docs/internals/contributing/writing-code/working-with-git.txt @@ -157,7 +157,7 @@ using interactive rebase:: The HEAD~2 above is shorthand for two latest commits. The above command will open an editor showing the two commits, prefixed with the word "pick". -Change the second line to "squash" instead. This will keep the +Change "pick" on the second line to "squash" instead. This will keep the first commit, and squash the second commit into the first one. Save and quit the editor. A second editor window should open, so you can reword the commit message for the commit now that it includes both your steps. diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index f0ac66bce8..7b8298597d 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -370,8 +370,11 @@ these changes. * Remove the backward compatible shims introduced to rename the attributes ``ChangeList.root_query_set`` and ``ChangeList.query_set``. -* ``django.conf.urls.shortcut`` and ``django.views.defaults.shortcut`` will be - removed. +* ``django.views.defaults.shortcut`` will be removed, as part of the + goal of removing all ``django.contrib`` references from the core + Django codebase. Instead use + ``django.contrib.contenttypes.views.shortcut``. ``django.conf.urls.shortcut`` + will also be removed. * Support for the Python Imaging Library (PIL) module will be removed, as it no longer appears to be actively maintained & does not work on Python 3. @@ -442,11 +445,5 @@ these changes. 2.0 --- -* ``django.views.defaults.shortcut()``. This function has been moved - to ``django.contrib.contenttypes.views.shortcut()`` as part of the - goal of removing all ``django.contrib`` references from the core - Django codebase. The old shortcut will be removed in the 2.0 - release. - * ``ssi`` and ``url`` template tags will be removed from the ``future`` template tag library (used during the 1.3/1.4 deprecation period). diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index 5bda2e8add..a926de27ab 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -83,10 +83,11 @@ A few items need to be taken care of before even beginning the release process. This stuff starts about a week before the release; most of it can be done any time leading up to the actual release: -#. If this is a security release, send out pre-notification **one week** - before the release. We maintain a list of who gets these pre-notification - emails at *FIXME WHERE?*. This email should be signed by the key you'll use - for the release, and should include patches for each issue being fixed. +#. If this is a security release, send out pre-notification **one week** before + the release. We maintain a list of who gets these pre-notification emails in + the private ``django-core`` repository. This email should be signed by the + key you'll use for the release, and should include patches for each issue + being fixed. #. As the release approaches, watch Trac to make sure no release blockers are left for the upcoming release. diff --git a/docs/internals/security.txt b/docs/internals/security.txt index 486b2c9968..327a6a5f60 100644 --- a/docs/internals/security.txt +++ b/docs/internals/security.txt @@ -108,8 +108,12 @@ On the day of disclosure, we will take the following steps: relevant patches and new releases, and crediting the reporter of the issue (if the reporter wishes to be publicly identified). +4. Post a notice to the `django-announce`_ mailing list that links to the blog + post. + .. _the Python Package Index: http://pypi.python.org/pypi .. _the official Django development blog: https://www.djangoproject.com/weblog/ +.. _django-announce: http://groups.google.com/group/django-announce If a reported issue is believed to be particularly time-sensitive -- due to a known exploit in the wild, for example -- the time between @@ -214,4 +218,4 @@ If you are added to the notification list, security-related emails will be sent to you by Django's release manager, and all notification emails will be signed with the same key used to sign Django releases; that key has the ID ``0x3684C0C08C8B2AE1``, and is available from most -commonly-used keyservers. \ No newline at end of file +commonly-used keyservers. diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index f2b01758aa..b99d3be3e2 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -600,7 +600,7 @@ for your own sanity when dealing with the interactive prompt, but also because objects' representations are used throughout Django's automatically-generated admin. -.. admonition:: `__unicode__` or `__str__`? +.. admonition:: ``__unicode__`` or ``__str__``? On Python 3, things are simpler, just use :meth:`~django.db.models.Model.__str__` and forget about diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index c5c5f8f288..3ffe475e1b 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -385,15 +385,6 @@ search terms, Django will search the ``question`` field. You can use as many fields as you'd like -- although because it uses a ``LIKE`` query behind the scenes, keep it reasonable, to keep your database happy. -Finally, because ``Poll`` objects have dates, it'd be convenient to be able to -drill down by date. Add this line:: - - date_hierarchy = 'pub_date' - -That adds hierarchical navigation, by date, to the top of the change list page. -At top level, it displays all available years. Then it drills down to months -and, ultimately, days. - Now's also a good time to note that change lists give you free pagination. The default is to display 100 items per page. Change-list pagination, search boxes, filters, date-hierarchies and column-header-ordering all work together like you diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 24058311a4..0db1e15ea9 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -104,12 +104,6 @@ TemplateView Renders a given template, with the context containing parameters captured in the URL. - .. versionchanged:: 1.5 - - The context used to be populated with a ``{{ params }}`` dictionary of - the parameters captured in the URL. Now those parameters are first-level - context variables. - **Ancestors (MRO)** This view inherits methods and attributes from the following views: diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt index 205d556463..427d8f6ceb 100644 --- a/docs/ref/class-based-views/generic-date-based.txt +++ b/docs/ref/class-based-views/generic-date-based.txt @@ -138,24 +138,16 @@ YearArchiveView * ``year``: A :class:`~datetime.date` object representing the given year. - .. versionchanged:: 1.5 - - Previously, this returned a string. - * ``next_year``: A :class:`~datetime.date` object representing the first day of the next year, according to :attr:`~BaseDateListView.allow_empty` and :attr:`~DateMixin.allow_future`. - .. versionadded:: 1.5 - * ``previous_year``: A :class:`~datetime.date` object representing the first day of the previous year, according to :attr:`~BaseDateListView.allow_empty` and :attr:`~DateMixin.allow_future`. - .. versionadded:: 1.5 - **Notes** * Uses a default ``template_name_suffix`` of ``_archive_year``. diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index 1162b2a194..48faf28a44 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -328,8 +328,3 @@ BaseDateListView :meth:`~BaseDateListView.get_date_list_period` is used. ``date_type`` and ``ordering`` are simply passed to :meth:`QuerySet.dates()`. - - .. versionchanged:: 1.5 - - The ``ordering`` parameter was added, and the default order was - changed to ascending. diff --git a/docs/ref/class-based-views/mixins-multiple-object.txt b/docs/ref/class-based-views/mixins-multiple-object.txt index 67ca5429fa..5a2ae85751 100644 --- a/docs/ref/class-based-views/mixins-multiple-object.txt +++ b/docs/ref/class-based-views/mixins-multiple-object.txt @@ -90,8 +90,6 @@ MultipleObjectMixin .. attribute:: page_kwarg - .. versionadded:: 1.5 - A string specifying the name to use for the page parameter. The view will expect this prameter to be available either as a query string parameter (via ``request.GET``) or as a kwarg variable specified diff --git a/docs/ref/class-based-views/mixins-simple.txt b/docs/ref/class-based-views/mixins-simple.txt index 377c85cc3b..3b12ee7689 100644 --- a/docs/ref/class-based-views/mixins-simple.txt +++ b/docs/ref/class-based-views/mixins-simple.txt @@ -7,8 +7,6 @@ ContextMixin .. class:: django.views.generic.base.ContextMixin - .. versionadded:: 1.5 - **Methods** .. method:: get_context_data(**kwargs) @@ -77,8 +75,6 @@ TemplateResponseMixin .. attribute:: content_type - .. versionadded:: 1.5 - The content type to use for the response. ``content_type`` is passed as a keyword argument to ``response_class``. Default is ``None`` -- meaning that Django uses :setting:`DEFAULT_CONTENT_TYPE`. diff --git a/docs/ref/contrib/admin/actions.txt b/docs/ref/contrib/admin/actions.txt index 58d8244525..b982aa64f7 100644 --- a/docs/ref/contrib/admin/actions.txt +++ b/docs/ref/contrib/admin/actions.txt @@ -269,8 +269,8 @@ Making actions available site-wide admin.site.add_action(export_selected_objects) - This makes the `export_selected_objects` action globally available as an - action named `"export_selected_objects"`. You can explicitly give the action + This makes the ``export_selected_objects`` action globally available as an + action named "export_selected_objects". You can explicitly give the action a name -- good if you later want to programmatically :ref:`remove the action ` -- by passing a second argument to :meth:`AdminSite.add_action()`:: diff --git a/docs/ref/contrib/admin/admindocs.txt b/docs/ref/contrib/admin/admindocs.txt index 6deb7bdbf8..8809502288 100644 --- a/docs/ref/contrib/admin/admindocs.txt +++ b/docs/ref/contrib/admin/admindocs.txt @@ -156,6 +156,6 @@ Edit this object Using these bookmarklets requires that you are either logged into the :mod:`Django admin ` as a :class:`~django.contrib.auth.models.User` with -:attr:`~django.contrib.auth.models.User.is_staff` set to `True`, or that the +:attr:`~django.contrib.auth.models.User.is_staff` set to ``True``, or that the ``XViewMiddleware`` is installed and you are accessing the site from an IP address listed in :setting:`INTERNAL_IPS`. diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 8feb574efd..ba5fa3bff6 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1235,8 +1235,6 @@ templates used by the :class:`ModelAdmin` views: .. method:: ModelAdmin.get_list_filter(self, request) - .. versionadded:: 1.5 - The ``get_list_filter`` method is given the ``HttpRequest`` and is expected to return the same kind of sequence type as for the :attr:`~ModelAdmin.list_filter` attribute. @@ -1251,8 +1249,6 @@ templates used by the :class:`ModelAdmin` views: .. method:: ModelAdmin.get_inline_instances(self, request, obj=None) - .. versionadded:: 1.5 - The ``get_inline_instances`` method is given the ``HttpRequest`` and the ``obj`` being edited (or ``None`` on an add form) and is expected to return a ``list`` or ``tuple`` of :class:`~django.contrib.admin.InlineModelAdmin` @@ -1506,10 +1502,6 @@ templates used by the :class:`ModelAdmin` views: Sends a message to the user using the :mod:`django.contrib.messages` backend. See the :ref:`custom ModelAdmin example `. - .. versionchanged:: 1.5 - - Keyword arguments were added in Django 1.5. - Keyword arguments allow you to change the message level, add extra CSS tags, or fail silently if the ``contrib.messages`` framework is not installed. These keyword arguments match those for diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index 4fe87e9872..799d3277ab 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -215,11 +215,16 @@ Methods (the Django app label). If the user is inactive, this method will always return ``False``. - .. method:: email_user(subject, message, from_email=None) + .. method:: email_user(subject, message, from_email=None, **kwargs) Sends an email to the user. If ``from_email`` is ``None``, Django uses the :setting:`DEFAULT_FROM_EMAIL`. + .. versionchanged:: 1.7 + + Any ``**kwargs`` are passed to the underlying + :meth:`~django.core.mail.send_mail()` call. + Manager methods --------------- @@ -384,8 +389,6 @@ can be used for notification when a user logs in or out. .. function:: user_login_failed - .. versionadded:: 1.5 - Sent when the user failed to login successfully ``sender`` diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index 89c4a88e00..21e65f168b 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -199,14 +199,18 @@ The ``ContentTypeManager`` Takes either a model class or an instance of a model, and returns the :class:`~django.contrib.contenttypes.models.ContentType` instance - representing that model. + representing that model. ``for_concrete_model=False`` allows fetching + the :class:`~django.contrib.contenttypes.models.ContentType` of a proxy + model. .. method:: get_for_models(*models[, for_concrete_models=True]) Takes a variadic number of model classes, and returns a dictionary mapping the model classes to the :class:`~django.contrib.contenttypes.models.ContentType` instances - representing them. + representing them. ``for_concrete_models=False`` allows fetching the + :class:`~django.contrib.contenttypes.models.ContentType` of proxy + models. .. method:: get_by_natural_key(app_label, model) @@ -232,21 +236,6 @@ lookup:: .. _generic-relations: -.. versionadded:: 1.5 - - Prior to Django 1.5, - :meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_model` and - :meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_models` - always returned the :class:`~django.contrib.contenttypes.models.ContentType` - associated with the concrete model of the specified one(s). That means there - was no way to retrieve the - :class:`~django.contrib.contenttypes.models.ContentType` of a proxy model - using those methods. As of Django 1.5 you can now pass a boolean flag – - ``for_concrete_model`` and ``for_concrete_models`` respectively – to specify - wether or not you want to retrieve the - :class:`~django.contrib.contenttypes.models.ContentType` for the concrete or - direct model. - Generic relations ================= diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index 5e51e4cbf3..66afc3d377 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -949,10 +949,6 @@ __ http://geohash.org/ *Availability*: PostGIS, SpatiaLite -.. versionchanged:: 1.5 - - ``geojson`` support for Spatialite > 3.0 has been added. - Attaches a ``geojson`` attribute to every model in the queryset that contains the `GeoJSON`__ representation of the geometry. diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index 12e4e55165..a9c947407f 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -276,10 +276,6 @@ that the SRID value is not included in this representation because it is not a part of the OGC specification (use the :attr:`GEOSGeometry.hexewkb` property instead). -.. versionchanged:: 1.5 - - Prior to Django 1.5, the Z value of the geometry was dropped. - .. attribute:: GEOSGeometry.hexewkb Returns the EWKB of this Geometry in hexadecimal form. This is an @@ -325,10 +321,6 @@ Returns the WKB (Well-Known Binary) representation of this Geometry as a Python buffer. SRID value is not included, use the :attr:`GEOSGeometry.ewkb` property instead. -.. versionchanged:: 1.5 - - Prior to Django 1.5, the Z value of the geometry was dropped. - .. _ewkb: .. attribute:: GEOSGeometry.ewkb @@ -426,8 +418,6 @@ geometry that do not make up other. .. method:: GEOSGeometry.interpolate(distance) .. method:: GEOSGeometry.interpolate_normalized(distance) -.. versionadded:: 1.5 - Given a distance (float), returns the point (or closest point) within the geometry (:class:`LineString` or :class:`MultiLineString`) at that distance. The normalized version takes the distance as a float between 0 (origin) and 1 @@ -443,8 +433,6 @@ geometry and other. .. method:: GEOSGeometry.project(point) .. method:: GEOSGeometry.project_normalized(point) -.. versionadded:: 1.5 - Returns the distance (float) from the origin of the geometry (:class:`LineString` or :class:`MultiLineString`) to the point projected on the geometry (that is to a point of the line the closest to the given point). diff --git a/docs/ref/contrib/messages.txt b/docs/ref/contrib/messages.txt index 608c37bb7f..2014c7c183 100644 --- a/docs/ref/contrib/messages.txt +++ b/docs/ref/contrib/messages.txt @@ -54,20 +54,21 @@ Storage backends The messages framework can use different backends to store temporary messages. -Django provides three built-in storage classes: +Django provides three built-in storage classes in +:mod:`django.contrib.messages`: -.. class:: django.contrib.messages.storage.session.SessionStorage +.. class:: storage.session.SessionStorage This class stores all messages inside of the request's session. Therefore it requires Django's ``contrib.sessions`` application. -.. class:: django.contrib.messages.storage.cookie.CookieStorage +.. class:: storage.cookie.CookieStorage This class stores the message data in a cookie (signed with a secret hash to prevent manipulation) to persist notifications across requests. Old messages are dropped if the cookie data size would exceed 2048 bytes. -.. class:: django.contrib.messages.storage.fallback.FallbackStorage +.. class:: storage.fallback.FallbackStorage This class first uses ``CookieStorage``, and falls back to using ``SessionStorage`` for the messages that could not fit in a single cookie. @@ -83,6 +84,8 @@ path, for example:: MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' +.. class:: storage.base.BaseStorage + To write your own storage class, subclass the ``BaseStorage`` class in ``django.contrib.messages.storage.base`` and implement the ``_get`` and ``_store`` methods. diff --git a/docs/ref/contrib/sites.txt b/docs/ref/contrib/sites.txt index f6480cae3a..fd2e917c1d 100644 --- a/docs/ref/contrib/sites.txt +++ b/docs/ref/contrib/sites.txt @@ -417,9 +417,10 @@ Here's how Django uses the sites framework: :class:`~django.contrib.sites.models.Site` name to the template as ``{{ site_name }}``. -* The shortcut view (``django.views.defaults.shortcut``) uses the domain - of the current :class:`~django.contrib.sites.models.Site` object when - calculating an object's URL. +* The shortcut view (``django.contrib.contenttypes.views.shortcut``) + uses the domain of the current + :class:`~django.contrib.sites.models.Site` object when calculating + an object's URL. * In the admin framework, the "view on site" link uses the current :class:`~django.contrib.sites.models.Site` to work out the domain for the diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index f96c2e8a1b..b9963414f4 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -255,8 +255,6 @@ CachedStaticFilesStorage .. method:: file_hash(name, content=None) - .. versionadded:: 1.5 - The method that is used when creating the hashed name of a file. Needs to return a hash for the given file name and content. By default it calculates a MD5 hash from the content's chunks as @@ -290,8 +288,6 @@ The previous example is equal to calling the ``url`` method of an instance of useful when using a non-local storage backend to deploy files as documented in :ref:`staticfiles-from-cdn`. -.. versionadded:: 1.5 - If you'd like to retrieve a static URL without displaying it, you can use a slightly different call: diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 60153eb735..707184c3ac 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -191,10 +191,6 @@ Django supports MySQL 5.0.3 and higher. `MySQL 5.0`_ adds the ``information_schema`` database, which contains detailed data on all database schema. Django's ``inspectdb`` feature uses it. -.. versionchanged:: 1.5 - - The minimum version requirement of MySQL 5.0.3 was set in Django 1.5. - Django expects the database to support Unicode (UTF-8 encoding) and delegates to it the task of enforcing transactions and referential integrity. It is important to be aware of the fact that the two latter ones aren't actually enforced by diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 27b68e4f1c..1385648d5d 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -254,8 +254,6 @@ to flush. ``--no-initial-data`` ~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.5 - Use ``--no-initial-data`` to avoid loading the initial_data fixture. @@ -332,8 +330,6 @@ onto which the data will be loaded. .. django-admin-option:: --ignorenonexistent -.. versionadded:: 1.5 - The :djadminopt:`--ignorenonexistent` option can be used to ignore fields that may have been removed from models since the fixture was originally generated. @@ -903,10 +899,6 @@ behavior you can use the ``--no-startup`` option. e.g.:: django-admin.py shell --plain --no-startup -.. versionadded:: 1.5 - - The ``--interface`` option was added in Django 1.5. - .. versionadded:: 1.6 The ``--no-startup`` option was added in Django 1.6. @@ -1337,8 +1329,6 @@ clearsessions .. django-admin:: clearsessions -.. versionadded:: 1.5 - Can be run as a cron job or directly to clean out expired sessions. ``django.contrib.sitemaps`` diff --git a/docs/ref/files/file.txt b/docs/ref/files/file.txt index 7562f9b6bf..6ca7ec121a 100644 --- a/docs/ref/files/file.txt +++ b/docs/ref/files/file.txt @@ -100,10 +100,6 @@ The ``ContentFile`` Class f1 = ContentFile("esta sentencia está en español") f2 = ContentFile(b"these are bytes") - .. versionchanged:: 1.5 - - ContentFile also accepts Unicode strings. - .. currentmodule:: django.core.files.images The ``ImageFile`` Class diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 780cb5d4f7..f084273cd9 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -211,11 +211,6 @@ only the valid fields:: >>> f.cleaned_data {'cc_myself': True, 'message': u'Hi there'} -.. versionchanged:: 1.5 - -Until Django 1.5, the ``cleaned_data`` attribute wasn't defined at all when -the ``Form`` didn't validate. - ``cleaned_data`` will always *only* contain a key for fields defined in the ``Form``, even if you pass extra data when you define the ``Form``. In this example, we pass a bunch of extra fields to the ``ContactForm`` constructor, diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index e7c6612a72..0e4d352132 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -573,16 +573,12 @@ For each field, we describe the default widget used if you don't specify .. attribute:: allow_files - .. versionadded:: 1.5 - Optional. Either ``True`` or ``False``. Default is ``True``. Specifies whether files in the specified location should be included. Either this or :attr:`allow_folders` must be ``True``. .. attribute:: allow_folders - .. versionadded:: 1.5 - Optional. Either ``True`` or ``False``. Default is ``False``. Specifies whether folders in the specified location should be included. Either this or :attr:`allow_files` must be ``True``. @@ -1065,11 +1061,6 @@ objects (in the case of ``ModelMultipleChoiceField``) into the * Error message keys: ``required``, ``list``, ``invalid_choice``, ``invalid_pk_value`` - .. versionchanged:: 1.5 - - The empty and normalized values were changed to be consistently - ``QuerySets`` instead of ``[]`` and ``QuerySet`` respectively. - .. versionchanged:: 1.6 The ``invalid_choice`` message may contain ``%(value)s`` and the diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt index 255b665c42..94e9308c9a 100644 --- a/docs/ref/forms/validation.txt +++ b/docs/ref/forms/validation.txt @@ -450,11 +450,5 @@ entries in ``_errors``. Secondly, once we have decided that the combined data in the two fields we are considering aren't valid, we must remember to remove them from the -``cleaned_data``. - -.. versionchanged:: 1.5 - - Django used to remove the ``cleaned_data`` attribute entirely if there were - any errors in the form. Since version 1.5, ``cleaned_data`` is present even if - the form doesn't validate, but it contains only field values that did - validate. +``cleaned_data``. `cleaned_data`` is present even if the form doesn't +validate, but it contains only field values that did validate. diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index 080d1fea86..47e14043fc 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -525,11 +525,6 @@ Selector and checkbox widgets A callable that takes the value of the CheckBoxInput and returns ``True`` if the checkbox should be checked for that value. - .. versionchanged:: 1.5 - - Exceptions from ``check_test`` used to be silenced by its caller, - this is no longer the case, they will propagate upwards. - ``Select`` ~~~~~~~~~~ @@ -590,25 +585,26 @@ Selector and checkbox widgets .. code-block:: html
- +
- +
- +
- +
That included the ``