From 186ec21a3dd8c87704c579b6620da931e0f68bb3 Mon Sep 17 00:00:00 2001 From: Deni Bertovic Date: Sat, 18 May 2013 16:06:08 +0200 Subject: [PATCH 1/9] Added stripping of whitespace for SlugField and URLField --- django/forms/fields.py | 8 ++++++++ tests/forms_tests/tests/test_extra.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/django/forms/fields.py b/django/forms/fields.py index 4ce57d34a3..3ef0d72463 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -670,6 +670,10 @@ class URLField(CharField): value = urlunsplit(url_fields) return value + def clean(self, value): + value = self.to_python(value).strip() + return super(URLField, self).clean(value) + class BooleanField(Field): widget = CheckboxInput @@ -1105,3 +1109,7 @@ class GenericIPAddressField(CharField): class SlugField(CharField): default_validators = [validators.validate_slug] + + def clean(self, value): + value = self.to_python(value).strip() + return super(SlugField, self).clean(value) diff --git a/tests/forms_tests/tests/test_extra.py b/tests/forms_tests/tests/test_extra.py index a83cdfc05f..ea0f063c30 100644 --- a/tests/forms_tests/tests/test_extra.py +++ b/tests/forms_tests/tests/test_extra.py @@ -569,6 +569,14 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): f = GenericIPAddressField(unpack_ipv4=True) self.assertEqual(f.clean(' ::ffff:0a0a:0a0a'), '10.10.10.10') + def test_slugfield_normalization(self): + f = SlugField() + self.assertEqual(f.clean(' aa-bb-cc '), 'aa-bb-cc') + + def test_urlfield_normalization(self): + f = URLField() + self.assertEqual(f.clean('http://example.com/ '), 'http://example.com/') + def test_smart_text(self): class Test: if six.PY3: From 63a9555d57069cee32de388821dbe580da1f97c0 Mon Sep 17 00:00:00 2001 From: Olivier Sels Date: Sat, 18 May 2013 13:09:38 +0200 Subject: [PATCH 2/9] Fixed #19436 -- Don't log warnings in ensure_csrf_cookie. --- AUTHORS | 1 + django/middleware/csrf.py | 34 +++++----------------- django/views/decorators/csrf.py | 2 +- tests/csrf_tests/tests.py | 51 +++++++++++++++++++++++++-------- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/AUTHORS b/AUTHORS index e83fc035a3..4a9981d1fe 100644 --- a/AUTHORS +++ b/AUTHORS @@ -505,6 +505,7 @@ answer newbie questions, and generally made Django that much better: Bernd Schlapsi schwank@gmail.com scott@staplefish.com + Olivier Sels Ilya Semenov Aleksandra Sendecka serbaut@gmail.com diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index 423034478b..98974f011a 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -83,6 +83,13 @@ class CsrfViewMiddleware(object): return None def _reject(self, request, reason): + logger.warning('Forbidden (%s): %s', + reason, request.path, + extra={ + 'status_code': 403, + 'request': request, + } + ) return _get_failure_view()(request, reason=reason) def process_view(self, request, callback, callback_args, callback_kwargs): @@ -134,38 +141,18 @@ class CsrfViewMiddleware(object): # we can use strict Referer checking. referer = request.META.get('HTTP_REFERER') if referer is None: - logger.warning('Forbidden (%s): %s', - REASON_NO_REFERER, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, REASON_NO_REFERER) # Note that request.get_host() includes the port. good_referer = 'https://%s/' % request.get_host() if not same_origin(referer, good_referer): reason = REASON_BAD_REFERER % (referer, good_referer) - logger.warning('Forbidden (%s): %s', reason, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, reason) if csrf_token is None: # No CSRF cookie. For POST requests, we insist on a CSRF cookie, # and in this way we can avoid all CSRF attacks, including login # CSRF. - logger.warning('Forbidden (%s): %s', - REASON_NO_CSRF_COOKIE, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, REASON_NO_CSRF_COOKIE) # Check non-cookie token for match. @@ -179,13 +166,6 @@ class CsrfViewMiddleware(object): request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '') if not constant_time_compare(request_csrf_token, csrf_token): - logger.warning('Forbidden (%s): %s', - REASON_BAD_TOKEN, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, REASON_BAD_TOKEN) return self._accept(request) diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py index 7a7eb6bba6..a6bd7d8526 100644 --- a/django/views/decorators/csrf.py +++ b/django/views/decorators/csrf.py @@ -15,7 +15,7 @@ using the decorator multiple times, is harmless and efficient. class _EnsureCsrfToken(CsrfViewMiddleware): # We need this to behave just like the CsrfViewMiddleware, but not reject - # requests. + # requests or log warnings. def _reject(self, request, reason): return None diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index b9e8cb5f75..841b24bb42 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import logging from django.conf import settings from django.core.context_processors import csrf @@ -78,18 +79,18 @@ class CsrfViewMiddlewareTest(TestCase): def _check_token_present(self, response, csrf_id=None): self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % (csrf_id or self._csrf_id)) - def test_process_view_token_too_long(self): - """ - Check that if the token is longer than expected, it is ignored and - a new token is created. - """ - req = self._get_GET_no_csrf_cookie_request() - req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 10000000 - CsrfViewMiddleware().process_view(req, token_view, (), {}) - resp = token_view(req) - resp2 = CsrfViewMiddleware().process_response(req, resp) - csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) - self.assertEqual(len(csrf_cookie.value), CSRF_KEY_LENGTH) + def test_process_view_token_too_long(self): + """ + Check that if the token is longer than expected, it is ignored and + a new token is created. + """ + req = self._get_GET_no_csrf_cookie_request() + req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 10000000 + CsrfViewMiddleware().process_view(req, token_view, (), {}) + resp = token_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) + self.assertEqual(len(csrf_cookie.value), CSRF_KEY_LENGTH) def test_process_response_get_token_used(self): """ @@ -353,3 +354,29 @@ class CsrfViewMiddlewareTest(TestCase): resp2 = CsrfViewMiddleware().process_response(req, resp) self.assertTrue(resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)) self.assertTrue('Cookie' in resp2.get('Vary','')) + + def test_ensures_csrf_cookie_no_logging(self): + """ + Tests that ensure_csrf_cookie doesn't log warnings. See #19436. + """ + @ensure_csrf_cookie + def view(request): + # Doesn't insert a token or anything + return HttpResponse(content="") + + class TestHandler(logging.Handler): + def emit(self, record): + raise Exception("This shouldn't have happened!") + + logger = logging.getLogger('django.request') + test_handler = TestHandler() + old_log_level = logger.level + try: + logger.addHandler(test_handler) + logger.setLevel(logging.WARNING) + + req = self._get_GET_no_csrf_cookie_request() + resp = view(req) + finally: + logger.removeHandler(test_handler) + logger.setLevel(old_log_level) From 3634948c883c89a9dbf303b9687331a2a5db43ce Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 18 May 2013 16:40:03 +0200 Subject: [PATCH 3/9] Moved IgnorePendingDeprecationWarningsMixin in django.test.utils. This mixin is useful whenever deprecating a large part of Django. --- django/test/utils.py | 21 +++++++++++++++++++++ tests/middleware/tests.py | 4 +--- tests/transactions/tests.py | 14 +------------- tests/transactions_regress/tests.py | 4 +--- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/django/test/utils.py b/django/test/utils.py index 92cef59f72..fb9221d25c 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -1,4 +1,5 @@ import re +import sys import warnings from functools import wraps from xml.dom.minidom import parseString, Node @@ -380,3 +381,23 @@ class CaptureQueriesContext(object): if exc_type is not None: return self.final_queries = len(self.connection.queries) + + +class IgnoreDeprecationWarningsMixin(object): + + warning_class = DeprecationWarning + + def setUp(self): + super(IgnoreDeprecationWarningsMixin, self).setUp() + self.catch_warnings = warnings.catch_warnings() + self.catch_warnings.__enter__() + warnings.filterwarnings("ignore", category=self.warning_class) + + def tearDown(self): + self.catch_warnings.__exit__(*sys.exc_info()) + super(IgnoreDeprecationWarningsMixin, self).tearDown() + + +class IgnorePendingDeprecationWarningsMixin(IgnoreDeprecationWarningsMixin): + + warning_class = PendingDeprecationWarning diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index e526da4772..1ff8390f31 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -18,14 +18,12 @@ from django.middleware.http import ConditionalGetMiddleware from django.middleware.gzip import GZipMiddleware from django.middleware.transaction import TransactionMiddleware from django.test import TransactionTestCase, TestCase, RequestFactory -from django.test.utils import override_settings +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin from django.utils import six from django.utils.encoding import force_str from django.utils.six.moves import xrange from django.utils.unittest import expectedFailure, skipIf -from transactions.tests import IgnorePendingDeprecationWarningsMixin - from .models import Band diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index aeb9bc3d2c..0f16a9c805 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -5,6 +5,7 @@ import warnings from django.db import connection, transaction, IntegrityError from django.test import TransactionTestCase, skipUnlessDBFeature +from django.test.utils import IgnorePendingDeprecationWarningsMixin from django.utils import six from django.utils.unittest import skipIf, skipUnless @@ -319,19 +320,6 @@ class AtomicMiscTests(TransactionTestCase): transaction.atomic(Callable()) -class IgnorePendingDeprecationWarningsMixin(object): - - def setUp(self): - super(IgnorePendingDeprecationWarningsMixin, self).setUp() - self.catch_warnings = warnings.catch_warnings() - self.catch_warnings.__enter__() - warnings.filterwarnings("ignore", category=PendingDeprecationWarning) - - def tearDown(self): - self.catch_warnings.__exit__(*sys.exc_info()) - super(IgnorePendingDeprecationWarningsMixin, self).tearDown() - - class TransactionTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): def create_a_reporter_then_fail(self, first, last): diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index fb3f257dab..5339b4a8ea 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -4,11 +4,9 @@ from django.db import (connection, connections, transaction, DEFAULT_DB_ALIAS, D IntegrityError) from django.db.transaction import commit_on_success, commit_manually, TransactionManagementError from django.test import TransactionTestCase, skipUnlessDBFeature -from django.test.utils import override_settings +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin from django.utils.unittest import skipIf, skipUnless -from transactions.tests import IgnorePendingDeprecationWarningsMixin - from .models import Mod, M2mA, M2mB, SubMod class ModelInheritanceTests(TransactionTestCase): From d5ce2ff5e485bf94fcade340bc803ba4671bd95a Mon Sep 17 00:00:00 2001 From: Erik Romijn Date: Sat, 18 May 2013 16:35:39 +0200 Subject: [PATCH 4/9] Fixed #20444 -- Cookie-based sessions does not include a remote code execution-warning --- docs/topics/http/sessions.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index acad61eb2a..0f2955fadd 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -125,6 +125,17 @@ and the :setting:`SECRET_KEY` setting. .. warning:: + **If the :setting:`SECRET_KEY` is not kept secret, this can lead to + arbitrary remote code execution.** + + An attacker in possession of the :setting:`SECRET_KEY` can not only + generate falsified session data, which your site will trust, but also + remotely execute arbitrary code, as the data is serialized using pickle. + + If you use cookie-based sessions, pay extra care that your secret key is + always kept completely secret, for any system which might be remotely + accessible. + **The session data is signed but not encrypted** When using the cookies backend the session data can be read by the client. From 64e11a68f19793d11915e83574b1bb693da3980e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sat, 18 May 2013 16:38:11 +0200 Subject: [PATCH 5/9] Fixed #13285: populate_xheaders breaks caching --- django/contrib/flatpages/views.py | 2 - django/core/xheaders.py | 24 ------- docs/releases/1.6.txt | 5 ++ tests/special_headers/__init__.py | 0 tests/special_headers/fixtures/data.xml | 20 ------ tests/special_headers/models.py | 5 -- .../special_headers/article_detail.html | 1 - tests/special_headers/tests.py | 62 ------------------- tests/special_headers/urls.py | 13 ---- tests/special_headers/views.py | 21 ------- 10 files changed, 5 insertions(+), 148 deletions(-) delete mode 100644 django/core/xheaders.py delete mode 100644 tests/special_headers/__init__.py delete mode 100644 tests/special_headers/fixtures/data.xml delete mode 100644 tests/special_headers/models.py delete mode 100644 tests/special_headers/templates/special_headers/article_detail.html delete mode 100644 tests/special_headers/tests.py delete mode 100644 tests/special_headers/urls.py delete mode 100644 tests/special_headers/views.py diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py index 497979e497..20e930f343 100644 --- a/django/contrib/flatpages/views.py +++ b/django/contrib/flatpages/views.py @@ -1,7 +1,6 @@ from django.conf import settings from django.contrib.flatpages.models import FlatPage from django.contrib.sites.models import get_current_site -from django.core.xheaders import populate_xheaders from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect from django.shortcuts import get_object_or_404 from django.template import loader, RequestContext @@ -70,5 +69,4 @@ def render_flatpage(request, f): 'flatpage': f, }) response = HttpResponse(t.render(c)) - populate_xheaders(request, response, FlatPage, f.id) return response diff --git a/django/core/xheaders.py b/django/core/xheaders.py deleted file mode 100644 index 3766628c98..0000000000 --- a/django/core/xheaders.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Pages in Django can are served up with custom HTTP headers containing useful -information about those pages -- namely, the content type and object ID. - -This module contains utility functions for retrieving and doing interesting -things with these special "X-Headers" (so called because the HTTP spec demands -that custom headers are prefixed with "X-"). - -Next time you're at slashdot.org, watch out for X-Fry and X-Bender. :) -""" - -def populate_xheaders(request, response, model, object_id): - """ - Adds the "X-Object-Type" and "X-Object-Id" headers to the given - HttpResponse according to the given model and object_id -- but only if the - given HttpRequest object has an IP address within the INTERNAL_IPS setting - or if the request is from a logged in staff member. - """ - from django.conf import settings - if (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS - or (hasattr(request, 'user') and request.user.is_active - and request.user.is_staff)): - response['X-Object-Type'] = "%s.%s" % (model._meta.app_label, model._meta.model_name) - response['X-Object-Id'] = str(object_id) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 60b3381dd6..452baf7f76 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -491,6 +491,11 @@ Miscellaneous memcache backend no longer uses the default timeout, and now will set-and-expire-immediately the value. +* The ``django.contrib.flatpages`` app used to set custom HTTP headers for + debugging purposes. This functionality was not documented and made caching + ineffective so it has been removed, along with its generic implementation, + previously available in ``django.core.xheaders``. + Features deprecated in 1.6 ========================== diff --git a/tests/special_headers/__init__.py b/tests/special_headers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/special_headers/fixtures/data.xml b/tests/special_headers/fixtures/data.xml deleted file mode 100644 index 7e60d45199..0000000000 --- a/tests/special_headers/fixtures/data.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - super - Super - User - super@example.com - sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158 - True - True - True - 2007-05-30 13:20:10 - 2007-05-30 13:20:10 - - - - - text - - diff --git a/tests/special_headers/models.py b/tests/special_headers/models.py deleted file mode 100644 index e05c5a6920..0000000000 --- a/tests/special_headers/models.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.db import models - - -class Article(models.Model): - text = models.TextField() diff --git a/tests/special_headers/templates/special_headers/article_detail.html b/tests/special_headers/templates/special_headers/article_detail.html deleted file mode 100644 index 3cbd38cb7e..0000000000 --- a/tests/special_headers/templates/special_headers/article_detail.html +++ /dev/null @@ -1 +0,0 @@ -{{ object }} diff --git a/tests/special_headers/tests.py b/tests/special_headers/tests.py deleted file mode 100644 index b4b704ae21..0000000000 --- a/tests/special_headers/tests.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.contrib.auth.models import User -from django.test import TestCase -from django.test.utils import override_settings - - -@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class SpecialHeadersTest(TestCase): - fixtures = ['data.xml'] - urls = 'special_headers.urls' - - def test_xheaders(self): - user = User.objects.get(username='super') - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - self.client.login(username='super', password='secret') - response = self.client.get('/special_headers/article/1/') - self.assertTrue('X-Object-Type' in response) - user.is_staff = False - user.save() - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - - def test_xview_func(self): - user = User.objects.get(username='super') - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - self.client.login(username='super', password='secret') - response = self.client.head('/special_headers/xview/func/') - self.assertTrue('X-View' in response) - self.assertEqual(response['X-View'], 'special_headers.views.xview') - user.is_staff = False - user.save() - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - - def test_xview_class(self): - user = User.objects.get(username='super') - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) - self.client.login(username='super', password='secret') - response = self.client.head('/special_headers/xview/class/') - self.assertTrue('X-View' in response) - self.assertEqual(response['X-View'], 'special_headers.views.XViewClass') - user.is_staff = False - user.save() - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) diff --git a/tests/special_headers/urls.py b/tests/special_headers/urls.py deleted file mode 100644 index f7ba141207..0000000000 --- a/tests/special_headers/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -# coding: utf-8 -from __future__ import absolute_import - -from django.conf.urls import patterns - -from . import views -from .models import Article - -urlpatterns = patterns('', - (r'^special_headers/article/(?P\d+)/$', views.xview_xheaders), - (r'^special_headers/xview/func/$', views.xview_dec(views.xview)), - (r'^special_headers/xview/class/$', views.xview_dec(views.XViewClass.as_view())), -) diff --git a/tests/special_headers/views.py b/tests/special_headers/views.py deleted file mode 100644 index a8bbd6542e..0000000000 --- a/tests/special_headers/views.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.core.xheaders import populate_xheaders -from django.http import HttpResponse -from django.utils.decorators import decorator_from_middleware -from django.views.generic import View -from django.middleware.doc import XViewMiddleware - -from .models import Article - -xview_dec = decorator_from_middleware(XViewMiddleware) - -def xview(request): - return HttpResponse() - -def xview_xheaders(request, object_id): - response = HttpResponse() - populate_xheaders(request, response, Article, 1) - return response - -class XViewClass(View): - def get(self, request): - return HttpResponse() From a19e9d80ffa10f8da43addcaa4ddd440beee8a4d Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Fri, 17 May 2013 15:31:41 -0400 Subject: [PATCH 6/9] Fixed #20430 - Enable iterable of iterables for model choices Allows for any iterable, not just lists or tuples, to be used as the inner item for a list of choices in a model. --- django/core/management/validation.py | 4 +-- docs/ref/models/fields.txt | 7 ++--- docs/releases/1.6.txt | 3 +++ tests/invalid_models/invalid_models/models.py | 4 +-- tests/model_validation/__init__.py | 0 tests/model_validation/models.py | 27 +++++++++++++++++++ tests/model_validation/tests.py | 14 ++++++++++ 7 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 tests/model_validation/__init__.py create mode 100644 tests/model_validation/models.py create mode 100644 tests/model_validation/tests.py diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 0f0eade569..2040a14582 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -118,8 +118,8 @@ def get_validation_errors(outfile, app=None): e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name) else: for c in f.choices: - if not isinstance(c, (list, tuple)) or len(c) != 2: - e.add(opts, '"%s": "choices" should be a sequence of two-tuples.' % f.name) + if isinstance(c, six.string_types) or not is_iterable(c) or len(c) != 2: + e.add(opts, '"%s": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples).' % f.name) if f.db_index not in (None, True, False): e.add(opts, '"%s": "db_index" should be either None, True or False.' % f.name) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 99ba78cb09..38b6459909 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -80,9 +80,10 @@ If a field has ``blank=False``, the field will be required. .. attribute:: Field.choices -An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this -field. If this is given, the default form widget will be a select box with -these choices instead of the standard text field. +An iterable (e.g., a list or tuple) consisting itself of iterables of exactly +two items (e.g. ``[(A, B), (A, B) ...]``) to use as choices for this field. If +this is given, the default form widget will be a select box with these choices +instead of the standard text field. The first element in each tuple is the actual value to be stored, and the second element is the human-readable name. For example:: diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 60b3381dd6..b10a9ef81e 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -238,6 +238,9 @@ Minor features Meta option: ``localized_fields``. Fields included in this list will be localized (by setting ``localize`` on the form field). +* The ``choices`` argument to model fields now accepts an iterable of iterables + instead of requiring an iterable of lists or tuples. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/invalid_models/invalid_models/models.py b/tests/invalid_models/invalid_models/models.py index 3c21e1ddb8..6ba3f04e5e 100644 --- a/tests/invalid_models/invalid_models/models.py +++ b/tests/invalid_models/invalid_models/models.py @@ -375,8 +375,8 @@ invalid_models.fielderrors: "decimalfield3": DecimalFields require a "max_digits invalid_models.fielderrors: "decimalfield4": DecimalFields require a "max_digits" attribute value that is greater than or equal to the value of the "decimal_places" attribute. invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute. invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list). -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). invalid_models.fielderrors: "index": "db_index" should be either None, True or False. invalid_models.fielderrors: "field_": Field names cannot end with underscores, because this would lead to ambiguous queryset filters. invalid_models.fielderrors: "nullbool": BooleanFields do not accept null values. Use a NullBooleanField instead. diff --git a/tests/model_validation/__init__.py b/tests/model_validation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/model_validation/models.py b/tests/model_validation/models.py new file mode 100644 index 0000000000..9a2a5f7cd0 --- /dev/null +++ b/tests/model_validation/models.py @@ -0,0 +1,27 @@ +from django.db import models + + +class ThingItem(object): + + def __init__(self, value, display): + self.value = value + self.display = display + + def __iter__(self): + return (x for x in [self.value, self.display]) + + def __len__(self): + return 2 + + +class Things(object): + + def __iter__(self): + return (x for x in [ThingItem(1, 2), ThingItem(3, 4)]) + + +class ThingWithIterableChoices(models.Model): + + # Testing choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + thing = models.CharField(max_length=100, blank=True, choices=Things()) diff --git a/tests/model_validation/tests.py b/tests/model_validation/tests.py new file mode 100644 index 0000000000..96baf640eb --- /dev/null +++ b/tests/model_validation/tests.py @@ -0,0 +1,14 @@ +import io + +from django.core import management +from django.test import TestCase + + +class ModelValidationTest(TestCase): + + def test_models_validate(self): + # All our models should validate properly + # Validation Tests: + # * choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + management.call_command("validate", stdout=io.BytesIO()) From 56e2f6ccae76a6f0a7f1d64677bf29e11518e5c6 Mon Sep 17 00:00:00 2001 From: Erik Romijn Date: Sat, 18 May 2013 17:16:07 +0200 Subject: [PATCH 7/9] Fixed #20446 -- Documentation for SmallIntegerField does not clarify 'small' --- docs/ref/models/fields.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 99ba78cb09..da5fe9bf60 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -889,7 +889,8 @@ The value ``0`` is accepted for backward compatibility reasons. .. class:: PositiveSmallIntegerField([**options]) Like a :class:`PositiveIntegerField`, but only allows values under a certain -(database-dependent) point. +(database-dependent) point. Values up to 32767 are safe in all databases +supported by Django. ``SlugField`` ------------- @@ -917,7 +918,8 @@ of some other value. You can do this automatically in the admin using .. class:: SmallIntegerField([**options]) Like an :class:`IntegerField`, but only allows values under a certain -(database-dependent) point. +(database-dependent) point. Values from -32768 to 32767 are safe in all databases +supported by Django. ``TextField`` ------------- From 96cabba80805ddda9c48307cb3d75177dc94ba29 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 18 May 2013 11:20:02 -0400 Subject: [PATCH 8/9] Fixed #20335 - Documented the {% language %} template tag. Thanks bmispelon for the suggestion and djangsters for the patch. --- docs/topics/i18n/translation.txt | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 5b4ffea528..72e000a86f 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -722,6 +722,31 @@ or with the ``{#`` ... ``#}`` :ref:`one-line comment constructs +

