From 9ce58906af4a5f5a0739598ac10103fef3642419 Mon Sep 17 00:00:00 2001 From: Nick Martini Date: Fri, 7 Sep 2012 13:05:23 -0400 Subject: [PATCH 01/75] removed unused import from tutorial 3 documentation as per ticket #18915 --- docs/intro/tutorial03.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 6d4fb7eef1..03d4bf68b3 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -513,11 +513,12 @@ Here's what happens if a user goes to "/polls/34/" in this system: further processing. Now that we've decoupled that, we need to decouple the ``polls.urls`` -URLconf by removing the leading "polls/" from each line, and removing the -lines registering the admin site. Your ``polls/urls.py`` file should now look like +URLconf by removing the leading "polls/" from each line, removing the +lines registering the admin site, and removing the ``include`` import which +is no longer used. Your ``polls/urls.py`` file should now look like this:: - from django.conf.urls import patterns, include, url + from django.conf.urls import patterns, url urlpatterns = patterns('polls.views', url(r'^$', 'index'), From 3a10bcc91726859f9f0f817b39586712c2d37c2b Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 7 Sep 2012 15:51:22 -0400 Subject: [PATCH 02/75] Document ``six.assertRaisesRegex``. --- docs/topics/python3.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt index d816db8046..89d0c9f91f 100644 --- a/docs/topics/python3.txt +++ b/docs/topics/python3.txt @@ -400,5 +400,12 @@ The version of six bundled with Django includes one extra function: 2 and :meth:`~django.utils.datastructures.MultiValueDict.lists()` on Python 3. +.. function:: assertRaisesRegex(testcase, *args, **kwargs) + + This replaces ``testcase.assertRaisesRegexp`` on Python 2, and + ``testcase.assertRaisesRegex`` on Python 3. ``assertRaisesRegexp`` still + exists in current Python3 versions, but issues a warning. + + In addition to six' defaults moves, Django's version provides ``thread`` as ``_thread`` and ``dummy_thread`` as ``_dummy_thread``. From e78f2f6a636288ed8d9808b093e809e87523b3ac Mon Sep 17 00:00:00 2001 From: Enrico Ehrhardt Date: Fri, 7 Sep 2012 23:26:37 +0300 Subject: [PATCH 03/75] remove unused import in tutorial 4 addition to #18915 (closed) After submitting ticket #18915, I noticed 2 additional occurrences of the issue. --- docs/intro/tutorial04.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 31680ea5e5..49e597ca29 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -218,7 +218,7 @@ Read on for details. First, open the ``polls/urls.py`` URLconf. It looks like this, according to the tutorial so far:: - from django.conf.urls import patterns, include, url + from django.conf.urls import patterns, url urlpatterns = patterns('polls.views', url(r'^$', 'index'), @@ -229,7 +229,7 @@ tutorial so far:: Change it like so:: - from django.conf.urls import patterns, include, url + from django.conf.urls import patterns, url from django.views.generic import DetailView, ListView from polls.models import Poll From b865009d414a0f6fd0c0f5ad7434b2c13eb761c7 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 7 Sep 2012 16:49:22 -0400 Subject: [PATCH 04/75] Fixed #12397 -- allow safe_join to work with the root file system path, which means you can have your root template or file upload path at this location. You almost certainly don't want to do this, except in *very* limited sandboxed situations. --- django/utils/_os.py | 17 ++++++++++------- tests/regressiontests/utils/os_utils.py | 21 +++++++++++++++++++++ tests/regressiontests/utils/tests.py | 1 + 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 tests/regressiontests/utils/os_utils.py diff --git a/django/utils/_os.py b/django/utils/_os.py index 9eb5e5e8ea..1ea12aed8a 100644 --- a/django/utils/_os.py +++ b/django/utils/_os.py @@ -1,6 +1,6 @@ import os import stat -from os.path import join, normcase, normpath, abspath, isabs, sep +from os.path import join, normcase, normpath, abspath, isabs, sep, dirname from django.utils.encoding import force_text from django.utils import six @@ -41,13 +41,16 @@ def safe_join(base, *paths): paths = [force_text(p) for p in paths] final_path = abspathu(join(base, *paths)) base_path = abspathu(base) - base_path_len = len(base_path) # Ensure final_path starts with base_path (using normcase to ensure we - # don't false-negative on case insensitive operating systems like Windows) - # and that the next character after the final path is os.sep (or nothing, - # in which case final_path must be equal to base_path). - if not normcase(final_path).startswith(normcase(base_path)) \ - or final_path[base_path_len:base_path_len+1] not in ('', sep): + # don't false-negative on case insensitive operating systems like Windows), + # further, one of the following conditions must be true: + # a) The next character is the path separator (to prevent conditions like + # safe_join("/dir", "/../d")) + # b) The final path must be the same as the base path. + # c) The base path must be the most root path (meaning either "/" or "C:\\") + if (not normcase(final_path).startswith(normcase(base_path + sep)) and + normcase(final_path) != normcase(base_path) and + dirname(normcase(base_path)) != normcase(base_path)): raise ValueError('The joined path (%s) is located outside of the base ' 'path component (%s)' % (final_path, base_path)) return final_path diff --git a/tests/regressiontests/utils/os_utils.py b/tests/regressiontests/utils/os_utils.py new file mode 100644 index 0000000000..a78f348cf5 --- /dev/null +++ b/tests/regressiontests/utils/os_utils.py @@ -0,0 +1,21 @@ +from django.utils import unittest +from django.utils._os import safe_join + + +class SafeJoinTests(unittest.TestCase): + def test_base_path_ends_with_sep(self): + self.assertEqual( + safe_join("/abc/", "abc"), + "/abc/abc", + ) + + def test_root_path(self): + self.assertEqual( + safe_join("/", "path"), + "/path", + ) + + self.assertEqual( + safe_join("/", ""), + "/", + ) diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index f4fa75b177..061c669eb7 100644 --- a/tests/regressiontests/utils/tests.py +++ b/tests/regressiontests/utils/tests.py @@ -21,6 +21,7 @@ from .http import TestUtilsHttp from .ipv6 import TestUtilsIPv6 from .jslex import JsToCForGettextTest, JsTokensTest from .module_loading import CustomLoader, DefaultLoader, EggLoader +from .os_utils import SafeJoinTests from .regex_helper import NormalizeTests from .simplelazyobject import TestUtilsSimpleLazyObject from .termcolors import TermColorTests From 7435cc0167d1edad7bf02f89e6494189327f0109 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 7 Sep 2012 17:12:11 -0400 Subject: [PATCH 05/75] Updated install docs to reflect Python 3 status. Closes #17452. --- docs/faq/install.txt | 38 ++++++++++++++++---------------------- docs/intro/install.txt | 8 +++----- docs/topics/install.txt | 6 ++---- 3 files changed, 21 insertions(+), 31 deletions(-) diff --git a/docs/faq/install.txt b/docs/faq/install.txt index a14615e47c..3704110650 100644 --- a/docs/faq/install.txt +++ b/docs/faq/install.txt @@ -16,8 +16,9 @@ How do I get started? What are Django's prerequisites? -------------------------------- -Django requires Python_, specifically Python 2.6.5 - 2.7.x. No other Python -libraries are required for basic Django usage. +Django requires Python, specifically Python 2.6.5 - 2.7.x. No other Python +libraries are required for basic Django usage. Django 1.5 also has +experimental support for Python 3.2 and above. For a development environment -- if you just want to experiment with Django -- you don't need to have a separate Web server installed; Django comes with its @@ -50,15 +51,12 @@ aren't available under older versions of Python. Third-party applications for use with Django are, of course, free to set their own version requirements. -Over the next year or two Django will begin dropping support for older Python -versions as part of a migration which will end with Django running on Python 3 -(see below for details). - All else being equal, we recommend that you use the latest 2.x release (currently Python 2.7). This will let you take advantage of the numerous -improvements and optimizations to the Python language since version 2.6, and -will help ease the process of dropping support for older Python versions on -the road to Python 3. +improvements and optimizations to the Python language since version 2.6. + +Generally speaking, we don't recommend running Django on Python 3 yet; see +below for more. What Python version can I use with Django? ------------------------------------------ @@ -71,25 +69,21 @@ Django version Python versions 1.2 2.4, 2.5, 2.6, 2.7 1.3 2.4, 2.5, 2.6, 2.7 **1.4** **2.5, 2.6, 2.7** -*1.5 (future)* *2.6, 2.7, 3.x (experimental)* +*1.5 (future)* *2.6, 2.7* and *3.2, 3.3 (experimental)* ============== =============== Can I use Django with Python 3? ------------------------------- -Not at the moment. Python 3.0 introduced a number of -backwards-incompatible changes to the Python language, and although -these changes are generally a good thing for Python's future, it will -be a while before most Python software catches up and is able to run -on Python 3.0. For larger Python-based software like Django, the -transition is expected to take at least a year or two (since it -involves dropping support for older Python releases and so must be -done gradually). +Django 1.5 introduces experimental support for Python 3.2 and 3.3. However, we +don't yet suggest that you use Django and Python 3 in production. -In the meantime, Python 2.x releases will be supported and provided -with bug fixes and security updates by the Python development team, so -continuing to use a Python 2.x release during the transition should -not present any risk. +Python 3 support should be considered a "preview". It's offered to bootstrap +the transition of the Django ecosystem to Python 3, and to help you start +porting your apps for future Python 3 compatibility. But we're not yet +confidant enough to promise stability in production. + +Our current plan is to make Django 1.6 suitable for general use with Python 3. Will Django run under shared hosting (like TextDrive or Dreamhost)? ------------------------------------------------------------------- diff --git a/docs/intro/install.txt b/docs/intro/install.txt index 70c8034c5d..f9b122e62d 100644 --- a/docs/intro/install.txt +++ b/docs/intro/install.txt @@ -10,11 +10,9 @@ Install Python -------------- Being a Python Web framework, Django requires Python. It works with any Python -version from 2.6.5 to 2.7 (due to backwards incompatibilities in Python 3.0, -Django does not currently work with Python 3.0; see :doc:`the Django FAQ -` for more information on supported Python versions and the 3.0 -transition), these versions of Python include a lightweight database called -SQLite_ so you won't need to set up a database just yet. +version from 2.6.5 to 2.7. It also features experimental support for versions +3.2 and 3.3. All these versions of Python include a lightweight database +called SQLite_ so you won't need to set up a database just yet. .. _sqlite: http://sqlite.org/ diff --git a/docs/topics/install.txt b/docs/topics/install.txt index a11a44baa1..890c5e3195 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -9,10 +9,8 @@ Install Python Being a Python Web framework, Django requires Python. -It works with any Python version from 2.6.5 to 2.7 (due to backwards -incompatibilities in Python 3.0, Django does not currently work with -Python 3.0; see :doc:`the Django FAQ ` for more -information on supported Python versions and the 3.0 transition). +It works with any Python version from 2.6.5 to 2.7. It also features +experimental support for versions 3.2 and 3.3. Get Python at http://www.python.org. If you're running Linux or Mac OS X, you probably already have it installed. From 2649cb8ff3000ae053624300d0e6ce219af77745 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 7 Sep 2012 17:37:08 -0400 Subject: [PATCH 06/75] Fixed typo from 7435cc01. Thanks kmike. --- docs/faq/install.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq/install.txt b/docs/faq/install.txt index 3704110650..a772a379d5 100644 --- a/docs/faq/install.txt +++ b/docs/faq/install.txt @@ -81,7 +81,7 @@ don't yet suggest that you use Django and Python 3 in production. Python 3 support should be considered a "preview". It's offered to bootstrap the transition of the Django ecosystem to Python 3, and to help you start porting your apps for future Python 3 compatibility. But we're not yet -confidant enough to promise stability in production. +confident enough to promise stability in production. Our current plan is to make Django 1.6 suitable for general use with Python 3. From 9b07b5edeb770b037dc735d48dfd6f979422f586 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 7 Sep 2012 19:08:57 -0400 Subject: [PATCH 07/75] Fixed #18916 -- Allowed non-ASCII headers. Thanks Malcolm Tredinnick for the review. --- django/http/__init__.py | 56 ++++++++++++-------- tests/regressiontests/httpwrappers/tests.py | 58 ++++++++++++++------- 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/django/http/__init__.py b/django/http/__init__.py index 2198f38cbb..ecb39129ad 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals import copy import datetime +from email.header import Header import os import re import sys @@ -560,31 +561,44 @@ class HttpResponse(object): else: __str__ = serialize - def _convert_to_ascii(self, *values): - """Converts all values to ascii strings.""" - for value in values: - if not isinstance(value, six.string_types): - value = str(value) - try: - if six.PY3: - # Ensure string only contains ASCII - value.encode('us-ascii') + def _convert_to_charset(self, value, charset, mime_encode=False): + """Converts headers key/value to ascii/latin1 native strings. + + `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and + `value` value can't be represented in the given charset, MIME-encoding + is applied. + """ + if not isinstance(value, (bytes, six.text_type)): + value = str(value) + try: + if six.PY3: + if isinstance(value, str): + # Ensure string is valid in given charset + value.encode(charset) else: - if isinstance(value, str): - # Ensure string only contains ASCII - value.decode('us-ascii') - else: - # Convert unicode to an ASCII string - value = value.encode('us-ascii') - except UnicodeError as e: - e.reason += ', HTTP response headers must be in US-ASCII format' + # Convert bytestring using given charset + value = value.decode(charset) + else: + if isinstance(value, str): + # Ensure string is valid in given charset + value.decode(charset) + else: + # Convert unicode string to given charset + value = value.encode(charset) + except UnicodeError as e: + if mime_encode: + # Wrapping in str() is a workaround for #12422 under Python 2. + value = str(Header(value, 'utf-8').encode()) + else: + e.reason += ', HTTP response headers must be in %s format' % charset raise - if '\n' in value or '\r' in value: - raise BadHeaderError("Header values can't contain newlines (got %r)" % value) - yield value + if str('\n') in value or str('\r') in value: + raise BadHeaderError("Header values can't contain newlines (got %r)" % value) + return value def __setitem__(self, header, value): - header, value = self._convert_to_ascii(header, value) + header = self._convert_to_charset(header, 'ascii') + value = self._convert_to_charset(value, 'latin1', mime_encode=True) self._headers[header.lower()] = (header, value) def __delitem__(self, header): diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index 21ba198bc3..4c6aed1b97 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -11,6 +11,7 @@ from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, SimpleCookie, BadHeaderError, parse_cookie) from django.test import TestCase +from django.utils.encoding import smart_str from django.utils import six from django.utils import unittest @@ -228,33 +229,52 @@ class QueryDictTests(unittest.TestCase): self.assertEqual(copy.deepcopy(q).encoding, 'iso-8859-15') class HttpResponseTests(unittest.TestCase): - def test_unicode_headers(self): + + def test_headers_type(self): r = HttpResponse() - # If we insert a unicode value it will be converted to an ascii - r['value'] = 'test value' - self.assertTrue(isinstance(r['value'], str)) + # The following tests explicitly test types in addition to values + # because in Python 2 u'foo' == b'foo'. - # An error is raised when a unicode object with non-ascii is assigned. - self.assertRaises(UnicodeEncodeError, r.__setitem__, 'value', 't\xebst value') + # ASCII unicode or bytes values are converted to native strings. + r['key'] = 'test' + self.assertEqual(r['key'], str('test')) + self.assertIsInstance(r['key'], str) + r['key'] = 'test'.encode('ascii') + self.assertEqual(r['key'], str('test')) + self.assertIsInstance(r['key'], str) - # An error is raised when a unicode object with non-ASCII format is - # passed as initial mimetype or content_type. - self.assertRaises(UnicodeEncodeError, HttpResponse, - content_type='t\xebst value') + # Latin-1 unicode or bytes values are also converted to native strings. + r['key'] = 'café' + self.assertEqual(r['key'], smart_str('café', 'latin-1')) + self.assertIsInstance(r['key'], str) + r['key'] = 'café'.encode('latin-1') + self.assertEqual(r['key'], smart_str('café', 'latin-1')) + self.assertIsInstance(r['key'], str) - # HttpResponse headers must be convertible to ASCII. - self.assertRaises(UnicodeEncodeError, HttpResponse, - content_type='t\xebst value') + # Other unicode values are MIME-encoded (there's no way to pass them as bytes). + r['key'] = '†' + self.assertEqual(r['key'], str('=?utf-8?b?4oCg?=')) + self.assertIsInstance(r['key'], str) - # The response also converts unicode keys to strings.) - r['test'] = 'testing key' + # The response also converts unicode or bytes keys to strings, but requires + # them to contain ASCII + r = HttpResponse() + r['foo'] = 'bar' l = list(r.items()) - l.sort() - self.assertEqual(l[1], ('test', 'testing key')) + self.assertEqual(l[0], ('foo', 'bar')) + self.assertIsInstance(l[0][0], str) + + r = HttpResponse() + r[b'foo'] = 'bar' + l = list(r.items()) + self.assertEqual(l[0], ('foo', 'bar')) + self.assertIsInstance(l[0][0], str) + + r = HttpResponse() + self.assertRaises(UnicodeError, r.__setitem__, 'føø', 'bar') + self.assertRaises(UnicodeError, r.__setitem__, 'føø'.encode('utf-8'), 'bar') - # It will also raise errors for keys with non-ascii data. - self.assertRaises(UnicodeEncodeError, r.__setitem__, 't\xebst key', 'value') def test_newlines_in_headers(self): # Bug #10188: Do not allow newlines in headers (CR or LF) From 6add6170c0aa7c870c4a66f8e33ecde93f7fd975 Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Fri, 7 Sep 2012 19:12:14 -0400 Subject: [PATCH 08/75] Small reorganisation of initial parts of URL documentation. Trying to move most of the introductory example stuff up to the top and pushing the reference bits further down. --- docs/topics/http/urls.txt | 102 +++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 7297184ed3..4503bbd6ef 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -20,18 +20,18 @@ Overview ======== To design URLs for an app, you create a Python module informally called a -**URLconf** (URL configuration). This module is pure Python code and -is a simple mapping between URL patterns (as simple regular expressions) to -Python callback functions (your views). +**URLconf** (URL configuration). This module is pure Python code and is a +simple mapping between URL patterns (simple regular expressions) to Python +functions (your views). This mapping can be as short or as long as needed. It can reference other mappings. And, because it's pure Python code, it can be constructed dynamically. .. versionadded:: 1.4 - Django also allows to translate URLs according to the active language. - This process is described in the - :ref:`internationalization docs `. + Django also provides a way to translate URLs according to the active + language. See the :ref:`internationalization documentation + ` for more information. .. _how-django-processes-a-request: @@ -154,11 +154,12 @@ The matching/grouping algorithm Here's the algorithm the URLconf parser follows, with respect to named groups vs. non-named groups in a regular expression: -If there are any named arguments, it will use those, ignoring non-named arguments. -Otherwise, it will pass all non-named arguments as positional arguments. +1. If there are any named arguments, it will use those, ignoring non-named + arguments. -In both cases, it will pass any extra keyword arguments as keyword arguments. -See "Passing extra options to view functions" below. +2. Otherwise, it will pass all non-named arguments as positional arguments. + +In both cases, any extra keyword arguments that have been given as per `Passing extra options to view functions`_ (below) will also be passed to the view. What the URLconf searches against ================================= @@ -176,6 +177,44 @@ The URLconf doesn't look at the request method. In other words, all request methods -- ``POST``, ``GET``, ``HEAD``, etc. -- will be routed to the same function for the same URL. +Notes on capturing text in URLs +=============================== + +Each captured argument is sent to the view as a plain Python string, regardless +of what sort of match the regular expression makes. For example, in this +URLconf line:: + + (r'^articles/(?P\d{4})/$', 'news.views.year_archive'), + +...the ``year`` argument to ``news.views.year_archive()`` will be a string, not +an integer, even though the ``\d{4}`` will only match integer strings. + +A convenient trick is to specify default parameters for your views' arguments. +Here's an example URLconf and view:: + + # URLconf + urlpatterns = patterns('', + (r'^blog/$', 'blog.views.page'), + (r'^blog/page(?P\d+)/$', 'blog.views.page'), + ) + + # View (in blog/views.py) + def page(request, num="1"): + # Output the appropriate page of blog entries, according to num. + +In the above example, both URL patterns point to the same view -- +``blog.views.page`` -- but the first pattern doesn't capture anything from the +URL. If the first pattern matches, the ``page()`` function will use its +default argument for ``num``, ``"1"``. If the second pattern matches, +``page()`` will use whatever ``num`` value was captured by the regex. + +Performance +=========== + +Each regular expression in a ``urlpatterns`` is compiled the first time it's +accessed. This makes the system blazingly fast. + + Syntax of the urlpatterns variable ================================== @@ -209,10 +248,10 @@ The first argument to ``patterns()`` is a string ``prefix``. See The remaining arguments should be tuples in this format:: - (regular expression, Python callback function [, optional dictionary [, optional name]]) + (regular expression, Python callback function [, optional_dictionary [, optional_name]]) -...where ``optional dictionary`` and ``optional name`` are optional. (See -`Passing extra options to view functions`_ below.) +The ``optional_dictionary`` and ``optional_name`` parameters are described in +`Passing extra options to view functions`_ below. .. note:: Because `patterns()` is a function call, it accepts a maximum of 255 @@ -332,43 +371,6 @@ value should suffice. See the documentation about :ref:`the 500 (HTTP Internal Server Error) view ` for more information. -Notes on capturing text in URLs -=============================== - -Each captured argument is sent to the view as a plain Python string, regardless -of what sort of match the regular expression makes. For example, in this -URLconf line:: - - (r'^articles/(?P\d{4})/$', 'news.views.year_archive'), - -...the ``year`` argument to ``news.views.year_archive()`` will be a string, not -an integer, even though the ``\d{4}`` will only match integer strings. - -A convenient trick is to specify default parameters for your views' arguments. -Here's an example URLconf and view:: - - # URLconf - urlpatterns = patterns('', - (r'^blog/$', 'blog.views.page'), - (r'^blog/page(?P\d+)/$', 'blog.views.page'), - ) - - # View (in blog/views.py) - def page(request, num="1"): - # Output the appropriate page of blog entries, according to num. - -In the above example, both URL patterns point to the same view -- -``blog.views.page`` -- but the first pattern doesn't capture anything from the -URL. If the first pattern matches, the ``page()`` function will use its -default argument for ``num``, ``"1"``. If the second pattern matches, -``page()`` will use whatever ``num`` value was captured by the regex. - -Performance -=========== - -Each regular expression in a ``urlpatterns`` is compiled the first time it's -accessed. This makes the system blazingly fast. - The view prefix =============== From 4e1fd38bd6f007b6bb4cc57a0a841b78475a9d26 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 7 Sep 2012 19:23:16 -0400 Subject: [PATCH 09/75] Fixed #18781 -- Reduced max session cookie size. --- django/contrib/messages/storage/cookie.py | 8 ++++---- django/contrib/messages/tests/base.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/django/contrib/messages/storage/cookie.py b/django/contrib/messages/storage/cookie.py index 5f64ccd0c5..6b5b016234 100644 --- a/django/contrib/messages/storage/cookie.py +++ b/django/contrib/messages/storage/cookie.py @@ -46,10 +46,10 @@ class CookieStorage(BaseStorage): Stores messages in a cookie. """ cookie_name = 'messages' - # We should be able to store 4K in a cookie, but Internet Explorer - # imposes 4K as the *total* limit for a domain. To allow other - # cookies, we go for 3/4 of 4K. - max_cookie_size = 3072 + # uwsgi's default configuration enforces a maximum size of 4kb for all the + # HTTP headers. In order to leave some room for other cookies and headers, + # restrict the session cookie to 1/2 of 4kb. See #18781. + max_cookie_size = 2048 not_finished = '__messagesnotfinished__' def _get(self, *args, **kwargs): diff --git a/django/contrib/messages/tests/base.py b/django/contrib/messages/tests/base.py index e9a67b0500..b3ced12773 100644 --- a/django/contrib/messages/tests/base.py +++ b/django/contrib/messages/tests/base.py @@ -152,7 +152,7 @@ class BaseTest(TestCase): cycle. """ data = { - 'messages': ['Test message %d' % x for x in range(10)], + 'messages': ['Test message %d' % x for x in range(5)], } show_url = reverse('django.contrib.messages.tests.urls.show') for level in ('debug', 'info', 'success', 'warning', 'error'): @@ -170,7 +170,7 @@ class BaseTest(TestCase): @override_settings(MESSAGE_LEVEL=constants.DEBUG) def test_with_template_response(self): data = { - 'messages': ['Test message %d' % x for x in range(10)], + 'messages': ['Test message %d' % x for x in range(5)], } show_url = reverse('django.contrib.messages.tests.urls.show_template_response') for level in self.levels.keys(): @@ -194,7 +194,7 @@ class BaseTest(TestCase): before a GET. """ data = { - 'messages': ['Test message %d' % x for x in range(10)], + 'messages': ['Test message %d' % x for x in range(5)], } show_url = reverse('django.contrib.messages.tests.urls.show') messages = [] @@ -226,7 +226,7 @@ class BaseTest(TestCase): when one attempts to store a message. """ data = { - 'messages': ['Test message %d' % x for x in range(10)], + 'messages': ['Test message %d' % x for x in range(5)], } show_url = reverse('django.contrib.messages.tests.urls.show') for level in ('debug', 'info', 'success', 'warning', 'error'): @@ -251,7 +251,7 @@ class BaseTest(TestCase): raised if 'fail_silently' = True """ data = { - 'messages': ['Test message %d' % x for x in range(10)], + 'messages': ['Test message %d' % x for x in range(5)], 'fail_silently': True, } show_url = reverse('django.contrib.messages.tests.urls.show') From fa8fb2b383cc7e73c9e34bca08f29d31ff73128b Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 7 Sep 2012 19:49:38 -0400 Subject: [PATCH 10/75] Fixed #18490 - Updated DateField input formats; thanks dloewenherz for the draft patch. --- docs/ref/forms/fields.txt | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 082ec17a35..2a8f449799 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -398,11 +398,21 @@ For each field, we describe the default widget used if you don't specify If no ``input_formats`` argument is provided, the default input formats are:: - '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' - '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006' - '%d %b %Y', '%d %b, %Y', # '25 Oct 2006', '25 Oct, 2006' - '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006' - '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006' + '%Y-%m-%d', # '2006-10-25' + '%m/%d/%Y', # '10/25/2006' + '%m/%d/%y', # '10/25/06' + + Additionally, if you specify :setting:`USE_L10N=False` in your settings, the + following will also be included in the default input formats:: + + '%b %m %d', # 'Oct 25 2006' + '%b %d, %Y', # 'Oct 25, 2006' + '%d %b %Y', # '25 Oct 2006' + '%d %b, %Y', # '25 Oct, 2006' + '%B %d %Y', # 'October 25 2006' + '%B %d, %Y', # 'October 25, 2006' + '%d %B %Y', # '25 October 2006' + '%d %B, %Y', # '25 October, 2006' ``DateTimeField`` ~~~~~~~~~~~~~~~~~ From ce53a1d0bf51f59118569ec81e1a30b013bd352e Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 7 Sep 2012 19:56:20 -0400 Subject: [PATCH 11/75] Clarified the messages documentation. * Stated upfront that the messages framework is enabled by default. * Explained why FallbackStorage, despites its unattractive name, is the default and likely the most efficient message storage class. Thanks Jeremy Dunck for the review. Closes #17026 (again). --- docs/ref/contrib/messages.txt | 96 +++++++++++++++++------------------ 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/docs/ref/contrib/messages.txt b/docs/ref/contrib/messages.txt index 4cf90ee381..bc921a9d33 100644 --- a/docs/ref/contrib/messages.txt +++ b/docs/ref/contrib/messages.txt @@ -5,14 +5,16 @@ The messages framework .. module:: django.contrib.messages :synopsis: Provides cookie- and session-based temporary message storage. -Quite commonly in web applications, you may need to display a one-time -notification message (also know as "flash message") to the user after -processing a form or some other types of user input. For this, Django provides -full support for cookie- and session-based messaging, for both anonymous and -authenticated users. The messages framework allows you to temporarily store -messages in one request and retrieve them for display in a subsequent request -(usually the next one). Every message is tagged with a specific ``level`` that -determines its priority (e.g., ``info``, ``warning``, or ``error``). +Quite commonly in web applications, you need to display a one-time +notification message (also known as "flash message") to the user after +processing a form or some other types of user input. + +For this, Django provides full support for cookie- and session-based +messaging, for both anonymous and authenticated users. The messages framework +allows you to temporarily store messages in one request and retrieve them for +display in a subsequent request (usually the next one). Every message is +tagged with a specific ``level`` that determines its priority (e.g., ``info``, +``warning``, or ``error``). Enabling messages ================= @@ -20,32 +22,27 @@ Enabling messages Messages are implemented through a :doc:`middleware ` class and corresponding :doc:`context processor `. -To enable message functionality, do the following: +The default ``settings.py`` created by ``django-admin.py startproject`` +already contains all the settings required to enable message functionality: -* Edit the :setting:`MIDDLEWARE_CLASSES` setting and make sure - it contains ``'django.contrib.messages.middleware.MessageMiddleware'``. +* ``'django.contrib.messages'`` is in :setting:`INSTALLED_APPS`. - If you are using a :ref:`storage backend ` that - relies on :doc:`sessions ` (the default), - ``'django.contrib.sessions.middleware.SessionMiddleware'`` must be - enabled and appear before ``MessageMiddleware`` in your +* :setting:`MIDDLEWARE_CLASSES` contains + ``'django.contrib.sessions.middleware.SessionMiddleware'`` and + ``'django.contrib.messages.middleware.MessageMiddleware'``. + + The default :ref:`storage backend ` relies on + :doc:`sessions `. That's why ``SessionMiddleware`` + must be enabled and appear before ``MessageMiddleware`` in :setting:`MIDDLEWARE_CLASSES`. -* Edit the :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting and make sure - it contains ``'django.contrib.messages.context_processors.messages'``. +* :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains + ``'django.contrib.messages.context_processors.messages'``. -* Add ``'django.contrib.messages'`` to your :setting:`INSTALLED_APPS` - setting - -The default ``settings.py`` created by ``django-admin.py startproject`` has -``MessageMiddleware`` activated and the ``django.contrib.messages`` app -installed. Also, the default value for :setting:`TEMPLATE_CONTEXT_PROCESSORS` -contains ``'django.contrib.messages.context_processors.messages'``. - -If you don't want to use messages, you can remove the -``MessageMiddleware`` line from :setting:`MIDDLEWARE_CLASSES`, the ``messages`` -context processor from :setting:`TEMPLATE_CONTEXT_PROCESSORS` and -``'django.contrib.messages'`` from your :setting:`INSTALLED_APPS`. +If you don't want to use messages, you can remove +``'django.contrib.messages'`` from your :setting:`INSTALLED_APPS`, the +``MessageMiddleware`` line from :setting:`MIDDLEWARE_CLASSES`, and the +``messages`` context processor from :setting:`TEMPLATE_CONTEXT_PROCESSORS`. Configuring the message engine ============================== @@ -56,34 +53,35 @@ Storage backends ---------------- The messages framework can use different backends to store temporary messages. -If the default FallbackStorage isn't suitable to your needs, you can change -which backend is being used by adding a `MESSAGE_STORAGE`_ to your -settings, referencing the module and class of the storage class. For -example:: - MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' +Django provides three built-in storage classes: -The value should be the full path of the desired storage class. +.. class:: django.contrib.messages.storage.session.SessionStorage -Three storage classes are available: + This class stores all messages inside of the request's session. Therefore + it requires Django's ``contrib.sessions`` application. -``'django.contrib.messages.storage.session.SessionStorage'`` - This class stores all messages inside of the request's session. It - requires Django's ``contrib.sessions`` application. +.. class:: django.contrib.messages.storage.cookie.CookieStorage -``'django.contrib.messages.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 4096 bytes. + messages are dropped if the cookie data size would exceed 2048 bytes. -``'django.contrib.messages.storage.fallback.FallbackStorage'`` - This is the default storage class. +.. class:: django.contrib.messages.storage.fallback.FallbackStorage - This class first uses CookieStorage for all messages, falling back to using - SessionStorage for the messages that could not fit in a single cookie. + This class first uses ``CookieStorage``, and falls back to using + ``SessionStorage`` for the messages that could not fit in a single cookie. + It also requires Django's ``contrib.sessions`` application. - Since it is uses SessionStorage, it also requires Django's - ``contrib.sessions`` application. + This behavior avoids writing to the session whenever possible. It should + provide the best performance in the general case. + +:class:`~django.contrib.messages.storage.fallback.FallbackStorage` is the +default storage class. If it isn't suitable to your needs, you can select +another storage class by setting `MESSAGE_STORAGE`_ to its full import path, +for example:: + + MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' To write your own storage class, subclass the ``BaseStorage`` class in ``django.contrib.messages.storage.base`` and implement the ``_get`` and @@ -97,8 +95,8 @@ to that of the Python logging module. Message levels allow you to group messages by type so they can be filtered or displayed differently in views and templates. -The built-in levels (which can be imported from ``django.contrib.messages`` -directly) are: +The built-in levels, which can be imported from ``django.contrib.messages`` +directly, are: =========== ======== Constant Purpose From 3622be42b0902391a93c30af8483cdf34bac2783 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Fri, 7 Sep 2012 18:08:57 -0700 Subject: [PATCH 12/75] Updated the Ubuntu installation section for 12.04 release. --- docs/ref/contrib/gis/install.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/ref/contrib/gis/install.txt b/docs/ref/contrib/gis/install.txt index 5dc3726ad1..b815973202 100644 --- a/docs/ref/contrib/gis/install.txt +++ b/docs/ref/contrib/gis/install.txt @@ -959,15 +959,15 @@ Ubuntu & Debian GNU/Linux Ubuntu ^^^^^^ -11.10 -~~~~~ +11.10 through 12.04 +~~~~~~~~~~~~~~~~~~~ -In Ubuntu 11.10, PostgreSQL was upgraded to 9.1. The installation commands are: +In Ubuntu 11.10, PostgreSQL was upgraded to 9.1. The installation command is: .. code-block:: bash - $ sudo apt-get install binutils gdal-bin libproj-dev postgresql-9.1-postgis \ - postgresql-server-dev-9.1 python-psycopg2 + $ sudo apt-get install binutils gdal-bin libproj-dev \ + postgresql-9.1-postgis postgresql-server-dev-9.1 python-psycopg2 .. _ubuntu10: @@ -976,7 +976,7 @@ In Ubuntu 11.10, PostgreSQL was upgraded to 9.1. The installation commands are: In Ubuntu 10.04, PostgreSQL was upgraded to 8.4 and GDAL was upgraded to 1.6. Ubuntu 10.04 uses PostGIS 1.4, while Ubuntu 10.10 uses PostGIS 1.5 (with -geography support). The installation commands are: +geography support). The installation command is: .. code-block:: bash From 8198a1923ebbf3181447b4efdc058a4d6a86d83d Mon Sep 17 00:00:00 2001 From: Shabda Raaj Date: Sat, 8 Sep 2012 11:11:37 +0530 Subject: [PATCH 13/75] Fixed #18928. Tightened language in docs/README. --- docs/README | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/README b/docs/README index b02499d738..05133d8917 100644 --- a/docs/README +++ b/docs/README @@ -1,9 +1,8 @@ The documentation in this tree is in plain text files and can be viewed using any text file viewer. -Technically speaking, it uses ReST (reStructuredText) [1], and the Sphinx -documentation system [2]. This allows it to be built into other forms for -easier viewing and browsing. +It uses ReST (reStructuredText) [1], and the Sphinx documentation system [2]. +This allows it to be built into other forms for easier viewing and browsing. To create an HTML version of the docs: From 09a99714c05316717d2797afd2e458dbf6aa880f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 8 Sep 2012 10:24:13 +0200 Subject: [PATCH 14/75] Moved get_primary_key_column implementation to base Refs #17574. --- django/db/backends/__init__.py | 7 +++++-- django/db/backends/mysql/introspection.py | 10 ---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index f0fd2f56d2..9b0f495749 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -1034,9 +1034,12 @@ class BaseDatabaseIntrospection(object): def get_primary_key_column(self, cursor, table_name): """ - Backends can override this to return the column name of the primary key for the given table. + Returns the name of the primary key column for the given table. """ - raise NotImplementedError + for column in six.iteritems(self.get_indexes(cursor, table_name)): + if column[1]['primary_key']: + return column[0] + return None def get_indexes(self, cursor, table_name): """ diff --git a/django/db/backends/mysql/introspection.py b/django/db/backends/mysql/introspection.py index 3b5a6fecd3..c552263e5e 100644 --- a/django/db/backends/mysql/introspection.py +++ b/django/db/backends/mysql/introspection.py @@ -2,7 +2,6 @@ import re from .base import FIELD_TYPE from django.db.backends import BaseDatabaseIntrospection -from django.utils import six foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)") @@ -88,15 +87,6 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): key_columns.extend(cursor.fetchall()) return key_columns - def get_primary_key_column(self, cursor, table_name): - """ - Returns the name of the primary key column for the given table - """ - for column in six.iteritems(self.get_indexes(cursor, table_name)): - if column[1]['primary_key']: - return column[0] - return None - def get_indexes(self, cursor, table_name): cursor.execute("SHOW INDEX FROM %s" % self.connection.ops.quote_name(table_name)) # Do a two-pass search for indexes: on first pass check which indexes From dc01e41d2343386b481b983828fd861303877e81 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 8 Sep 2012 06:38:41 -0400 Subject: [PATCH 15/75] Fixed #15566 - Documented that update() doesn't honor DateField.auto_now Thanks Shabda Raaj for the draft patch. --- docs/howto/custom-model-fields.txt | 8 ++++---- docs/topics/db/queries.txt | 11 ++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index e73ef9aa42..9ff06479c6 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -181,10 +181,10 @@ card values plus their suits; 104 characters in total. Many of Django's model fields accept options that they don't do anything with. For example, you can pass both :attr:`~django.db.models.Field.editable` and - :attr:`~django.db.models.Field.auto_now` to a + :attr:`~django.db.models.DateField.auto_now` to a :class:`django.db.models.DateField` and it will simply ignore the :attr:`~django.db.models.Field.editable` parameter - (:attr:`~django.db.models.Field.auto_now` being set implies + (:attr:`~django.db.models.DateField.auto_now` being set implies ``editable=False``). No error is raised in this case. This behavior simplifies the field classes, because they don't need to @@ -516,8 +516,8 @@ for the first time, the ``add`` parameter will be ``True``, otherwise it will be You only need to override this method if you want to preprocess the value somehow, just before saving. For example, Django's :class:`~django.db.models.DateTimeField` uses this method to set the attribute -correctly in the case of :attr:`~django.db.models.Field.auto_now` or -:attr:`~django.db.models.Field.auto_now_add`. +correctly in the case of :attr:`~django.db.models.DateField.auto_now` or +:attr:`~django.db.models.DateField.auto_now_add`. If you do override this method, you must return the value of the attribute at the end. You should also update the model's attribute if you make any changes diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index f87fa2920f..12e9b447b3 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -968,11 +968,12 @@ Be aware that the ``update()`` method is converted directly to an SQL statement. It is a bulk operation for direct updates. It doesn't run any :meth:`~django.db.models.Model.save` methods on your models, or emit the ``pre_save`` or ``post_save`` signals (which are a consequence of calling -:meth:`~django.db.models.Model.save`). If you want to save every item in a -:class:`~django.db.models.query.QuerySet` and make sure that the -:meth:`~django.db.models.Model.save` method is called on each instance, you -don't need any special function to handle that. Just loop over them and call -:meth:`~django.db.models.Model.save`:: +:meth:`~django.db.models.Model.save`), or honor the +:attr:`~django.db.models.DateField.auto_now` field option. +If you want to save every item in a :class:`~django.db.models.query.QuerySet` +and make sure that the :meth:`~django.db.models.Model.save` method is called on +each instance, you don't need any special function to handle that. Just loop +over them and call :meth:`~django.db.models.Model.save`:: for item in my_queryset: item.save() From b7d3b057f32ed6aa7ee0941e1f0dec9d3e9223a3 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 8 Sep 2012 06:56:07 -0400 Subject: [PATCH 16/75] Fixed #18365 - Added a reminder of the context processor required for the set_language view. Thanks Nick Martini for the patch. --- docs/topics/i18n/translation.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 9bd53da2b9..a7f48fe1fd 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -1251,6 +1251,11 @@ As a convenience, Django comes with a view, :func:`django.views.i18n.set_languag that sets a user's language preference and redirects to a given URL or, by default, back to the previous page. +Make sure that the following item is in your +:setting:`TEMPLATE_CONTEXT_PROCESSORS` list in your settings file:: + + 'django.core.context_processors.i18n' + Activate this view by adding the following line to your URLconf:: (r'^i18n/', include('django.conf.urls.i18n')), From e69348b4e7f07ef927edaecc7126901fc91c79d0 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 8 Sep 2012 11:00:04 -0400 Subject: [PATCH 17/75] Avoided mixing dates and datetimes in the examples. Refs #16023. --- docs/faq/models.txt | 2 +- docs/intro/overview.txt | 8 ++++---- docs/ref/contrib/comments/moderation.txt | 8 ++++---- docs/ref/models/fields.txt | 4 ++-- docs/ref/models/instances.txt | 2 +- docs/ref/models/querysets.txt | 13 ++++++++++++- docs/topics/db/examples/many_to_one.txt | 8 ++++---- docs/topics/db/queries.txt | 12 ++++++------ 8 files changed, 34 insertions(+), 23 deletions(-) diff --git a/docs/faq/models.txt b/docs/faq/models.txt index 4a83aa9f2c..69965b66e1 100644 --- a/docs/faq/models.txt +++ b/docs/faq/models.txt @@ -11,7 +11,7 @@ Then, just do this:: >>> from django.db import connection >>> connection.queries - [{'sql': 'SELECT polls_polls.id,polls_polls.question,polls_polls.pub_date FROM polls_polls', + [{'sql': 'SELECT polls_polls.id, polls_polls.question, polls_polls.pub_date FROM polls_polls', 'time': '0.002'}] ``connection.queries`` is only available if :setting:`DEBUG` is ``True``. diff --git a/docs/intro/overview.txt b/docs/intro/overview.txt index 49233fb8a7..4d5a86f82b 100644 --- a/docs/intro/overview.txt +++ b/docs/intro/overview.txt @@ -31,7 +31,7 @@ the file ``mysite/news/models.py``:: return self.full_name class Article(models.Model): - pub_date = models.DateTimeField() + pub_date = models.DateField() headline = models.CharField(max_length=200) content = models.TextField() reporter = models.ForeignKey(Reporter) @@ -96,8 +96,8 @@ access your data. The API is created on the fly, no code generation necessary:: DoesNotExist: Reporter matching query does not exist. Lookup parameters were {'id': 2} # Create an article. - >>> from datetime import datetime - >>> a = Article(pub_date=datetime.now(), headline='Django is cool', + >>> from datetime import date + >>> a = Article(pub_date=date.today(), headline='Django is cool', ... content='Yeah.', reporter=r) >>> a.save() @@ -140,7 +140,7 @@ as registering your model in the admin site:: from django.db import models class Article(models.Model): - pub_date = models.DateTimeField() + pub_date = models.DateField() headline = models.CharField(max_length=200) content = models.TextField() reporter = models.ForeignKey(Reporter) diff --git a/docs/ref/contrib/comments/moderation.txt b/docs/ref/contrib/comments/moderation.txt index 4f4b326cb2..f03c7fda0d 100644 --- a/docs/ref/contrib/comments/moderation.txt +++ b/docs/ref/contrib/comments/moderation.txt @@ -32,11 +32,11 @@ A simple example is the best illustration of this. Suppose we have the following model, which would represent entries in a Weblog:: from django.db import models - + class Entry(models.Model): title = models.CharField(maxlength=250) body = models.TextField() - pub_date = models.DateTimeField() + pub_date = models.DateField() enable_comments = models.BooleanField() Now, suppose that we want the following steps to be applied whenever a @@ -55,11 +55,11 @@ Accomplishing this is fairly straightforward and requires very little code:: from django.contrib.comments.moderation import CommentModerator, moderator - + class EntryModerator(CommentModerator): email_notification = True enable_field = 'enable_comments' - + moderator.register(Entry, EntryModerator) The :class:`CommentModerator` class pre-defines a number of useful moderation diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 275c696230..190c0037ca 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -983,10 +983,10 @@ define the details of how the relation works. this with functions from the Python ``datetime`` module to limit choices of objects by date. For example:: - limit_choices_to = {'pub_date__lte': datetime.now} + limit_choices_to = {'pub_date__lte': datetime.date.today} only allows the choice of related objects with a ``pub_date`` before the - current date/time to be chosen. + current date to be chosen. Instead of a dictionary this can also be a :class:`~django.db.models.Q` object for more :ref:`complex queries `. However, diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 472ac96457..2fdc87df8c 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -135,7 +135,7 @@ access to more than a single field:: raise ValidationError('Draft entries may not have a publication date.') # Set the pub_date for published items if it hasn't been set already. if self.status == 'published' and self.pub_date is None: - self.pub_date = datetime.datetime.now() + self.pub_date = datetime.date.today() Any :exc:`~django.core.exceptions.ValidationError` exceptions raised by ``Model.clean()`` will be stored in a special key error dictionary key, diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 4f5f8858b5..96fa5c9f26 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1945,6 +1945,17 @@ SQL equivalent:: You can use ``range`` anywhere you can use ``BETWEEN`` in SQL — for dates, numbers and even characters. +.. warning:: + + Filtering a ``DateTimeField`` with dates won't include items on the last + day, because the bounds are interpreted as "0am on the given date". If + ``pub_date`` was a ``DateTimeField``, the above expression would be turned + into this SQL:: + + SELECT ... WHERE pub_date BETWEEN '2005-01-01 00:00:00' and '2005-03-31 00:00:00'; + + Generally speaking, you can't mix dates and datetimes. + .. fieldlookup:: year year @@ -1958,7 +1969,7 @@ Example:: SQL equivalent:: - SELECT ... WHERE pub_date BETWEEN '2005-01-01' AND '2005-12-31 23:59:59.999999'; + SELECT ... WHERE pub_date BETWEEN '2005-01-01' AND '2005-12-31'; (The exact SQL syntax varies for each database engine.) diff --git a/docs/topics/db/examples/many_to_one.txt b/docs/topics/db/examples/many_to_one.txt index 0a9978b8d1..c869362d16 100644 --- a/docs/topics/db/examples/many_to_one.txt +++ b/docs/topics/db/examples/many_to_one.txt @@ -42,8 +42,8 @@ Create a few Reporters:: Create an Article:: - >>> from datetime import datetime - >>> a = Article(id=None, headline="This is a test", pub_date=datetime(2005, 7, 27), reporter=r) + >>> from datetime import date + >>> a = Article(id=None, headline="This is a test", pub_date=date(2005, 7, 27), reporter=r) >>> a.save() >>> a.reporter.id @@ -65,7 +65,7 @@ database, which always returns unicode strings):: Create an Article via the Reporter object:: - >>> new_article = r.article_set.create(headline="John's second story", pub_date=datetime(2005, 7, 29)) + >>> new_article = r.article_set.create(headline="John's second story", pub_date=date(2005, 7, 29)) >>> new_article >>> new_article.reporter @@ -75,7 +75,7 @@ Create an Article via the Reporter object:: Create a new article, and add it to the article set:: - >>> new_article2 = Article(headline="Paul's story", pub_date=datetime(2006, 1, 17)) + >>> new_article2 = Article(headline="Paul's story", pub_date=date(2006, 1, 17)) >>> r.article_set.add(new_article2) >>> new_article2.reporter diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 12e9b447b3..5385b2a72d 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -35,8 +35,8 @@ models, which comprise a Weblog application: blog = models.ForeignKey(Blog) headline = models.CharField(max_length=255) body_text = models.TextField() - pub_date = models.DateTimeField() - mod_date = models.DateTimeField() + pub_date = models.DateField() + mod_date = models.DateField() authors = models.ManyToManyField(Author) n_comments = models.IntegerField() n_pingbacks = models.IntegerField() @@ -233,7 +233,7 @@ refinements together. For example:: >>> Entry.objects.filter( ... headline__startswith='What' ... ).exclude( - ... pub_date__gte=datetime.now() + ... pub_date__gte=datetime.date.today() ... ).filter( ... pub_date__gte=datetime(2005, 1, 30) ... ) @@ -258,8 +258,8 @@ stored, used and reused. Example:: >> q1 = Entry.objects.filter(headline__startswith="What") - >> q2 = q1.exclude(pub_date__gte=datetime.now()) - >> q3 = q1.filter(pub_date__gte=datetime.now()) + >> q2 = q1.exclude(pub_date__gte=datetime.date.today()) + >> q3 = q1.filter(pub_date__gte=datetime.date.today()) These three ``QuerySets`` are separate. The first is a base :class:`~django.db.models.query.QuerySet` containing all entries that contain a @@ -282,7 +282,7 @@ actually run the query until the :class:`~django.db.models.query.QuerySet` is *evaluated*. Take a look at this example:: >>> q = Entry.objects.filter(headline__startswith="What") - >>> q = q.filter(pub_date__lte=datetime.now()) + >>> q = q.filter(pub_date__lte=datetime.date.today()) >>> q = q.exclude(body_text__icontains="food") >>> print(q) From d823bb790d3b70e19e7f8bc7cfb11f3a1728a799 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 8 Sep 2012 11:19:49 -0400 Subject: [PATCH 18/75] Fixed #17156 -- Added documentation examples for exists() Thanks mrmagooey for the draft patch. --- docs/ref/models/querysets.txt | 44 +++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 96fa5c9f26..269e2ce61c 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -31,6 +31,9 @@ You can evaluate a ``QuerySet`` in the following ways: for e in Entry.objects.all(): print(e.headline) + Note: Don't use this if all you want to do is determine if at least one + result exists. It's more efficient to use :meth:`~QuerySet.exists`. + * **Slicing.** As explained in :ref:`limiting-querysets`, a ``QuerySet`` can be sliced, using Python's array-slicing syntax. Slicing an unevaluated ``QuerySet`` usually returns another unevaluated ``QuerySet``, but Django @@ -75,7 +78,7 @@ You can evaluate a ``QuerySet`` in the following ways: Note: *Don't* use this if all you want to do is determine if at least one result exists, and don't need the actual objects. It's more efficient to - use :meth:`exists() ` (see below). + use :meth:`~QuerySet.exists` (see below). .. _pickling QuerySets: @@ -1268,7 +1271,7 @@ The :exc:`~django.core.exceptions.DoesNotExist` exception inherits from e = Entry.objects.get(id=3) b = Blog.objects.get(id=1) except ObjectDoesNotExist: - print("Either the entry or blog doesn't exist.") + print "Either the entry or blog doesn't exist." create ~~~~~~ @@ -1523,9 +1526,40 @@ exists Returns ``True`` if the :class:`.QuerySet` contains any results, and ``False`` if not. This tries to perform the query in the simplest and fastest way -possible, but it *does* execute nearly the same query. This means that calling -:meth:`.QuerySet.exists` is faster than ``bool(some_query_set)``, but not by -a large degree. If ``some_query_set`` has not yet been evaluated, but you know +possible, but it *does* execute nearly the same query as a normal +:class:`.QuerySet` query. + +:meth:`~.QuerySet.exists` is useful for searches relating to both +object membership in a :class:`.QuerySet` and to the existence of any objects in +a :class:`.QuerySet`, particularly in the context of a large :class:`.QuerySet`. + +The most efficient method of finding whether a model with a unique field +(e.g. ``primary_key``) is a member of a :class:`.QuerySet` is:: + + entry = Entry.objects.get(pk=123) + if some_query_set.filter(pk=entry.pk).exists(): + print "Entry contained in queryset" + +Which will be faster than the following which requires evaluating and iterating +through the entire queryset:: + + if entry in some_query_set: + print "Entry contained in QuerySet" + +And to find whether a queryset contains any items:: + + if some_query_set.exists(): + print "There is at least one object in some_query_set" + +Which will be faster than:: + + if some_query_set: + print "There is at least one object in some_query_set" + +... but not by a large degree (hence needing a large queryset for efficiency +gains). + +Additionally, if a ``some_query_set`` has not yet been evaluated, but you know that it will be at some point, then using ``some_query_set.exists()`` will do more overall work (one query for the existence check plus an extra one to later retrieve the results) than simply using ``bool(some_query_set)``, which From ccd1bb0d81381b5611fb5caf2c4ebe5927fe2095 Mon Sep 17 00:00:00 2001 From: Travis Swicegood Date: Sat, 8 Sep 2012 11:18:08 -0400 Subject: [PATCH 19/75] Remove Admin's swallowing of AttributeError (#16655, #18593, #18747) During the new-admin changes, catching of AttributeError was added to the admin. This patch removes that as it's no longer possible to add a value to a ModelAdmin that is not available. Adding an attribute that can not be called causes an ImproperlyConfigured exception to be raised. --- django/contrib/admin/templatetags/admin_list.py | 2 +- tests/regressiontests/admin_views/admin.py | 16 ++++++++++++++-- tests/regressiontests/admin_views/customadmin.py | 1 + tests/regressiontests/admin_views/models.py | 6 ++++++ tests/regressiontests/admin_views/tests.py | 16 +++++++++++++++- 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 1873d44989..ce435dea81 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -182,7 +182,7 @@ def items_for_result(cl, result, form): row_class = '' try: f, attr, value = lookup_field(field_name, result, cl.model_admin) - except (AttributeError, ObjectDoesNotExist): + except ObjectDoesNotExist: result_repr = EMPTY_CHANGELIST_VALUE else: if f is None: diff --git a/tests/regressiontests/admin_views/admin.py b/tests/regressiontests/admin_views/admin.py index 293ddfebf6..fe291ebfb8 100644 --- a/tests/regressiontests/admin_views/admin.py +++ b/tests/regressiontests/admin_views/admin.py @@ -27,11 +27,14 @@ from .models import (Article, Chapter, Account, Media, Child, Parent, Picture, Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated, - RelatedPrepopulated, UndeletableObject) + RelatedPrepopulated, UndeletableObject, Simple) def callable_year(dt_value): - return dt_value.year + try: + return dt_value.year + except AttributeError: + return None callable_year.admin_order_field = 'date' @@ -575,6 +578,14 @@ class UndeletableObjectAdmin(admin.ModelAdmin): return super(UndeletableObjectAdmin, self).change_view(*args, **kwargs) +def callable_on_unknown(obj): + return obj.unknown + + +class AttributeErrorRaisingAdmin(admin.ModelAdmin): + list_display = [callable_on_unknown, ] + + site = admin.AdminSite(name="admin") site.register(Article, ArticleAdmin) site.register(CustomArticle, CustomArticleAdmin) @@ -648,6 +659,7 @@ site.register(AdminOrderedModelMethod, AdminOrderedModelMethodAdmin) site.register(AdminOrderedAdminMethod, AdminOrderedAdminMethodAdmin) site.register(AdminOrderedCallable, AdminOrderedCallableAdmin) site.register(Color2, CustomTemplateFilterColorAdmin) +site.register(Simple, AttributeErrorRaisingAdmin) # Register core models we need in our tests from django.contrib.auth.models import User, Group diff --git a/tests/regressiontests/admin_views/customadmin.py b/tests/regressiontests/admin_views/customadmin.py index 142527b022..031fb50f0f 100644 --- a/tests/regressiontests/admin_views/customadmin.py +++ b/tests/regressiontests/admin_views/customadmin.py @@ -49,3 +49,4 @@ site.register(models.Fabric, base_admin.FabricAdmin) site.register(models.ChapterXtra1, base_admin.ChapterXtra1Admin) site.register(User, UserLimitedAdmin) site.register(models.UndeletableObject, base_admin.UndeletableObjectAdmin) +site.register(models.Simple, base_admin.AttributeErrorRaisingAdmin) diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 0d5e327ecf..2c935c05a5 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -649,3 +649,9 @@ class UndeletableObject(models.Model): Refs #10057. """ name = models.CharField(max_length=255) + + +class Simple(models.Model): + """ + Simple model with nothing on it for use in testing + """ diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index cf7d4855fb..9f56daa743 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -46,7 +46,7 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount, OtherStory, ComplexSortedPerson, Parent, Child, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject, - UndeletableObject) + Simple, UndeletableObject) ERROR_MESSAGE = "Please enter the correct username and password \ @@ -578,6 +578,20 @@ class AdminViewBasicTest(TestCase): (self.urlbit, instance.pk)) self.assertNotContains(response, 'deletelink') + def test_allows_attributeerror_to_bubble_up(self): + """ + Ensure that AttributeErrors are allowed to bubble when raised inside + a change list view. + + Requires a model to be created so there's something to be displayed + + Refs: #16655, #18593, and #18747 + """ + Simple.objects.create() + with self.assertRaises(AttributeError): + self.client.get('/test_admin/%s/admin_views/simple/' % self.urlbit) + + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewFormUrlTest(TestCase): urls = "regressiontests.admin_views.urls" From 210fd7c658a44e51f4696555b3ce71498db55d96 Mon Sep 17 00:00:00 2001 From: Caleb Smith Date: Sat, 8 Sep 2012 11:58:48 -0400 Subject: [PATCH 20/75] refs #12836 - Added a test to assure permalink wraps method attributes --- tests/regressiontests/model_permalink/models.py | 13 +++++++++++++ tests/regressiontests/model_permalink/tests.py | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/regressiontests/model_permalink/models.py b/tests/regressiontests/model_permalink/models.py index 4823fd46c7..dacf2a3fb3 100644 --- a/tests/regressiontests/model_permalink/models.py +++ b/tests/regressiontests/model_permalink/models.py @@ -1,6 +1,13 @@ from django.db import models +def set_attr(name, value): + def wrapper(function): + setattr(function, name, value) + return function + return wrapper + + class Guitarist(models.Model): name = models.CharField(max_length=50) slug = models.CharField(max_length=50) @@ -9,3 +16,9 @@ class Guitarist(models.Model): def url(self): "Returns the URL for this guitarist." return ('guitarist_detail', [self.slug]) + + @models.permalink + @set_attr('attribute', 'value') + def url_with_attribute(self): + "Returns the URL for this guitarist and holds an attribute" + return ('guitarist_detail', [self.slug]) diff --git a/tests/regressiontests/model_permalink/tests.py b/tests/regressiontests/model_permalink/tests.py index 8286f6811a..049f338c2e 100644 --- a/tests/regressiontests/model_permalink/tests.py +++ b/tests/regressiontests/model_permalink/tests.py @@ -16,3 +16,12 @@ class PermalinkTests(TestCase): "Methods using the @permalink decorator retain their docstring." g = Guitarist(name='Adrien Moignard', slug='adrienmoignard') self.assertEqual(g.url.__doc__, "Returns the URL for this guitarist.") + + def test_wrapped_attribute(self): + """ + Methods using the @permalink decorator can have attached attributes + from other decorators + """ + g = Guitarist(name='Adrien Moignard', slug='adrienmoignard') + self.assertTrue(hasattr(g.url_with_attribute, 'attribute')) + self.assertEqual(g.url_with_attribute.attribute, 'value') From 3da43c11113e0ef109ffbecae528aef853879281 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sat, 23 Jun 2012 17:57:20 +0300 Subject: [PATCH 21/75] Fixed #18054 -- Deprecated contrib.markup. Thanks to simukis for the initial patch. --- django/contrib/markup/templatetags/markup.py | 6 ++++++ django/contrib/markup/tests.py | 11 ++++++++++- docs/internals/deprecation.txt | 3 +++ docs/ref/contrib/markup.txt | 3 +++ docs/releases/1.5.txt | 8 ++++++++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py index 18b7475ca7..389c919c07 100644 --- a/django/contrib/markup/templatetags/markup.py +++ b/django/contrib/markup/templatetags/markup.py @@ -47,6 +47,9 @@ def markdown(value, arg=''): they will be silently ignored. """ + import warnings + warnings.warn('The markdown filter has been deprecated', + category=DeprecationWarning) try: import markdown except ImportError: @@ -72,6 +75,9 @@ def markdown(value, arg=''): @register.filter(is_safe=True) def restructuredtext(value): + import warnings + warnings.warn('The restructuredtext filter has been deprecated', + category=DeprecationWarning) try: from docutils.core import publish_parts except ImportError: diff --git a/django/contrib/markup/tests.py b/django/contrib/markup/tests.py index 7b050ace82..19a3b7e9d0 100644 --- a/django/contrib/markup/tests.py +++ b/django/contrib/markup/tests.py @@ -1,7 +1,9 @@ # Quick tests for the markup templatetags (django.contrib.markup) import re +import warnings from django.template import Template, Context +from django import test from django.utils import unittest from django.utils.html import escape @@ -21,7 +23,7 @@ try: except ImportError: docutils = None -class Templates(unittest.TestCase): +class Templates(test.TestCase): textile_content = """Paragraph 1 @@ -37,6 +39,13 @@ Paragraph 2 with a link_ .. _link: http://www.example.com/""" + def setUp(self): + self.save_warnings_state() + warnings.filterwarnings('ignore', category=DeprecationWarning, module='django.contrib.markup') + + def tearDown(self): + self.restore_warnings_state() + @unittest.skipUnless(textile, 'textile not installed') def test_textile(self): t = Template("{% load markup %}{{ textile_content|textile }}") diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 9359c82e46..4add751912 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -264,6 +264,9 @@ these changes. in 1.4. The backward compatibility will be removed -- ``HttpRequest.raw_post_data`` will no longer work. +* ``django.contrib.markup`` will be removed following an accelerated + deprecation. + 1.7 --- diff --git a/docs/ref/contrib/markup.txt b/docs/ref/contrib/markup.txt index 8f3e0a95f9..9215c64f93 100644 --- a/docs/ref/contrib/markup.txt +++ b/docs/ref/contrib/markup.txt @@ -5,6 +5,9 @@ django.contrib.markup .. module:: django.contrib.markup :synopsis: A collection of template filters that implement common markup languages. +.. deprecated:: 1.5 + This module has been deprecated. + Django provides template filters that implement the following markup languages: diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 5728d8559a..5578e8efcb 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -358,3 +358,11 @@ the built-in :func:`itertools.product` instead. The :class:`~django.utils.encoding.StrAndUnicode` mix-in has been deprecated. Define a ``__str__`` method and apply the :func:`~django.utils.encoding.python_2_unicode_compatible` decorator instead. + +``django.utils.markup`` +~~~~~~~~~~~~~~~~~~~~~~~ + +The markup contrib module has been deprecated and will follow an accelerated +deprecation schedule. Direct use of python markup libraries or 3rd party tag +libraries is preferred to Django maintaining this functionality in the +framework. From 7207327dd313522954da167f9a41396b97354c78 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 8 Sep 2012 12:20:20 -0400 Subject: [PATCH 22/75] Updated docs for dates generic views. Fixes #18245. Refs #3542. --- django/views/generic/dates.py | 3 + .../class-based-views/generic-date-based.txt | 128 ++++++++----- .../class-based-views/mixins-date-based.txt | 173 ++++++++++++------ 3 files changed, 196 insertions(+), 108 deletions(-) diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py index d44246f0b7..52e13a4533 100644 --- a/django/views/generic/dates.py +++ b/django/views/generic/dates.py @@ -372,6 +372,9 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): return qs def get_date_list_period(self): + """ + Get the aggregation period for the list of dates: 'year', 'month', or 'day'. + """ return self.date_list_period def get_date_list(self, queryset, date_type=None): diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt index 12776cbb94..64b269f514 100644 --- a/docs/ref/class-based-views/generic-date-based.txt +++ b/docs/ref/class-based-views/generic-date-based.txt @@ -2,13 +2,15 @@ Generic date views ================== -Date-based generic views (in the module :mod:`django.views.generic.dates`) -are views for displaying drilldown pages for date-based data. +.. module:: django.views.generic.dates + +Date-based generic views, provided in :mod:`django.views.generic.dates`, are +views for displaying drilldown pages for date-based data. ArchiveIndexView ---------------- -.. class:: django.views.generic.dates.ArchiveIndexView +.. class:: ArchiveIndexView A top-level index page showing the "latest" objects, by date. Objects with a date in the *future* are not included unless you set ``allow_future`` to @@ -36,7 +38,7 @@ ArchiveIndexView YearArchiveView --------------- -.. class:: django.views.generic.dates.YearArchiveView +.. class:: YearArchiveView A yearly archive page showing all available months in a given year. Objects with a date in the *future* are not displayed unless you set @@ -58,13 +60,15 @@ YearArchiveView A boolean specifying whether to retrieve the full list of objects for this year and pass those to the template. If ``True``, the list of - objects will be made available to the context. By default, this is + objects will be made available to the context. If ``False``, the + ``None`` queryset will be used as the object list. By default, this is ``False``. .. method:: get_make_object_list() - Determine if an object list will be returned as part of the context. If - ``False``, the ``None`` queryset will be used as the object list. + Determine if an object list will be returned as part of the context. + Returns :attr:`~YearArchiveView.make_object_list` by default. + **Context** @@ -80,16 +84,18 @@ YearArchiveView :class:`datetime.datetime` objects, in ascending order. - * ``year``: A :class:`datetime.date` object + * ``year``: A :class:`~datetime.date` object representing the given year. - * ``next_year``: A :class:`datetime.date` object - representing the first day of the next year. If the next year is in the - future, this will be ``None``. + * ``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`. - * ``previous_year``: A :class:`datetime.date` object - representing the first day of the previous year. Unlike ``next_year``, - this will never be ``None``. + * ``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`. **Notes** @@ -98,7 +104,7 @@ YearArchiveView MonthArchiveView ---------------- -.. class:: django.views.generic.dates.MonthArchiveView +.. class:: MonthArchiveView A monthly archive page showing all objects in a given month. Objects with a date in the *future* are not displayed unless you set ``allow_future`` to @@ -131,16 +137,18 @@ MonthArchiveView :class:`datetime.datetime` objects, in ascending order. - * ``month``: A :class:`datetime.date` object + * ``month``: A :class:`~datetime.date` object representing the given month. - * ``next_month``: A :class:`datetime.date` object - representing the first day of the next month. If the next month is in the - future, this will be ``None``. + * ``next_month``: A :class:`~datetime.date` object + representing the first day of the next month, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. - * ``previous_month``: A :class:`datetime.date` object - representing the first day of the previous month. Unlike ``next_month``, - this will never be ``None``. + * ``previous_month``: A :class:`~datetime.date` object + representing the first day of the previous month, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. **Notes** @@ -149,7 +157,7 @@ MonthArchiveView WeekArchiveView --------------- -.. class:: django.views.generic.dates.WeekArchiveView +.. class:: WeekArchiveView A weekly archive page showing all objects in a given week. Objects with a date in the *future* are not displayed unless you set ``allow_future`` to @@ -175,16 +183,18 @@ WeekArchiveView :class:`~django.views.generic.dates.BaseDateListView`), the template's context will be: - * ``week``: A :class:`datetime.date` object + * ``week``: A :class:`~datetime.date` object representing the first day of the given week. - * ``next_week``: A :class:`datetime.date` object - representing the first day of the next week. If the next week is in the - future, this will be ``None``. + * ``next_week``: A :class:`~datetime.date` object + representing the first day of the next week, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. - * ``previous_week``: A :class:`datetime.date` object - representing the first day of the previous week. Unlike ``next_week``, - this will never be ``None``. + * ``previous_week``: A :class:`~datetime.date` object + representing the first day of the previous week, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. **Notes** @@ -193,7 +203,7 @@ WeekArchiveView DayArchiveView -------------- -.. class:: django.views.generic.dates.DayArchiveView +.. class:: DayArchiveView A day archive page showing all objects in a given day. Days in the future throw a 404 error, regardless of whether any objects exist for future days, @@ -220,24 +230,28 @@ DayArchiveView :class:`~django.views.generic.dates.BaseDateListView`), the template's context will be: - * ``day``: A :class:`datetime.date` object + * ``day``: A :class:`~datetime.date` object representing the given day. - * ``next_day``: A :class:`datetime.date` object - representing the next day. If the next day is in the future, this will be - ``None``. + * ``next_day``: A :class:`~datetime.date` object + representing the next day, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. - * ``previous_day``: A :class:`datetime.date` object - representing the previous day. Unlike ``next_day``, this will never be - ``None``. + * ``previous_day``: A :class:`~datetime.date` object + representing the previous day, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. - * ``next_month``: A :class:`datetime.date` object - representing the first day of the next month. If the next month is in the - future, this will be ``None``. + * ``next_month``: A :class:`~datetime.date` object + representing the first day of the next month, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. - * ``previous_month``: A :class:`datetime.date` object - representing the first day of the previous month. Unlike ``next_month``, - this will never be ``None``. + * ``previous_month``: A :class:`~datetime.date` object + representing the first day of the previous month, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. **Notes** @@ -246,7 +260,7 @@ DayArchiveView TodayArchiveView ---------------- -.. class:: django.views.generic.dates.TodayArchiveView +.. class:: TodayArchiveView A day archive page showing all objects for *today*. This is exactly the same as :class:`django.views.generic.dates.DayArchiveView`, except today's @@ -271,7 +285,7 @@ TodayArchiveView DateDetailView -------------- -.. class:: django.views.generic.dates.DateDetailView +.. class:: DateDetailView A page representing an individual object. If the object has a date value in the future, the view will throw a 404 error by default, unless you set @@ -293,6 +307,22 @@ DateDetailView .. note:: - All of the generic views listed above have matching Base* views that only - differ in that the they do not include the - :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`. + All of the generic views listed above have matching ``Base`` views that + only differ in that the they do not include the + :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`: + + .. class:: BaseArchiveIndexView + + .. class:: BaseYearArchiveView + + .. class:: BaseMonthArchiveView + + .. class:: BaseWeekArchiveView + + .. class:: BaseDayArchiveView + + .. class:: BaseTodayArchiveView + + .. class:: BaseDateDetailView + + diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index 6bf6f10b5d..01181ebb6c 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -2,11 +2,12 @@ Date-based mixins ================= +.. currentmodule:: django.views.generic.dates YearMixin --------- -.. class:: django.views.generic.dates.YearMixin +.. class:: YearMixin A mixin that can be used to retrieve and provide parsing information for a year component of a date. @@ -20,29 +21,45 @@ YearMixin .. attribute:: year - **Optional** The value for the year (as a string). By default, set to + **Optional** The value for the year, as a string. By default, set to ``None``, which means the year will be determined using other means. .. method:: get_year_format() - Returns the :func:`~time.strftime` format to use when parsing the year. Returns - :attr:`YearMixin.year_format` by default. + Returns the :func:`~time.strftime` format to use when parsing the + year. Returns :attr:`~YearMixin.year_format` by default. .. method:: get_year() - Returns the year for which this view will display data. Tries the - following sources, in order: + Returns the year for which this view will display data, as a string. + Tries the following sources, in order: * The value of the :attr:`YearMixin.year` attribute. - * The value of the `year` argument captured in the URL pattern + * The value of the `year` argument captured in the URL pattern. * The value of the `year` GET query argument. Raises a 404 if no valid year specification can be found. + .. method:: get_next_year(date) + + Returns a date object containing the first day of the year after the + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. + + .. method:: get_previous_year(date) + + Returns a date object containing the first day of the year before the + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. + MonthMixin ---------- -.. class:: django.views.generic.dates.MonthMixin +.. class:: MonthMixin A mixin that can be used to retrieve and provide parsing information for a month component of a date. @@ -51,26 +68,26 @@ MonthMixin .. attribute:: month_format - The :func:`~time.strftime` format to use when parsing the month. By default, this is - ``'%b'``. + The :func:`~time.strftime` format to use when parsing the month. By + default, this is ``'%b'``. .. attribute:: month - **Optional** The value for the month (as a string). By default, set to + **Optional** The value for the month, as a string. By default, set to ``None``, which means the month will be determined using other means. .. method:: get_month_format() - Returns the :func:`~time.strftime` format to use when parsing the month. Returns - :attr:`MonthMixin.month_format` by default. + Returns the :func:`~time.strftime` format to use when parsing the + month. Returns :attr:`~MonthMixin.month_format` by default. .. method:: get_month() - Returns the month for which this view will display data. Tries the - following sources, in order: + Returns the month for which this view will display data, as a string. + Tries the following sources, in order: * The value of the :attr:`MonthMixin.month` attribute. - * The value of the `month` argument captured in the URL pattern + * The value of the `month` argument captured in the URL pattern. * The value of the `month` GET query argument. Raises a 404 if no valid month specification can be found. @@ -78,20 +95,23 @@ MonthMixin .. method:: get_next_month(date) Returns a date object containing the first day of the month after the - date provided. Returns ``None`` if mixed with a view that sets - ``allow_future = False``, and the next month is in the future. If - ``allow_empty = False``, returns the next month that contains data. + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. .. method:: get_prev_month(date) Returns a date object containing the first day of the month before the - date provided. If ``allow_empty = False``, returns the previous month - that contained data. + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. DayMixin -------- -.. class:: django.views.generic.dates.DayMixin +.. class:: DayMixin A mixin that can be used to retrieve and provide parsing information for a day component of a date. @@ -100,46 +120,50 @@ DayMixin .. attribute:: day_format - The :func:`~time.strftime` format to use when parsing the day. By default, this is - ``'%d'``. + The :func:`~time.strftime` format to use when parsing the day. By + default, this is ``'%d'``. .. attribute:: day - **Optional** The value for the day (as a string). By default, set to + **Optional** The value for the day, as a string. By default, set to ``None``, which means the day will be determined using other means. .. method:: get_day_format() - Returns the :func:`~time.strftime` format to use when parsing the day. Returns - :attr:`DayMixin.day_format` by default. + Returns the :func:`~time.strftime` format to use when parsing the day. + Returns :attr:`~DayMixin.day_format` by default. .. method:: get_day() - Returns the day for which this view will display data. Tries the - following sources, in order: + Returns the day for which this view will display data, as a string. + Tries the following sources, in order: * The value of the :attr:`DayMixin.day` attribute. - * The value of the `day` argument captured in the URL pattern + * The value of the `day` argument captured in the URL pattern. * The value of the `day` GET query argument. Raises a 404 if no valid day specification can be found. .. method:: get_next_day(date) - Returns a date object containing the next day after the date provided. - Returns ``None`` if mixed with a view that sets ``allow_future = False``, - and the next day is in the future. If ``allow_empty = False``, returns - the next day that contains data. + Returns a date object containing the next valid day after the date + provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. .. method:: get_prev_day(date) - Returns a date object containing the previous day. If - ``allow_empty = False``, returns the previous day that contained data. + Returns a date object containing the previous valid day. This function + can also return ``None`` or raise an :class:`~django.http.Http404` + exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. WeekMixin --------- -.. class:: django.views.generic.dates.WeekMixin +.. class:: WeekMixin A mixin that can be used to retrieve and provide parsing information for a week component of a date. @@ -148,23 +172,24 @@ WeekMixin .. attribute:: week_format - The :func:`~time.strftime` format to use when parsing the week. By default, this is - ``'%U'``. + The :func:`~time.strftime` format to use when parsing the week. By + default, this is ``'%U'``, which means the week starts on Sunday. Set + it to ``'%W'`` if your week starts on Monday. .. attribute:: week - **Optional** The value for the week (as a string). By default, set to + **Optional** The value for the week, as a string. By default, set to ``None``, which means the week will be determined using other means. .. method:: get_week_format() - Returns the :func:`~time.strftime` format to use when parsing the week. Returns - :attr:`WeekMixin.week_format` by default. + Returns the :func:`~time.strftime` format to use when parsing the + week. Returns :attr:`~WeekMixin.week_format` by default. .. method:: get_week() - Returns the week for which this view will display data. Tries the - following sources, in order: + Returns the week for which this view will display data, as a string. + Tries the following sources, in order: * The value of the :attr:`WeekMixin.week` attribute. * The value of the `week` argument captured in the URL pattern @@ -172,11 +197,26 @@ WeekMixin Raises a 404 if no valid week specification can be found. + .. method:: get_next_week(date) + + Returns a date object containing the first day of the week after the + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. + + .. method:: get_prev_week(date) + + Returns a date object containing the first day of the week before the + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. DateMixin --------- -.. class:: django.views.generic.dates.DateMixin +.. class:: DateMixin A mixin class providing common behavior for all date-based views. @@ -186,7 +226,7 @@ DateMixin The name of the ``DateField`` or ``DateTimeField`` in the ``QuerySet``'s model that the date-based archive should use to - determine the objects on the page. + determine the list of objects to display on the page. When :doc:`time zone support ` is enabled and ``date_field`` is a ``DateTimeField``, dates are assumed to be in the @@ -210,26 +250,26 @@ DateMixin .. method:: get_date_field() Returns the name of the field that contains the date data that this - view will operate on. Returns :attr:`DateMixin.date_field` by default. + view will operate on. Returns :attr:`~DateMixin.date_field` by default. .. method:: get_allow_future() Determine whether to include "future" objects on this page, where "future" means objects in which the field specified in ``date_field`` is greater than the current date/time. Returns - :attr:`DateMixin.allow_future` by default. + :attr:`~DateMixin.allow_future` by default. BaseDateListView ---------------- -.. class:: django.views.generic.dates.BaseDateListView +.. class:: BaseDateListView A base class that provides common behavior for all date-based views. There won't normally be a reason to instantiate :class:`~django.views.generic.dates.BaseDateListView`; instantiate one of the subclasses instead. - While this view (and it's subclasses) are executing, ``self.object_list`` + While this view (and its subclasses) are executing, ``self.object_list`` will contain the list of objects that the view is operating upon, and ``self.date_list`` will contain the list of dates for which data is available. @@ -245,10 +285,18 @@ BaseDateListView A boolean specifying whether to display the page if no objects are available. If this is ``True`` and no objects are available, the view - will display an empty page instead of raising a 404. By default, this - is ``False``. + will display an empty page instead of raising a 404. - .. method:: get_dated_items(): + This is identical to :attr:`MultipleObjectMixin.allow_empty`, except + for the default value, which is ``False``. + + .. attribute:: date_list_period + + **Optional** A string defining the aggregation period for + ``date_list``. It must be one of ``'year'`` (default), ``'month'``, or + ``'day'``. + + .. method:: get_dated_items() Returns a 3-tuple containing (``date_list``, ``object_list``, ``extra_context``). @@ -265,10 +313,17 @@ BaseDateListView ``lookup``. Enforces any restrictions on the queryset, such as ``allow_empty`` and ``allow_future``. - .. method:: get_date_list(queryset, date_type) + .. method:: get_date_list_period() - Returns the list of dates of type ``date_type`` for which - ``queryset`` contains entries. For example, ``get_date_list(qs, - 'year')`` will return the list of years for which ``qs`` has entries. - See :meth:`~django.db.models.query.QuerySet.dates()` for the - ways that the ``date_type`` argument can be used. + Returns the aggregation period for ``date_list``. Returns + :attr:`~BaseDateListView.date_list_period` by default. + + .. method:: get_date_list(queryset, date_type=None) + + Returns the list of dates of type ``date_type`` for which ``queryset`` + contains entries. For example, ``get_date_list(qs, 'year')`` will + return the list of years for which ``qs`` has entries. If + ``date_type`` isn't provided, the result of + :meth:`BaseDateListView.get_date_list_period` is used. See + :meth:`~django.db.models.query.QuerySet.dates()` for the ways that the + ``date_type`` argument can be used. From 571698997f0ce3f362ee684754447e2e8a177863 Mon Sep 17 00:00:00 2001 From: Nick Martini Date: Sat, 8 Sep 2012 12:30:41 -0400 Subject: [PATCH 23/75] fixing modelforms example code, ticket #18832 --- docs/topics/forms/modelforms.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 8159c8850c..caff03c581 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -311,18 +311,18 @@ model fields: to exclude from the form. For example, if you want a form for the ``Author`` model (defined -above) that includes only the ``name`` and ``title`` fields, you would +above) that includes only the ``name`` and ``birth_date`` fields, you would specify ``fields`` or ``exclude`` like this:: class PartialAuthorForm(ModelForm): class Meta: model = Author - fields = ('name', 'title') + fields = ('name', 'birth_date') class PartialAuthorForm(ModelForm): class Meta: model = Author - exclude = ('birth_date',) + exclude = ('title',) Since the Author model has only 3 fields, 'name', 'title', and 'birth_date', the forms above will contain exactly the same fields. From 86e149ae055872fae0c2646cdfb002af287376d7 Mon Sep 17 00:00:00 2001 From: Michal Petrucha Date: Sat, 8 Sep 2012 18:54:33 +0200 Subject: [PATCH 24/75] Fixed a typo in the Python 3 compatibility docs. --- docs/topics/python3.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt index 89d0c9f91f..f5749faaf2 100644 --- a/docs/topics/python3.txt +++ b/docs/topics/python3.txt @@ -324,8 +324,8 @@ Writing compatible code with six six_ is the canonical compatibility library for supporting Python 2 and 3 in a single codebase. Read its documentation! -:mod`six` is bundled with Django as of version 1.4.2. You can import it as -:mod`django.utils.six`. +:mod:`six` is bundled with Django as of version 1.4.2. You can import it as +:mod:`django.utils.six`. Here are the most common changes required to write compatible code. From b139cfc0f7e70e37b9a31e9931c6b03b8a68e918 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 8 Sep 2012 13:19:58 -0400 Subject: [PATCH 25/75] Fixed #15730 - Documented the as_view() method for CBVs. --- docs/ref/class-based-views/base.txt | 6 ++++++ docs/ref/class-based-views/index.txt | 9 +++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 3f82b44f46..42448ebc80 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -43,6 +43,12 @@ View **Methods** + .. classmethod:: as_view(**initkwargs) + + Returns a callable view that takes a request and returns a response:: + + response = MyView.as_view(request) + .. method:: dispatch(request, *args, **kwargs) The ``view`` part of the view -- the method that accepts a ``request`` diff --git a/docs/ref/class-based-views/index.txt b/docs/ref/class-based-views/index.txt index f0e7bbc6c1..c4b632604a 100644 --- a/docs/ref/class-based-views/index.txt +++ b/docs/ref/class-based-views/index.txt @@ -23,7 +23,7 @@ it is safe to store state variables on the instance (i.e., ``self.foo = 3`` is a thread-safe operation). A class-based view is deployed into a URL pattern using the -:meth:`~View.as_view()` classmethod:: +:meth:`~django.views.generic.base.View.as_view()` classmethod:: urlpatterns = patterns('', (r'^view/$', MyView.as_view(size=42)), @@ -37,9 +37,10 @@ A class-based view is deployed into a URL pattern using the is modified, the actions of one user visiting your view could have an effect on subsequent users visiting the same view. -Any argument passed into :meth:`~View.as_view()` will be assigned onto the -instance that is used to service a request. Using the previous example, -this means that every request on ``MyView`` is able to use ``self.size``. +Any argument passed into :meth:`~django.views.generic.base.View.as_view()` will +be assigned onto the instance that is used to service a request. Using the +previous example, this means that every request on ``MyView`` is able to use +``self.size``. Base vs Generic views --------------------- From 20ee727ac9e0a374401473d90f055393944f197e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 8 Sep 2012 13:46:08 -0400 Subject: [PATCH 26/75] Fixed #18628 - Added methods/attributes to CBV docs. Thanks Daniel Greenfeld! --- docs/ref/class-based-views/base.txt | 54 ++++++++---------- .../ref/class-based-views/generic-display.txt | 57 ++++++++++++++++++- .../mixins-multiple-object.txt | 3 +- docs/ref/class-based-views/mixins-simple.txt | 25 +++++--- 4 files changed, 99 insertions(+), 40 deletions(-) diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 42448ebc80..e717a89b1f 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -8,6 +8,11 @@ themselves or inherited from. They may not provide all the capabilities required for projects, in which case there are Mixins and Generic class-based views. +Many of Django's built-in class-based views inherit from other class-based +views or various mixins. Because this inheritence chain is very important, the +ancestor classes are documented under the section title of **Ancestors (MRO)**. +MRO is an acronym for Method Resolution Order. + View ---- @@ -20,6 +25,7 @@ View 1. :meth:`dispatch()` 2. :meth:`http_method_not_allowed()` + 3. :meth:`options()` **Example views.py**:: @@ -41,6 +47,12 @@ View url(r'^mine/$', MyView.as_view(), name='my-view'), ) + **Attributes** + + .. attribute:: http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] + + The default list of HTTP method names that this view will accept. + **Methods** .. classmethod:: as_view(**initkwargs) @@ -68,14 +80,13 @@ View If the view was called with a HTTP method it doesn't support, this method is called instead. - The default implementation returns ``HttpResponseNotAllowed`` with list - of allowed methods in plain text. + The default implementation returns ``HttpResponseNotAllowed`` with a + list of allowed methods in plain text. - .. note:: + .. method:: options(request, *args, **kwargs) - Documentation on class-based views is a work in progress. As yet, only the - methods defined directly on the class are documented here, not methods - defined on superclasses. + Handles responding to requests for the OPTIONS HTTP verb. Returns a + list of the allowed HTTP method names for the view. TemplateView ------------ @@ -87,6 +98,8 @@ TemplateView **Ancestors (MRO)** + This view inherits methods and attributes from the following views: + * :class:`django.views.generic.base.TemplateView` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.base.View` @@ -122,28 +135,11 @@ TemplateView url(r'^$', HomePageView.as_view(), name='home'), ) - **Methods and Attributes** - - .. attribute:: template_name - - The full name of a template to use. - - .. method:: get_context_data(**kwargs) - - Return a context data dictionary consisting of the contents of - ``kwargs`` stored in the context variable ``params``. - **Context** * ``params``: The dictionary of keyword arguments captured from the URL pattern that served the view. - .. note:: - - Documentation on class-based views is a work in progress. As yet, only the - methods defined directly on the class are documented here, not methods - defined on superclasses. - RedirectView ------------ @@ -162,6 +158,8 @@ RedirectView **Ancestors (MRO)** + This view inherits methods and attributes from the following view: + * :class:`django.views.generic.base.View` **Method Flowchart** @@ -200,7 +198,7 @@ RedirectView url(r'^go-to-django/$', RedirectView.as_view(url='http://djangoproject.com'), name='go-to-django'), ) - **Methods and Attributes** + **Attributes** .. attribute:: url @@ -221,6 +219,8 @@ RedirectView then the query string is discarded. By default, ``query_string`` is ``False``. + **Methods** + .. method:: get_redirect_url(**kwargs) Constructs the target URL for redirection. @@ -231,9 +231,3 @@ RedirectView :attr:`~RedirectView.query_string`. Subclasses may implement any behavior they wish, as long as the method returns a redirect-ready URL string. - - .. note:: - - Documentation on class-based views is a work in progress. As yet, only the - methods defined directly on the class are documented here, not methods - defined on superclasses. diff --git a/docs/ref/class-based-views/generic-display.txt b/docs/ref/class-based-views/generic-display.txt index ef3bc179ee..12603ff0df 100644 --- a/docs/ref/class-based-views/generic-display.txt +++ b/docs/ref/class-based-views/generic-display.txt @@ -15,6 +15,8 @@ DetailView **Ancestors (MRO)** + This view inherits methods and attributes from the following views: + * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.detail.BaseDetailView` @@ -71,7 +73,9 @@ ListView objects (usually, but not necessarily a queryset) that the view is operating upon. - **Mixins** + **Ancestors (MRO)** + + This view inherits methods and attributes from the following views: * :class:`django.views.generic.list.ListView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` @@ -90,3 +94,54 @@ ListView 6. :meth:`get_context_data()` 7. :meth:`get()` 8. :meth:`render_to_response()` + + + **Example views.py**:: + + from django.views.generic.list import ListView + from django.utils import timezone + + from articles.models import Article + + class ArticleListView(ListView): + + model = Article + + def get_context_data(self, **kwargs): + context = super(ArticleListView, self).get_context_data(**kwargs) + context['now'] = timezone.now() + return context + + **Example urls.py**:: + + from django.conf.urls import patterns, url + + from article.views import ArticleListView + + urlpatterns = patterns('', + url(r'^$', ArticleListView.as_view(), name='article-list'), + ) + +.. class:: django.views.generic.list.BaseListView + + A base view for displaying a list of objects. It is not intended to be used + directly, but rather as a parent class of the + :class:`django.views.generic.list.ListView` or other views representing + lists of objects. + + **Ancestors (MRO)** + + This view inherits methods and attributes from the following views: + + * :class:`django.views.generic.list.MultipleObjectMixin` + * :class:`django.views.generic.base.View` + + **Methods** + + .. method:: get(request, *args, **kwargs) + + Adds :attr:`object_list` to the context. If + :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` + is True then display an empty list. If + :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` is + False then raise a 404 error. diff --git a/docs/ref/class-based-views/mixins-multiple-object.txt b/docs/ref/class-based-views/mixins-multiple-object.txt index 8bc613b887..cdb743fcbd 100644 --- a/docs/ref/class-based-views/mixins-multiple-object.txt +++ b/docs/ref/class-based-views/mixins-multiple-object.txt @@ -86,7 +86,8 @@ MultipleObjectMixin .. method:: get_queryset() - Returns the queryset that represents the data this view will display. + Get the list of items for this view. This must be an iterable and may + be a queryset (in which queryset-specific behavior will be enabled). .. method:: paginate_queryset(queryset, page_size) diff --git a/docs/ref/class-based-views/mixins-simple.txt b/docs/ref/class-based-views/mixins-simple.txt index 61fc945cd3..d2f0df241e 100644 --- a/docs/ref/class-based-views/mixins-simple.txt +++ b/docs/ref/class-based-views/mixins-simple.txt @@ -9,16 +9,17 @@ ContextMixin .. versionadded:: 1.5 - **classpath** - - ``django.views.generic.base.ContextMixin`` - **Methods** .. method:: get_context_data(**kwargs) Returns a dictionary representing the template context. The keyword - arguments provided will make up the returned context. + arguments provided will make up the returned context. Example usage:: + + def get_context_data(self, **kwargs): + context = super(RandomNumberView, self).get_context_data(**kwargs) + context['number'] = random.randrange(1, 100) + return context The template context of all class-based generic views include a ``view`` variable that points to the ``View`` instance. @@ -42,7 +43,13 @@ TemplateResponseMixin suitable context. The template to use is configurable and can be further customized by subclasses. - **Methods and Attributes** + **Attributes** + + .. attribute:: template_name + + The full name of a template to use as defined by a string. Not defining + a template_name will raise a + :class:`django.core.exceptions.ImproperlyConfigured` exception. .. attribute:: response_class @@ -57,12 +64,14 @@ TemplateResponseMixin instantiation, create a ``TemplateResponse`` subclass and assign it to ``response_class``. + **Methods** + .. method:: render_to_response(context, **response_kwargs) Returns a ``self.response_class`` instance. - If any keyword arguments are provided, they will be - passed to the constructor of the response class. + If any keyword arguments are provided, they will be passed to the + constructor of the response class. Calls :meth:`~TemplateResponseMixin.get_template_names()` to obtain the list of template names that will be searched looking for an existent From 6e2bb344e40dafdf462f6fb660837fa061faf549 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 8 Sep 2012 14:52:17 -0400 Subject: [PATCH 27/75] Fixed #18478 - Documented how to use a mutable default in a model field. --- docs/ref/models/fields.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 190c0037ca..8b3c31f029 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -195,6 +195,14 @@ support tablespaces for indexes, this option is ignored. The default value for the field. This can be a value or a callable object. If callable it will be called every time a new object is created. +The default cannot be a mutable object (model instance, list, set, etc.), as a +reference to the same instance of that object would be used as the default +value in all new model instances. Instead, wrap the desired default in a +callable. For example, if you had a custom ``JSONField`` and wanted to specify +a dictionary as the default, use a ``lambda`` as follows:: + + contact_info = JSONField("ContactInfo", default=lambda:{"email": "to1@example.com"}) + ``editable`` ------------ From 4754f122dd9b41fc9b2dee3fa74e19fc384237ab Mon Sep 17 00:00:00 2001 From: Travis Swicegood Date: Sat, 8 Sep 2012 15:07:33 -0400 Subject: [PATCH 28/75] Moved the admin inline JS to new JS files for cleanliness. --- AUTHORS | 1 + django/contrib/admin/options.py | 2 + .../contrib/admin/static/admin/js/inlines.js | 372 ++++++++++++------ .../admin/static/admin/js/inlines.min.js | 14 +- .../templates/admin/edit_inline/stacked.html | 64 +-- .../templates/admin/edit_inline/tabular.html | 65 +-- tests/regressiontests/admin_inlines/tests.py | 61 ++- 7 files changed, 338 insertions(+), 241 deletions(-) diff --git a/AUTHORS b/AUTHORS index 5fa957885c..0a3699d516 100644 --- a/AUTHORS +++ b/AUTHORS @@ -506,6 +506,7 @@ answer newbie questions, and generally made Django that much better: Johan C. Stöver Nowell Strite Thomas Stromberg + Travis Swicegood Pascal Varet SuperJared Radek Švarz diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 081d00121b..67b59cc31c 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1456,8 +1456,10 @@ class InlineModelAdmin(BaseModelAdmin): return request.user.has_perm( self.opts.app_label + '.' + self.opts.get_delete_permission()) + class StackedInline(InlineModelAdmin): template = 'admin/edit_inline/stacked.html' + class TabularInline(InlineModelAdmin): template = 'admin/edit_inline/tabular.html' diff --git a/django/contrib/admin/static/admin/js/inlines.js b/django/contrib/admin/static/admin/js/inlines.js index c11af82f76..4dc9459ff3 100644 --- a/django/contrib/admin/static/admin/js/inlines.js +++ b/django/contrib/admin/static/admin/js/inlines.js @@ -9,128 +9,264 @@ * All rights reserved. * * Spiced up with Code from Zain Memon's GSoC project 2009 - * and modified for Django by Jannis Leidel + * and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip. * * Licensed under the New BSD License * See: http://www.opensource.org/licenses/bsd-license.php */ (function($) { - $.fn.formset = function(opts) { - var options = $.extend({}, $.fn.formset.defaults, opts); - var updateElementIndex = function(el, prefix, ndx) { - var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); - var replacement = prefix + "-" + ndx; - if ($(el).attr("for")) { - $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); - } - if (el.id) { - el.id = el.id.replace(id_regex, replacement); - } - if (el.name) { - el.name = el.name.replace(id_regex, replacement); - } - }; - var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off"); - var nextIndex = parseInt(totalForms.val(), 10); - var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off"); - // only show the add button if we are allowed to add more items, + $.fn.formset = function(opts) { + var options = $.extend({}, $.fn.formset.defaults, opts); + var $this = $(this); + var $parent = $this.parent(); + var updateElementIndex = function(el, prefix, ndx) { + var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); + var replacement = prefix + "-" + ndx; + if ($(el).attr("for")) { + $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); + } + if (el.id) { + el.id = el.id.replace(id_regex, replacement); + } + if (el.name) { + el.name = el.name.replace(id_regex, replacement); + } + }; + var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off"); + var nextIndex = parseInt(totalForms.val(), 10); + var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off"); + // only show the add button if we are allowed to add more items, // note that max_num = None translates to a blank string. - var showAddButton = maxForms.val() === '' || (maxForms.val()-totalForms.val()) > 0; - $(this).each(function(i) { - $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); - }); - if ($(this).length && showAddButton) { - var addButton; - if ($(this).attr("tagName") == "TR") { - // If forms are laid out as table rows, insert the - // "add" button in a new table row: - var numCols = this.eq(-1).children().length; - $(this).parent().append('' + options.addText + ""); - addButton = $(this).parent().find("tr:last a"); - } else { - // Otherwise, insert it immediately after the last form: - $(this).filter(":last").after('"); - addButton = $(this).filter(":last").next().find("a"); - } - addButton.click(function(e) { - e.preventDefault(); - var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS"); - var template = $("#" + options.prefix + "-empty"); - var row = template.clone(true); - row.removeClass(options.emptyCssClass) - .addClass(options.formCssClass) - .attr("id", options.prefix + "-" + nextIndex); - if (row.is("tr")) { - // If the forms are laid out in table rows, insert - // the remove button into the last table cell: - row.children(":last").append('"); - } else if (row.is("ul") || row.is("ol")) { - // If they're laid out as an ordered/unordered list, - // insert an
  • after the last list item: - row.append('
  • ' + options.deleteText + "
  • "); - } else { - // Otherwise, just insert the remove button as the - // last child element of the form's container: - row.children(":first").append('' + options.deleteText + ""); - } - row.find("*").each(function() { - updateElementIndex(this, options.prefix, totalForms.val()); - }); - // Insert the new form when it has been fully edited - row.insertBefore($(template)); - // Update number of total forms - $(totalForms).val(parseInt(totalForms.val(), 10) + 1); - nextIndex += 1; - // Hide add button in case we've hit the max, except we want to add infinitely - if ((maxForms.val() !== '') && (maxForms.val()-totalForms.val()) <= 0) { - addButton.parent().hide(); - } - // The delete button of each row triggers a bunch of other things - row.find("a." + options.deleteCssClass).click(function(e) { - e.preventDefault(); - // Remove the parent form containing this button: - var row = $(this).parents("." + options.formCssClass); - row.remove(); - nextIndex -= 1; - // If a post-delete callback was provided, call it with the deleted form: - if (options.removed) { - options.removed(row); - } - // Update the TOTAL_FORMS form count. - var forms = $("." + options.formCssClass); - $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); - // Show add button again once we drop below max - if ((maxForms.val() === '') || (maxForms.val()-forms.length) > 0) { - addButton.parent().show(); - } - // Also, update names and ids for all remaining form controls - // so they remain in sequence: - for (var i=0, formCount=forms.length; i 0; + $this.each(function(i) { + $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); + }); + if ($this.length && showAddButton) { + var addButton; + if ($this.attr("tagName") == "TR") { + // If forms are laid out as table rows, insert the + // "add" button in a new table row: + var numCols = this.eq(-1).children().length; + $parent.append('' + options.addText + ""); + addButton = $parent.find("tr:last a"); + } else { + // Otherwise, insert it immediately after the last form: + $this.filter(":last").after('"); + addButton = $this.filter(":last").next().find("a"); + } + addButton.click(function(e) { + e.preventDefault(); + var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS"); + var template = $("#" + options.prefix + "-empty"); + var row = template.clone(true); + row.removeClass(options.emptyCssClass) + .addClass(options.formCssClass) + .attr("id", options.prefix + "-" + nextIndex); + if (row.is("tr")) { + // If the forms are laid out in table rows, insert + // the remove button into the last table cell: + row.children(":last").append('"); + } else if (row.is("ul") || row.is("ol")) { + // If they're laid out as an ordered/unordered list, + // insert an
  • after the last list item: + row.append('
  • ' + options.deleteText + "
  • "); + } else { + // Otherwise, just insert the remove button as the + // last child element of the form's container: + row.children(":first").append('' + options.deleteText + ""); + } + row.find("*").each(function() { + updateElementIndex(this, options.prefix, totalForms.val()); + }); + // Insert the new form when it has been fully edited + row.insertBefore($(template)); + // Update number of total forms + $(totalForms).val(parseInt(totalForms.val(), 10) + 1); + nextIndex += 1; + // Hide add button in case we've hit the max, except we want to add infinitely + if ((maxForms.val() !== '') && (maxForms.val()-totalForms.val()) <= 0) { + addButton.parent().hide(); + } + // The delete button of each row triggers a bunch of other things + row.find("a." + options.deleteCssClass).click(function(e) { + e.preventDefault(); + // Remove the parent form containing this button: + var row = $(this).parents("." + options.formCssClass); + row.remove(); + nextIndex -= 1; + // If a post-delete callback was provided, call it with the deleted form: + if (options.removed) { + options.removed(row); + } + // Update the TOTAL_FORMS form count. + var forms = $("." + options.formCssClass); + $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); + // Show add button again once we drop below max + if ((maxForms.val() === '') || (maxForms.val()-forms.length) > 0) { + addButton.parent().show(); + } + // Also, update names and ids for all remaining form controls + // so they remain in sequence: + for (var i=0, formCount=forms.length; i0;b(this).each(function(){b(this).not("."+ -a.emptyCssClass).addClass(a.formCssClass)});if(b(this).length&&g){var j;if(b(this).attr("tagName")=="TR"){g=this.eq(-1).children().length;b(this).parent().append(''+a.addText+"");j=b(this).parent().find("tr:last a")}else{b(this).filter(":last").after('");j=b(this).filter(":last").next().find("a")}j.click(function(c){c.preventDefault(); -var f=b("#id_"+a.prefix+"-TOTAL_FORMS");c=b("#"+a.prefix+"-empty");var e=c.clone(true);e.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+l);if(e.is("tr"))e.children(":last").append('");else e.is("ul")||e.is("ol")?e.append('
  • '+a.deleteText+"
  • "):e.children(":first").append(''+ -a.deleteText+"");e.find("*").each(function(){k(this,a.prefix,f.val())});e.insertBefore(b(c));b(f).val(parseInt(f.val(),10)+1);l+=1;h.val()!==""&&h.val()-f.val()<=0&&j.parent().hide();e.find("a."+a.deleteCssClass).click(function(d){d.preventDefault();d=b(this).parents("."+a.formCssClass);d.remove();l-=1;a.removed&&a.removed(d);d=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(d.length);if(h.val()===""||h.val()-d.length>0)j.parent().show();for(var i=0,m=d.length;i'+a.addText+""),h=d.find("tr:last a")):(c.filter(":last").after('"),h=c.filter(":last").next().find("a"));h.click(function(d){d.preventDefault();var f=b("#id_"+a.prefix+"-TOTAL_FORMS"),d=b("#"+a.prefix+ +"-empty"),c=d.clone(true);c.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+g);c.is("tr")?c.children(":last").append('"):c.is("ul")||c.is("ol")?c.append('
  • '+a.deleteText+"
  • "):c.children(":first").append(''+a.deleteText+"");c.find("*").each(function(){i(this, +a.prefix,f.val())});c.insertBefore(b(d));b(f).val(parseInt(f.val(),10)+1);g=g+1;e.val()!==""&&e.val()-f.val()<=0&&h.parent().hide();c.find("a."+a.deleteCssClass).click(function(d){d.preventDefault();d=b(this).parents("."+a.formCssClass);d.remove();g=g-1;a.removed&&a.removed(d);d=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(d.length);(e.val()===""||e.val()-d.length>0)&&h.parent().show();for(var c=0,f=d.length;c (function($) { - $(document).ready(function() { - var rows = "#{{ inline_admin_formset.formset.prefix }}-group .inline-related"; - var updateInlineLabel = function(row) { - $(rows).find(".inline_label").each(function(i) { - var count = i + 1; - $(this).html($(this).html().replace(/(#\d+)/g, "#" + count)); - }); - }; - var reinitDateTimeShortCuts = function() { - // Reinitialize the calendar and clock widgets by force, yuck. - if (typeof DateTimeShortcuts != "undefined") { - $(".datetimeshortcuts").remove(); - DateTimeShortcuts.init(); - } - }; - var updateSelectFilter = function() { - // If any SelectFilter widgets were added, instantiate a new instance. - if (typeof SelectFilter != "undefined"){ - $(".selectfilter").each(function(index, value){ - var namearr = value.name.split('-'); - SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% static "admin/" %}"); - }); - $(".selectfilterstacked").each(function(index, value){ - var namearr = value.name.split('-'); - SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% static "admin/" %}"); - }); - } - }; - var initPrepopulatedFields = function(row) { - row.find('.prepopulated_field').each(function() { - var field = $(this); - var input = field.find('input, select, textarea'); - var dependency_list = input.data('dependency_list') || []; - var dependencies = []; - $.each(dependency_list, function(i, field_name) { - dependencies.push('#' + row.find('.form-row .field-' + field_name).find('input, select, textarea').attr('id')); - }); - if (dependencies.length) { - input.prepopulate(dependencies, input.attr('maxlength')); - } - }); - }; - $(rows).formset({ - prefix: "{{ inline_admin_formset.formset.prefix }}", - addText: "{% blocktrans with verbose_name=inline_admin_formset.opts.verbose_name|title %}Add another {{ verbose_name }}{% endblocktrans %}", - formCssClass: "dynamic-{{ inline_admin_formset.formset.prefix }}", - deleteCssClass: "inline-deletelink", - deleteText: "{% trans "Remove" %}", - emptyCssClass: "empty-form", - removed: updateInlineLabel, - added: (function(row) { - initPrepopulatedFields(row); - reinitDateTimeShortCuts(); - updateSelectFilter(); - updateInlineLabel(row); - }) - }); - }); + $("#{{ inline_admin_formset.formset.prefix }}-group .inline-related").stackedFormset({ + prefix: '{{ inline_admin_formset.formset.prefix }}', + adminStaticPrefix: '{% static "admin/" %}', + deleteText: "{% trans "Remove" %}", + addText: "{% blocktrans with verbose_name=inline_admin_formset.opts.verbose_name|title %}Add another {{ verbose_name }}{% endblocktrans %}" + }); })(django.jQuery); diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html index 4f49153819..f2757ede48 100644 --- a/django/contrib/admin/templates/admin/edit_inline/tabular.html +++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html @@ -67,64 +67,13 @@ diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py index 4f25d3dbfb..57f45ab0ff 100644 --- a/tests/regressiontests/admin_inlines/tests.py +++ b/tests/regressiontests/admin_inlines/tests.py @@ -8,10 +8,11 @@ from django.test import TestCase from django.test.utils import override_settings # local test models -from .admin import InnerInline +from .admin import InnerInline, TitleInline, site from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile, - ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2) + ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, + Title) @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -408,6 +409,47 @@ class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase): fixtures = ['admin-views-users.xml'] urls = "regressiontests.admin_inlines.urls" + def test_add_stackeds(self): + """ + Ensure that the "Add another XXX" link correctly adds items to the + stacked formset. + """ + self.admin_login(username='super', password='secret') + self.selenium.get('%s%s' % (self.live_server_url, + '/admin/admin_inlines/holder4/add/')) + + inline_id = '#inner4stacked_set-group' + rows_length = lambda: len(self.selenium.find_elements_by_css_selector( + '%s .dynamic-inner4stacked_set' % inline_id)) + self.assertEqual(rows_length(), 3) + + add_button = self.selenium.find_element_by_link_text( + 'Add another Inner4 Stacked') + add_button.click() + + self.assertEqual(rows_length(), 4) + + def test_delete_stackeds(self): + self.admin_login(username='super', password='secret') + self.selenium.get('%s%s' % (self.live_server_url, + '/admin/admin_inlines/holder4/add/')) + + inline_id = '#inner4stacked_set-group' + rows_length = lambda: len(self.selenium.find_elements_by_css_selector( + '%s .dynamic-inner4stacked_set' % inline_id)) + self.assertEqual(rows_length(), 3) + + add_button = self.selenium.find_element_by_link_text( + 'Add another Inner4 Stacked') + add_button.click() + add_button.click() + + self.assertEqual(rows_length(), 5, msg="sanity check") + for delete_link in self.selenium.find_elements_by_css_selector( + '%s .inline-deletelink' % inline_id): + delete_link.click() + self.assertEqual(rows_length(), 3) + def test_add_inlines(self): """ Ensure that the "Add another XXX" link correctly adds items to the @@ -516,6 +558,21 @@ class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase): self.assertEqual(len(self.selenium.find_elements_by_css_selector( 'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1) + def test_alternating_rows(self): + self.admin_login(username='super', password='secret') + self.selenium.get('%s%s' % (self.live_server_url, + '/admin/admin_inlines/profilecollection/add/')) + + # Add a few inlines + self.selenium.find_element_by_link_text('Add another Profile').click() + self.selenium.find_element_by_link_text('Add another Profile').click() + + row_selector = 'form#profilecollection_form tr.dynamic-profile_set' + self.assertEqual(len(self.selenium.find_elements_by_css_selector( + "%s.row1" % row_selector)), 2, msg="Expect two row1 styled rows") + self.assertEqual(len(self.selenium.find_elements_by_css_selector( + "%s.row2" % row_selector)), 1, msg="Expect one row2 styled row") + class SeleniumChromeTests(SeleniumFirefoxTests): webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver' From 3bdb65dc5959b62f910ca498cce19682e2db6308 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 8 Sep 2012 15:15:10 -0400 Subject: [PATCH 29/75] Updated print statements to work with py3; thanks Claude Paroz noting this. --- docs/ref/models/querysets.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 269e2ce61c..80b3158f01 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1271,7 +1271,7 @@ The :exc:`~django.core.exceptions.DoesNotExist` exception inherits from e = Entry.objects.get(id=3) b = Blog.objects.get(id=1) except ObjectDoesNotExist: - print "Either the entry or blog doesn't exist." + print("Either the entry or blog doesn't exist.") create ~~~~~~ @@ -1538,23 +1538,23 @@ The most efficient method of finding whether a model with a unique field entry = Entry.objects.get(pk=123) if some_query_set.filter(pk=entry.pk).exists(): - print "Entry contained in queryset" + print("Entry contained in queryset") Which will be faster than the following which requires evaluating and iterating through the entire queryset:: if entry in some_query_set: - print "Entry contained in QuerySet" + print("Entry contained in QuerySet") And to find whether a queryset contains any items:: if some_query_set.exists(): - print "There is at least one object in some_query_set" + print("There is at least one object in some_query_set") Which will be faster than:: if some_query_set: - print "There is at least one object in some_query_set" + print("There is at least one object in some_query_set") ... but not by a large degree (hence needing a large queryset for efficiency gains). From d7853c55ed027cdffcda88205b9d5c16861a5bcb Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 8 Sep 2012 21:25:57 +0200 Subject: [PATCH 30/75] Removed warning check in test_load_overlong_key Some backends issue a warning here, others not. This is not the primary goal of the test, so the assertion about the warning has been removed. Thanks Carl Meyer for noticing the issue and suggesting the fix. --- django/contrib/sessions/tests.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index 7de2941122..d738100af0 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -302,11 +302,10 @@ class CacheDBSessionTests(SessionTestsMixin, TestCase): self.assertTrue(self.session.exists(self.session.session_key)) def test_load_overlong_key(self): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + # Some backends might issue a warning + with warnings.catch_warnings(): self.session._session_key = (string.ascii_letters + string.digits) * 20 self.assertEqual(self.session.load(), {}) - self.assertEqual(len(w), 1) @override_settings(USE_TZ=True) @@ -352,11 +351,10 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase): backend = CacheSession def test_load_overlong_key(self): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + # Some backends might issue a warning + with warnings.catch_warnings(): self.session._session_key = (string.ascii_letters + string.digits) * 20 self.assertEqual(self.session.load(), {}) - self.assertEqual(len(w), 1) class SessionMiddlewareTests(unittest.TestCase): From 5d1f09f450686c53ead6f2784ba5c94ea6dccf36 Mon Sep 17 00:00:00 2001 From: James Bennett Date: Sat, 8 Sep 2012 16:02:00 -0400 Subject: [PATCH 31/75] Ticket 18657: Fix inconsistent DB names in router example. This rewrites the entire example to use the same DB names throughout, and also is hopefully a bit more sensibly described. Additionally, the missing import of the random module for choosing a read slave is included in the example now. --- docs/topics/db/multi-db.txt | 160 ++++++++++++++++++++++++------------ 1 file changed, 108 insertions(+), 52 deletions(-) diff --git a/docs/topics/db/multi-db.txt b/docs/topics/db/multi-db.txt index 03a7d3b7cd..ef9c5a2648 100644 --- a/docs/topics/db/multi-db.txt +++ b/docs/topics/db/multi-db.txt @@ -201,73 +201,129 @@ An example write to propagate to the slaves). It also doesn't consider the interaction of transactions with the database utilization strategy. -So - what does this mean in practice? Say you want ``myapp`` to -exist on the ``other`` database, and you want all other models in a -master/slave relationship between the databases ``master``, ``slave1`` and -``slave2``. To implement this, you would need 2 routers:: +So - what does this mean in practice? Let's consider another sample +configuration. This one will have several databases: one for the +``auth`` application, and all other apps using a master/slave setup +with two read slaves. Here are the settings specifying these +databases:: - class MyAppRouter(object): - """A router to control all database operations on models in - the myapp application""" + DATABASES = { + 'auth_db': { + 'NAME': 'auth_db', + 'ENGINE': 'django.db.backends.mysql', + 'USER': 'mysql_user', + 'PASSWORD': 'swordfish', + }, + 'master': { + 'NAME': 'master', + 'ENGINE': 'django.db.backends.mysql', + 'USER': 'mysql_user', + 'PASSWORD': 'spam', + }, + 'slave1': { + 'NAME': 'slave1', + 'ENGINE': 'django.db.backends.mysql', + 'USER': 'mysql_user', + 'PASSWORD': 'eggs', + }, + 'slave2': { + 'NAME': 'slave2', + 'ENGINE': 'django.db.backends.mysql', + 'USER': 'mysql_user', + 'PASSWORD': 'bacon', + }, + } - def db_for_read(self, model, **hints): - "Point all operations on myapp models to 'other'" - if model._meta.app_label == 'myapp': - return 'other' - return None +Now we'll need to handle routing. First we want a router that knows to +send queries for the ``auth`` app to ``auth_db``:: - def db_for_write(self, model, **hints): - "Point all operations on myapp models to 'other'" - if model._meta.app_label == 'myapp': - return 'other' - return None + class AuthRouter(object): + """ + A router to control all database operations on models in the + auth application. + """ + def db_for_read(self, model, **hints): + """ + Attempts to read auth models go to auth_db. + """ + if model._meta.app_label == 'auth': + return 'auth_db' + return None - def allow_relation(self, obj1, obj2, **hints): - "Allow any relation if a model in myapp is involved" - if obj1._meta.app_label == 'myapp' or obj2._meta.app_label == 'myapp': - return True - return None + def db_for_write(self, model, **hints): + """ + Attempts to write auth models go to auth_db. + """ + if model._meta.app_label == 'auth': + return 'auth_db' + return Non - def allow_syncdb(self, db, model): - "Make sure the myapp app only appears on the 'other' db" - if db == 'other': - return model._meta.app_label == 'myapp' - elif model._meta.app_label == 'myapp': - return False - return None + def allow_relation(self, obj1, obj2, **hints): + """ + Allow relations if a model in the auth app is involved. + """ + if obj1._meta.app_label == 'auth' or \ + obj2._meta.app_label == 'auth': + return True + return None + + def allow_syncdb(self, db, model): + """ + Make sure the auth app only appears in the 'auth_db' + database. + """ + if db == 'auth_db': + return model._meta.app_label == 'auth' + elif model._meta.app_label == 'auth': + return False + return None + +And we also want a router that sends all other apps to the +master/slave configuration, and randomly chooses a slave to read +from:: + + import random class MasterSlaveRouter(object): - """A router that sets up a simple master/slave configuration""" - def db_for_read(self, model, **hints): - "Point all read operations to a random slave" - return random.choice(['slave1','slave2']) + """ + Reads go to a randomly-chosen slave. + """ + return random.choice(['slave1', 'slave2']) - def db_for_write(self, model, **hints): - "Point all write operations to the master" - return 'master' + def db_for_write(self, model, **hints): + """ + Writes always go to master. + """ + return 'master' - def allow_relation(self, obj1, obj2, **hints): - "Allow any relation between two objects in the db pool" - db_list = ('master','slave1','slave2') - if obj1._state.db in db_list and obj2._state.db in db_list: - return True - return None + def allow_relation(self, obj1, obj2, **hints): + """ + Relations between objects are allowed if both objects are + in the master/slave pool. + """ + db_list = ('master', 'slave1', 'slave2') + if obj1.state.db in db_list and obj2.state.db in db_list: + return True + return None - def allow_syncdb(self, db, model): - "Explicitly put all models on all databases." - return True + def allow_syncdb(self, db, model): + """ + All non-auth models end up in this pool. + """ + return True -Then, in your settings file, add the following (substituting ``path.to.`` with -the actual python path to the module where you define the routers):: +Finally, in the settings file, we add the following (substituting +``path.to.`` with the actual python path to the module(s) where the +routers are defined):: - DATABASE_ROUTERS = ['path.to.MyAppRouter', 'path.to.MasterSlaveRouter'] + DATABASE_ROUTERS = ['path.to.AuthRouter', 'path.to.MasterSlaveRouter'] The order in which routers are processed is significant. Routers will be queried in the order the are listed in the :setting:`DATABASE_ROUTERS` setting . In this example, the -``MyAppRouter`` is processed before the ``MasterSlaveRouter``, and as a -result, decisions concerning the models in ``myapp`` are processed +``AuthRouter`` is processed before the ``MasterSlaveRouter``, and as a +result, decisions concerning the models in ``auth`` are processed before any other decision is made. If the :setting:`DATABASE_ROUTERS` setting listed the two routers in the other order, ``MasterSlaveRouter.allow_syncdb()`` would be processed first. The @@ -276,11 +332,11 @@ that all models would be available on all databases. With this setup installed, lets run some Django code:: - >>> # This retrieval will be performed on the 'credentials' database + >>> # This retrieval will be performed on the 'auth_db' database >>> fred = User.objects.get(username='fred') >>> fred.first_name = 'Frederick' - >>> # This save will also be directed to 'credentials' + >>> # This save will also be directed to 'auth_db' >>> fred.save() >>> # These retrieval will be randomly allocated to a slave database From 408c10e541440b078ccf5d6fcb3f344b7a94d048 Mon Sep 17 00:00:00 2001 From: James Bennett Date: Sat, 8 Sep 2012 16:08:01 -0400 Subject: [PATCH 32/75] Untabify multi-db docs. --- docs/topics/db/multi-db.txt | 108 ++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/topics/db/multi-db.txt b/docs/topics/db/multi-db.txt index ef9c5a2648..82218692d8 100644 --- a/docs/topics/db/multi-db.txt +++ b/docs/topics/db/multi-db.txt @@ -242,41 +242,41 @@ send queries for the ``auth`` app to ``auth_db``:: A router to control all database operations on models in the auth application. """ - def db_for_read(self, model, **hints): - """ - Attempts to read auth models go to auth_db. - """ - if model._meta.app_label == 'auth': - return 'auth_db' - return None + def db_for_read(self, model, **hints): + """ + Attempts to read auth models go to auth_db. + """ + if model._meta.app_label == 'auth': + return 'auth_db' + return None - def db_for_write(self, model, **hints): - """ - Attempts to write auth models go to auth_db. - """ - if model._meta.app_label == 'auth': - return 'auth_db' - return Non + def db_for_write(self, model, **hints): + """ + Attempts to write auth models go to auth_db. + """ + if model._meta.app_label == 'auth': + return 'auth_db' + return Non - def allow_relation(self, obj1, obj2, **hints): - """ - Allow relations if a model in the auth app is involved. - """ - if obj1._meta.app_label == 'auth' or \ + def allow_relation(self, obj1, obj2, **hints): + """ + Allow relations if a model in the auth app is involved. + """ + if obj1._meta.app_label == 'auth' or \ obj2._meta.app_label == 'auth': - return True - return None + return True + return None - def allow_syncdb(self, db, model): - """ - Make sure the auth app only appears in the 'auth_db' - database. - """ - if db == 'auth_db': - return model._meta.app_label == 'auth' - elif model._meta.app_label == 'auth': - return False - return None + def allow_syncdb(self, db, model): + """ + Make sure the auth app only appears in the 'auth_db' + database. + """ + if db == 'auth_db': + return model._meta.app_label == 'auth' + elif model._meta.app_label == 'auth': + return False + return None And we also want a router that sends all other apps to the master/slave configuration, and randomly chooses a slave to read @@ -286,32 +286,32 @@ from:: class MasterSlaveRouter(object): def db_for_read(self, model, **hints): - """ - Reads go to a randomly-chosen slave. - """ - return random.choice(['slave1', 'slave2']) + """ + Reads go to a randomly-chosen slave. + """ + return random.choice(['slave1', 'slave2']) - def db_for_write(self, model, **hints): - """ - Writes always go to master. - """ - return 'master' + def db_for_write(self, model, **hints): + """ + Writes always go to master. + """ + return 'master' - def allow_relation(self, obj1, obj2, **hints): - """ - Relations between objects are allowed if both objects are - in the master/slave pool. - """ - db_list = ('master', 'slave1', 'slave2') - if obj1.state.db in db_list and obj2.state.db in db_list: - return True - return None + def allow_relation(self, obj1, obj2, **hints): + """ + Relations between objects are allowed if both objects are + in the master/slave pool. + """ + db_list = ('master', 'slave1', 'slave2') + if obj1.state.db in db_list and obj2.state.db in db_list: + return True + return None - def allow_syncdb(self, db, model): - """ - All non-auth models end up in this pool. - """ - return True + def allow_syncdb(self, db, model): + """ + All non-auth models end up in this pool. + """ + return True Finally, in the settings file, we add the following (substituting ``path.to.`` with the actual python path to the module(s) where the From 5c5226a52029ae75a27d6006528163a699ed0617 Mon Sep 17 00:00:00 2001 From: James Bennett Date: Sat, 8 Sep 2012 16:14:13 -0400 Subject: [PATCH 33/75] Fix typo: Non -> None --- docs/topics/db/multi-db.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/db/multi-db.txt b/docs/topics/db/multi-db.txt index 82218692d8..d2ff8645a9 100644 --- a/docs/topics/db/multi-db.txt +++ b/docs/topics/db/multi-db.txt @@ -256,7 +256,7 @@ send queries for the ``auth`` app to ``auth_db``:: """ if model._meta.app_label == 'auth': return 'auth_db' - return Non + return None def allow_relation(self, obj1, obj2, **hints): """ From 307706d082d20ac868654ccbaee18879db5197db Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 8 Sep 2012 14:12:18 -0600 Subject: [PATCH 34/75] Fixed #18545 -- Make the 'no DJANGO_SETTINGS_MODULE' error message more useful.Thanks Nick Coghlan for the report, and Malcolm Tredinnick for review. --- django/conf/__init__.py | 17 ++++++--- django/core/management/__init__.py | 3 +- tests/regressiontests/admin_scripts/tests.py | 38 ++++++++++---------- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/django/conf/__init__.py b/django/conf/__init__.py index f4d17ca9f3..f5ced60735 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -26,7 +26,7 @@ class LazySettings(LazyObject): The user can manually configure settings prior to using them. Otherwise, Django uses the settings module pointed to by DJANGO_SETTINGS_MODULE. """ - def _setup(self): + def _setup(self, name): """ Load the settings module pointed to by the environment variable. This is used the first time we need any settings at all, if the user has not @@ -37,12 +37,21 @@ class LazySettings(LazyObject): if not settings_module: # If it's set but is an empty string. raise KeyError except KeyError: - # NOTE: This is arguably an EnvironmentError, but that causes - # problems with Python's interactive help. - raise ImportError("Settings cannot be imported, because environment variable %s is undefined." % ENVIRONMENT_VARIABLE) + raise ImproperlyConfigured( + "Requested setting %s, but settings are not configured. " + "You must either define the environment variable %s " + "or call settings.configure() before accessing settings." + % (name, ENVIRONMENT_VARIABLE)) self._wrapped = Settings(settings_module) + + def __getattr__(self, name): + if self._wrapped is empty: + self._setup(name) + return getattr(self._wrapped, name) + + def configure(self, default_settings=global_settings, **options): """ Called to manually configure the settings. The 'default_settings' diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 98f75e0310..b40570efc9 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -5,6 +5,7 @@ from optparse import OptionParser, NO_DEFAULT import imp import warnings +from django.core.exceptions import ImproperlyConfigured from django.core.management.base import BaseCommand, CommandError, handle_default_options from django.core.management.color import color_style from django.utils.importlib import import_module @@ -105,7 +106,7 @@ def get_commands(): try: from django.conf import settings apps = settings.INSTALLED_APPS - except (AttributeError, EnvironmentError, ImportError): + except (AttributeError, ImproperlyConfigured): apps = [] # Find and load the management module for each installed app. diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index bc0f684563..6028eac846 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -181,11 +181,11 @@ class DjangoAdminNoSettings(AdminScriptTestCase): "A series of tests for django-admin.py when there is no settings.py file." def test_builtin_command(self): - "no settings: django-admin builtin commands fail with an import error when no settings provided" + "no settings: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_bad_settings(self): "no settings: django-admin builtin commands fail if settings file (from argument) doesn't exist" @@ -213,11 +213,11 @@ class DjangoAdminDefaultSettings(AdminScriptTestCase): self.remove_settings('settings.py') def test_builtin_command(self): - "default: django-admin builtin commands fail with an import error when no settings provided" + "default: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_settings(self): "default: django-admin builtin commands succeed if settings are provided as argument" @@ -279,11 +279,11 @@ class DjangoAdminFullPathDefaultSettings(AdminScriptTestCase): self.remove_settings('settings.py') def test_builtin_command(self): - "fulldefault: django-admin builtin commands fail with an import error when no settings provided" + "fulldefault: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_settings(self): "fulldefault: django-admin builtin commands succeed if a settings file is provided" @@ -345,11 +345,11 @@ class DjangoAdminMinimalSettings(AdminScriptTestCase): self.remove_settings('settings.py') def test_builtin_command(self): - "minimal: django-admin builtin commands fail with an import error when no settings provided" + "minimal: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_settings(self): "minimal: django-admin builtin commands fail if settings are provided as argument" @@ -411,11 +411,11 @@ class DjangoAdminAlternateSettings(AdminScriptTestCase): self.remove_settings('alternate_settings.py') def test_builtin_command(self): - "alternate: django-admin builtin commands fail with an import error when no settings provided" + "alternate: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_settings(self): "alternate: django-admin builtin commands succeed if settings are provided as argument" @@ -482,11 +482,11 @@ class DjangoAdminMultipleSettings(AdminScriptTestCase): self.remove_settings('alternate_settings.py') def test_builtin_command(self): - "alternate: django-admin builtin commands fail with an import error when no settings provided" + "alternate: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_settings(self): "alternate: django-admin builtin commands succeed if settings are provided as argument" @@ -570,11 +570,11 @@ class DjangoAdminSettingsDirectory(AdminScriptTestCase): self.assertTrue(os.path.exists(os.path.join(app_path, 'api.py'))) def test_builtin_command(self): - "directory: django-admin builtin commands fail with an import error when no settings provided" + "directory: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_bad_settings(self): "directory: django-admin builtin commands fail if settings file (from argument) doesn't exist" @@ -621,7 +621,7 @@ class ManageNoSettings(AdminScriptTestCase): "A series of tests for manage.py when there is no settings.py file." def test_builtin_command(self): - "no settings: manage.py builtin commands fail with an import error when no settings provided" + "no settings: manage.py builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) @@ -786,7 +786,7 @@ class ManageMinimalSettings(AdminScriptTestCase): self.remove_settings('settings.py') def test_builtin_command(self): - "minimal: manage.py builtin commands fail with an import error when no settings provided" + "minimal: manage.py builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) @@ -852,7 +852,7 @@ class ManageAlternateSettings(AdminScriptTestCase): self.remove_settings('alternate_settings.py') def test_builtin_command(self): - "alternate: manage.py builtin commands fail with an import error when no default settings provided" + "alternate: manage.py builtin commands fail with an error when no default settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) @@ -895,7 +895,7 @@ class ManageAlternateSettings(AdminScriptTestCase): args = ['noargs_command'] out, err = self.run_manage(args) self.assertNoOutput(out) - self.assertOutput(err, "Unknown command: 'noargs_command'") + self.assertOutput(err, "Could not import settings 'regressiontests.settings'") def test_custom_command_with_settings(self): "alternate: manage.py can execute user commands if settings are provided as argument" @@ -927,7 +927,7 @@ class ManageMultipleSettings(AdminScriptTestCase): self.remove_settings('alternate_settings.py') def test_builtin_command(self): - "multiple: manage.py builtin commands fail with an import error when no settings provided" + "multiple: manage.py builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) From 67dceeef446c7c9fb5a3eaefad9c15bb298cfa3d Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 8 Sep 2012 14:30:11 -0600 Subject: [PATCH 35/75] Remove a couple unused imports. --- django/conf/__init__.py | 1 - django/contrib/sessions/tests.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/django/conf/__init__.py b/django/conf/__init__.py index f5ced60735..6272f4ed5d 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -7,7 +7,6 @@ a list of all possible variables. """ import os -import re import time # Needed for Windows import warnings diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index d738100af0..dbc68652ac 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta import shutil import string import tempfile From 72ca530af5e85bef6292bb1a16f3d9062f172f63 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 8 Sep 2012 17:34:22 -0400 Subject: [PATCH 36/75] =?UTF-8?q?Fixed=20typo=20in=20commit=20for=20#15730?= =?UTF-8?q?;=20thanks=20Bruno=20Reni=C3=A9=20for=20the=20catch.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ref/class-based-views/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index e717a89b1f..954aee7c3d 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -59,7 +59,7 @@ View Returns a callable view that takes a request and returns a response:: - response = MyView.as_view(request) + response = MyView.as_view()(request) .. method:: dispatch(request, *args, **kwargs) From 518c58296667ffc36f90b873eb97aa99fa00eb1e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 8 Sep 2012 18:45:02 -0400 Subject: [PATCH 37/75] Fixed #15906 - Documented HEAD method in CBVs; thanks zsiciarz for the patch. --- docs/ref/class-based-views/base.txt | 5 +++ docs/topics/class-based-views/index.txt | 44 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 954aee7c3d..cc9aa852f1 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -71,6 +71,11 @@ View delegated to :meth:`~View.get()`, a ``POST`` to :meth:`~View.post()`, and so on. + By default, a ``HEAD`` request will be delegated to :meth:`~View.get()`. + If you need to handle ``HEAD`` requests in a different way than ``GET``, + you can override the :meth:`~View.head()` method. See + :ref:`supporting-other-http-methods` for an example. + The default implementation also sets ``request``, ``args`` and ``kwargs`` as instance variables, so any method on the view can know the full details of the request that was made to invoke the view. diff --git a/docs/topics/class-based-views/index.txt b/docs/topics/class-based-views/index.txt index 6637bd5fcb..2d3e00ab4c 100644 --- a/docs/topics/class-based-views/index.txt +++ b/docs/topics/class-based-views/index.txt @@ -84,6 +84,50 @@ function-like entry to class-based views:: For more information on how to use the built in generic views, consult the next topic on :doc:`generic class based views`. +.. _supporting-other-http-methods: + +Supporting other HTTP methods +----------------------------- + +Suppose somebody wants to access our book library over HTTP using the views +as an API. The API client would connect every now and then and download book +data for the books published since last visit. But if no new books appeared +since then, it is a waste of CPU time and bandwidth to fetch the books from the +database, render a full response and send it to the client. It might be +preferable to ask the API when the most recent book was published. + +We map the URL to book list view in the URLconf:: + + from django.conf.urls import patterns + from books.views import BookListView + + urlpatterns = patterns('', + (r'^books/$', BookListView.as_view()), + ) + +And the view:: + + from django.http import HttpResponse + from django.views.generic import ListView + from books.models import Book + + class BookListView(ListView): + model = Book + + def head(self, *args, **kwargs): + last_book = self.get_queryset().latest('publication_date') + response = HttpResponse('') + # RFC 1123 date format + response['Last-Modified'] = last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT') + return response + +If the view is accessed from a ``GET`` request, a plain-and-simple object +list is returned in the response (using ``book_list.html`` template). But if +the client issues a ``HEAD`` request, the response has an empty body and +the ``Last-Modified`` header indicates when the most recent book was published. +Based on this information, the client may or may not download the full object +list. + Decorating class-based views ============================ From a78dd109e6c81c49e90e36e9b793bad67c46c23c Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 8 Sep 2012 16:55:29 -0600 Subject: [PATCH 38/75] Fixed #15552 -- LOGIN_URL and LOGIN_REDIRECT_URL can take URLpattern names. Thanks UloPe and Eric Florenzano for the patch, and Malcolm Tredinnick for review. --- django/contrib/auth/decorators.py | 6 +- django/contrib/auth/tests/decorators.py | 2 +- django/contrib/auth/views.py | 18 ++--- django/shortcuts/__init__.py | 49 ++++++++----- docs/ref/settings.txt | 33 ++++----- docs/releases/1.5.txt | 6 ++ docs/topics/auth.txt | 7 ++ .../comment_tests/tests/__init__.py | 2 +- .../comment_tests/urls_default.py | 9 +++ tests/regressiontests/resolve_url/__init__.py | 0 tests/regressiontests/resolve_url/models.py | 12 ++++ tests/regressiontests/resolve_url/tests.py | 68 +++++++++++++++++++ tests/runtests.py | 2 +- 13 files changed, 162 insertions(+), 52 deletions(-) create mode 100644 tests/regressiontests/comment_tests/urls_default.py create mode 100644 tests/regressiontests/resolve_url/__init__.py create mode 100644 tests/regressiontests/resolve_url/models.py create mode 100644 tests/regressiontests/resolve_url/tests.py diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py index beeb284998..0fc9f3754a 100644 --- a/django/contrib/auth/decorators.py +++ b/django/contrib/auth/decorators.py @@ -8,6 +8,7 @@ from django.contrib.auth import REDIRECT_FIELD_NAME from django.core.exceptions import PermissionDenied from django.utils.decorators import available_attrs from django.utils.encoding import force_str +from django.shortcuts import resolve_url def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): @@ -23,11 +24,10 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE if test_func(request.user): return view_func(request, *args, **kwargs) path = request.build_absolute_uri() - # urlparse chokes on lazy objects in Python 3 - login_url_as_str = force_str(login_url or settings.LOGIN_URL) + resolved_login_url = resolve_url(login_url or settings.LOGIN_URL) # If the login url is the same scheme and net location then just # use the path as the "next" url. - login_scheme, login_netloc = urlparse(login_url_as_str)[:2] + login_scheme, login_netloc = urlparse(resolved_login_url)[:2] current_scheme, current_netloc = urlparse(path)[:2] if ((not login_scheme or login_scheme == current_scheme) and (not login_netloc or login_netloc == current_netloc)): diff --git a/django/contrib/auth/tests/decorators.py b/django/contrib/auth/tests/decorators.py index bd3f0115f5..cefc310e40 100644 --- a/django/contrib/auth/tests/decorators.py +++ b/django/contrib/auth/tests/decorators.py @@ -25,7 +25,7 @@ class LoginRequiredTestCase(AuthViewsTestCase): pass login_required(normal_view) - def testLoginRequired(self, view_url='/login_required/', login_url=settings.LOGIN_URL): + def testLoginRequired(self, view_url='/login_required/', login_url='/login/'): """ Check that login_required works on a simple view wrapped in a login_required decorator. diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index f93541b4bf..024be5e46d 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -7,9 +7,9 @@ from django.conf import settings from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect, QueryDict from django.template.response import TemplateResponse -from django.utils.encoding import force_str from django.utils.http import base36_to_int from django.utils.translation import ugettext as _ +from django.shortcuts import resolve_url from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect @@ -38,16 +38,16 @@ def login(request, template_name='registration/login.html', if request.method == "POST": form = authentication_form(data=request.POST) if form.is_valid(): - netloc = urlparse(redirect_to)[1] - # Use default setting if redirect_to is empty if not redirect_to: redirect_to = settings.LOGIN_REDIRECT_URL + redirect_to = resolve_url(redirect_to) + netloc = urlparse(redirect_to)[1] # Heavier security check -- don't allow redirection to a different # host. - elif netloc and netloc != request.get_host(): - redirect_to = settings.LOGIN_REDIRECT_URL + if netloc and netloc != request.get_host(): + redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL) # Okay, security checks complete. Log the user in. auth_login(request, form.get_user()) @@ -110,6 +110,7 @@ def logout_then_login(request, login_url=None, current_app=None, extra_context=N """ if not login_url: login_url = settings.LOGIN_URL + login_url = resolve_url(login_url) return logout(request, login_url, current_app=current_app, extra_context=extra_context) def redirect_to_login(next, login_url=None, @@ -117,10 +118,9 @@ def redirect_to_login(next, login_url=None, """ Redirects the user to the login page, passing the given 'next' page """ - # urlparse chokes on lazy objects in Python 3 - login_url_as_str = force_str(login_url or settings.LOGIN_URL) + resolved_url = resolve_url(login_url or settings.LOGIN_URL) - login_url_parts = list(urlparse(login_url_as_str)) + login_url_parts = list(urlparse(resolved_url)) if redirect_field_name: querystring = QueryDict(login_url_parts[4], mutable=True) querystring[redirect_field_name] = next @@ -229,7 +229,7 @@ def password_reset_complete(request, template_name='registration/password_reset_complete.html', current_app=None, extra_context=None): context = { - 'login_url': settings.LOGIN_URL + 'login_url': resolve_url(settings.LOGIN_URL) } if extra_context is not None: context.update(extra_context) diff --git a/django/shortcuts/__init__.py b/django/shortcuts/__init__.py index 154f224671..a824446b7e 100644 --- a/django/shortcuts/__init__.py +++ b/django/shortcuts/__init__.py @@ -66,23 +66,7 @@ def redirect(to, *args, **kwargs): else: redirect_class = HttpResponseRedirect - # If it's a model, use get_absolute_url() - if hasattr(to, 'get_absolute_url'): - return redirect_class(to.get_absolute_url()) - - # Next try a reverse URL resolution. - try: - return redirect_class(urlresolvers.reverse(to, args=args, kwargs=kwargs)) - except urlresolvers.NoReverseMatch: - # If this is a callable, re-raise. - if callable(to): - raise - # If this doesn't "feel" like a URL, re-raise. - if '/' not in to and '.' not in to: - raise - - # Finally, fall back and assume it's a URL - return redirect_class(to) + return redirect_class(resolve_url(to, *args, **kwargs)) def _get_queryset(klass): """ @@ -128,3 +112,34 @@ def get_list_or_404(klass, *args, **kwargs): raise Http404('No %s matches the given query.' % queryset.model._meta.object_name) return obj_list +def resolve_url(to, *args, **kwargs): + """ + Return a URL appropriate for the arguments passed. + + The arguments could be: + + * A model: the model's `get_absolute_url()` function will be called. + + * A view name, possibly with arguments: `urlresolvers.reverse()` will + be used to reverse-resolve the name. + + * A URL, which will be returned as-is. + + """ + # If it's a model, use get_absolute_url() + if hasattr(to, 'get_absolute_url'): + return to.get_absolute_url() + + # Next try a reverse URL resolution. + try: + return urlresolvers.reverse(to, args=args, kwargs=kwargs) + except urlresolvers.NoReverseMatch: + # If this is a callable, re-raise. + if callable(to): + raise + # If this doesn't "feel" like a URL, re-raise. + if '/' not in to and '.' not in to: + raise + + # Finally, fall back and assume it's a URL + return to diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 4729a2b6f1..f443138569 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1304,25 +1304,13 @@ The URL where requests are redirected after login when the This is used by the :func:`~django.contrib.auth.decorators.login_required` decorator, for example. -.. _`note on LOGIN_REDIRECT_URL setting`: +.. versionchanged:: 1.5 -.. note:: - You can use :func:`~django.core.urlresolvers.reverse_lazy` to reference - URLs by their name instead of providing a hardcoded value. Assuming a - ``urls.py`` with an URLpattern named ``home``:: - - urlpatterns = patterns('', - url('^welcome/$', 'test_app.views.home', name='home'), - ) - - You can use :func:`~django.core.urlresolvers.reverse_lazy` like this:: - - from django.core.urlresolvers import reverse_lazy - - LOGIN_REDIRECT_URL = reverse_lazy('home') - - This also works fine with localized URLs using - :func:`~django.conf.urls.i18n.i18n_patterns`. +This setting now also accepts view function names and +:ref:`named URL patterns ` which can be used to reduce +configuration duplication since you no longer have to define the URL in two +places (``settings`` and URLconf). +For backward compatibility reasons the default remains unchanged. .. setting:: LOGIN_URL @@ -1334,8 +1322,13 @@ Default: ``'/accounts/login/'`` The URL where requests are redirected for login, especially when using the :func:`~django.contrib.auth.decorators.login_required` decorator. -.. note:: - See the `note on LOGIN_REDIRECT_URL setting`_ +.. versionchanged:: 1.5 + +This setting now also accepts view function names and +:ref:`named URL patterns ` which can be used to reduce +configuration duplication since you no longer have to define the URL in two +places (``settings`` and URLconf). +For backward compatibility reasons the default remains unchanged. .. setting:: LOGOUT_URL diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 5578e8efcb..6420239f47 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -121,6 +121,12 @@ Django 1.5 also includes several smaller improvements worth noting: argument. By default the batch_size is unlimited except for SQLite where single batch is limited so that 999 parameters per query isn't exceeded. +* The :setting:`LOGIN_URL` and :setting:`LOGIN_REDIRECT_URL` settings now also + accept view function names and + :ref:`named URL patterns `. This allows you to reduce + configuration duplication. More information can be found in the + :func:`~django.contrib.auth.decorators.login_required` documentation. + Backwards incompatible changes in 1.5 ===================================== diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index f8dbbb65a6..c45e4bbaf7 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -947,6 +947,13 @@ The login_required decorator (r'^accounts/login/$', 'django.contrib.auth.views.login'), + .. versionchanged:: 1.5 + + As of version 1.5 :setting:`settings.LOGIN_URL ` now also accepts + view function names and :ref:`named URL patterns `. + This allows you to freely remap your login view within your URLconf + without having to update the setting. + .. function:: views.login(request, [template_name, redirect_field_name, authentication_form]) **URL name:** ``login`` diff --git a/tests/regressiontests/comment_tests/tests/__init__.py b/tests/regressiontests/comment_tests/tests/__init__.py index 40c0de0d57..f80b29e17b 100644 --- a/tests/regressiontests/comment_tests/tests/__init__.py +++ b/tests/regressiontests/comment_tests/tests/__init__.py @@ -17,7 +17,7 @@ CT = ContentType.objects.get_for_model @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',)) class CommentTestCase(TestCase): fixtures = ["comment_tests"] - urls = 'django.contrib.comments.urls' + urls = 'regressiontests.comment_tests.urls_default' def createSomeComments(self): # Two anonymous comments on two different objects diff --git a/tests/regressiontests/comment_tests/urls_default.py b/tests/regressiontests/comment_tests/urls_default.py new file mode 100644 index 0000000000..bfce8ffc90 --- /dev/null +++ b/tests/regressiontests/comment_tests/urls_default.py @@ -0,0 +1,9 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + (r'^', include('django.contrib.comments.urls')), + + # Provide the auth system login and logout views + (r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}), + (r'^accounts/logout/$', 'django.contrib.auth.views.logout'), +) diff --git a/tests/regressiontests/resolve_url/__init__.py b/tests/regressiontests/resolve_url/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/resolve_url/models.py b/tests/regressiontests/resolve_url/models.py new file mode 100644 index 0000000000..238902edd2 --- /dev/null +++ b/tests/regressiontests/resolve_url/models.py @@ -0,0 +1,12 @@ +""" +Regression tests for the resolve_url function. +""" + +from django.db import models + + +class UnimportantThing(models.Model): + importance = models.IntegerField() + + def get_absolute_url(self): + return '/importance/%d/' % (self.importance,) diff --git a/tests/regressiontests/resolve_url/tests.py b/tests/regressiontests/resolve_url/tests.py new file mode 100644 index 0000000000..d0bf44abde --- /dev/null +++ b/tests/regressiontests/resolve_url/tests.py @@ -0,0 +1,68 @@ +from __future__ import unicode_literals + +from django.core.urlresolvers import NoReverseMatch +from django.contrib.auth.views import logout +from django.utils.unittest import TestCase +from django.shortcuts import resolve_url + +from .models import UnimportantThing + + +class ResolveUrlTests(TestCase): + """ + Tests for the ``resolve_url`` function. + """ + + def test_url_path(self): + """ + Tests that passing a URL path to ``resolve_url`` will result in the + same url. + """ + self.assertEqual('/something/', resolve_url('/something/')) + + def test_full_url(self): + """ + Tests that passing a full URL to ``resolve_url`` will result in the + same url. + """ + url = 'http://example.com/' + self.assertEqual(url, resolve_url(url)) + + def test_model(self): + """ + Tests that passing a model to ``resolve_url`` will result in + ``get_absolute_url`` being called on that model instance. + """ + m = UnimportantThing(importance=1) + self.assertEqual(m.get_absolute_url(), resolve_url(m)) + + def test_view_function(self): + """ + Tests that passing a view name to ``resolve_url`` will result in the + URL path mapping to that view name. + """ + resolved_url = resolve_url(logout) + self.assertEqual('/accounts/logout/', resolved_url) + + def test_valid_view_name(self): + """ + Tests that passing a view function to ``resolve_url`` will result in + the URL path mapping to that view. + """ + resolved_url = resolve_url('django.contrib.auth.views.logout') + self.assertEqual('/accounts/logout/', resolved_url) + + def test_domain(self): + """ + Tests that passing a domain to ``resolve_url`` returns the same domain. + """ + self.assertEqual(resolve_url('example.com'), 'example.com') + + def test_non_view_callable_raises_no_reverse_match(self): + """ + Tests that passing a non-view callable into ``resolve_url`` raises a + ``NoReverseMatch`` exception. + """ + with self.assertRaises(NoReverseMatch): + resolve_url(lambda: 'asdf') + diff --git a/tests/runtests.py b/tests/runtests.py index c548d2745b..a81fee6858 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -92,7 +92,7 @@ def setup(verbosity, test_labels): settings.TEMPLATE_DIRS = (os.path.join(RUNTESTS_DIR, TEST_TEMPLATE_DIR),) settings.USE_I18N = True settings.LANGUAGE_CODE = 'en' - settings.LOGIN_URL = '/accounts/login/' + settings.LOGIN_URL = 'django.contrib.auth.views.login' settings.MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', From c4aa26a983c91b1ec015fe0552077847116dfab7 Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Sat, 8 Sep 2012 19:51:36 -0400 Subject: [PATCH 39/75] Internal refactoring; moving LOOKUP_SEP up one level. In an ideal world, nothing except django.db.models.query should have to import stuff from django.models.sql.*. A few things were needing to get hold of sql.constants.LOOKUP_SEP, so this commit moves it up to django.db.models.constants.LOOKUP_SEP. There are still a couple of places (admin) poking into sql.* to get QUERY_TERMS, which is unfortunate, but a slightly different issue and harder to adjust. --- django/contrib/admin/options.py | 3 ++- django/contrib/admin/util.py | 2 +- django/contrib/gis/db/models/sql/where.py | 2 +- django/db/models/constants.py | 7 +++++++ django/db/models/query.py | 3 +-- django/db/models/sql/compiler.py | 3 ++- django/db/models/sql/constants.py | 11 +++++++---- django/db/models/sql/expressions.py | 2 +- django/db/models/sql/query.py | 5 +++-- django/db/models/sql/subqueries.py | 1 + 10 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 django/db/models/constants.py diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 67b59cc31c..f4205f2ce7 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -14,9 +14,10 @@ from django.core.exceptions import PermissionDenied, ValidationError from django.core.paginator import Paginator from django.core.urlresolvers import reverse from django.db import models, transaction, router +from django.db.models.constants import LOOKUP_SEP from django.db.models.related import RelatedObject from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist -from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS +from django.db.models.sql.constants import QUERY_TERMS from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.template.response import SimpleTemplateResponse, TemplateResponse diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 889f692ac3..f95fe53de1 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -4,7 +4,7 @@ import datetime import decimal from django.db import models -from django.db.models.sql.constants import LOOKUP_SEP +from django.db.models.constants import LOOKUP_SEP from django.db.models.deletion import Collector from django.db.models.related import RelatedObject from django.forms.forms import pretty_name diff --git a/django/contrib/gis/db/models/sql/where.py b/django/contrib/gis/db/models/sql/where.py index 0e152221ac..ec078aebed 100644 --- a/django/contrib/gis/db/models/sql/where.py +++ b/django/contrib/gis/db/models/sql/where.py @@ -1,5 +1,5 @@ +from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import FieldDoesNotExist -from django.db.models.sql.constants import LOOKUP_SEP from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.where import Constraint, WhereNode from django.contrib.gis.db.models.fields import GeometryField diff --git a/django/db/models/constants.py b/django/db/models/constants.py new file mode 100644 index 0000000000..629497eb3d --- /dev/null +++ b/django/db/models/constants.py @@ -0,0 +1,7 @@ +""" +Constants used across the ORM in general. +""" + +# Separator used to split filter strings apart. +LOOKUP_SEP = '__' + diff --git a/django/db/models/query.py b/django/db/models/query.py index 05c049b31f..8bf08b7a93 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -8,6 +8,7 @@ import sys from django.core import exceptions from django.db import connections, router, transaction, IntegrityError +from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import AutoField from django.db.models.query_utils import (Q, select_related_descend, deferred_class_factory, InvalidQuery) @@ -1613,8 +1614,6 @@ def prefetch_related_objects(result_cache, related_lookups): Populates prefetched objects caches for a list of results from a QuerySet """ - from django.db.models.sql.constants import LOOKUP_SEP - if len(result_cache) == 0: return # nothing to do diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index caf2330bd1..28d2404858 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -3,9 +3,10 @@ from django.utils.six.moves import zip from django.core.exceptions import FieldError from django.db import transaction from django.db.backends.util import truncate_name +from django.db.models.constants import LOOKUP_SEP from django.db.models.query_utils import select_related_descend from django.db.models.sql.constants import (SINGLE, MULTI, ORDER_DIR, - LOOKUP_SEP, GET_ITERATOR_CHUNK_SIZE) + GET_ITERATOR_CHUNK_SIZE) from django.db.models.sql.datastructures import EmptyResultSet from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.query import get_order_dir, Query diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py index b9cf2c96fd..f750310624 100644 --- a/django/db/models/sql/constants.py +++ b/django/db/models/sql/constants.py @@ -1,7 +1,13 @@ +""" +Constants specific to the SQL storage portion of the ORM. +""" + from collections import namedtuple import re -# Valid query types (a set is used for speedy lookups). +# Valid query types (a set is used for speedy lookups). These are (currently) +# considered SQL-specific; other storage systems may choose to use different +# lookup types. QUERY_TERMS = set([ 'exact', 'iexact', 'contains', 'icontains', 'gt', 'gte', 'lt', 'lte', 'in', 'startswith', 'istartswith', 'endswith', 'iendswith', 'range', 'year', @@ -12,9 +18,6 @@ QUERY_TERMS = set([ # Larger values are slightly faster at the expense of more storage space. GET_ITERATOR_CHUNK_SIZE = 100 -# Separator used to split filter strings apart. -LOOKUP_SEP = '__' - # Constants to make looking up tuple values clearer. # Join lists (indexes into the tuples that are values in the alias_map # dictionary in the Query class). diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py index 1bbf742b5c..ac8fea6da3 100644 --- a/django/db/models/sql/expressions.py +++ b/django/db/models/sql/expressions.py @@ -1,6 +1,6 @@ from django.core.exceptions import FieldError +from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import FieldDoesNotExist -from django.db.models.sql.constants import LOOKUP_SEP class SQLEvaluator(object): def __init__(self, expression, query, allow_joins=True): diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index f259a2c7d5..77f24fcf24 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -15,11 +15,12 @@ from django.utils.tree import Node from django.utils import six from django.db import connections, DEFAULT_DB_ALIAS from django.db.models import signals +from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import ExpressionNode from django.db.models.fields import FieldDoesNotExist from django.db.models.sql import aggregates as base_aggregates_module -from django.db.models.sql.constants import (QUERY_TERMS, LOOKUP_SEP, ORDER_DIR, - SINGLE, ORDER_PATTERN, JoinInfo) +from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE, + ORDER_PATTERN, JoinInfo) from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode, diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 937505b9b0..c6995c6abb 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -3,6 +3,7 @@ Query subclasses which provide extra functionality beyond simple data retrieval. """ from django.core.exceptions import FieldError +from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import DateField, FieldDoesNotExist from django.db.models.sql.constants import * from django.db.models.sql.datastructures import Date From a3b2136b284c34fb9f675b85495f2c7951d31aff Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 8 Sep 2012 18:22:03 -0600 Subject: [PATCH 40/75] Remove an outdated import inadvertently introduced in tests. --- tests/regressiontests/comment_tests/urls_default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regressiontests/comment_tests/urls_default.py b/tests/regressiontests/comment_tests/urls_default.py index bfce8ffc90..e204f9ebcb 100644 --- a/tests/regressiontests/comment_tests/urls_default.py +++ b/tests/regressiontests/comment_tests/urls_default.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import * +from django.conf.urls import patterns, include urlpatterns = patterns('', (r'^', include('django.contrib.comments.urls')), From 5e99a3d41b5d7b7d8d135283fd25262fdcb1b7fd Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Sat, 8 Sep 2012 20:28:31 -0400 Subject: [PATCH 41/75] Adjust d7853c5 to not show ignorable warnings when running tests. --- django/contrib/sessions/tests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index dbc68652ac..9aa602f416 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -304,6 +304,7 @@ class CacheDBSessionTests(SessionTestsMixin, TestCase): def test_load_overlong_key(self): # Some backends might issue a warning with warnings.catch_warnings(): + warnings.simplefilter("ignore") self.session._session_key = (string.ascii_letters + string.digits) * 20 self.assertEqual(self.session.load(), {}) @@ -353,6 +354,7 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase): def test_load_overlong_key(self): # Some backends might issue a warning with warnings.catch_warnings(): + warnings.simplefilter("ignore") self.session._session_key = (string.ascii_letters + string.digits) * 20 self.assertEqual(self.session.load(), {}) From 3fb2662edcdfb03ebb4b2ab60ffe309104809246 Mon Sep 17 00:00:00 2001 From: Martey Dodoo Date: Sun, 9 Sep 2012 00:35:40 -0400 Subject: [PATCH 42/75] Fixes #18933. Fixes code example in docstring. Makes code example of silent keyword docstring in cycle templatetag method the same as in the documentation. --- django/template/defaulttags.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index cb2ecd26d8..ea1dd0281e 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -531,11 +531,9 @@ def cycle(parser, token): The optional flag "silent" can be used to prevent the cycle declaration from returning any value:: - {% cycle 'row1' 'row2' as rowcolors silent %}{# no value here #} {% for o in some_list %} - {# first value will be "row1" #} - ... - + {% cycle 'row1' 'row2' as rowcolors silent %} + {% include "subtemplate.html " %} {% endfor %} """ From 9ca17f883d7a637581befe8092120242d0534673 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Sun, 9 Sep 2012 13:00:10 +0200 Subject: [PATCH 43/75] Replace nested try/finally try/except with try/except/finally. --- django/core/handlers/wsgi.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index e445d07a61..7a32a3dac7 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -223,18 +223,17 @@ class WSGIHandler(base.BaseHandler): set_script_prefix(base.get_script_name(environ)) signals.request_started.send(sender=self.__class__) try: - try: - request = self.request_class(environ) - except UnicodeDecodeError: - logger.warning('Bad Request (UnicodeDecodeError)', - exc_info=sys.exc_info(), - extra={ - 'status_code': 400, - } - ) - response = http.HttpResponseBadRequest() - else: - response = self.get_response(request) + request = self.request_class(environ) + except UnicodeDecodeError: + logger.warning('Bad Request (UnicodeDecodeError)', + exc_info=sys.exc_info(), + extra={ + 'status_code': 400, + } + ) + response = http.HttpResponseBadRequest() + else: + response = self.get_response(request) finally: signals.request_finished.send(sender=self.__class__) From ffe8bc00bf9b94a366ea3d1998a2712a908a9f1b Mon Sep 17 00:00:00 2001 From: Karen Tracey Date: Sun, 9 Sep 2012 12:53:32 -0400 Subject: [PATCH 44/75] Replaced backwards-incompatible changes reference The previously-referenced wiki page documents backwards-incompatible changes from .96 to 1.0. Changed that referece to point to current in-development release notes, which is where such changes are now documented. --- docs/releases/index.txt | 1 + docs/topics/install.txt | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/releases/index.txt b/docs/releases/index.txt index fa55a4d206..2329d1effa 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -14,6 +14,7 @@ up to and including the new version. Final releases ============== +.. _development_release_notes: 1.5 release ----------- diff --git a/docs/topics/install.txt b/docs/topics/install.txt index 890c5e3195..39b9a93c04 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -257,15 +257,14 @@ Installing the development version If you decide to use the latest development version of Django, you'll want to pay close attention to `the development timeline`_, - and you'll want to keep an eye on `the list of - backwards-incompatible changes`_. This will help you stay on top - of any new features you might want to use, as well as any changes + and you'll want to keep an eye on the :ref:`release notes for the + upcoming release `. This will help you stay + on top of any new features you might want to use, as well as any changes you'll need to make to your code when updating your copy of Django. (For stable releases, any necessary changes are documented in the release notes.) .. _the development timeline: https://code.djangoproject.com/timeline -.. _the list of backwards-incompatible changes: https://code.djangoproject.com/wiki/BackwardsIncompatibleChanges If you'd like to be able to update your Django code occasionally with the latest bug fixes and improvements, follow these instructions: From 75ef980e20fc0441aedb0645aa471c9fe606e3b0 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sun, 9 Sep 2012 11:37:16 -0600 Subject: [PATCH 45/75] Fix Python 3 test failure introduced in a78dd109. --- django/contrib/auth/decorators.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py index 0fc9f3754a..11518193e7 100644 --- a/django/contrib/auth/decorators.py +++ b/django/contrib/auth/decorators.py @@ -24,7 +24,9 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE if test_func(request.user): return view_func(request, *args, **kwargs) path = request.build_absolute_uri() - resolved_login_url = resolve_url(login_url or settings.LOGIN_URL) + # urlparse chokes on lazy objects in Python 3, force to str + resolved_login_url = force_str( + resolve_url(login_url or settings.LOGIN_URL)) # If the login url is the same scheme and net location then just # use the path as the "next" url. login_scheme, login_netloc = urlparse(resolved_login_url)[:2] @@ -33,7 +35,8 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE (not login_netloc or login_netloc == current_netloc)): path = request.get_full_path() from django.contrib.auth.views import redirect_to_login - return redirect_to_login(path, login_url, redirect_field_name) + return redirect_to_login( + path, resolved_login_url, redirect_field_name) return _wrapped_view return decorator From fcec904e4f3582a45d4d8e309e71e9f0c4d79a0c Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sun, 9 Sep 2012 12:13:42 -0600 Subject: [PATCH 46/75] Fix an HTML-parser test that's failed in Python 2.6.8 since 5c79dd58. The problem description in #18239 asserted that http://bugs.python.org/issue670664 was fixed in Python 2.6.8, but based on http://bugs.python.org/issue670664#msg146770 it appears that's not correct; the fix was only applied in 2.7, 3.2, and Python trunk. Therefore we must use our patched HTMLParser subclass in all Python 2.6 versions. --- django/utils/html_parser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/django/utils/html_parser.py b/django/utils/html_parser.py index d7311f253b..6ccb665249 100644 --- a/django/utils/html_parser.py +++ b/django/utils/html_parser.py @@ -5,8 +5,7 @@ import sys current_version = sys.version_info use_workaround = ( - (current_version < (2, 6, 8)) or - (current_version >= (2, 7) and current_version < (2, 7, 3)) or + (current_version < (2, 7, 3)) or (current_version >= (3, 0) and current_version < (3, 2, 3)) ) From 2e928583861bc35f1ea137cb6f3353867f160bdb Mon Sep 17 00:00:00 2001 From: Karen Tracey Date: Sun, 9 Sep 2012 15:07:04 -0400 Subject: [PATCH 47/75] Fixed a couple of test failures on Windows. --- tests/regressiontests/utils/os_utils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/regressiontests/utils/os_utils.py b/tests/regressiontests/utils/os_utils.py index a78f348cf5..a205d67431 100644 --- a/tests/regressiontests/utils/os_utils.py +++ b/tests/regressiontests/utils/os_utils.py @@ -1,21 +1,26 @@ +import os + from django.utils import unittest from django.utils._os import safe_join class SafeJoinTests(unittest.TestCase): def test_base_path_ends_with_sep(self): + drive, path = os.path.splitdrive(safe_join("/abc/", "abc")) self.assertEqual( - safe_join("/abc/", "abc"), - "/abc/abc", + path, + "{0}abc{0}abc".format(os.path.sep) ) def test_root_path(self): + drive, path = os.path.splitdrive(safe_join("/", "path")) self.assertEqual( - safe_join("/", "path"), - "/path", + path, + "{0}path".format(os.path.sep), ) + drive, path = os.path.splitdrive(safe_join("/", "")) self.assertEqual( - safe_join("/", ""), - "/", + path, + os.path.sep, ) From cb1614f7b30f336db2a807b43696e20fdab7b78c Mon Sep 17 00:00:00 2001 From: Mike Grouchy Date: Mon, 10 Sep 2012 13:35:21 +0200 Subject: [PATCH 48/75] Fixed #18611 -- Display current date/time when running runserver --- django/core/management/commands/runserver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index c067c9c322..9c24701d2e 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -1,4 +1,5 @@ from optparse import make_option +from datetime import datetime import os import re import sys @@ -90,10 +91,12 @@ class Command(BaseCommand): self.stdout.write("Validating models...\n\n") self.validate(display_num_errors=True) self.stdout.write(( + "%(started_at)s\n" "Django version %(version)s, using settings %(settings)r\n" "Development server is running at http://%(addr)s:%(port)s/\n" "Quit the server with %(quit_command)s.\n" ) % { + "started_at": datetime.now().strftime('%B %d, %Y - %X'), "version": self.get_version(), "settings": settings.SETTINGS_MODULE, "addr": self._raw_ipv6 and '[%s]' % self.addr or self.addr, From f416ea9c8d8a5a7224236e429d51ce5606c95c5b Mon Sep 17 00:00:00 2001 From: Collin Anderson Date: Mon, 10 Sep 2012 12:11:24 -0300 Subject: [PATCH 49/75] fixed rfc comment typo in middleware/csrf.py --- django/middleware/csrf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index 305b20e1c4..c9e8d73c82 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -105,7 +105,7 @@ class CsrfViewMiddleware(object): if getattr(callback, 'csrf_exempt', False): return None - # Assume that anything not defined as 'safe' by RC2616 needs protection + # Assume that anything not defined as 'safe' by RFC2616 needs protection if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'): if getattr(request, '_dont_enforce_csrf_checks', False): # Mechanism to turn off CSRF checks for test suite. From f1bdfbd24bcc76d21c4bf7442959bdf630ac4dec Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 10 Sep 2012 19:21:29 +0200 Subject: [PATCH 50/75] Document and test 'type' usage in Widget attrs Refs #16630. --- django/forms/fields.py | 2 +- django/forms/widgets.py | 18 +++++++++++------- docs/ref/forms/widgets.txt | 11 ++++++----- tests/regressiontests/forms/tests/widgets.py | 18 +++++++++--------- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/django/forms/fields.py b/django/forms/fields.py index 7f0d26d1aa..124e4f669a 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -199,7 +199,7 @@ class CharField(Field): def widget_attrs(self, widget): attrs = super(CharField, self).widget_attrs(widget) - if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)): + if self.max_length is not None and isinstance(widget, TextInput): # The HTML attribute is maxlength, not max_length. attrs.update({'maxlength': str(self.max_length)}) return attrs diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 7651efccd0..763da0cff2 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -260,10 +260,17 @@ class Input(Widget): final_attrs['value'] = force_text(self._format_value(value)) return format_html('', flatatt(final_attrs)) + class TextInput(Input): input_type = 'text' -class PasswordInput(Input): + def __init__(self, attrs=None): + if attrs is not None: + self.input_type = attrs.pop('type', self.input_type) + super(TextInput, self).__init__(attrs) + + +class PasswordInput(TextInput): input_type = 'password' def __init__(self, attrs=None, render_value=False): @@ -400,9 +407,8 @@ class Textarea(Widget): flatatt(final_attrs), force_text(value)) -class DateInput(Input): - input_type = 'text' +class DateInput(TextInput): def __init__(self, attrs=None, format=None): super(DateInput, self).__init__(attrs) if format: @@ -431,9 +437,8 @@ class DateInput(Input): pass return super(DateInput, self)._has_changed(self._format_value(initial), data) -class DateTimeInput(Input): - input_type = 'text' +class DateTimeInput(TextInput): def __init__(self, attrs=None, format=None): super(DateTimeInput, self).__init__(attrs) if format: @@ -462,9 +467,8 @@ class DateTimeInput(Input): pass return super(DateTimeInput, self)._has_changed(self._format_value(initial), data) -class TimeInput(Input): - input_type = 'text' +class TimeInput(TextInput): def __init__(self, attrs=None, format=None): super(TimeInput, self).__init__(attrs) if format: diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index eab314a4cd..1935cb23bc 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -126,8 +126,9 @@ provided for each widget will be rendered exactly the same:: On a real Web page, you probably don't want every widget to look the same. You might want a larger input element for the comment, and you might want the -'name' widget to have some special CSS class. To do this, you use the -:attr:`Widget.attrs` argument when creating the widget: +'name' widget to have some special CSS class. It is also possible to specify +the 'type' attribute to take advantage of the new HTML5 input types. To do +this, you use the :attr:`Widget.attrs` argument when creating the widget: For example:: @@ -245,7 +246,7 @@ commonly used groups of widgets: Date input as a simple text box: ```` - Takes one optional argument: + Takes same arguments as :class:`TextInput`, with one more optional argument: .. attribute:: DateInput.format @@ -262,7 +263,7 @@ commonly used groups of widgets: Date/time input as a simple text box: ```` - Takes one optional argument: + Takes same arguments as :class:`TextInput`, with one more optional argument: .. attribute:: DateTimeInput.format @@ -279,7 +280,7 @@ commonly used groups of widgets: Time input as a simple text box: ```` - Takes one optional argument: + Takes same arguments as :class:`TextInput`, with one more optional argument: .. attribute:: TimeInput.format diff --git a/tests/regressiontests/forms/tests/widgets.py b/tests/regressiontests/forms/tests/widgets.py index 544ca41642..104144b288 100644 --- a/tests/regressiontests/forms/tests/widgets.py +++ b/tests/regressiontests/forms/tests/widgets.py @@ -31,9 +31,9 @@ class FormsWidgetTestCase(TestCase): self.assertHTMLEqual(w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), '') # You can also pass 'attrs' to the constructor: - w = TextInput(attrs={'class': 'fun'}) - self.assertHTMLEqual(w.render('email', ''), '') - self.assertHTMLEqual(w.render('email', 'foo@example.com'), '') + w = TextInput(attrs={'class': 'fun', 'type': 'email'}) + self.assertHTMLEqual(w.render('email', ''), '') + self.assertHTMLEqual(w.render('email', 'foo@example.com'), '') # 'attrs' passed to render() get precedence over those passed to the constructor: w = TextInput(attrs={'class': 'pretty'}) @@ -915,8 +915,8 @@ beatle J R Ringo False""") self.assertHTMLEqual(w.render('date', datetime.datetime(2007, 9, 17, 12, 51)), '') # Use 'format' to change the way a value is displayed. - w = DateTimeInput(format='%d/%m/%Y %H:%M') - self.assertHTMLEqual(w.render('date', d), '') + w = DateTimeInput(format='%d/%m/%Y %H:%M', attrs={'type': 'datetime'}) + self.assertHTMLEqual(w.render('date', d), '') self.assertFalse(w._has_changed(d, '17/09/2007 12:51')) # Make sure a custom format works with _has_changed. The hidden input will use @@ -938,8 +938,8 @@ beatle J R Ringo False""") self.assertHTMLEqual(w.render('date', '2007-09-17'), '') # Use 'format' to change the way a value is displayed. - w = DateInput(format='%d/%m/%Y') - self.assertHTMLEqual(w.render('date', d), '') + w = DateInput(format='%d/%m/%Y', attrs={'type': 'date'}) + self.assertHTMLEqual(w.render('date', d), '') self.assertFalse(w._has_changed(d, '17/09/2007')) # Make sure a custom format works with _has_changed. The hidden input will use @@ -963,8 +963,8 @@ beatle J R Ringo False""") self.assertHTMLEqual(w.render('time', '13:12:11'), '') # Use 'format' to change the way a value is displayed. - w = TimeInput(format='%H:%M') - self.assertHTMLEqual(w.render('time', t), '') + w = TimeInput(format='%H:%M', attrs={'type': 'time'}) + self.assertHTMLEqual(w.render('time', t), '') self.assertFalse(w._has_changed(t, '12:51')) # Make sure a custom format works with _has_changed. The hidden input will use From 6eb4f2569266391e223410ab7eaa2555ac0a4f10 Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Mon, 10 Sep 2012 17:19:35 -0700 Subject: [PATCH 51/75] Removed an obsolete documentation page about admin style customization. --- docs/contents.txt | 11 -- docs/faq/admin.txt | 5 +- docs/intro/whatsnext.txt | 3 +- docs/obsolete/_images/formrow.png | Bin 16601 -> 0 bytes docs/obsolete/_images/module.png | Bin 10072 -> 0 bytes docs/obsolete/_images/objecttools_01.png | Bin 1398 -> 0 bytes docs/obsolete/_images/objecttools_02.png | Bin 2268 -> 0 bytes docs/obsolete/admin-css.txt | 186 ----------------------- docs/obsolete/index.txt | 12 -- 9 files changed, 3 insertions(+), 214 deletions(-) delete mode 100644 docs/obsolete/_images/formrow.png delete mode 100644 docs/obsolete/_images/module.png delete mode 100644 docs/obsolete/_images/objecttools_01.png delete mode 100644 docs/obsolete/_images/objecttools_02.png delete mode 100644 docs/obsolete/admin-css.txt delete mode 100644 docs/obsolete/index.txt diff --git a/docs/contents.txt b/docs/contents.txt index 9bf0d685c4..736e1f62bf 100644 --- a/docs/contents.txt +++ b/docs/contents.txt @@ -28,14 +28,3 @@ Indices, glossary and tables * :ref:`genindex` * :ref:`modindex` * :ref:`glossary` - -Deprecated/obsolete documentation -================================= - -The following documentation covers features that have been deprecated or that -have been replaced in newer versions of Django. - -.. toctree:: - :maxdepth: 2 - - obsolete/index diff --git a/docs/faq/admin.txt b/docs/faq/admin.txt index 8ec7491903..ea6aa2e74e 100644 --- a/docs/faq/admin.txt +++ b/docs/faq/admin.txt @@ -91,8 +91,7 @@ The dynamically-generated admin site is ugly! How can I change it? We like it, but if you don't agree, you can modify the admin site's presentation by editing the CSS stylesheet and/or associated image files. The site is built using semantic HTML and plenty of CSS hooks, so any changes you'd -like to make should be possible by editing the stylesheet. We've got a -:doc:`guide to the CSS used in the admin ` to get you started. +like to make should be possible by editing the stylesheet. What browsers are supported for using the admin? ------------------------------------------------ @@ -104,5 +103,5 @@ There *may* be minor stylistic differences between supported browsers—for example, some browsers may not support rounded corners. These are considered acceptable variations in rendering. -.. _YUI's A-grade: http://yuilibrary.com/yui/docs/tutorials/gbs/ +.. _YUI's A-grade: http://yuilibrary.com/yui/docs/tutorials/gbs/ diff --git a/docs/intro/whatsnext.txt b/docs/intro/whatsnext.txt index cc793c8129..ea4b18de03 100644 --- a/docs/intro/whatsnext.txt +++ b/docs/intro/whatsnext.txt @@ -67,8 +67,7 @@ different needs: whathaveyou. * Finally, there's some "specialized" documentation not usually relevant to - most developers. This includes the :doc:`release notes `, - :doc:`documentation of obsolete features `, + most developers. This includes the :doc:`release notes ` and :doc:`internals documentation ` for those who want to add code to Django itself, and a :doc:`few other things that simply don't fit elsewhere `. diff --git a/docs/obsolete/_images/formrow.png b/docs/obsolete/_images/formrow.png deleted file mode 100644 index 164dd262b5f8b39891b77a1406a2df01184771e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16601 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

    J)M$@o6R|ZZj=T)4)Y}97lK;c$jfz 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 diff --git a/docs/obsolete/_images/module.png b/docs/obsolete/_images/module.png deleted file mode 100644 index 6acda978091070ec62c96c9d335b5bb0fdfb06e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10072 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 Ef&~hkumAu6 diff --git a/docs/obsolete/_images/objecttools_02.png b/docs/obsolete/_images/objecttools_02.png deleted file mode 100644 index 06a854009bcef5dd425f9ca50b6239a60cd09b7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2268 zcmV<22qX82P)e5Szl8VSr)(0-H=QOn~*V*HEf*DLITdf$Pz`&0v431 zVoR;9ota84I!_i~z>nagQ*hMS%A${(-=bk>lU!Okr_D$KFH*Zj>l^7_1f${=uN+u-+Xa-8TQp$ru z87SpSp-h$R@LUwYK4xWQ#pQCndi5$C4(Bra|LQReQ!14Q4jeF>%{4VOWo2b~;(0F+ z<%xfrL+8(*zj*OtE+;sSt5hnIBwJfswOXwtlhV18PNyF}eE9U~(_F6ih@PS-$YW?u zr?auK@z}9rfLSuO_XUw;@H?VRCX;#m06)KMwnvX1wY9a`Y&J2I5^E4l8y+6s`-vhz z4go{h5WGMS-S-rc7|3=s8yg#v)5SUAa5%1AyC!B*m@81IA3S&v2m~bk?r|lA4Z#ae z0p4pw{{zC?+uLF35!`5ge*X6D+X5zexdLOMPzd%Yf|lH^z(Bvvlrb>b`UeKS zZc-wp$p-qqCbi^5{o&qIokq!_?JFU8fWU$*4iRp&L)-&@BN2VVGL(xW*D)MD+WezGsbjFNXJ5`*pTqT1PA~mlflIza!oB*dPrTP3C)bjjb;r(DZk5CraY2? zgXD*MA=N>R!t>)d?=AQKy-&S5@y$(#srRccl9;$L=!iAhzp|qFABOJEvSCTV?6mh7 zW5I|*t07P-=o~{N7GEM8rzc#B8j#m)yoCy?(gqBv5n8NTW zR7bUmtR|?4@2!afz5xPTIg3RU_e=*VglUYh41Lm62j09>ZWc~$JDn&IpL4<;R2eL% zj$+FRB-4?4Ty3mVA*3J=)~f&5P>rG0g+&_XX3GInP9Q?1m3f_WF^bTdk2lGg2?&XJ zt7T$Ha|sm2b6mKNWIYxjDHMt`XU=d9BasNpBuoE!tyrvq|l925y1;h15f8;@YKz%ha0p6qWsg|?7MpK50DU4B8h!eL9ezdBk6Vd z;CiU-Gc{d0k>Wt(=XBKxJ7=S;<2*3^W5Xl=y*uWkiQ|6ki+N+bQCtE=@zitjjhRvK zH5Q9qzkWTBId2UDJE%o4C(i`&P-5|?;X8D_HTL%R(9bm6yrXx<(Gf%K#~PJNU)}yi zRr;Tvj6wFk>2=yZK~dRh2(7QtF{A@O&m|6#WJf0@d~94}feaA1nqm>nPJ66mv%b{= zP9pA^WrUQhJ32;8QB@UB#{GUj6%W65FGkw+hGsL^&ighD`uci$*>Xb$37RBKYI8l! zaKT+#rFh%&hvOUqMezu3nyY*H@?~}i4y8IeIueP*)vH&z3lJDUrD4rL5Z0A_`}T2% z3zC_=EzStctjbRNmlpHySEhfsH$KlO=_{dbXm6@r2}XeWmcG8W!<~PzK>bVwmsc}p z7|IYBot_DTxeP;*xhZ&Tg){*KLHF$*0?y6%91K&>r|H7Rw&mA!0f?=5pZk+qovwqHo@86f4 zDfU&MMy^!iC>@Qm3qP(@%Hz@KRyyEvr5w+8IZknO!p`TI2t_z2 z=8%jFr6yMagqM;L>2KQ~NidxUb|h(};V ztJM%ZKwuou*BwWcGhFUn#d2<#V%v-+_lgp0O$;%aZ7t71b z;SXEb?@Ufk?#x*WFR6{%DLAN<%xgq3^g<~ zSglr#M#E(i!sF!%^nqMB#e;vPEfz~%T^%e&us42(Lzf*v9$yW3@>VxEh=JUP{D7sW zwzd}jRfpi&M9IXkrx5K3ExX|aRwDT70S+NifJ|v7x%-vf2%_ZkgJFR89rVGVDgoII q21>cI8?1hqK1;dsVLsR0K=m89nOK7bs>~Sx0000 -

  • Add redirect
  • - - -.. image:: _images/objecttools_01.png - :alt: Object tools on a changelist page - -and from a form page: - -.. code-block:: html+django - - - -.. image:: _images/objecttools_02.png - :alt: Object tools on a form page - -Form Styles -=========== - -Fieldsets ---------- - -Admin forms are broken up into groups by ``fieldset`` elements. Each form fieldset -should have a class ``.module``. Each fieldset should have a header ``h2`` within the -fieldset at the top (except the first group in the form, and in some cases where the -group of fields doesn't have a logical label). - -Each fieldset can also take extra classes in addition to ``.module`` to apply -appropriate formatting to the group of fields. - -.aligned - This will align the labels and inputs side by side on the same line. -.wide - Used in combination with ``.aligned`` to widen the space available for the - labels. - -Form Rows ---------- - -Each row of the form (within the ``fieldset``) should be enclosed in a ``div`` -with class ``form-row``. If the field in the row is required, a class of -``required`` should also be added to the ``div.form-row``. - -.. image:: _images/formrow.png - :alt: Example use of form-row class - -Labels ------- - -Form labels should always precede the field, except in the case -of checkboxes and radio buttons, where the ``input`` should come first. Any -explanation or help text should follow the ``label`` in a ``p`` with class -``.help``. diff --git a/docs/obsolete/index.txt b/docs/obsolete/index.txt deleted file mode 100644 index ddc86237cc..0000000000 --- a/docs/obsolete/index.txt +++ /dev/null @@ -1,12 +0,0 @@ -Deprecated/obsolete documentation -================================= - -These documents cover features that have been deprecated or that have been -replaced in newer versions of Django. They're preserved here for folks using old -versions of Django or those still using deprecated APIs. No new code based on -these APIs should be written. - -.. toctree:: - :maxdepth: 1 - - admin-css \ No newline at end of file From 0133d66734f5b0470436a22f5b85bdf7266bf1cf Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Mon, 10 Sep 2012 17:27:50 -0700 Subject: [PATCH 52/75] Removed a colloquialism ("and then some") from the documentation index page that would be confusing to non-native English speakers. --- docs/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.txt b/docs/index.txt index 3f4e30385c..8b29c95fa2 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -5,7 +5,7 @@ Django documentation ==================== -.. rubric:: Everything you need to know about Django (and then some). +.. rubric:: Everything you need to know about Django. Getting help ============ From 859aa2a6c4496a0feafebef7c7ea8fc57d9913f4 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 12 Sep 2012 10:16:49 +0200 Subject: [PATCH 53/75] Fixed #18790 -- Encoded database password on Python 2 Thanks thcourbon@gmail.com for the report. --- django/db/backends/mysql/base.py | 3 ++- django/db/backends/postgresql_psycopg2/base.py | 3 ++- tests/regressiontests/backends/tests.py | 13 +++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index cec3b04f7e..4043014b8e 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -37,6 +37,7 @@ from django.db.backends.mysql.client import DatabaseClient from django.db.backends.mysql.creation import DatabaseCreation from django.db.backends.mysql.introspection import DatabaseIntrospection from django.db.backends.mysql.validation import DatabaseValidation +from django.utils.encoding import force_str from django.utils.functional import cached_property from django.utils.safestring import SafeBytes, SafeText from django.utils import six @@ -390,7 +391,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): if settings_dict['NAME']: kwargs['db'] = settings_dict['NAME'] if settings_dict['PASSWORD']: - kwargs['passwd'] = settings_dict['PASSWORD'] + kwargs['passwd'] = force_str(settings_dict['PASSWORD']) if settings_dict['HOST'].startswith('/'): kwargs['unix_socket'] = settings_dict['HOST'] elif settings_dict['HOST']: diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index f6f534da8c..c8b88d5619 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -13,6 +13,7 @@ from django.db.backends.postgresql_psycopg2.client import DatabaseClient from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation from django.db.backends.postgresql_psycopg2.version import get_version from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection +from django.utils.encoding import force_str from django.utils.log import getLogger from django.utils.safestring import SafeText, SafeBytes from django.utils import six @@ -172,7 +173,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): if settings_dict['USER']: conn_params['user'] = settings_dict['USER'] if settings_dict['PASSWORD']: - conn_params['password'] = settings_dict['PASSWORD'] + conn_params['password'] = force_str(settings_dict['PASSWORD']) if settings_dict['HOST']: conn_params['host'] = settings_dict['HOST'] if settings_dict['PORT']: diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index e53b02032e..cfa298253c 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -401,6 +401,19 @@ class BackendTestCase(TestCase): self.assertEqual(list(cursor.fetchmany(2)), [('Jane', 'Doe'), ('John', 'Doe')]) self.assertEqual(list(cursor.fetchall()), [('Mary', 'Agnelline'), ('Peter', 'Parker')]) + def test_unicode_password(self): + old_password = connection.settings_dict['PASSWORD'] + connection.settings_dict['PASSWORD'] = "françois" + try: + cursor = connection.cursor() + except backend.Database.DatabaseError: + # As password is probably wrong, a database exception is expected + pass + except Exception as e: + self.fail("Unexpected error raised with unicode password: %s" % e) + finally: + connection.settings_dict['PASSWORD'] = old_password + def test_database_operations_helper_class(self): # Ticket #13630 self.assertTrue(hasattr(connection, 'ops')) From 703c266682be39f7153498ad0d8031231f12ee79 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 12 Sep 2012 11:21:58 +0200 Subject: [PATCH 54/75] Fixed #18182 -- Made is_usable_password check if hashing algorithm is correct The display of the ReadOnlyPasswordHashWidget has also been improved to distinguish empty/unusable password from erroneous password. Fixed #18453 also. Thanks danielr and Leo for the reports and Moritz Sichert for the initial patch. --- django/contrib/auth/forms.py | 28 ++++++++++++++-------------- django/contrib/auth/hashers.py | 8 +++++++- django/contrib/auth/tests/forms.py | 19 +++++++++++++------ django/contrib/auth/tests/hashers.py | 4 ++++ 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 75b3ca4ece..08488237c7 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -11,7 +11,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _ from django.contrib.auth import authenticate from django.contrib.auth.models import User -from django.contrib.auth.hashers import UNUSABLE_PASSWORD, is_password_usable, identify_hasher +from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.models import get_current_site @@ -24,22 +24,22 @@ mask_password = lambda p: "%s%s" % (p[:UNMASKED_DIGITS_TO_SHOW], "*" * max(len(p class ReadOnlyPasswordHashWidget(forms.Widget): def render(self, name, value, attrs): encoded = value - - if not is_password_usable(encoded): - return "None" - final_attrs = self.build_attrs(attrs) - try: - hasher = identify_hasher(encoded) - except ValueError: - summary = mark_safe("Invalid password format or unknown hashing algorithm.") + if encoded == '' or encoded == UNUSABLE_PASSWORD: + summary = mark_safe("%s" % ugettext("No password set.")) else: - summary = format_html_join('', - "{0}: {1} ", - ((ugettext(key), value) - for key, value in hasher.safe_summary(encoded).items()) - ) + try: + hasher = identify_hasher(encoded) + except ValueError: + summary = mark_safe("%s" % ugettext( + "Invalid password format or unknown hashing algorithm.")) + else: + summary = format_html_join('', + "{0}: {1} ", + ((ugettext(key), value) + for key, value in hasher.safe_summary(encoded).items()) + ) return format_html("{1}", flatatt(final_attrs), summary) diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index bd0c6778c9..c628059d34 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -28,7 +28,13 @@ def reset_hashers(**kwargs): def is_password_usable(encoded): - return (encoded is not None and encoded != UNUSABLE_PASSWORD) + if encoded is None or encoded == UNUSABLE_PASSWORD: + return False + try: + hasher = identify_hasher(encoded) + except ValueError: + return False + return True def check_password(password, encoded, setter=None, preferred='default'): diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index 594b55c633..8fee3501c5 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -236,23 +236,30 @@ class UserChangeFormTest(TestCase): # Just check we can create it form = MyUserForm({}) + def test_unsuable_password(self): + user = User.objects.get(username='empty_password') + user.set_unusable_password() + user.save() + form = UserChangeForm(instance=user) + self.assertIn(_("No password set."), form.as_table()) + def test_bug_17944_empty_password(self): user = User.objects.get(username='empty_password') form = UserChangeForm(instance=user) - # Just check that no error is raised. - form.as_table() + self.assertIn(_("Invalid password format or unknown hashing algorithm."), + form.as_table()) def test_bug_17944_unmanageable_password(self): user = User.objects.get(username='unmanageable_password') form = UserChangeForm(instance=user) - # Just check that no error is raised. - form.as_table() + self.assertIn(_("Invalid password format or unknown hashing algorithm."), + form.as_table()) def test_bug_17944_unknown_password_algorithm(self): user = User.objects.get(username='unknown_password') form = UserChangeForm(instance=user) - # Just check that no error is raised. - form.as_table() + self.assertIn(_("Invalid password format or unknown hashing algorithm."), + form.as_table()) @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) diff --git a/django/contrib/auth/tests/hashers.py b/django/contrib/auth/tests/hashers.py index 673263b566..d867a57d98 100644 --- a/django/contrib/auth/tests/hashers.py +++ b/django/contrib/auth/tests/hashers.py @@ -100,6 +100,10 @@ class TestUtilsHashPass(unittest.TestCase): self.assertRaises(ValueError, doit) self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash") + def test_bad_encoded(self): + self.assertFalse(is_password_usable('letmein_badencoded')) + self.assertFalse(is_password_usable('')) + def test_low_level_pkbdf2(self): hasher = PBKDF2PasswordHasher() encoded = hasher.encode('letmein', 'seasalt') From 1aa218b857ccd5e7db3718e2c0b4d4739dcb075c Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 12 Sep 2012 11:56:58 +0200 Subject: [PATCH 55/75] Fixed test output check when password is blank --- django/contrib/auth/tests/forms.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index 8fee3501c5..74aa47e199 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -246,8 +246,7 @@ class UserChangeFormTest(TestCase): def test_bug_17944_empty_password(self): user = User.objects.get(username='empty_password') form = UserChangeForm(instance=user) - self.assertIn(_("Invalid password format or unknown hashing algorithm."), - form.as_table()) + self.assertIn(_("No password set."), form.as_table()) def test_bug_17944_unmanageable_password(self): user = User.objects.get(username='unmanageable_password') From c2c8d4044e2c6345f6e8d0f8617755dec7cd55d5 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 12 Sep 2012 14:50:16 +0200 Subject: [PATCH 56/75] Made minimal changes to make gis test suite start with Python 3 --- django/contrib/gis/geos/libgeos.py | 2 +- django/contrib/gis/geos/mutable_list.py | 9 ++++++--- django/contrib/gis/geos/tests/__init__.py | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index a4f5adf4d0..aed6cf366c 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -110,7 +110,7 @@ def geos_version_info(): is a release candidate (and what number release candidate), and the C API version. """ - ver = geos_version() + ver = geos_version().decode() m = version_regex.match(ver) if not m: raise GEOSException('Could not parse version info string "%s"' % ver) return dict((key, m.group(key)) for key in ('version', 'release_candidate', 'capi_version', 'major', 'minor', 'subminor')) diff --git a/django/contrib/gis/geos/mutable_list.py b/django/contrib/gis/geos/mutable_list.py index 69e50e6b3f..820cdfa5a4 100644 --- a/django/contrib/gis/geos/mutable_list.py +++ b/django/contrib/gis/geos/mutable_list.py @@ -215,15 +215,18 @@ class ListMixin(object): "Standard list reverse method" self[:] = self[-1::-1] - def sort(self, cmp=cmp, key=None, reverse=False): + def sort(self, cmp=None, key=None, reverse=False): "Standard list sort method" if key: temp = [(key(v),v) for v in self] - temp.sort(cmp=cmp, key=lambda x: x[0], reverse=reverse) + temp.sort(key=lambda x: x[0], reverse=reverse) self[:] = [v[1] for v in temp] else: temp = list(self) - temp.sort(cmp=cmp, reverse=reverse) + if cmp is not None: + temp.sort(cmp=cmp, reverse=reverse) + else: + temp.sort(reverse=reverse) self[:] = temp ### Private routines ### diff --git a/django/contrib/gis/geos/tests/__init__.py b/django/contrib/gis/geos/tests/__init__.py index ccf960c68f..6b715d8c59 100644 --- a/django/contrib/gis/geos/tests/__init__.py +++ b/django/contrib/gis/geos/tests/__init__.py @@ -16,7 +16,8 @@ test_suites = [ def suite(): "Builds a test suite for the GEOS tests." s = TestSuite() - map(s.addTest, test_suites) + for suite in test_suites: + s.addTest(suite) return s def run(verbosity=1): From fbd4b3a5188d0b488648f1fdd2339e1eac6722a2 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 12 Sep 2012 16:13:58 +0200 Subject: [PATCH 57/75] [py3] Fixed GeoDjango mutable list tests --- .../contrib/gis/geos/tests/test_mutable_list.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/django/contrib/gis/geos/tests/test_mutable_list.py b/django/contrib/gis/geos/tests/test_mutable_list.py index cd174d7cfa..675505f0f9 100644 --- a/django/contrib/gis/geos/tests/test_mutable_list.py +++ b/django/contrib/gis/geos/tests/test_mutable_list.py @@ -55,14 +55,14 @@ class ListMixinTest(unittest.TestCase): def lists_of_len(self, length=None): if length is None: length = self.limit - pl = range(length) + pl = list(range(length)) return pl, self.listType(pl) def limits_plus(self, b): return range(-self.limit - b, self.limit + b) def step_range(self): - return range(-1 - self.limit, 0) + range(1, 1 + self.limit) + return list(range(-1 - self.limit, 0)) + list(range(1, 1 + self.limit)) def test01_getslice(self): 'Slice retrieval' @@ -160,13 +160,13 @@ class ListMixinTest(unittest.TestCase): del pl[i:j] del ul[i:j] self.assertEqual(pl[:], ul[:], 'del slice [%d:%d]' % (i,j)) - for k in range(-Len - 1,0) + range(1,Len): + for k in list(range(-Len - 1, 0)) + list(range(1, Len)): pl, ul = self.lists_of_len(Len) del pl[i:j:k] del ul[i:j:k] self.assertEqual(pl[:], ul[:], 'del slice [%d:%d:%d]' % (i,j,k)) - for k in range(-Len - 1,0) + range(1,Len): + for k in list(range(-Len - 1, 0)) + list(range(1, Len)): pl, ul = self.lists_of_len(Len) del pl[:i:k] del ul[:i:k] @@ -177,7 +177,7 @@ class ListMixinTest(unittest.TestCase): del ul[i::k] self.assertEqual(pl[:], ul[:], 'del slice [%d::%d]' % (i,k)) - for k in range(-Len - 1,0) + range(1,Len): + for k in list(range(-Len - 1, 0)) + list(range(1, Len)): pl, ul = self.lists_of_len(Len) del pl[::k] del ul[::k] @@ -320,7 +320,7 @@ class ListMixinTest(unittest.TestCase): pl.sort() ul.sort() self.assertEqual(pl[:], ul[:], 'sort') - mid = pl[len(pl) / 2] + mid = pl[len(pl) // 2] pl.sort(key=lambda x: (mid-x)**2) ul.sort(key=lambda x: (mid-x)**2) self.assertEqual(pl[:], ul[:], 'sort w/ key') @@ -330,7 +330,7 @@ class ListMixinTest(unittest.TestCase): pl.sort(reverse=True) ul.sort(reverse=True) self.assertEqual(pl[:], ul[:], 'sort w/ reverse') - mid = pl[len(pl) / 2] + mid = pl[len(pl) // 2] pl.sort(key=lambda x: (mid-x)**2) ul.sort(key=lambda x: (mid-x)**2) self.assertEqual(pl[:], ul[:], 'sort w/ key') @@ -338,7 +338,7 @@ class ListMixinTest(unittest.TestCase): def test_12_arithmetic(self): 'Arithmetic' pl, ul = self.lists_of_len() - al = range(10,14) + al = list(range(10,14)) self.assertEqual(list(pl + al), list(ul + al), 'add') self.assertEqual(type(ul), type(ul + al), 'type of add result') self.assertEqual(list(al + pl), list(al + ul), 'radd') From 9db7652eef7152d06e177a4886b3484f658c1101 Mon Sep 17 00:00:00 2001 From: Jeremy Dunck Date: Sun, 9 Sep 2012 01:36:38 -0400 Subject: [PATCH 58/75] Fixed my terribly outdated profile in committers.txt in celebration of my commit bit. --- AUTHORS | 2 +- docs/internals/committers.txt | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/AUTHORS b/AUTHORS index 0a3699d516..af84cbff0b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -31,6 +31,7 @@ The PRIMARY AUTHORS are (and/or have been): * Claude Paroz * Anssi Kääriäinen * Florian Apolloner + * Jeremy Dunck More information on the main contributors to Django can be found in docs/internals/committers.txt. @@ -167,7 +168,6 @@ answer newbie questions, and generally made Django that much better: dready Maximillian Dornseif Daniel Duan - Jeremy Dunck Andrew Durdin dusk@woofle.net Andy Dustman diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 2faace99a5..63c8f5d618 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -379,6 +379,20 @@ Florian Apolloner .. _Graz University of Technology: http://tugraz.at/ .. _Ubuntuusers webteam: http://wiki.ubuntuusers.de/ubuntuusers/Webteam +Jeremy Dunck + Jeremy was rescued from corporate IT drudgery by Free Software and, in part, + Django. Many of Jeremy's interests center around access to information. + + Jeremy was the lead developer of Pegasus News, one of the first uses of + Django outside World Online, and has since joined Votizen, a startup intent + on reducing the influence of money in politics. + + He serves as DSF Secretary, organizes and helps organize sprints, cares + about the health and equity of the Django community. He has gone an + embarrassingly long time without a working blog. + + Jeremy lives in Mountain View, CA, USA. + Specialists ----------- @@ -403,16 +417,6 @@ Ian Kelly Matt Boersma Matt is also responsible for Django's Oracle support. -Jeremy Dunck - Jeremy is the lead developer of Pegasus News, a personalized local site based - in Dallas, Texas. An early contributor to Greasemonkey and Django, he sees - technology as a tool for communication and access to knowledge. - - Jeremy helped kick off GeoDjango development, and is mostly responsible for - the serious speed improvements that signals received in Django 1.0. - - Jeremy lives in Dallas, Texas, USA. - `Simon Meers`_ Simon discovered Django 0.96 during his Computer Science PhD research and has been developing with it full-time ever since. His core code From 7e5ebcce5365a172d802c76b57771c321430317f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 13 Sep 2012 16:17:32 +0200 Subject: [PATCH 59/75] Fixed #18795 -- Fixed failing GeoDjango tests Proj.4 and SRS strings may slightly vary depending on the installed libraries. Made some tests pass again with recent Proj.4/GDAL lib versions. --- django/contrib/gis/gdal/tests/test_ds.py | 6 +++- django/contrib/gis/gdal/tests/test_geom.py | 6 ++-- django/contrib/gis/geos/tests/test_geos.py | 6 ++-- .../contrib/gis/tests/test_spatialrefsys.py | 32 +++++++------------ 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/django/contrib/gis/gdal/tests/test_ds.py b/django/contrib/gis/gdal/tests/test_ds.py index 71d22a0a27..22394a2888 100644 --- a/django/contrib/gis/gdal/tests/test_ds.py +++ b/django/contrib/gis/gdal/tests/test_ds.py @@ -181,7 +181,11 @@ class DataSourceTest(unittest.TestCase): # Making sure the SpatialReference is as expected. if hasattr(source, 'srs_wkt'): - self.assertEqual(source.srs_wkt, g.srs.wkt) + self.assertEqual( + source.srs_wkt, + # Depending on lib versions, WGS_84 might be WGS_1984 + g.srs.wkt.replace('SPHEROID["WGS_84"', 'SPHEROID["WGS_1984"') + ) def test06_spatial_filter(self): "Testing the Layer.spatial_filter property." diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py index a0b2593605..dda22036e3 100644 --- a/django/contrib/gis/gdal/tests/test_geom.py +++ b/django/contrib/gis/gdal/tests/test_geom.py @@ -1,3 +1,4 @@ +import json from binascii import b2a_hex try: from django.utils.six.moves import cPickle as pickle @@ -111,8 +112,9 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): for g in self.geometries.json_geoms: geom = OGRGeometry(g.wkt) if not hasattr(g, 'not_equal'): - self.assertEqual(g.json, geom.json) - self.assertEqual(g.json, geom.geojson) + # Loading jsons to prevent decimal differences + self.assertEqual(json.loads(g.json), json.loads(geom.json)) + self.assertEqual(json.loads(g.json), json.loads(geom.geojson)) self.assertEqual(OGRGeometry(g.wkt), OGRGeometry(geom.json)) def test02_points(self): diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index d621c6b4d4..7300ab9c63 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -1,4 +1,5 @@ import ctypes +import json import random from django.contrib.gis.geos import (GEOSException, GEOSIndexError, GEOSGeometry, @@ -204,8 +205,9 @@ class GEOSTest(unittest.TestCase, TestDataMixin): for g in self.geometries.json_geoms: geom = GEOSGeometry(g.wkt) if not hasattr(g, 'not_equal'): - self.assertEqual(g.json, geom.json) - self.assertEqual(g.json, geom.geojson) + # Loading jsons to prevent decimal differences + self.assertEqual(json.loads(g.json), json.loads(geom.json)) + self.assertEqual(json.loads(g.json), json.loads(geom.geojson)) self.assertEqual(GEOSGeometry(g.wkt), GEOSGeometry(geom.json)) def test_fromfile(self): diff --git a/django/contrib/gis/tests/test_spatialrefsys.py b/django/contrib/gis/tests/test_spatialrefsys.py index 5cdc68a74d..7f7a0111f1 100644 --- a/django/contrib/gis/tests/test_spatialrefsys.py +++ b/django/contrib/gis/tests/test_spatialrefsys.py @@ -8,9 +8,11 @@ from django.utils import unittest test_srs = ({'srid' : 4326, 'auth_name' : ('EPSG', True), 'auth_srid' : 4326, - 'srtext' : 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]', - 'srtext14' : 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]', - 'proj4' : '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs ', + # Only the beginning, because there are differences depending on installed libs + 'srtext' : 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84"', + 'proj4' : ['+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs ', + # +ellps=WGS84 has been removed in the 4326 proj string in proj-4.8 + '+proj=longlat +datum=WGS84 +no_defs '], 'spheroid' : 'WGS 84', 'name' : 'WGS 84', 'geographic' : True, 'projected' : False, 'spatialite' : True, 'ellipsoid' : (6378137.0, 6356752.3, 298.257223563), # From proj's "cs2cs -le" and Wikipedia (semi-minor only) @@ -19,9 +21,9 @@ test_srs = ({'srid' : 4326, {'srid' : 32140, 'auth_name' : ('EPSG', False), 'auth_srid' : 32140, - 'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","32140"]]', - 'srtext14': 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],AUTHORITY["EPSG","32140"],AXIS["X",EAST],AXIS["Y",NORTH]]', - 'proj4' : '+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +datum=NAD83 +units=m +no_defs ', + 'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980"', + 'proj4' : ['+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +datum=NAD83 +units=m +no_defs ', + '+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '], 'spheroid' : 'GRS 1980', 'name' : 'NAD83 / Texas South Central', 'geographic' : False, 'projected' : True, 'spatialite' : False, 'ellipsoid' : (6378137.0, 6356752.31414, 298.257222101), # From proj's "cs2cs -le" and Wikipedia (semi-minor only) @@ -51,17 +53,12 @@ class SpatialRefSysTest(unittest.TestCase): # No proj.4 and different srtext on oracle backends :( if postgis: - if connection.ops.spatial_version >= (1, 4, 0): - srtext = sd['srtext14'] - else: - srtext = sd['srtext'] - self.assertEqual(srtext, srs.wkt) - self.assertEqual(sd['proj4'], srs.proj4text) + self.assertTrue(srs.wkt.startswith(sd['srtext'])) + self.assertTrue(srs.proj4text in sd['proj4']) @no_mysql def test02_osr(self): "Testing getting OSR objects from SpatialRefSys model objects." - from django.contrib.gis.gdal import GDAL_VERSION for sd in test_srs: sr = SpatialRefSys.objects.get(srid=sd['srid']) self.assertEqual(True, sr.spheroid.startswith(sd['spheroid'])) @@ -76,15 +73,10 @@ class SpatialRefSysTest(unittest.TestCase): # Testing the SpatialReference object directly. if postgis or spatialite: srs = sr.srs - if GDAL_VERSION <= (1, 8): - self.assertEqual(sd['proj4'], srs.proj4) + self.assertTrue(srs.proj4 in sd['proj4']) # No `srtext` field in the `spatial_ref_sys` table in SpatiaLite if not spatialite: - if connection.ops.spatial_version >= (1, 4, 0): - srtext = sd['srtext14'] - else: - srtext = sd['srtext'] - self.assertEqual(srtext, srs.wkt) + self.assertTrue(srs.wkt.startswith(sd['srtext'])) @no_mysql def test03_ellipsoid(self): From 690170a8b9bbcea1ae51e4020aea49987fefdbd2 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 13 Sep 2012 20:17:52 +0200 Subject: [PATCH 60/75] Removed unused quoting/encoding in gis db backend --- django/contrib/gis/db/backends/base.py | 2 -- django/contrib/gis/db/backends/util.py | 14 -------------- 2 files changed, 16 deletions(-) diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py index f7af420a8d..2b8924d92e 100644 --- a/django/contrib/gis/db/backends/base.py +++ b/django/contrib/gis/db/backends/base.py @@ -90,8 +90,6 @@ class BaseSpatialOperations(object): # For quoting column values, rather than columns. def geo_quote_name(self, name): - if isinstance(name, six.text_type): - name = name.encode('ascii') return "'%s'" % name # GeometryField operations diff --git a/django/contrib/gis/db/backends/util.py b/django/contrib/gis/db/backends/util.py index 648fcfe963..2fc9123d26 100644 --- a/django/contrib/gis/db/backends/util.py +++ b/django/contrib/gis/db/backends/util.py @@ -3,20 +3,6 @@ A collection of utility routines and classes used by the spatial backends. """ -from django.utils import six - -def gqn(val): - """ - The geographic quote name function; used for quoting tables and - geometries (they use single rather than the double quotes of the - backend quotename function). - """ - if isinstance(val, six.string_types): - if isinstance(val, six.text_type): val = val.encode('ascii') - return "'%s'" % val - else: - return str(val) - class SpatialOperation(object): """ Base class for generating spatial SQL. From a01bce1e26ebd6f67809455282a26789d09cc1df Mon Sep 17 00:00:00 2001 From: Bryan Veloso Date: Fri, 14 Sep 2012 19:24:39 -0700 Subject: [PATCH 61/75] Adding myself to the committers list. --- AUTHORS | 1 + docs/internals/committers.txt | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/AUTHORS b/AUTHORS index af84cbff0b..6a7f22ada4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -32,6 +32,7 @@ The PRIMARY AUTHORS are (and/or have been): * Anssi Kääriäinen * Florian Apolloner * Jeremy Dunck + * Bryan Veloso More information on the main contributors to Django can be found in docs/internals/committers.txt. diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 63c8f5d618..f12ced2c07 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -393,6 +393,20 @@ Jeremy Dunck Jeremy lives in Mountain View, CA, USA. +`Bryan Veloso`_ + Bryan found Django 0.96 through a fellow designer who was evangelizing + its use. It was his for foray outside of the land that was PHP-based + templating. Although he has only ever used Django for personal projects, + it is the very reason he considers himself a designer/developer + hybrid and is working to further design within the Django community. + + Bryan works as a designer at GitHub by day, and masquerades as a `vlogger`_ + and `shoutcaster`_ in the after-hours. Bryan lives in Los Angeles, CA, USA. + +.. _bryan veloso: http://avalonstar.com/ +.. _vlogger: http://youtube.com/bryanveloso/ +.. _shoutcaster: http://twitch.tv/vlogalonstar/ + Specialists ----------- From abfba3bb65c9a28ca3756ec5d091e2e8561fc450 Mon Sep 17 00:00:00 2001 From: Bryan Veloso Date: Fri, 14 Sep 2012 19:26:11 -0700 Subject: [PATCH 62/75] Correcting my English. --- docs/internals/committers.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index f12ced2c07..ca56d36880 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -395,7 +395,7 @@ Jeremy Dunck `Bryan Veloso`_ Bryan found Django 0.96 through a fellow designer who was evangelizing - its use. It was his for foray outside of the land that was PHP-based + its use. It was his first foray outside of the land that was PHP-based templating. Although he has only ever used Django for personal projects, it is the very reason he considers himself a designer/developer hybrid and is working to further design within the Django community. From 65793d714ca6b03d2cd0fcfeec54652396f7ab48 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 15 Sep 2012 11:56:39 +0200 Subject: [PATCH 63/75] Used ST_AsText for testing PostGIS raw query AsText will not be supported in further versions of PostGIS (>=2). --- django/contrib/gis/tests/geoapp/tests.py | 3 ++- docs/ref/contrib/gis/tutorial.txt | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index fbe30e8841..cd3cec3074 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -191,7 +191,8 @@ class GeoModelTest(TestCase): cities1 = City.objects.all() # Only PostGIS would support a 'select *' query because of its recognized # HEXEWKB format for geometry fields - cities2 = City.objects.raw('select id, name, asText(point) from geoapp_city') + as_text = 'ST_AsText' if postgis else 'asText' + cities2 = City.objects.raw('select id, name, %s(point) from geoapp_city' % as_text) self.assertEqual(len(cities1), len(list(cities2))) self.assertTrue(isinstance(cities2[0].point, Point)) diff --git a/docs/ref/contrib/gis/tutorial.txt b/docs/ref/contrib/gis/tutorial.txt index 15863aee7b..ec265342b3 100644 --- a/docs/ref/contrib/gis/tutorial.txt +++ b/docs/ref/contrib/gis/tutorial.txt @@ -674,8 +674,8 @@ __ http://spatialreference.org/ref/epsg/32140/ .. admonition:: Raw queries When using :doc:`raw queries `, you should generally wrap - your geometry fields with the ``asText()`` SQL function so as the field - value will be recognized by GEOS:: + your geometry fields with the ``asText()`` SQL function (or ``ST_AsText`` + for PostGIS) so as the field value will be recognized by GEOS:: City.objects.raw('SELECT id, name, asText(point) from myapp_city') From a73838fde33374573b8e765dfcb0225287d396c0 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 12 Sep 2012 06:59:19 -0400 Subject: [PATCH 64/75] Fixed #11185 - Expanded docs on customizing widgets; thanks fadeev for the draft patch. --- docs/ref/forms/fields.txt | 41 +++++-- docs/ref/forms/widgets.txt | 227 +++++++++++++++++++++++++++--------- docs/topics/forms/media.txt | 24 ++-- 3 files changed, 214 insertions(+), 78 deletions(-) diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 2a8f449799..7c06bf97ee 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -852,7 +852,7 @@ Slightly complex built-in ``Field`` classes ``MultiValueField`` ~~~~~~~~~~~~~~~~~~~ -.. class:: MultiValueField(**kwargs) +.. class:: MultiValueField(fields=(), **kwargs) * Default widget: ``TextInput`` * Empty value: ``''`` (an empty string) @@ -861,22 +861,39 @@ Slightly complex built-in ``Field`` classes as an argument to the ``MultiValueField``. * Error message keys: ``required``, ``invalid`` - This abstract field (must be subclassed) aggregates the logic of multiple - fields. Subclasses should not have to implement clean(). Instead, they must - implement compress(), which takes a list of valid values and returns a - "compressed" version of those values -- a single value. For example, - :class:`SplitDateTimeField` is a subclass which combines a time field and - a date field into a datetime object. + Aggregates the logic of multiple fields that together produce a single + value. + + This field is abstract and must be subclassed. In contrast with the + single-value fields, subclasses of :class:`MultiValueField` must not + implement :meth:`~django.forms.Field.clean` but instead - implement + :meth:`~MultiValueField.compress`. Takes one extra required argument: .. attribute:: fields - A list of fields which are cleaned into a single field. Each value in - ``clean`` is cleaned by the corresponding field in ``fields`` -- the first - value is cleaned by the first field, the second value is cleaned by - the second field, etc. Once all fields are cleaned, the list of clean - values is "compressed" into a single value. + A tuple of fields whose values are cleaned and subsequently combined + into a single value. Each value of the field is cleaned by the + corresponding field in ``fields`` -- the first value is cleaned by the + first field, the second value is cleaned by the second field, etc. + Once all fields are cleaned, the list of clean values is combined into + a single value by :meth:`~MultiValueField.compress`. + + .. attribute:: MultiValueField.widget + + Must be a subclass of :class:`django.forms.MultiWidget`. + Default value is :class:`~django.forms.widgets.TextInput`, which + probably is not very useful in this case. + + .. method:: compress(data_list) + + Takes a list of valid values and returns a "compressed" version of + those values -- in a single value. For example, + :class:`SplitDateTimeField` is a subclass which combines a time field + and a date field into a ``datetime`` object. + + This method must be implemented in the subclasses. ``SplitDateTimeField`` ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index 1935cb23bc..4724cbdec2 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -11,6 +11,16 @@ A widget is Django's representation of a HTML input element. The widget handles the rendering of the HTML, and the extraction of data from a GET/POST dictionary that corresponds to the widget. +.. tip:: + + Widgets should not be confused with the :doc:`form fields `. + Form fields deal with the logic of input validation and are used directly + in templates. Widgets deal with rendering of HTML form input elements on + the web page and extraction of raw submitted data. However, widgets do + need to be :ref:`assigned ` to form fields. + +.. _widget-to-field: + Specifying widgets ------------------ @@ -95,15 +105,23 @@ choices are inherent to the model and not just the representational widget. Customizing widget instances ---------------------------- -When Django renders a widget as HTML, it only renders the bare minimum -HTML - Django doesn't add a class definition, or any other widget-specific -attributes. This means that all :class:`TextInput` widgets will appear the same -on your Web page. +When Django renders a widget as HTML, it only renders very minimal markup - +Django doesn't add class names, or any other widget-specific attributes. This +means, for example, that all :class:`TextInput` widgets will appear the same +on your Web pages. -If you want to make one widget look different to another, you need to -specify additional attributes for each widget. When you specify a -widget, you can provide a list of attributes that will be added to the -rendered HTML for the widget. +There are two ways to customize widgets: :ref:`per widget instance +` and :ref:`per widget class `. + +.. _styling-widget-instances: + +Styling widget instances +^^^^^^^^^^^^^^^^^^^^^^^^ + +If you want to make one widget instance look different from another, you will +need to specify additional attributes at the time when the widget object is +instantiated and assigned to a form field (and perhaps add some rules to your +CSS files). For example, take the following simple form:: @@ -128,9 +146,7 @@ On a real Web page, you probably don't want every widget to look the same. You might want a larger input element for the comment, and you might want the 'name' widget to have some special CSS class. It is also possible to specify the 'type' attribute to take advantage of the new HTML5 input types. To do -this, you use the :attr:`Widget.attrs` argument when creating the widget: - -For example:: +this, you use the :attr:`Widget.attrs` argument when creating the widget:: class CommentForm(forms.Form): name = forms.CharField( @@ -147,24 +163,41 @@ Django will then include the extra attributes in the rendered output: Url: Comment: -.. _built-in widgets: +.. _styling-widget-classes: -Built-in widgets ----------------- +Styling widget classes +^^^^^^^^^^^^^^^^^^^^^^ -Django provides a representation of all the basic HTML widgets, plus some -commonly used groups of widgets: +With widgets, it is possible to add media (``css`` and ``javascript``) +and more deeply customize their appearance and behavior. -``Widget`` -~~~~~~~~~~ +In a nutshell, you will need to subclass the widget and either +:ref:`define a class "Media" ` as a member of the +subclass, or :ref:`create a property "media" `, returning an +instance of that class. -.. class:: Widget +These methods involve somewhat advanced Python programming and are described in +detail in the :doc:`Form Media ` topic guide. - This abstract class cannot be rendered, but provides the basic attribute :attr:`~Widget.attrs`. +.. _base-widget-classes: + +Base Widget classes +------------------- + +Base widget classes :class:`Widget` and :class:`MultiWidget` are subclassed by +all the :ref:`built-in widgets ` and may serve as a +foundation for custom widgets. + +.. class:: Widget(attrs=None) + + This abstract class cannot be rendered, but provides the basic attribute + :attr:`~Widget.attrs`. You may also implement or override the + :meth:`~Widget.render()` method on custom widgets. .. attribute:: Widget.attrs - A dictionary containing HTML attributes to be set on the rendered widget. + A dictionary containing HTML attributes to be set on the rendered + widget. .. code-block:: python @@ -172,6 +205,74 @@ commonly used groups of widgets: >>> name.render('name', 'A name') u'' + .. method:: render(name, value, attrs=None) + + Returns HTML for the widget, as a Unicode string. This method must be + implemented by the subclass, otherwise ``NotImplementedError`` will be + raised. + + The 'value' given is not guaranteed to be valid input, therefore + subclass implementations should program defensively. + +.. class:: MultiWidget(widgets, attrs=None) + + A widget that is composed of multiple widgets. + :class:`~django.forms.widgets.MultiWidget` works hand in hand with the + :class:`~django.forms.MultiValueField`. + + .. method:: render(name, value, attrs=None) + + Argument `value` is handled differently in this method from the + subclasses of :class:`~Widget`. + + If `value` is a list, output of :meth:`~MultiWidget.render` will be a + concatenation of rendered child widgets. If `value` is not a list, it + will be first processed by the method :meth:`~MultiWidget.decompress()` + to create the list and then processed as above. + + Unlike in the single value widgets, method :meth:`~MultiWidget.render` + need not be implemented in the subclasses. + + .. method:: decompress(value) + + Returns a list of "decompressed" values for the given value of the + multi-value field that makes use of the widget. The input value can be + assumed as valid, but not necessarily non-empty. + + This method **must be implemented** by the subclass, and since the + value may be empty, the implementation must be defensive. + + The rationale behind "decompression" is that it is necessary to "split" + the combined value of the form field into the values of the individual + field encapsulated within the multi-value field (e.g. when displaying + the partially or fully filled-out form). + + .. tip:: + + Note that :class:`~django.forms.MultiValueField` has a + complementary method :meth:`~django.forms.MultiValueField.compress` + with the opposite responsibility - to combine cleaned values of + all member fields into one. + + +.. _built-in widgets: + +Built-in widgets +---------------- + +Django provides a representation of all the basic HTML widgets, plus some +commonly used groups of widgets in the ``django.forms.widgets`` module, +including :ref:`the input of text `, :ref:`various checkboxes +and selectors `, :ref:`uploading files `, +and :ref:`handling of multi-valued input `. + +.. _text-widgets: + +Widgets handling input of text +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +These widgets make use of the HTML elements ``input`` and ``textarea``. + ``TextInput`` ~~~~~~~~~~~~~ @@ -205,39 +306,8 @@ commonly used groups of widgets: Hidden input: ```` -``MultipleHiddenInput`` -~~~~~~~~~~~~~~~~~~~~~~~ - -.. class:: MultipleHiddenInput - - Multiple ```` widgets. - - A widget that handles multiple hidden widgets for fields that have a list - of values. - - .. attribute:: MultipleHiddenInput.choices - - This attribute is optional when the field does not have a - :attr:`~Field.choices` attribute. If it does, it will override anything - you set here when the attribute is updated on the :class:`Field`. - -``FileInput`` -~~~~~~~~~~~~~ - -.. class:: FileInput - - File upload input: ```` - -``ClearableFileInput`` -~~~~~~~~~~~~~~~~~~~~~~ - -.. class:: ClearableFileInput - - .. versionadded:: 1.3 - - File upload input: ````, with an additional checkbox - input to clear the field's value, if the field is not required and has - initial data. + Note that there also is a :class:`MultipleHiddenInput` widget that + encapsulates a set of hidden input elements. ``DateInput`` ~~~~~~~~~~~~~ @@ -297,6 +367,11 @@ commonly used groups of widgets: Text area: ```` +.. _selector-widgets: + +Selector and checkbox widgets +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ``CheckboxInput`` ~~~~~~~~~~~~~~~~~ @@ -440,6 +515,50 @@ commonly used groups of widgets: ... +.. _file-upload-widgets: + +File upload widgets +^^^^^^^^^^^^^^^^^^^ + +``FileInput`` +~~~~~~~~~~~~~ + +.. class:: FileInput + + File upload input: ```` + +``ClearableFileInput`` +~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: ClearableFileInput + + .. versionadded:: 1.3 + + File upload input: ````, with an additional checkbox + input to clear the field's value, if the field is not required and has + initial data. + +.. _composite-widgets: + +Composite widgets +^^^^^^^^^^^^^^^^^ + +``MultipleHiddenInput`` +~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: MultipleHiddenInput + + Multiple ```` widgets. + + A widget that handles multiple hidden widgets for fields that have a list + of values. + + .. attribute:: MultipleHiddenInput.choices + + This attribute is optional when the field does not have a + :attr:`~Field.choices` attribute. If it does, it will override anything + you set here when the attribute is updated on the :class:`Field`. + ``MultiWidget`` ~~~~~~~~~~~~~~~ diff --git a/docs/topics/forms/media.txt b/docs/topics/forms/media.txt index 615dd7193c..29a7829799 100644 --- a/docs/topics/forms/media.txt +++ b/docs/topics/forms/media.txt @@ -38,6 +38,8 @@ in a form suitable for easy inclusion on your Web page. whichever toolkit suits your requirements. Django is able to integrate with any JavaScript toolkit. +.. _media-as-a-static-definition: + Media as a static definition ---------------------------- @@ -78,10 +80,8 @@ A dictionary describing the CSS files required for various forms of output media. The values in the dictionary should be a tuple/list of file names. See -`the section on media paths`_ for details of how to specify paths to media -files. - -.. _the section on media paths: `Paths in media definitions`_ +:ref:`the section on media paths ` for details of how to +specify paths to media files. The keys in the dictionary are the output media types. These are the same types accepted by CSS files in media declarations: 'all', 'aural', 'braille', @@ -117,8 +117,8 @@ If this last CSS definition were to be rendered, it would become the following H ``js`` ~~~~~~ -A tuple describing the required JavaScript files. See -`the section on media paths`_ for details of how to specify paths to media +A tuple describing the required JavaScript files. See :ref:`the section on +media paths ` for details of how to specify paths to media files. ``extend`` @@ -164,10 +164,10 @@ declaration to the media declaration:: If you require even more control over media inheritance, define your media -using a `dynamic property`_. Dynamic properties give you complete control over -which media files are inherited, and which are not. +using a :ref:`dynamic property `. Dynamic properties give +you complete control over which media files are inherited, and which are not. -.. _dynamic property: `Media as a dynamic property`_ +.. _dynamic-property: Media as a dynamic property --------------------------- @@ -198,9 +198,9 @@ Paths in media definitions .. versionchanged:: 1.3 Paths used to specify media can be either relative or absolute. If a path -starts with '/', 'http://' or 'https://', it will be interpreted as an absolute -path, and left as-is. All other paths will be prepended with the value of -the appropriate prefix. +starts with ``/``, ``http://`` or ``https://``, it will be interpreted as an +absolute path, and left as-is. All other paths will be prepended with the value +of the appropriate prefix. As part of the introduction of the :doc:`staticfiles app ` two new settings were added From e5f8fe27acdfc0ec2e3f3f03e80311d413ba0a75 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 15 Sep 2012 06:41:58 -0400 Subject: [PATCH 65/75] Removed reference to note removed in a78dd109e6c81c49e90e36e9b793bad67c46c23c; refs #15552 --- docs/ref/settings.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index f443138569..16d067172d 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1339,9 +1339,6 @@ Default: ``'/accounts/logout/'`` LOGIN_URL counterpart. -.. note:: - See the `note on LOGIN_REDIRECT_URL setting`_ - .. setting:: MANAGERS MANAGERS From 553583958d907de09c2b87788ec7454c1bde7d1c Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 15 Sep 2012 07:16:07 -0400 Subject: [PATCH 66/75] Added an example of using a form wizard with different templates; thanks Lorin Hochstein for the patch. --- docs/ref/contrib/formtools/form-wizard.txt | 62 +++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index b8e585a4d2..d5231de3e5 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -155,7 +155,8 @@ or the :meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` method, which are documented in the :class:`~django.views.generic.base.TemplateResponseMixin` documentation. The -latter one allows you to use a different template for each form. +latter one allows you to use a different template for each form (:ref:`see the +example below `). This template expects a ``wizard`` object that has various items attached to it: @@ -238,6 +239,65 @@ wizard's :meth:`as_view` method takes a list of your (r'^contact/$', ContactWizard.as_view([ContactForm1, ContactForm2])), ) +.. _wizard-template-for-each-form: + +Using a different template for each form +---------------------------------------- + +As mentioned above, you may specify a different template for each form. +Consider an example using a form wizard to implement a multi-step checkout +process for an online store. In the first step, the user specifies a billing +and shipping address. In the second step, the user chooses payment type. If +they chose to pay by credit card, they will enter credit card information in +the next step. In the final step, they will confirm the purchase. + +Here's what the view code might look like:: + + from django.http import HttpResponseRedirect + from django.contrib.formtools.wizard.views import SessionWizardView + + FORMS = [("address", myapp.forms.AddressForm), + ("paytype", myapp.forms.PaymentChoiceForm), + ("cc", myapp.forms.CreditCardForm), + ("confirmation", myapp.forms.OrderForm)] + + TEMPLATES = {"address": "checkout/billingaddress.html", + "paytype": "checkout/paymentmethod.html", + "cc": "checkout/creditcard.html", + "confirmation": "checkout/confirmation.html"} + + def pay_by_credit_card(wizard): + """Return true if user opts to pay by credit card""" + # Get cleaned data from payment step + cleaned_data = wizard.get_cleaned_data_for_step('paytype') or {'method': 'none'} + # Return true if the user selected credit card + return cleaned_data['method'] == 'cc' + + + class OrderWizard(SessionWizardView): + def get_template_names(self): + return [TEMPLATES[self.steps.current]] + + def done(self, form_list, **kwargs): + do_something_with_the_form_data(form_list) + return HttpResponseRedirect('/page-to-redirect-to-when-done/') + ... + +The ``urls.py`` file would contain something like:: + + urlpatterns = patterns('', + (r'^checkout/$', OrderWizard.as_view(FORMS, condition_dict={'cc': pay_by_credit_card})), + ) + +Note that the ``OrderWizard`` object is initialized with a list of pairs. +The first element in the pair is a string that corresponds to the name of the +step and the second is the form class. + +In this example, the +:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` +method returns a list containing a single template, which is selected based on +the name of the current step. + .. _wizardview-advanced-methods: Advanced ``WizardView`` methods From 22242c510f84c53803afe2907649c892cb1b3d9a Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 15 Sep 2012 07:37:33 -0400 Subject: [PATCH 67/75] Fixed #16929 - Documented how to extend UserAdmin with UserProfile fields; thanks charettes for the draft example. --- docs/topics/auth.txt | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index c45e4bbaf7..ef03d5479c 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -650,6 +650,36 @@ the handler, if ``created`` is ``True``, create the associated user profile:: .. seealso:: :doc:`/topics/signals` for more information on Django's signal dispatcher. +Adding UserProfile fields to the admin +-------------------------------------- + +To add the UserProfile fields to the user page in the admin, define an +:class:`~django.contrib.admin.InlineModelAdmin` (for this example, we'll use a +:class:`~django.contrib.admin.StackedInline`) in your app's ``admin.py`` and +add it to a ``UserAdmin`` class which is registered with the +:class:`~django.contrib.auth.models.User` class:: + + from django.contrib import admin + from django.contrib.auth.admin import UserAdmin + from django.contrib.auth.models import User + + from my_user_profile_app.models import UserProfile + + # Define an inline admin descriptor for UserProfile model + # which acts a bit like a singleton + class UserProfileInline(admin.StackedInline): + model = UserProfile + can_delete = False + verbose_name_plural = 'profile' + + # Define a new User admin + class UserAdmin(UserAdmin): + inlines = (UserProfileInline, ) + + # Re-register UserAdmin + admin.site.unregister(User) + admin.site.register(User, UserAdmin) + Authentication in Web requests ============================== @@ -948,7 +978,7 @@ The login_required decorator (r'^accounts/login/$', 'django.contrib.auth.views.login'), .. versionchanged:: 1.5 - + As of version 1.5 :setting:`settings.LOGIN_URL ` now also accepts view function names and :ref:`named URL patterns `. This allows you to freely remap your login view within your URLconf From 93e6733e4cbbdad34f1f0f59303ae01f577e4e58 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 15 Sep 2012 08:15:54 -0400 Subject: [PATCH 68/75] Fixed #18131 - Documented ContentTypeManager.get_for_id; thanks sir_sigurd for the report. --- docs/ref/contrib/contenttypes.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index 0226435159..e98da6e429 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -187,6 +187,14 @@ The ``ContentTypeManager`` probably won't ever need to call this method yourself; Django will call it automatically when it's needed. + .. method:: get_for_id(id) + + Lookup a :class:`~django.contrib.contenttypes.models.ContentType` by ID. + Since this method uses the same shared cache as + :meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_model`, + it's preferred to use this method over the usual + ``ContentType.objects.get(pk=id)`` + .. method:: get_for_model(model[, for_concrete_model=True]) Takes either a model class or an instance of a model, and returns the From b1fb6f48201e00c6ea0eae37a839525657cf71e2 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 15 Sep 2012 21:07:31 +0200 Subject: [PATCH 69/75] Modernized views.DebugViewTests --- tests/regressiontests/views/tests/debug.py | 25 +++++++--------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py index 56383ac196..8592b07efe 100644 --- a/tests/regressiontests/views/tests/debug.py +++ b/tests/regressiontests/views/tests/debug.py @@ -1,36 +1,27 @@ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import inspect import os import sys from django.conf import settings -from django.core.files.uploadedfile import SimpleUploadedFile -from django.test import TestCase, RequestFactory -from django.test.utils import (setup_test_template_loader, - restore_template_loaders) -from django.core.urlresolvers import reverse -from django.views.debug import ExceptionReporter from django.core import mail +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.urlresolvers import reverse +from django.test import TestCase, RequestFactory +from django.test.utils import (override_settings, setup_test_template_loader, + restore_template_loaders) +from django.views.debug import ExceptionReporter from .. import BrokenException, except_args from ..views import (sensitive_view, non_sensitive_view, paranoid_view, custom_exception_reporter_filter_view, sensitive_method_view) +@override_settings(DEBUG=True, TEMPLATE_DEBUG=True) class DebugViewTests(TestCase): urls = "regressiontests.views.urls" - def setUp(self): - self.old_debug = settings.DEBUG - settings.DEBUG = True - self.old_template_debug = settings.TEMPLATE_DEBUG - settings.TEMPLATE_DEBUG = True - - def tearDown(self): - settings.DEBUG = self.old_debug - settings.TEMPLATE_DEBUG = self.old_template_debug - def test_files(self): response = self.client.get('/raises/') self.assertEqual(response.status_code, 500) From c7f44ae085df3a270aa998cdedb56f36900cb9ef Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 15 Sep 2012 21:39:08 +0200 Subject: [PATCH 70/75] Fixed #17948 -- Isolated auth tests from custom template loaders Thanks andrey@kostenko.name for the report. --- django/contrib/auth/tests/views.py | 5 +++-- django/test/signals.py | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index 3c847f456a..e3402b13b9 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -1,7 +1,7 @@ import os import re -from django.conf import settings +from django.conf import global_settings, settings from django.contrib.sites.models import Site, RequestSite from django.contrib.auth.models import User from django.core import mail @@ -23,7 +23,8 @@ from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm, ('en', 'English'), ), LANGUAGE_CODE='en', - TEMPLATE_DIRS = ( + TEMPLATE_LOADERS=global_settings.TEMPLATE_LOADERS, + TEMPLATE_DIRS=( os.path.join(os.path.dirname(__file__), 'templates'), ), USE_TZ=False, diff --git a/django/test/signals.py b/django/test/signals.py index 5b0a9a19ca..d140304f1d 100644 --- a/django/test/signals.py +++ b/django/test/signals.py @@ -51,6 +51,13 @@ def clear_context_processors_cache(**kwargs): context._standard_context_processors = None +@receiver(setting_changed) +def clear_template_loaders_cache(**kwargs): + if kwargs['setting'] == 'TEMPLATE_LOADERS': + from django.template import loader + loader.template_source_loaders = None + + @receiver(setting_changed) def clear_serializers_cache(**kwargs): if kwargs['setting'] == 'SERIALIZATION_MODULES': From 17149496a001b0ec8af382ef5a65499c0a651f43 Mon Sep 17 00:00:00 2001 From: Camilo Nova Date: Sat, 15 Sep 2012 15:45:13 -0500 Subject: [PATCH 71/75] Fixes docs typo --- docs/topics/logging.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index b54a9475ae..28baf87522 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -255,12 +255,12 @@ complex logging setup, configured using :meth:`logging.dictConfig`:: }, 'handlers': { 'null': { - 'level':'DEBUG', - 'class':'django.utils.log.NullHandler', + 'level': 'DEBUG', + 'class': 'django.utils.log.NullHandler', }, 'console':{ - 'level':'DEBUG', - 'class':'logging.StreamHandler', + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', 'formatter': 'simple' }, 'mail_admins': { @@ -271,9 +271,9 @@ complex logging setup, configured using :meth:`logging.dictConfig`:: }, 'loggers': { 'django': { - 'handlers':['null'], + 'handlers': ['null'], 'propagate': True, - 'level':'INFO', + 'level': 'INFO', }, 'django.request': { 'handlers': ['mail_admins'], From 8d75b1176f280b8949aee8dc961a2acd0dacffcd Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sat, 15 Sep 2012 14:24:01 -0700 Subject: [PATCH 72/75] Clearer wording for defer docs --- docs/ref/models/querysets.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 80b3158f01..8ec7cfc791 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1050,7 +1050,7 @@ defer In some complex data-modeling situations, your models might contain a lot of fields, some of which could contain a lot of data (for example, text fields), or require expensive processing to convert them to Python objects. If you are -using the results of a queryset in some situation where you know you don't know +using the results of a queryset in some situation where you don't know if you need those particular fields when you initially fetch the data, you can tell Django not to retrieve them from the database. From c555741aa7840d8ee4b3a191550924cca6f27105 Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Sat, 15 Sep 2012 16:20:56 -0700 Subject: [PATCH 73/75] Fixed #18530 -- Fixed a small regression in the admin filters where wrongly formatted dates passed as url parameters caused an unhandled ValidationError. Thanks to david for the report. --- django/contrib/admin/filters.py | 9 ++++++--- tests/regressiontests/admin_views/admin.py | 2 +- tests/regressiontests/admin_views/models.py | 1 + tests/regressiontests/admin_views/tests.py | 4 ++++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index cecae216c0..0a34f807b3 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -8,13 +8,13 @@ certain test -- e.g. being a DateField or ForeignKey. import datetime from django.db import models -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, ValidationError from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy as _ from django.utils import timezone - from django.contrib.admin.util import (get_model_from_relation, reverse_field_path, get_limit_choices_to_from_path, prepare_lookup_value) +from django.contrib.admin.options import IncorrectLookupParameters class ListFilter(object): title = None # Human-readable title to appear in the right sidebar. @@ -129,7 +129,10 @@ class FieldListFilter(ListFilter): return True def queryset(self, request, queryset): - return queryset.filter(**self.used_parameters) + try: + return queryset.filter(**self.used_parameters) + except ValidationError as e: + raise IncorrectLookupParameters(e) @classmethod def register(cls, test, list_filter_class, take_priority=False): diff --git a/tests/regressiontests/admin_views/admin.py b/tests/regressiontests/admin_views/admin.py index fe291ebfb8..a5476e9eb7 100644 --- a/tests/regressiontests/admin_views/admin.py +++ b/tests/regressiontests/admin_views/admin.py @@ -130,7 +130,7 @@ class CustomArticleAdmin(admin.ModelAdmin): class ThingAdmin(admin.ModelAdmin): - list_filter = ('color__warm', 'color__value') + list_filter = ('color__warm', 'color__value', 'pub_date',) class InquisitionAdmin(admin.ModelAdmin): diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 2c935c05a5..2b143004d9 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -128,6 +128,7 @@ class Color2(Color): class Thing(models.Model): title = models.CharField(max_length=20) color = models.ForeignKey(Color, limit_choices_to={'warm': True}) + pub_date = models.DateField(blank=True, null=True) def __str__(self): return self.title diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 9f56daa743..36fea59f2e 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -436,6 +436,10 @@ class AdminViewBasicTest(TestCase): response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'}) self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit) + # Regression test for #18530 + response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'pub_date__gte': 'foo'}) + self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit) + def testIsNullLookups(self): """Ensure is_null is handled correctly.""" Article.objects.create(title="I Could Go Anywhere", content="Versatile", date=datetime.datetime.now()) From f399a804c9436a5d3ef9e2b73c337904af2c753d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sat, 15 Sep 2012 21:11:14 +0300 Subject: [PATCH 74/75] Fixed #17485 regression -- only + select_related interaction When doing deeper than one level select_related() + only queries(), the code introduced in b6c356b7bb97f3d6d4831b31e67868313bbbc090 errored incorrectly. Thanks to mrmachine for report & test case. --- django/db/models/sql/compiler.py | 6 +++++- tests/regressiontests/defer_regress/models.py | 3 +++ tests/regressiontests/defer_regress/tests.py | 16 +++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 28d2404858..f06d6b11a4 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -609,8 +609,12 @@ class SQLCompiler(object): restricted = False for f, model in opts.get_fields_with_model(): + # The get_fields_with_model() returns None for fields that live + # in the field's local model. So, for those fields we want to use + # the f.model - that is the field's local model. + field_model = model or f.model if not select_related_descend(f, restricted, requested, - only_load.get(model or self.query.model)): + only_load.get(field_model)): continue # The "avoid" set is aliases we want to avoid just for this # particular branch of the recursion. They aren't permanently diff --git a/tests/regressiontests/defer_regress/models.py b/tests/regressiontests/defer_regress/models.py index d02adccde9..444649ac92 100644 --- a/tests/regressiontests/defer_regress/models.py +++ b/tests/regressiontests/defer_regress/models.py @@ -52,6 +52,9 @@ class SimpleItem(models.Model): class Feature(models.Model): item = models.ForeignKey(SimpleItem) +class SpecialFeature(models.Model): + feature = models.ForeignKey(Feature) + class ItemAndSimpleItem(models.Model): item = models.ForeignKey(Item) simple = models.ForeignKey(SimpleItem) diff --git a/tests/regressiontests/defer_regress/tests.py b/tests/regressiontests/defer_regress/tests.py index 53bb59f5b3..c77ca32135 100644 --- a/tests/regressiontests/defer_regress/tests.py +++ b/tests/regressiontests/defer_regress/tests.py @@ -9,7 +9,7 @@ from django.db.models.loading import cache from django.test import TestCase from .models import (ResolveThis, Item, RelatedItem, Child, Leaf, Proxy, - SimpleItem, Feature, ItemAndSimpleItem) + SimpleItem, Feature, ItemAndSimpleItem, SpecialFeature) class DeferRegressionTest(TestCase): @@ -115,6 +115,7 @@ class DeferRegressionTest(TestCase): RelatedItem, ResolveThis, SimpleItem, + SpecialFeature, ] ) @@ -152,6 +153,7 @@ class DeferRegressionTest(TestCase): "RelatedItem_Deferred_item_id", "ResolveThis", "SimpleItem", + "SpecialFeature", ] ) @@ -197,6 +199,18 @@ class DeferRegressionTest(TestCase): self.assertEqual(obj.item, item2) self.assertEqual(obj.item_id, item2.id) + def test_only_with_select_related(self): + # Test for #17485. + item = SimpleItem.objects.create(name='first', value=47) + feature = Feature.objects.create(item=item) + SpecialFeature.objects.create(feature=feature) + + qs = Feature.objects.only('item__name').select_related('item') + self.assertEqual(len(qs), 1) + + qs = SpecialFeature.objects.only('feature__item__name').select_related('feature__item') + self.assertEqual(len(qs), 1) + def test_deferred_class_factory(self): from django.db.models.query_utils import deferred_class_factory new_class = deferred_class_factory(Item, From d21f3d9b171a3cbff4c8ce7a9dbb8b8be3f21bac Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Mon, 17 Sep 2012 19:52:22 +0200 Subject: [PATCH 75/75] Only update `last_login` instead of the whole user object in `update_last_login`. --- django/contrib/auth/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 1c21917a8c..98eb44ea05 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -25,7 +25,7 @@ def update_last_login(sender, user, **kwargs): the user logging in. """ user.last_login = timezone.now() - user.save() + user.save(update_fields=['last_login']) user_logged_in.connect(update_last_login)