{% trans "Welcome to our page" %}

+ + {% language 'en' %} + {% get_current_language as LANGUAGE_CODE %} + +

{% trans "Welcome to our page" %}

+ {% endlanguage %} + +While the first occurrence of "Welcome to our page" uses the current language, +the second will always be in English. + .. _template-translation-vars: Other tags @@ -1126,13 +1151,11 @@ active language. Example:: .. _reversing_in_templates: -.. templatetag:: language - Reversing in templates ---------------------- If localized URLs get reversed in templates they always use the current -language. To link to a URL in another language use the ``language`` +language. To link to a URL in another language use the :ttag:`language` template tag. It enables the given language in the enclosed template section: .. code-block:: html+django From bd97f7d0cb72191744552142817184e88ce8841d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sat, 18 May 2013 16:10:14 +0200 Subject: [PATCH 9/9] Fixed #15201: Marked CACHE_MIDDLEWARE_ANONYMOUS_ONLY as deprecated --- django/middleware/cache.py | 11 ++++++----- docs/faq/admin.txt | 6 ------ docs/internals/deprecation.txt | 2 ++ docs/ref/settings.txt | 8 ++++++-- docs/releases/1.6.txt | 17 +++++++++++++++++ docs/topics/cache.txt | 12 +++--------- tests/cache/tests.py | 8 +++++--- 7 files changed, 39 insertions(+), 25 deletions(-) diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 83860e15f3..e13a8c3918 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -29,11 +29,6 @@ More details about how the caching works: of the response's "Cache-Control" header, falling back to the CACHE_MIDDLEWARE_SECONDS setting if the section was not found. -* If CACHE_MIDDLEWARE_ANONYMOUS_ONLY is set to True, only anonymous requests - (i.e., those not made by a logged-in user) will be cached. This is a simple - and effective way of avoiding the caching of the Django admin (and any other - user-specific content). - * This middleware expects that a HEAD request is answered with the same response headers exactly like the corresponding GET request. @@ -48,6 +43,8 @@ More details about how the caching works: """ +import warnings + from django.conf import settings from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age @@ -200,5 +197,9 @@ class CacheMiddleware(UpdateCacheMiddleware, FetchFromCacheMiddleware): else: self.cache_anonymous_only = cache_anonymous_only + if self.cache_anonymous_only: + msg = "CACHE_MIDDLEWARE_ANONYMOUS_ONLY has been deprecated and will be removed in Django 1.8." + warnings.warn(msg, PendingDeprecationWarning, stacklevel=1) + self.cache = get_cache(self.cache_alias, **cache_kwargs) self.cache_timeout = self.cache.default_timeout diff --git a/docs/faq/admin.txt b/docs/faq/admin.txt index 1d9a7c7427..ec40754094 100644 --- a/docs/faq/admin.txt +++ b/docs/faq/admin.txt @@ -27,12 +27,6 @@ account has :attr:`~django.contrib.auth.models.User.is_active` and :attr:`~django.contrib.auth.models.User.is_staff` set to True. The admin site only allows access to users with those two fields both set to True. -How can I prevent the cache middleware from caching the admin site? -------------------------------------------------------------------- - -Set the :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY` setting to ``True``. See the -:doc:`cache documentation ` for more information. - How do I automatically set a field's value to the user who last edited the object in the admin? ----------------------------------------------------------------------------------------------- diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 774de2a2fd..095b6d0a33 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -390,6 +390,8 @@ these changes. ``django.test.testcases.OutputChecker`` will be removed. Instead use the doctest module from the Python standard library. +* The ``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting will be removed. + 2.0 --- diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index eb470cdd14..8ef59064f7 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -280,6 +280,12 @@ CACHE_MIDDLEWARE_ANONYMOUS_ONLY Default: ``False`` +.. deprecated:: 1.6 + + This setting was largely ineffective because of using cookies for sessions + and CSRF. See the :doc:`Django 1.6 release notes` for more + information. + If the value of this setting is ``True``, only anonymous requests (i.e., not those made by a logged-in user) will be cached. Otherwise, the middleware caches every page that doesn't have GET or POST parameters. @@ -287,8 +293,6 @@ caches every page that doesn't have GET or POST parameters. If you set the value of this setting to ``True``, you should make sure you've activated ``AuthenticationMiddleware``. -See :doc:`/topics/cache`. - .. setting:: CACHE_MIDDLEWARE_KEY_PREFIX CACHE_MIDDLEWARE_KEY_PREFIX diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index ef79e5770c..f8e1fd6339 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -569,6 +569,23 @@ If necessary, you can temporarily disable auto-escaping with :func:`~django.utils.safestring.mark_safe` or :ttag:`{% autoescape off %} `. +``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``CacheMiddleware`` used to provide a way to cache requests only if they +weren't made by a logged-in user. This mechanism was largely ineffective +because the middleware correctly takes into account the ``Vary: Cookie`` HTTP +header, and this header is being set on a variety of occasions, such as: + +* accessing the session, or +* using CSRF protection, which is turned on by default, or +* using a client-side library which sets cookies, like `Google Analytics`__. + +This makes the cache effectively work on a per-session basis regardless of the +``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting. + +__ http://www.google.com/analytics/ + ``SEND_BROKEN_LINK_EMAILS`` setting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index a7d54fbeb0..46911a593f 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -443,15 +443,9 @@ Then, add the following required settings to your Django settings file: The cache middleware caches GET and HEAD responses with status 200, where the request and response headers allow. Responses to requests for the same URL with different query parameters are considered to be unique pages and are cached separately. -Optionally, if the :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY` -setting is ``True``, only anonymous requests (i.e., not those made by a -logged-in user) will be cached. This is a simple and effective way of disabling -caching for any user-specific pages (including Django's admin interface). Note -that if you use :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY`, you should make -sure you've activated ``AuthenticationMiddleware``. The cache middleware -expects that a HEAD request is answered with the same response headers as -the corresponding GET request; in which case it can return a cached GET -response for HEAD request. +The cache middleware expects that a HEAD request is answered with the same +response headers as the corresponding GET request; in which case it can return +a cached GET response for HEAD request. Additionally, the cache middleware automatically sets a few headers in each :class:`~django.http.HttpResponse`: diff --git a/tests/cache/tests.py b/tests/cache/tests.py index 4f7ee8b525..231a3bfb50 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -28,8 +28,8 @@ from django.middleware.cache import (FetchFromCacheMiddleware, from django.template import Template from django.template.response import TemplateResponse from django.test import TestCase, TransactionTestCase, RequestFactory -from django.test.utils import override_settings, six -from django.utils import timezone, translation, unittest +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin +from django.utils import six, timezone, translation, unittest from django.utils.cache import (patch_vary_headers, get_cache_key, learn_cache_key, patch_cache_control, patch_response_headers) from django.utils.encoding import force_text @@ -1592,9 +1592,10 @@ def hello_world_view(request, value): }, }, ) -class CacheMiddlewareTest(TestCase): +class CacheMiddlewareTest(IgnorePendingDeprecationWarningsMixin, TestCase): def setUp(self): + super(CacheMiddlewareTest, self).setUp() self.factory = RequestFactory() self.default_cache = get_cache('default') self.other_cache = get_cache('other') @@ -1602,6 +1603,7 @@ class CacheMiddlewareTest(TestCase): def tearDown(self): self.default_cache.clear() self.other_cache.clear() + super(CacheMiddlewareTest, self).tearDown() def test_constructor(self): """