From b16f8b5fbe4d912daa48b0209dea871b854d8376 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Mon, 9 Jul 2012 08:58:24 +0100 Subject: [PATCH 001/265] Add example of AJAX form submission. Credit goes to @SystemParadox. Originally developed at #DjangoCon Europe but wasn't tested enough to merge in. For history, please see https://github.com/pydanny/django/pull/4 --- .../class-based-views/generic-editing.txt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/topics/class-based-views/generic-editing.txt b/docs/topics/class-based-views/generic-editing.txt index 23d346a32a..7bae3c692d 100644 --- a/docs/topics/class-based-views/generic-editing.txt +++ b/docs/topics/class-based-views/generic-editing.txt @@ -203,3 +203,43 @@ Note that you'll need to :ref:`decorate this view` using :func:`~django.contrib.auth.decorators.login_required`, or alternatively handle unauthorised users in the :meth:`form_valid()`. + +AJAX example +------------ + +Here is a simple example showing how you might go about implementing a form that +works for AJAX requests as well as 'normal' form POSTs:: + + import json + + from django.http import HttpResponse + from django.views.generic.edit import CreateView + from django.views.generic.detail import SingleObjectTemplateResponseMixin + + class AjaxableResponseMixin(object): + """ + Mixin to add AJAX support to a form. + Must be used with an object-based FormView (e.g. CreateView) + """ + def render_to_json_response(self, context, **response_kwargs): + data = json.dumps(context) + response_kwargs['content_type'] = 'application/json' + return HttpResponse(data, **response_kwargs) + + def form_invalid(self, form): + if self.request.is_ajax(): + return self.render_to_json_response(form.errors, status=400) + else: + return super(AjaxableResponseMixin, self).form_invalid(form) + + def form_valid(self, form): + if self.request.is_ajax(): + data = { + 'pk': form.instance.pk, + } + return self.render_to_json_response(data) + else: + return super(AjaxableResponseMixin, self).form_valid(form) + + class AuthorCreate(AjaxableResponseMixin, CreateView): + model = Author From 06f79354d009b9fe10149bf597c1570569e7d348 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Mon, 17 Sep 2012 21:50:13 +0200 Subject: [PATCH 002/265] Added tests for d21f3d9b171a3cbff4c8ce7a9dbb8b8be3f21bac. --- django/contrib/auth/tests/signals.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/django/contrib/auth/tests/signals.py b/django/contrib/auth/tests/signals.py index 51f14d35f0..e570280ada 100644 --- a/django/contrib/auth/tests/signals.py +++ b/django/contrib/auth/tests/signals.py @@ -1,6 +1,8 @@ -from django.test import TestCase -from django.test.utils import override_settings from django.contrib.auth import signals +from django.contrib.auth.models import User +from django.test import TestCase +from django.test.client import RequestFactory +from django.test.utils import override_settings @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -47,3 +49,16 @@ class SignalTestCase(TestCase): self.client.get('/logout/next_page/') self.assertEqual(len(self.logged_out), 1) self.assertEqual(self.logged_out[0].username, 'testclient') + + def test_update_last_login(self): + """Ensure that only `last_login` is updated in `update_last_login`""" + user = User.objects.get(pk=3) + old_last_login = user.last_login + + user.username = "This username shouldn't get saved" + request = RequestFactory().get('/login') + signals.user_logged_in.send(sender=user.__class__, request=request, + user=user) + user = User.objects.get(pk=3) + self.assertEqual(user.username, 'staff') + self.assertNotEqual(user.last_login, old_last_login) From e8c6aff3bf31b775dd70581b1cf6f86d5abd4001 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Wed, 5 Sep 2012 18:05:28 +0300 Subject: [PATCH 003/265] Fixed #18947 -- Don't make uploaded files executeable by default. Thanks to Lauri Tirkkonen for the patch. --- django/core/files/storage.py | 5 ++++- docs/releases/1.5.txt | 5 +++++ tests/regressiontests/file_storage/tests.py | 17 +++++++++++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/django/core/files/storage.py b/django/core/files/storage.py index 0b300cd31e..650373f0c3 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -192,7 +192,10 @@ class FileSystemStorage(Storage): else: # This fun binary flag incantation makes os.open throw an # OSError if the file already exists before we open it. - fd = os.open(full_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_BINARY', 0)) + flags = (os.O_WRONLY | os.O_CREAT | os.O_EXCL | + getattr(os, 'O_BINARY', 0)) + # The current umask value is masked out by os.open! + fd = os.open(full_path, flags, 0o666) try: locks.lock(fd, locks.LOCK_EX) _file = None diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 6420239f47..26b6ad1bfa 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -333,6 +333,11 @@ Miscellaneous function at :func:`django.utils.text.slugify`. Similarly, ``remove_tags`` is available at :func:`django.utils.html.remove_tags`. +* Uploaded files are no longer created as executable by default. If you need + them to be executeable change :setting:`FILE_UPLOAD_PERMISSIONS` to your + needs. The new default value is `0666` (octal) and the current umask value + is first masked out. + Features deprecated in 1.5 ========================== diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py index 281041e651..6b57ad6160 100644 --- a/tests/regressiontests/file_storage/tests.py +++ b/tests/regressiontests/file_storage/tests.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, unicode_literals import errno import os import shutil +import sys import tempfile import time from datetime import datetime, timedelta @@ -23,6 +24,7 @@ from django.core.files.uploadedfile import UploadedFile from django.test import SimpleTestCase from django.utils import six from django.utils import unittest +from django.test.utils import override_settings from ..servers.tests import LiveServerBase # Try to import PIL in either of the two ways it can end up installed. @@ -433,22 +435,29 @@ class FileSaveRaceConditionTest(unittest.TestCase): self.storage.delete('conflict') self.storage.delete('conflict_1') +@unittest.skipIf(sys.platform.startswith('win'), "Windows only partially supports umasks and chmod.") class FileStoragePermissions(unittest.TestCase): def setUp(self): - self.old_perms = settings.FILE_UPLOAD_PERMISSIONS - settings.FILE_UPLOAD_PERMISSIONS = 0o666 + self.umask = 0o027 + self.old_umask = os.umask(self.umask) self.storage_dir = tempfile.mkdtemp() self.storage = FileSystemStorage(self.storage_dir) def tearDown(self): - settings.FILE_UPLOAD_PERMISSIONS = self.old_perms shutil.rmtree(self.storage_dir) + os.umask(self.old_umask) + @override_settings(FILE_UPLOAD_PERMISSIONS=0o654) def test_file_upload_permissions(self): name = self.storage.save("the_file", ContentFile("data")) actual_mode = os.stat(self.storage.path(name))[0] & 0o777 - self.assertEqual(actual_mode, 0o666) + self.assertEqual(actual_mode, 0o654) + @override_settings(FILE_UPLOAD_PERMISSIONS=None) + def test_file_upload_default_permissions(self): + fname = self.storage.save("some_file", ContentFile("data")) + mode = os.stat(self.storage.path(fname))[0] & 0o777 + self.assertEqual(mode, 0o666 & ~self.umask) class FileStoragePathParsing(unittest.TestCase): def setUp(self): From b1b32b2074a1aa906ba02fe221bfabef25617026 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Mon, 17 Sep 2012 22:02:16 -0700 Subject: [PATCH 004/265] Added myself as a committer. --- AUTHORS | 1 + docs/internals/committers.txt | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/AUTHORS b/AUTHORS index 6a7f22ada4..2904bd0d99 100644 --- a/AUTHORS +++ b/AUTHORS @@ -33,6 +33,7 @@ The PRIMARY AUTHORS are (and/or have been): * Florian Apolloner * Jeremy Dunck * Bryan Veloso + * Preston Holmes 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 ca56d36880..7900dd8cd0 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -407,6 +407,18 @@ Jeremy Dunck .. _vlogger: http://youtube.com/bryanveloso/ .. _shoutcaster: http://twitch.tv/vlogalonstar/ +`Preston Holmes`_ + Preston is a recovering neuroscientist who originally discovered Django as + part of a sweeping move to Python from a grab bag of half a dozen + languages. He was drawn to Django's balance of practical batteries included + philosophy, care and thought in code design, and strong open source + community. In addition to his current job in private progressive education, + Preston contributes some developer time to local non-profits. + + Preston lives with his family and animal menagerie in Santa Barbara, CA, USA. + +.. _Preston Holmes: http://www.ptone.com/ + Specialists ----------- From 70248cce0a8a3144df9f36e15fbeb5cc1b091c3e Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Mon, 17 Sep 2012 21:27:07 -0700 Subject: [PATCH 005/265] Fixed #18971 -- added root level CONTRIBUTING doc --- CONTRIBUTING.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 CONTRIBUTING.rst diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000000..3b1734157d --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,16 @@ +====================== +Contributing to Django +====================== + +As an open source project, Django welcomes contributions of many forms. + +Examples of contributions include: + +* Code patches +* Documentation improvements +* Bug reports and patch reviews + +Extensive contribution guidelines are available in the repository at +``docs/internals/contributing/``, or online at: + +https://docs.djangoproject.com/en/dev/internals/contributing/ From b771bcc7b4ce37368d28db307030cf7c4c773ea2 Mon Sep 17 00:00:00 2001 From: Collin Anderson Date: Tue, 18 Sep 2012 10:56:39 -0400 Subject: [PATCH 006/265] document changes for YearArchiveView. --- docs/ref/class-based-views/generic-date-based.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt index 64b269f514..c6af23e421 100644 --- a/docs/ref/class-based-views/generic-date-based.txt +++ b/docs/ref/class-based-views/generic-date-based.txt @@ -87,16 +87,24 @@ YearArchiveView * ``year``: A :class:`~datetime.date` object representing the given year. + .. versionchanged:: 1.5 + + Previously, this returned a string. + * ``next_year``: A :class:`~datetime.date` object representing the first day of the next year, according to :attr:`~BaseDateListView.allow_empty` and :attr:`~DateMixin.allow_future`. + .. versionadded:: 1.5 + * ``previous_year``: A :class:`~datetime.date` object representing the first day of the previous year, according to :attr:`~BaseDateListView.allow_empty` and :attr:`~DateMixin.allow_future`. + .. versionadded:: 1.5 + **Notes** * Uses a default ``template_name_suffix`` of ``_archive_year``. From 319e1355190d414ee1b095b26fbb494cf9a27578 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Sat, 8 Sep 2012 12:29:29 -0400 Subject: [PATCH 007/265] Fixed #18800 -- Support numbers bigger than max float in `numberformat`. Thanks to jbvsmo for the patch and Brad Pitcher for the tests. --- django/utils/numberformat.py | 7 +-- tests/regressiontests/utils/numberformat.py | 47 +++++++++++++++++++++ tests/regressiontests/utils/tests.py | 1 + 3 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 tests/regressiontests/utils/numberformat.py diff --git a/django/utils/numberformat.py b/django/utils/numberformat.py index d51b230823..6a31237f13 100644 --- a/django/utils/numberformat.py +++ b/django/utils/numberformat.py @@ -21,12 +21,10 @@ def format(number, decimal_sep, decimal_pos=None, grouping=0, thousand_sep='', if isinstance(number, int) and not use_grouping and not decimal_pos: return mark_safe(six.text_type(number)) # sign - if float(number) < 0: - sign = '-' - else: - sign = '' + sign = '' str_number = six.text_type(number) if str_number[0] == '-': + sign = '-' str_number = str_number[1:] # decimal part if '.' in str_number: @@ -48,4 +46,3 @@ def format(number, decimal_sep, decimal_pos=None, grouping=0, thousand_sep='', int_part_gd += digit int_part = int_part_gd[::-1] return sign + int_part + dec_part - diff --git a/tests/regressiontests/utils/numberformat.py b/tests/regressiontests/utils/numberformat.py new file mode 100644 index 0000000000..f9d9031e48 --- /dev/null +++ b/tests/regressiontests/utils/numberformat.py @@ -0,0 +1,47 @@ +from unittest import TestCase +from django.utils.numberformat import format as nformat +from sys import float_info + + +class TestNumberFormat(TestCase): + + def test_format_number(self): + self.assertEqual(nformat(1234, '.'), '1234') + self.assertEqual(nformat(1234.2, '.'), '1234.2') + self.assertEqual(nformat(1234, '.', decimal_pos=2), '1234.00') + self.assertEqual(nformat(1234, '.', grouping=2, thousand_sep=','), + '1234') + self.assertEqual(nformat(1234, '.', grouping=2, thousand_sep=',', + force_grouping=True), '12,34') + self.assertEqual(nformat(-1234.33, '.', decimal_pos=1), '-1234.3') + + def test_format_string(self): + self.assertEqual(nformat('1234', '.'), '1234') + self.assertEqual(nformat('1234.2', '.'), '1234.2') + self.assertEqual(nformat('1234', '.', decimal_pos=2), '1234.00') + self.assertEqual(nformat('1234', '.', grouping=2, thousand_sep=','), + '1234') + self.assertEqual(nformat('1234', '.', grouping=2, thousand_sep=',', + force_grouping=True), '12,34') + self.assertEqual(nformat('-1234.33', '.', decimal_pos=1), '-1234.3') + + def test_large_number(self): + most_max = ('{0}179769313486231570814527423731704356798070567525844996' + '598917476803157260780028538760589558632766878171540458953' + '514382464234321326889464182768467546703537516986049910576' + '551282076245490090389328944075868508455133942304583236903' + '222948165808559332123348274797826204144723168738177180919' + '29988125040402618412485836{1}') + most_max2 = ('{0}35953862697246314162905484746340871359614113505168999' + '31978349536063145215600570775211791172655337563430809179' + '07028764928468642653778928365536935093407075033972099821' + '15310256415249098018077865788815173701691026788460916647' + '38064458963316171186642466965495956524082894463374763543' + '61838599762500808052368249716736') + int_max = int(float_info.max) + self.assertEqual(nformat(int_max, '.'), most_max.format('', '8')) + self.assertEqual(nformat(int_max + 1, '.'), most_max.format('', '9')) + self.assertEqual(nformat(int_max * 2, '.'), most_max2.format('')) + self.assertEqual(nformat(0 - int_max, '.'), most_max.format('-', '8')) + self.assertEqual(nformat(-1 - int_max, '.'), most_max.format('-', '9')) + self.assertEqual(nformat(-2 * int_max, '.'), most_max2.format('-')) diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index 061c669eb7..47af07561a 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 .numberformat import TestNumberFormat from .os_utils import SafeJoinTests from .regex_helper import NormalizeTests from .simplelazyobject import TestUtilsSimpleLazyObject From 40e62a5ccd08f241e977c9ffcb96005b9f2d95e6 Mon Sep 17 00:00:00 2001 From: Jeremy Dunck Date: Tue, 18 Sep 2012 20:58:40 +0200 Subject: [PATCH 008/265] Fixed #18980 -- Fixed assertContains regression when passed an object --- django/test/testcases.py | 2 ++ tests/regressiontests/test_client_regress/tests.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/django/test/testcases.py b/django/test/testcases.py index 3c681db329..d37be58a71 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -647,6 +647,7 @@ class TransactionTestCase(SimpleTestCase): self.assertEqual(response.status_code, status_code, msg_prefix + "Couldn't retrieve content: Response code was %d" " (expected %d)" % (response.status_code, status_code)) + text = force_text(text, encoding=response._charset) content = response.content.decode(response._charset) if html: content = assert_and_parse_html(self, content, None, @@ -682,6 +683,7 @@ class TransactionTestCase(SimpleTestCase): self.assertEqual(response.status_code, status_code, msg_prefix + "Couldn't retrieve content: Response code was %d" " (expected %d)" % (response.status_code, status_code)) + text = force_text(text, encoding=response._charset) content = response.content.decode(response._charset) if html: content = assert_and_parse_html(self, content, None, diff --git a/tests/regressiontests/test_client_regress/tests.py b/tests/regressiontests/test_client_regress/tests.py index d80293b358..9deb8a4755 100644 --- a/tests/regressiontests/test_client_regress/tests.py +++ b/tests/regressiontests/test_client_regress/tests.py @@ -16,6 +16,7 @@ from django.test import Client, TestCase from django.test.client import encode_file, RequestFactory from django.test.utils import ContextList, override_settings, str_prefix from django.template.response import SimpleTemplateResponse +from django.utils.translation import ugettext_lazy from django.http import HttpResponse @@ -129,6 +130,14 @@ class AssertContainsTests(TestCase): self.assertNotContains(r, 'はたけ') self.assertNotContains(r, b'\xe3\x81\xaf\xe3\x81\x9f\xe3\x81\x91'.decode('utf-8')) + def test_nontext_contains(self): + r = self.client.get('/test_client_regress/no_template_view/') + self.assertContains(r, ugettext_lazy('once')) + + def test_nontext_not_contains(self): + r = self.client.get('/test_client_regress/no_template_view/') + self.assertNotContains(r, ugettext_lazy('never')) + def test_assert_contains_renders_template_response(self): """ Test that we can pass in an unrendered SimpleTemplateReponse without throwing an error. From 44767f2caf028d89e1a283d04bb552d0e18bb936 Mon Sep 17 00:00:00 2001 From: Dave Hall Date: Tue, 18 Sep 2012 11:28:49 +0100 Subject: [PATCH 009/265] Use unicode.translate to speed up js escaping. --- django/utils/html.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/django/utils/html.py b/django/utils/html.py index 2b669cc8ec..cc8372906b 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -42,29 +42,26 @@ def escape(text): return mark_safe(force_text(text).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')) escape = allow_lazy(escape, six.text_type) -_base_js_escapes = ( - ('\\', '\\u005C'), - ('\'', '\\u0027'), - ('"', '\\u0022'), - ('>', '\\u003E'), - ('<', '\\u003C'), - ('&', '\\u0026'), - ('=', '\\u003D'), - ('-', '\\u002D'), - (';', '\\u003B'), - ('\u2028', '\\u2028'), - ('\u2029', '\\u2029') -) +_js_escapes = { + ord('\\'): '\\u005C', + ord('\''): '\\u0027', + ord('"'): '\\u0022', + ord('>'): '\\u003E', + ord('<'): '\\u003C', + ord('&'): '\\u0026', + ord('='): '\\u003D', + ord('-'): '\\u002D', + ord(';'): '\\u003B', + ord('\u2028'): '\\u2028', + ord('\u2029'): '\\u2029' +} # Escape every ASCII character with a value less than 32. -_js_escapes = (_base_js_escapes + - tuple([('%c' % z, '\\u%04X' % z) for z in range(32)])) +_js_escapes.update((ord('%c' % z), '\\u%04X' % z) for z in range(32)) def escapejs(value): """Hex encodes characters for use in JavaScript strings.""" - for bad, good in _js_escapes: - value = mark_safe(force_text(value).replace(bad, good)) - return value + return mark_safe(force_text(value).translate(_js_escapes)) escapejs = allow_lazy(escapejs, six.text_type) def conditional_escape(text): From 901af865505310a70dd02ea5b3becbf45819b652 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 19 Sep 2012 10:06:53 -0600 Subject: [PATCH 010/265] Fixed #16865 -- Made get_or_create use read database for initial get query. Thanks Rick van Hattem for the report and trbs for the patch. --- django/db/models/query.py | 2 +- docs/releases/1.5.txt | 9 +++++++++ tests/modeltests/get_or_create/tests.py | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index 8bf08b7a93..441426a107 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -455,9 +455,9 @@ class QuerySet(object): if f.attname in lookup: lookup[f.name] = lookup.pop(f.attname) try: - self._for_write = True return self.get(**lookup), False except self.model.DoesNotExist: + self._for_write = True try: params = dict([(k, v) for k, v in kwargs.items() if '__' not in k]) params.update(defaults) diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 26b6ad1bfa..84c37af9ee 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -338,6 +338,15 @@ Miscellaneous needs. The new default value is `0666` (octal) and the current umask value is first masked out. +* In a multi-database situation, ``get_or_create()`` will now use a read + database for the initial ``get`` attempt (previously, it used only the write + database for all queries). This change reduces load on the write (master) + database, in exchange for slightly more frequent false-negatives on the + initial ``get`` due to replication lag. In those cases the subsequent insert + will still go to the master and fail, after which the existing object will be + fetched from the master. + + Features deprecated in 1.5 ========================== diff --git a/tests/modeltests/get_or_create/tests.py b/tests/modeltests/get_or_create/tests.py index 1e300fbb4d..cc7d2c29ab 100644 --- a/tests/modeltests/get_or_create/tests.py +++ b/tests/modeltests/get_or_create/tests.py @@ -64,3 +64,27 @@ class GetOrCreateTests(TestCase): formatted_traceback = traceback.format_exc() self.assertIn('obj.save', formatted_traceback) + + def test_initial_get_on_read_db(self): + """ + get_or_create should only set _for_write when it's actually doing a + create action. This makes sure that the initial .get() will be able to + use a slave database. Specially when some form of database pinning is + in place this will help to not put all the SELECT queries on the + master. Refs #16865. + + """ + qs = Person.objects.get_query_set() + p, created = qs.get_or_create( + first_name="Stuart", last_name="Sutcliffe", defaults={ + "birthday": date(1940, 6, 23), + } + ) + self.assertTrue(created) + self.assertTrue(qs._for_write) + + qs = Person.objects.get_query_set() + p, created = qs.get_or_create( + first_name="Stuart", last_name="Sutcliffe") + self.assertFalse(created) + self.assertFalse(qs._for_write) From 4e9a74b81df1c7aaea2f90a3a4911920e134b275 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 19 Sep 2012 11:15:12 -0600 Subject: [PATCH 011/265] Revert "Fixed #16865 -- Made get_or_create use read database for initial get query." Thanks to Jeremy Dunck for pointing out the problem with this change. If in a single transaction, the master deletes a record and then get_or_creates a similar record, under the new behavior the get_or_create would find the record in the slave db and fail to re-create it, leaving the record nonexistent, which violates the contract of get_or_create that the record should always exist afterwards. We need to do everything against the master here in order to ensure correctness. This reverts commit 901af865505310a70dd02ea5b3becbf45819b652. --- django/db/models/query.py | 2 +- docs/releases/1.5.txt | 9 --------- tests/modeltests/get_or_create/tests.py | 24 ------------------------ 3 files changed, 1 insertion(+), 34 deletions(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index 441426a107..8bf08b7a93 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -455,9 +455,9 @@ class QuerySet(object): if f.attname in lookup: lookup[f.name] = lookup.pop(f.attname) try: + self._for_write = True return self.get(**lookup), False except self.model.DoesNotExist: - self._for_write = True try: params = dict([(k, v) for k, v in kwargs.items() if '__' not in k]) params.update(defaults) diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 84c37af9ee..26b6ad1bfa 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -338,15 +338,6 @@ Miscellaneous needs. The new default value is `0666` (octal) and the current umask value is first masked out. -* In a multi-database situation, ``get_or_create()`` will now use a read - database for the initial ``get`` attempt (previously, it used only the write - database for all queries). This change reduces load on the write (master) - database, in exchange for slightly more frequent false-negatives on the - initial ``get`` due to replication lag. In those cases the subsequent insert - will still go to the master and fail, after which the existing object will be - fetched from the master. - - Features deprecated in 1.5 ========================== diff --git a/tests/modeltests/get_or_create/tests.py b/tests/modeltests/get_or_create/tests.py index cc7d2c29ab..1e300fbb4d 100644 --- a/tests/modeltests/get_or_create/tests.py +++ b/tests/modeltests/get_or_create/tests.py @@ -64,27 +64,3 @@ class GetOrCreateTests(TestCase): formatted_traceback = traceback.format_exc() self.assertIn('obj.save', formatted_traceback) - - def test_initial_get_on_read_db(self): - """ - get_or_create should only set _for_write when it's actually doing a - create action. This makes sure that the initial .get() will be able to - use a slave database. Specially when some form of database pinning is - in place this will help to not put all the SELECT queries on the - master. Refs #16865. - - """ - qs = Person.objects.get_query_set() - p, created = qs.get_or_create( - first_name="Stuart", last_name="Sutcliffe", defaults={ - "birthday": date(1940, 6, 23), - } - ) - self.assertTrue(created) - self.assertTrue(qs._for_write) - - qs = Person.objects.get_query_set() - p, created = qs.get_or_create( - first_name="Stuart", last_name="Sutcliffe") - self.assertFalse(created) - self.assertFalse(qs._for_write) From 1360bd4186239d7e4c4481b7d6a1a650fe69d12f Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 19 Sep 2012 07:13:10 -0400 Subject: [PATCH 012/265] Fixed #13586 - Added an example of how to connect a m2m_changed signal handler. --- docs/ref/signals.txt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index b2f2e85abc..4b463e03ea 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -287,13 +287,22 @@ like this:: # ... toppings = models.ManyToManyField(Topping) -If we would do something like this: +If we connected a handler like this:: + + def toppings_changed(sender, **kwargs): + # Do something + pass + + m2m_changed.connect(toppings_changed, sender=Pizza.toppings.through) + +and then did something like this:: >>> p = Pizza.object.create(...) >>> t = Topping.objects.create(...) >>> p.toppings.add(t) -the arguments sent to a :data:`m2m_changed` handler would be: +the arguments sent to a :data:`m2m_changed` handler (``topppings_changed`` in +the example above) would be: ============== ============================================================ Argument Value From acd74ffa358a64861fae8fd7bf020fc3a50341b2 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 19 Sep 2012 16:36:34 -0400 Subject: [PATCH 013/265] Fixed #14829 - Added references to CBVs in the URLConf docs; thanks Andrew Willey for the suggestion. --- docs/topics/http/urls.txt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 4503bbd6ef..69089af8e9 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -55,7 +55,8 @@ algorithm the system follows to determine which Python code to execute: one that matches the requested URL. 4. Once one of the regexes matches, Django imports and calls the given - view, which is a simple Python function. The view gets passed an + view, which is a simple Python function (or a :doc:`class based view + `). The view gets passed an :class:`~django.http.HttpRequest` as its first argument and any values captured in the regex as remaining arguments. @@ -673,6 +674,15 @@ The style you use is up to you. Note that if you use this technique -- passing objects rather than strings -- the view prefix (as explained in "The view prefix" above) will have no effect. +Note that :doc:`class based views` must be +imported:: + + from mysite.views import ClassBasedView + + urlpatterns = patterns('', + (r'^myview/$', ClassBasedView.as_view()), + ) + .. _naming-url-patterns: Naming URL patterns From 0fdfcee257155ad29ff161725d94f41f0e77691f Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 19 Sep 2012 16:09:46 -0400 Subject: [PATCH 014/265] Fixed #15325 - Added a link to RelatedManager in the ManytoManyField docs; thanks jammon for the suggestion. --- docs/ref/models/fields.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 8b3c31f029..4f6aaab134 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -1081,6 +1081,9 @@ the model is related. This works exactly the same as it does for :class:`ForeignKey`, including all the options regarding :ref:`recursive ` and :ref:`lazy ` relationships. +Related objects can be added, removed, or created with the field's +:class:`~django.db.models.fields.related.RelatedManager`. + Database Representation ~~~~~~~~~~~~~~~~~~~~~~~ From 3ae397a98c427a56717a20933d76ade97bfb0886 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 20 Sep 2012 09:36:48 +0200 Subject: [PATCH 015/265] Added a note about GEOS support for 3D/4D WKT notation See also http://trac.osgeo.org/geos/ticket/347 --- docs/ref/contrib/gis/geos.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index f4e706d275..a68ff36453 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -163,6 +163,11 @@ WKB / EWKB ``buffer`` GeoJSON ``str`` or ``unicode`` ============= ====================== +.. note:: + + The new 3D/4D WKT notation with an intermediary Z or M (like + ``POINT Z (3, 4, 5)``) is only supported with GEOS 3.3.0 or later. + Properties ~~~~~~~~~~ From 7e32dab3a64570c67993d38b5bc25c9bd1f7455e Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 20 Sep 2012 10:08:08 +0200 Subject: [PATCH 016/265] Fixed #17687 -- Made LayerMapping router-aware Thanks nosamanuel@gmail.com for the report and the initial patch. --- django/contrib/gis/tests/layermap/tests.py | 30 ++++++++++++++++++++++ django/contrib/gis/utils/layermapping.py | 8 +++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index 85b8d0c8b5..4912e645c2 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -8,6 +8,7 @@ from django.contrib.gis.gdal import DataSource from django.contrib.gis.tests.utils import mysql from django.contrib.gis.utils.layermapping import (LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey) +from django.db import router from django.test import TestCase from .models import ( @@ -26,6 +27,7 @@ NAMES = ['Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo'] NUMS = [1, 2, 1, 19, 1] # Number of polygons for each. STATES = ['Texas', 'Texas', 'Texas', 'Hawaii', 'Colorado'] + class LayerMapTest(TestCase): def test_init(self): @@ -281,3 +283,31 @@ class LayerMapTest(TestCase): lm.save(silent=True, strict=True) self.assertEqual(City.objects.count(), 3) self.assertEqual(City.objects.all().order_by('name_txt')[0].name_txt, "Houston") + + +class OtherRouter(object): + def db_for_read(self, model, **hints): + return 'other' + + def db_for_write(self, model, **hints): + return self.db_for_read(model, **hints) + + def allow_relation(self, obj1, obj2, **hints): + return None + + def allow_syncdb(self, db, model): + return True + + +class LayerMapRouterTest(TestCase): + + def setUp(self): + self.old_routers = router.routers + router.routers = [OtherRouter()] + + def tearDown(self): + router.routers = self.old_routers + + def test_layermapping_default_db(self): + lm = LayerMapping(City, city_shp, city_mapping) + self.assertEqual(lm.using, 'other') diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index e898f6de2e..9511815426 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -9,7 +9,7 @@ import sys from decimal import Decimal from django.core.exceptions import ObjectDoesNotExist -from django.db import connections, DEFAULT_DB_ALIAS +from django.db import connections, router from django.contrib.gis.db.models import GeometryField from django.contrib.gis.gdal import (CoordTransform, DataSource, OGRException, OGRGeometry, OGRGeomType, SpatialReference) @@ -67,7 +67,7 @@ class LayerMapping(object): def __init__(self, model, data, mapping, layer=0, source_srs=None, encoding=None, transaction_mode='commit_on_success', - transform=True, unique=None, using=DEFAULT_DB_ALIAS): + transform=True, unique=None, using=None): """ A LayerMapping object is initialized using the given Model (not an instance), a DataSource (or string path to an OGR-supported data file), and a mapping @@ -81,8 +81,8 @@ class LayerMapping(object): self.ds = data self.layer = self.ds[layer] - self.using = using - self.spatial_backend = connections[using].ops + self.using = using if using is not None else router.db_for_write(model) + self.spatial_backend = connections[self.using].ops # Setting the mapping & model attributes. self.mapping = mapping From 89136b2725db3cb774ae4b39849684ae8f3847aa Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 20 Sep 2012 10:31:37 +0200 Subject: [PATCH 017/265] Fixed #16577 -- Added a map_creation block in openlayers.js template --- django/contrib/gis/templates/gis/admin/openlayers.js | 2 ++ docs/ref/contrib/gis/admin.txt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/django/contrib/gis/templates/gis/admin/openlayers.js b/django/contrib/gis/templates/gis/admin/openlayers.js index f54b75e258..a67980da40 100644 --- a/django/contrib/gis/templates/gis/admin/openlayers.js +++ b/django/contrib/gis/templates/gis/admin/openlayers.js @@ -109,10 +109,12 @@ OpenLayers.Projection.addTransform("EPSG:4326", "EPSG:3857", OpenLayers.Layer.Sp {% autoescape off %}{% for item in map_options.items %} '{{ item.0 }}' : {{ item.1 }}{% if not forloop.last %},{% endif %} {% endfor %}{% endautoescape %} };{% endblock %} // The admin map for this geometry field. + {% block map_creation %} {{ module }}.map = new OpenLayers.Map('{{ id }}_map', options); // Base Layer {{ module }}.layers.base = {% block base_layer %}new OpenLayers.Layer.WMS("{{ wms_name }}", "{{ wms_url }}", {layers: '{{ wms_layer }}'{{ wms_options|safe }}});{% endblock %} {{ module }}.map.addLayer({{ module }}.layers.base); + {% endblock %} {% block extra_layers %}{% endblock %} {% if is_linestring %}OpenLayers.Feature.Vector.style["default"]["strokeWidth"] = 3; // Default too thin for linestrings. {% endif %} {{ module }}.layers.vector = new OpenLayers.Layer.Vector(" {{ field_name }}"); diff --git a/docs/ref/contrib/gis/admin.txt b/docs/ref/contrib/gis/admin.txt index aa6ba58630..d1a9fc1dcb 100644 --- a/docs/ref/contrib/gis/admin.txt +++ b/docs/ref/contrib/gis/admin.txt @@ -45,7 +45,7 @@ GeoDjango's admin site .. attribute:: openlayers_url Link to the URL of the OpenLayers JavaScript. Defaults to - ``'http://openlayers.org/api/2.8/OpenLayers.js'``. + ``'http://openlayers.org/api/2.11/OpenLayers.js'``. .. attribute:: modifiable From 26ff2be787d3c1fad2df4a009fa05b4c11b8cf30 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 20 Sep 2012 21:03:24 +0200 Subject: [PATCH 018/265] Imported getLogger directly from logging module This was a remainder of some 2.4 compatibility code. --- django/core/handlers/base.py | 4 ++-- django/core/handlers/wsgi.py | 4 ++-- django/db/backends/postgresql_psycopg2/base.py | 4 ++-- django/db/backends/util.py | 4 ++-- django/middleware/common.py | 5 +++-- django/middleware/csrf.py | 5 +++-- django/views/decorators/http.py | 4 ++-- django/views/generic/base.py | 5 +++-- tests/regressiontests/logging_tests/tests.py | 11 ++++++----- 9 files changed, 25 insertions(+), 21 deletions(-) diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 791382bac0..ba6e8f0be7 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import logging import sys import types @@ -7,10 +8,9 @@ from django import http from django.core import signals from django.utils.encoding import force_text from django.utils.importlib import import_module -from django.utils.log import getLogger from django.utils import six -logger = getLogger('django.request') +logger = logging.getLogger('django.request') class BaseHandler(object): diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 7a32a3dac7..7d2ee44e80 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import logging import sys from io import BytesIO from threading import Lock @@ -10,9 +11,8 @@ from django.core.handlers import base from django.core.urlresolvers import set_script_prefix from django.utils import datastructures from django.utils.encoding import force_str, force_text, iri_to_uri -from django.utils.log import getLogger -logger = getLogger('django.request') +logger = logging.getLogger('django.request') # See http://www.iana.org/assignments/http-status-codes diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index c8b88d5619..5837922061 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -3,6 +3,7 @@ PostgreSQL database backend for Django. Requires psycopg 2: http://initd.org/projects/psycopg2 """ +import logging import sys from django.db import utils @@ -14,7 +15,6 @@ 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 from django.utils.timezone import utc @@ -33,7 +33,7 @@ psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString) psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString) -logger = getLogger('django.db.backends') +logger = logging.getLogger('django.db.backends') def utc_tzinfo_factory(offset): if offset != 0: diff --git a/django/db/backends/util.py b/django/db/backends/util.py index 75d4d07a66..e029c42899 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -3,15 +3,15 @@ from __future__ import unicode_literals import datetime import decimal import hashlib +import logging from time import time from django.conf import settings from django.utils.encoding import force_bytes -from django.utils.log import getLogger from django.utils.timezone import utc -logger = getLogger('django.db.backends') +logger = logging.getLogger('django.db.backends') class CursorWrapper(object): diff --git a/django/middleware/common.py b/django/middleware/common.py index bb24977ce8..0ec17fbe92 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -1,4 +1,5 @@ import hashlib +import logging import re from django.conf import settings @@ -6,9 +7,9 @@ from django import http from django.core.mail import mail_managers from django.utils.http import urlquote from django.core import urlresolvers -from django.utils.log import getLogger -logger = getLogger('django.request') + +logger = logging.getLogger('django.request') class CommonMiddleware(object): diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index c9e8d73c82..b2eb0df3f5 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -7,6 +7,7 @@ against request forgeries from other sites. from __future__ import unicode_literals import hashlib +import logging import re import random @@ -15,10 +16,10 @@ from django.core.urlresolvers import get_callable from django.utils.cache import patch_vary_headers from django.utils.encoding import force_text from django.utils.http import same_origin -from django.utils.log import getLogger from django.utils.crypto import constant_time_compare, get_random_string -logger = getLogger('django.request') + +logger = logging.getLogger('django.request') REASON_NO_REFERER = "Referer checking failed - no Referer." REASON_BAD_REFERER = "Referer checking failed - %s does not match %s." diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py index d5c4bff744..410979e1e4 100644 --- a/django/views/decorators/http.py +++ b/django/views/decorators/http.py @@ -2,18 +2,18 @@ Decorators for views based on HTTP headers. """ +import logging from calendar import timegm from functools import wraps from django.utils.decorators import decorator_from_middleware, available_attrs from django.utils.http import http_date, parse_http_date_safe, parse_etags, quote_etag -from django.utils.log import getLogger from django.middleware.http import ConditionalGetMiddleware from django.http import HttpResponseNotAllowed, HttpResponseNotModified, HttpResponse conditional_page = decorator_from_middleware(ConditionalGetMiddleware) -logger = getLogger('django.request') +logger = logging.getLogger('django.request') def require_http_methods(request_method_list): diff --git a/django/views/generic/base.py b/django/views/generic/base.py index e11412ba4d..d2349e1fca 100644 --- a/django/views/generic/base.py +++ b/django/views/generic/base.py @@ -1,14 +1,15 @@ from __future__ import unicode_literals +import logging from functools import update_wrapper + from django import http from django.core.exceptions import ImproperlyConfigured from django.template.response import TemplateResponse -from django.utils.log import getLogger from django.utils.decorators import classonlymethod from django.utils import six -logger = getLogger('django.request') +logger = logging.getLogger('django.request') class ContextMixin(object): diff --git a/tests/regressiontests/logging_tests/tests.py b/tests/regressiontests/logging_tests/tests.py index 4ae3c1b1a9..f444e0ff46 100644 --- a/tests/regressiontests/logging_tests/tests.py +++ b/tests/regressiontests/logging_tests/tests.py @@ -1,13 +1,14 @@ from __future__ import unicode_literals import copy +import logging import warnings from django.conf import compat_patch_logging_config from django.core import mail from django.test import TestCase, RequestFactory from django.test.utils import override_settings -from django.utils.log import CallbackFilter, RequireDebugFalse, getLogger +from django.utils.log import CallbackFilter, RequireDebugFalse # logging config prior to using filter with mail_admins @@ -153,7 +154,7 @@ class AdminEmailHandlerTest(TestCase): token1 = 'ping' token2 = 'pong' - logger = getLogger('django.request') + logger = logging.getLogger('django.request') admin_email_handler = self.get_admin_email_handler(logger) # Backup then override original filters orig_filters = admin_email_handler.filters @@ -184,7 +185,7 @@ class AdminEmailHandlerTest(TestCase): token1 = 'ping' token2 = 'pong' - logger = getLogger('django.request') + logger = logging.getLogger('django.request') admin_email_handler = self.get_admin_email_handler(logger) # Backup then override original filters orig_filters = admin_email_handler.filters @@ -222,7 +223,7 @@ class AdminEmailHandlerTest(TestCase): self.assertEqual(len(mail.outbox), 0) - logger = getLogger('django.request') + logger = logging.getLogger('django.request') logger.error(message) self.assertEqual(len(mail.outbox), 1) @@ -247,7 +248,7 @@ class AdminEmailHandlerTest(TestCase): self.assertEqual(len(mail.outbox), 0) - logger = getLogger('django.request') + logger = logging.getLogger('django.request') logger.error(message) self.assertEqual(len(mail.outbox), 1) From 2315f1a2ee4e83f7514f20302cdac4782b63751a Mon Sep 17 00:00:00 2001 From: Ian Clelland Date: Thu, 20 Sep 2012 12:07:34 -0700 Subject: [PATCH 019/265] Add documentation for get_caches function --- docs/topics/cache.txt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index f13238e342..f84c20a952 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -673,12 +673,27 @@ dictionaries, lists of model objects, and so forth. (Most common Python objects can be pickled; refer to the Python documentation for more information about pickling.) +Accessing the cache +------------------- + The cache module, ``django.core.cache``, has a ``cache`` object that's automatically created from the ``'default'`` entry in the :setting:`CACHES` setting:: >>> from django.core.cache import cache +If you have multiple caches defined in :setting:`CACHES`, then you can use +:func:`django.core.cache.get_cache` to retrieve a cache object for any key:: + + >>> from django.core.cache import get_cache + >>> cache = get_cache('alternate') + +If the named key does not exist, :exc:`InvalidCacheBackendError` will be raised. + + +Basic usage +----------- + The basic interface is ``set(key, value, timeout)`` and ``get(key)``:: >>> cache.set('my_key', 'hello, world!', 30) @@ -686,7 +701,7 @@ The basic interface is ``set(key, value, timeout)`` and ``get(key)``:: 'hello, world!' The ``timeout`` argument is optional and defaults to the ``timeout`` -argument of the ``'default'`` backend in :setting:`CACHES` setting +argument of the appropriate backend in the :setting:`CACHES` setting (explained above). It's the number of seconds the value should be stored in the cache. From e06b54391dd06a0448b7676ec38f3734a4f86300 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 20 Sep 2012 13:49:26 -0700 Subject: [PATCH 020/265] Removed an excess colon. Thanks to jMyles for the patch. --- docs/internals/deprecation.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 4add751912..976371516e 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -134,7 +134,7 @@ these changes. * The function-based generic view modules will be removed in favor of their class-based equivalents, outlined :doc:`here - `: + `. * The :class:`~django.core.servers.basehttp.AdminMediaHandler` will be removed. In its place use From 837425b425c2d58596f3ed04a7ed79541279ee7e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 19 Sep 2012 16:39:14 -0400 Subject: [PATCH 021/265] Fixed #18934 - Removed versionadded/changed annotations for Django 1.3 --- docs/howto/custom-template-tags.txt | 2 -- docs/howto/error-reporting.txt | 4 --- docs/howto/static-files.txt | 2 -- docs/misc/api-stability.txt | 2 -- docs/ref/contrib/admin/index.txt | 21 ++------------ docs/ref/contrib/comments/example.txt | 21 -------------- docs/ref/contrib/comments/moderation.txt | 4 --- docs/ref/contrib/contenttypes.txt | 2 -- docs/ref/contrib/flatpages.txt | 2 -- docs/ref/contrib/gis/geos.txt | 4 --- docs/ref/contrib/gis/testing.txt | 2 -- docs/ref/contrib/localflavor.txt | 7 ----- docs/ref/contrib/sitemaps.txt | 2 -- docs/ref/contrib/sites.txt | 8 ++---- docs/ref/contrib/staticfiles.txt | 2 -- docs/ref/django-admin.txt | 28 +++---------------- docs/ref/files/storage.txt | 12 ++------ docs/ref/forms/api.txt | 2 -- docs/ref/forms/fields.txt | 2 -- docs/ref/forms/widgets.txt | 7 ----- docs/ref/models/fields.txt | 2 -- docs/ref/models/querysets.txt | 18 ++++-------- docs/ref/request-response.txt | 13 --------- docs/ref/settings.txt | 26 ++--------------- docs/ref/signals.txt | 10 ------- docs/ref/template-response.txt | 2 -- docs/ref/templates/api.txt | 12 -------- docs/ref/templates/builtins.txt | 12 -------- docs/topics/auth.txt | 13 --------- docs/topics/cache.txt | 22 --------------- .../class-based-views/generic-display.txt | 5 ---- docs/topics/class-based-views/index.txt | 2 -- docs/topics/class-based-views/mixins.txt | 2 -- docs/topics/db/queries.txt | 3 -- docs/topics/db/sql.txt | 9 ++---- docs/topics/db/transactions.txt | 5 ---- docs/topics/email.txt | 5 ---- docs/topics/forms/formsets.txt | 16 ++--------- docs/topics/forms/media.txt | 2 -- docs/topics/http/middleware.txt | 2 -- docs/topics/http/shortcuts.txt | 2 -- docs/topics/http/urls.txt | 7 ----- docs/topics/i18n/formatting.txt | 6 ---- docs/topics/i18n/translation.txt | 15 ---------- docs/topics/logging.txt | 2 -- docs/topics/signals.txt | 4 --- docs/topics/testing.txt | 16 ----------- 47 files changed, 23 insertions(+), 346 deletions(-) diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 5b27af82d6..70b6288bee 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -760,8 +760,6 @@ A few things to note about the ``simple_tag`` helper function: * If the argument was a template variable, our function is passed the current value of the variable, not the variable itself. -.. versionadded:: 1.3 - If your template tag needs to access the current context, you can use the ``takes_context`` argument when registering your tag: diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 64af2a0980..78e797b607 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -44,8 +44,6 @@ setting. .. seealso:: - .. versionadded:: 1.3 - Server error emails are sent using the logging framework, so you can customize this behavior by :doc:`customizing your logging configuration `. @@ -99,8 +97,6 @@ The best way to disable this behavior is to set .. seealso:: - .. versionadded:: 1.3 - 404 errors are logged using the logging framework. By default, these log records are ignored, but you can use them for error reporting by writing a handler and :doc:`configuring logging ` appropriately. diff --git a/docs/howto/static-files.txt b/docs/howto/static-files.txt index f8c591891d..964b5fab61 100644 --- a/docs/howto/static-files.txt +++ b/docs/howto/static-files.txt @@ -2,8 +2,6 @@ Managing static files ===================== -.. versionadded:: 1.3 - Django developers mostly concern themselves with the dynamic parts of web applications -- the views and templates that render anew for each request. But web applications have other parts: the static files (images, CSS, diff --git a/docs/misc/api-stability.txt b/docs/misc/api-stability.txt index 2839ee3594..4f232e795b 100644 --- a/docs/misc/api-stability.txt +++ b/docs/misc/api-stability.txt @@ -155,8 +155,6 @@ Certain APIs are explicitly marked as "internal" in a couple of ways: Local flavors ------------- -.. versionchanged:: 1.3 - :mod:`django.contrib.localflavor` contains assorted pieces of code that are useful for particular countries or cultures. This data is local in nature, and is subject to change on timelines that will diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 66a5a2cc4f..2aabc55908 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -129,8 +129,6 @@ subclass:: date_hierarchy = 'pub_date' - .. versionadded:: 1.3 - This will intelligently populate itself based on available data, e.g. if all the dates are in one month, it'll show the day-level drill-down only. @@ -576,8 +574,6 @@ subclass:: class PersonAdmin(ModelAdmin): list_filter = ('is_staff', 'company') - .. versionadded:: 1.3 - Field names in ``list_filter`` can also span relations using the ``__`` lookup, for example:: @@ -748,8 +744,6 @@ subclass:: .. attribute:: ModelAdmin.paginator - .. versionadded:: 1.3 - The paginator class to be used for pagination. By default, :class:`django.core.paginator.Paginator` is used. If the custom paginator class doesn't have the same constructor interface as @@ -966,8 +960,6 @@ templates used by the :class:`ModelAdmin` views: .. method:: ModelAdmin.delete_model(self, request, obj) - .. versionadded:: 1.3 - The ``delete_model`` method is given the ``HttpRequest`` and a model instance. Use this method to do pre- or post-delete operations. @@ -1213,8 +1205,6 @@ templates used by the :class:`ModelAdmin` views: .. method:: ModelAdmin.get_paginator(queryset, per_page, orphans=0, allow_empty_first_page=True) - .. versionadded:: 1.3 - Returns an instance of the paginator to use for this view. By default, instantiates an instance of :attr:`paginator`. @@ -1295,8 +1285,6 @@ on your ``ModelAdmin``:: } js = ("my_code.js",) -.. versionchanged:: 1.3 - The :doc:`staticfiles app ` prepends :setting:`STATIC_URL` (or :setting:`MEDIA_URL` if :setting:`STATIC_URL` is ``None``) to any media paths. The same rules apply as :ref:`regular media @@ -1394,18 +1382,15 @@ adds some of its own (the shared features are actually defined in the - :attr:`~ModelAdmin.exclude` - :attr:`~ModelAdmin.filter_horizontal` - :attr:`~ModelAdmin.filter_vertical` +- :attr:`~ModelAdmin.ordering` - :attr:`~ModelAdmin.prepopulated_fields` +- :meth:`~ModelAdmin.queryset` - :attr:`~ModelAdmin.radio_fields` - :attr:`~ModelAdmin.readonly_fields` - :attr:`~InlineModelAdmin.raw_id_fields` - :meth:`~ModelAdmin.formfield_for_foreignkey` - :meth:`~ModelAdmin.formfield_for_manytomany` -.. versionadded:: 1.3 - -- :attr:`~ModelAdmin.ordering` -- :meth:`~ModelAdmin.queryset` - .. versionadded:: 1.4 - :meth:`~ModelAdmin.has_add_permission` @@ -1813,8 +1798,6 @@ Templates can override or extend base admin templates as described in .. attribute:: AdminSite.login_form - .. versionadded:: 1.3 - Subclass of :class:`~django.contrib.auth.forms.AuthenticationForm` that will be used by the admin site login view. diff --git a/docs/ref/contrib/comments/example.txt b/docs/ref/contrib/comments/example.txt index e78d83c35d..2bff778c2f 100644 --- a/docs/ref/contrib/comments/example.txt +++ b/docs/ref/contrib/comments/example.txt @@ -152,27 +152,6 @@ enable it in your project's ``urls.py``: Now you should have the latest comment feeds being served off ``/feeds/latest/``. -.. versionchanged:: 1.3 - -Prior to Django 1.3, the LatestCommentFeed was deployed using the -syndication feed view: - -.. code-block:: python - - from django.conf.urls import patterns - from django.contrib.comments.feeds import LatestCommentFeed - - feeds = { - 'latest': LatestCommentFeed, - } - - urlpatterns = patterns('', - # ... - (r'^feeds/(?P.*)/$', 'django.contrib.syndication.views.feed', - {'feed_dict': feeds}), - # ... - ) - Moderation ========== diff --git a/docs/ref/contrib/comments/moderation.txt b/docs/ref/contrib/comments/moderation.txt index f03c7fda0d..39b3ea7913 100644 --- a/docs/ref/contrib/comments/moderation.txt +++ b/docs/ref/contrib/comments/moderation.txt @@ -136,10 +136,6 @@ Simply subclassing :class:`CommentModerator` and changing the values of these options will automatically enable the various moderation methods for any models registered using the subclass. -.. versionchanged:: 1.3 - -``moderate_after`` and ``close_after`` now accept 0 as a valid value. - Adding custom moderation methods -------------------------------- diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index e98da6e429..dfbeabc302 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -423,8 +423,6 @@ pointing at it will be deleted as well. In the example above, this means that if a ``Bookmark`` object were deleted, any ``TaggedItem`` objects pointing at it would be deleted at the same time. -.. versionadded:: 1.3 - Unlike :class:`~django.db.models.ForeignKey`, :class:`~django.contrib.contenttypes.generic.GenericForeignKey` does not accept an :attr:`~django.db.models.ForeignKey.on_delete` argument to customize this diff --git a/docs/ref/contrib/flatpages.txt b/docs/ref/contrib/flatpages.txt index 3de449708f..38cedc40fe 100644 --- a/docs/ref/contrib/flatpages.txt +++ b/docs/ref/contrib/flatpages.txt @@ -239,8 +239,6 @@ template. Getting a list of :class:`~django.contrib.flatpages.models.FlatPage` objects in your templates ============================================================================================== -.. versionadded:: 1.3 - The flatpages app provides a template tag that allows you to iterate over all of the available flatpages on the :ref:`current site `. diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index a68ff36453..b569a74fe3 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -237,8 +237,6 @@ Returns a boolean indicating whether the geometry is valid. .. attribute:: GEOSGeometry.valid_reason -.. versionadded:: 1.3 - Returns a string describing the reason why a geometry is invalid. .. attribute:: GEOSGeometry.srid @@ -535,8 +533,6 @@ corresponding to the SRID of the geometry or ``None``. .. method:: GEOSGeometry.transform(ct, clone=False) -.. versionchanged:: 1.3 - Transforms the geometry according to the given coordinate transformation paramter (``ct``), which may be an integer SRID, spatial reference WKT string, a PROJ.4 string, a :class:`~django.contrib.gis.gdal.SpatialReference` object, or a diff --git a/docs/ref/contrib/gis/testing.txt b/docs/ref/contrib/gis/testing.txt index d12c884a1b..86979f0308 100644 --- a/docs/ref/contrib/gis/testing.txt +++ b/docs/ref/contrib/gis/testing.txt @@ -134,8 +134,6 @@ your settings:: GeoDjango tests =============== -.. versionchanged:: 1.3 - GeoDjango's test suite may be run in one of two ways, either by itself or with the rest of :ref:`Django's unit tests `. diff --git a/docs/ref/contrib/localflavor.txt b/docs/ref/contrib/localflavor.txt index 4595f51d9e..0d1319ec61 100644 --- a/docs/ref/contrib/localflavor.txt +++ b/docs/ref/contrib/localflavor.txt @@ -267,8 +267,6 @@ Austria (``at``) Belgium (``be``) ================ -.. versionadded:: 1.3 - .. class:: be.forms.BEPhoneNumberField A form field that validates input as a Belgium phone number, with one of @@ -658,11 +656,6 @@ Indonesia (``id``) A ``Select`` widget that uses a list of Indonesian provinces as its choices. -.. versionchanged:: 1.3 - The province "Nanggroe Aceh Darussalam (NAD)" has been removed - from the province list in favor of the new official designation - "Aceh (ACE)". - .. class:: id.forms.IDPhoneNumberField A form field that validates input as an Indonesian telephone number. diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index 2393a4a9a3..ef6c64dc61 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -330,8 +330,6 @@ with a caching decorator -- you must name your sitemap view and pass Template customization ====================== -.. versionadded:: 1.3 - If you wish to use a different template for each sitemap or sitemap index available on your site, you may specify it by passing a ``template_name`` parameter to the ``sitemap`` and ``index`` views via the URLconf:: diff --git a/docs/ref/contrib/sites.txt b/docs/ref/contrib/sites.txt index 8fc434ba9b..8bb7b27f32 100644 --- a/docs/ref/contrib/sites.txt +++ b/docs/ref/contrib/sites.txt @@ -159,8 +159,6 @@ the :class:`~django.contrib.sites.models.Site` model's manager has a else: # Do something else. -.. versionchanged:: 1.3 - For code which relies on getting the current domain but cannot be certain that the sites framework will be installed for any given project, there is a utility function :func:`~django.contrib.sites.models.get_current_site` that @@ -169,12 +167,10 @@ the sites framework is installed) or a RequestSite instance (if it is not). This allows loose coupling with the sites framework and provides a usable fallback for cases where it is not installed. -.. versionadded:: 1.3 - .. function:: get_current_site(request) Checks if contrib.sites is installed and returns either the current - :class:`~django.contrib.sites.models.Site` object or a + :class:`~django.contrib.sites.models.Site` object or a :class:`~django.contrib.sites.models.RequestSite` object based on the request. @@ -437,7 +433,7 @@ fallback when the database-backed sites framework is not available. Sets the ``name`` and ``domain`` attributes to the value of :meth:`~django.http.HttpRequest.get_host`. - + A :class:`~django.contrib.sites.models.RequestSite` object has a similar interface to a normal :class:`~django.contrib.sites.models.Site` object, except diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index cbe8ad54b8..3a74797145 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -5,8 +5,6 @@ The staticfiles app .. module:: django.contrib.staticfiles :synopsis: An app for handling static files. -.. versionadded:: 1.3 - ``django.contrib.staticfiles`` collects static files from each of your applications (and any other places you specify) into a single location that can easily be served in production. diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 5ff7ecba2c..467e32c86d 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -176,8 +176,6 @@ records to dump. If you're using a :ref:`custom manager ` as the default manager and it filters some of the available records, not all of the objects will be dumped. -.. versionadded:: 1.3 - The :djadminopt:`--all` option may be provided to specify that ``dumpdata`` should use Django's base manager, dumping records which might otherwise be filtered or modified by a custom manager. @@ -195,18 +193,10 @@ easy for humans to read, so you can use the ``--indent`` option to pretty-print the output with a number of indentation spaces. The :djadminopt:`--exclude` option may be provided to prevent specific -applications from being dumped. - -.. versionadded:: 1.3 - -The :djadminopt:`--exclude` option may also be provided to prevent specific -models (specified as in the form of ``appname.ModelName``) from being dumped. - -In addition to specifying application names, you can provide a list of -individual models, in the form of ``appname.Model``. If you specify a model -name to ``dumpdata``, the dumped output will be restricted to that model, -rather than the entire application. You can also mix application names and -model names. +applications or models (specified as in the form of ``appname.ModelName``) from +being dumped. If you specify a model name to ``dumpdata``, the dumped output +will be restricted to that model, rather than the entire application. You can +also mix application names and model names. The :djadminopt:`--database` option can be used to specify the database from which data will be dumped. @@ -463,8 +453,6 @@ Use the ``--no-default-ignore`` option to disable the default values of .. django-admin-option:: --no-wrap -.. versionadded:: 1.3 - Use the ``--no-wrap`` option to disable breaking long message lines into several lines in language files. @@ -640,15 +628,11 @@ machines on your network. To make your development server viewable to other machines on the network, use its own IP address (e.g. ``192.168.2.1``) or ``0.0.0.0`` or ``::`` (with IPv6 enabled). -.. versionchanged:: 1.3 - You can provide an IPv6 address surrounded by brackets (e.g. ``[200a::1]:8000``). This will automatically enable IPv6 support. A hostname containing ASCII-only characters can also be used. -.. versionchanged:: 1.3 - If the :doc:`staticfiles` contrib app is enabled (default in new projects) the :djadmin:`runserver` command will be overriden with an own :djadmin:`runserver` command. @@ -674,8 +658,6 @@ development server. .. django-admin-option:: --ipv6, -6 -.. versionadded:: 1.3 - Use the ``--ipv6`` (or shorter ``-6``) option to tell Django to use IPv6 for the development server. This changes the default IP address from ``127.0.0.1`` to ``::1``. @@ -1113,8 +1095,6 @@ To run on 1.2.3.4:7000 with a ``test`` fixture:: django-admin.py testserver --addrport 1.2.3.4:7000 test -.. versionadded:: 1.3 - The :djadminopt:`--noinput` option may be provided to suppress all user prompts. diff --git a/docs/ref/files/storage.txt b/docs/ref/files/storage.txt index b3f8909847..f9bcf9b61e 100644 --- a/docs/ref/files/storage.txt +++ b/docs/ref/files/storage.txt @@ -18,7 +18,7 @@ Django provides two convenient ways to access the current storage class: .. function:: get_storage_class([import_path=None]) Returns a class or module which implements the storage API. - + When called without the ``import_path`` parameter ``get_storage_class`` will return the current default storage system as defined by :setting:`DEFAULT_FILE_STORAGE`. If ``import_path`` is provided, @@ -35,9 +35,9 @@ The FileSystemStorage Class basic file storage on a local filesystem. It inherits from :class:`~django.core.files.storage.Storage` and provides implementations for all the public methods thereof. - + .. note:: - + The :class:`FileSystemStorage.delete` method will not raise raise an exception if the given file name does not exist. @@ -53,16 +53,12 @@ The Storage Class .. method:: accessed_time(name) - .. versionadded:: 1.3 - Returns a ``datetime`` object containing the last accessed time of the file. For storage systems that aren't able to return the last accessed time this will raise ``NotImplementedError`` instead. .. method:: created_time(name) - .. versionadded:: 1.3 - Returns a ``datetime`` object containing the creation time of the file. For storage systems that aren't able to return the creation time this will raise ``NotImplementedError`` instead. @@ -100,8 +96,6 @@ The Storage Class .. method:: modified_time(name) - .. versionadded:: 1.3 - Returns a ``datetime`` object containing the last modified time. For storage systems that aren't able to return the last modified time, this will raise ``NotImplementedError`` instead. diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 777d73e015..2323425277 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -658,8 +658,6 @@ those classes as an argument:: .. method:: BoundField.value() - .. versionadded:: 1.3 - Use this method to render the raw value of this field as it would be rendered by a ``Widget``:: diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 7c06bf97ee..9f3dc68b4d 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -704,8 +704,6 @@ For each field, we describe the default widget used if you don't specify ``TypedMultipleChoiceField`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.3 - .. class:: TypedMultipleChoiceField(**kwargs) Just like a :class:`MultipleChoiceField`, except :class:`TypedMultipleChoiceField` diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index 4724cbdec2..3c458930fa 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -294,11 +294,6 @@ These widgets make use of the HTML elements ``input`` and ``textarea``. Determines whether the widget will have a value filled in when the form is re-displayed after a validation error (default is ``False``). - .. versionchanged:: 1.3 - The default value for - :attr:`~PasswordInput.render_value` was - changed from ``True`` to ``False`` - ``HiddenInput`` ~~~~~~~~~~~~~~~ @@ -532,8 +527,6 @@ File upload widgets .. 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. diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 4f6aaab134..4797e8b26b 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -1023,8 +1023,6 @@ define the details of how the relation works. The field on the related object that the relation is to. By default, Django uses the primary key of the related object. -.. versionadded:: 1.3 - .. attribute:: ForeignKey.on_delete When an object referenced by a :class:`ForeignKey` is deleted, Django by diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 8ec7cfc791..749a979db6 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -505,15 +505,8 @@ followed (optionally) by any output-affecting methods (such as ``values()``), but it doesn't really matter. This is your chance to really flaunt your individualism. -.. versionchanged:: 1.3 - -The ``values()`` method previously did not return anything for -:class:`~django.db.models.ManyToManyField` attributes and would raise an error -if you tried to pass this type of field to it. - -This restriction has been lifted, and you can now also refer to fields on -related models with reverse relations through ``OneToOneField``, ``ForeignKey`` -and ``ManyToManyField`` attributes:: +You can also refer to fields on related models with reverse relations through +``OneToOneField``, ``ForeignKey`` and ``ManyToManyField`` attributes:: Blog.objects.values('name', 'entry__headline') [{'name': 'My blog', 'entry__headline': 'An entry'}, @@ -1664,10 +1657,9 @@ For example:: # This will delete all Blogs and all of their Entry objects. blogs.delete() -.. versionadded:: 1.3 - This cascade behavior is customizable via the - :attr:`~django.db.models.ForeignKey.on_delete` argument to the - :class:`~django.db.models.ForeignKey`. +This cascade behavior is customizable via the +:attr:`~django.db.models.ForeignKey.on_delete` argument to the +:class:`~django.db.models.ForeignKey`. The ``delete()`` method does a bulk delete and does not call any ``delete()`` methods on your models. It does, however, emit the diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 21e99de10d..cc2a351d8e 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -42,8 +42,6 @@ All attributes should be considered read-only, unless stated otherwise below. data in different ways than conventional HTML forms: binary images, XML payload etc. For processing conventional form data, use ``HttpRequest.POST``. - .. versionadded:: 1.3 - You can also read from an HttpRequest using a file-like interface. See :meth:`HttpRequest.read()`. @@ -305,8 +303,6 @@ Methods .. method:: HttpRequest.xreadlines() .. method:: HttpRequest.__iter__() - .. versionadded:: 1.3 - Methods implementing a file-like interface for reading from an HttpRequest instance. This makes it possible to consume an incoming request in a streaming fashion. A common use-case would be to process a @@ -509,9 +505,6 @@ In addition, ``QueryDict`` has the following methods: >>> q.urlencode() 'a=2&b=3&b=5' - .. versionchanged:: 1.3 - The ``safe`` parameter was added. - Optionally, urlencode can be passed characters which do not require encoding. For example:: @@ -648,12 +641,6 @@ Methods .. method:: HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=True) - .. versionchanged:: 1.3 - - The possibility of specifying a ``datetime.datetime`` object in - ``expires``, and the auto-calculation of ``max_age`` in such case - was added. The ``httponly`` argument was also added. - .. versionchanged:: 1.4 The default value for httponly was changed from ``False`` to ``True``. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 16d067172d..1159d1ecee 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -125,8 +125,6 @@ The site-specific user profile model used by this site. See CACHES ------ -.. versionadded:: 1.3 - Default:: { @@ -167,12 +165,6 @@ backend class (i.e. ``mypackage.backends.whatever.WhateverCache``). Writing a whole new cache backend from scratch is left as an exercise to the reader; see the other backends for examples. -.. note:: - Prior to Django 1.3, you could use a URI based version of the backend - name to reference the built-in cache backends (e.g., you could use - ``'db://tablename'`` to refer to the database backend). This format has - been deprecated, and will be removed in Django 1.5. - .. setting:: CACHES-KEY_FUNCTION KEY_FUNCTION @@ -534,8 +526,6 @@ Only supported for the ``mysql`` backend (see the `MySQL manual`_ for details). TEST_DEPENDENCIES ~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.3 - Default: ``['default']``, for all databases other than ``default``, which has no dependencies. @@ -1262,8 +1252,6 @@ the ``locale`` directory (i.e. ``'/path/to/locale'``). LOGGING ------- -.. versionadded:: 1.3 - Default: A logging configuration dictionary. A data structure containing configuration information. The contents of @@ -1278,8 +1266,6 @@ email log handler; all other log messages are given to a NullHandler. LOGGING_CONFIG -------------- -.. versionadded:: 1.3 - Default: ``'django.utils.log.dictConfig'`` A path to a callable that will be used to configure logging in the @@ -1371,13 +1357,11 @@ MEDIA_URL Default: ``''`` (Empty string) URL that handles the media served from :setting:`MEDIA_ROOT`, used -for :doc:`managing stored files `. +for :doc:`managing stored files `. It must end in a slash if set +to a non-empty value. Example: ``"http://media.example.com/"`` -.. versionchanged:: 1.3 - It must end in a slash if set to a non-empty value. - MESSAGE_LEVEL ------------- @@ -1896,10 +1880,6 @@ A tuple of callables that are used to populate the context in ``RequestContext`` These callables take a request object as their argument and return a dictionary of items to be merged into the context. -.. versionadded:: 1.3 - The ``django.core.context_processors.static`` context processor - was added in this release. - .. versionadded:: 1.4 The ``django.core.context_processors.tz`` context processor was added in this release. @@ -2160,8 +2140,6 @@ See also :setting:`TIME_ZONE`, :setting:`USE_I18N` and :setting:`USE_L10N`. USE_X_FORWARDED_HOST -------------------- -.. versionadded:: 1.3.1 - Default: ``False`` A boolean that specifies whether to use the X-Forwarded-Host header in diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 4b463e03ea..1312c64570 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -118,8 +118,6 @@ Arguments sent with this signal: records in the database as the database might not be in a consistent state yet. -.. versionadded:: 1.3 - ``using`` The database alias being used. @@ -155,8 +153,6 @@ Arguments sent with this signal: records in the database as the database might not be in a consistent state yet. -.. versionadded:: 1.3 - ``using`` The database alias being used. @@ -183,8 +179,6 @@ Arguments sent with this signal: ``instance`` The actual instance being deleted. -.. versionadded:: 1.3 - ``using`` The database alias being used. @@ -209,8 +203,6 @@ Arguments sent with this signal: Note that the object will no longer be in the database, so be very careful what you do with this instance. -.. versionadded:: 1.3 - ``using`` The database alias being used. @@ -271,8 +263,6 @@ Arguments sent with this signal: For the ``pre_clear`` and ``post_clear`` actions, this is ``None``. -.. versionadded:: 1.3 - ``using`` The database alias being used. diff --git a/docs/ref/template-response.txt b/docs/ref/template-response.txt index 9e09077adc..d9b7130362 100644 --- a/docs/ref/template-response.txt +++ b/docs/ref/template-response.txt @@ -2,8 +2,6 @@ TemplateResponse and SimpleTemplateResponse =========================================== -.. versionadded:: 1.3 - .. module:: django.template.response :synopsis: Classes dealing with lazy-rendered HTTP responses. diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index 48bd346788..f29d2acc12 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -160,11 +160,6 @@ it. Example:: >>> t.render(Context({"person": PersonClass2})) "My name is Samantha." -.. versionchanged:: 1.3 - Previously, only variables that originated with an attribute lookup would - be called by the template system. This change was made for consistency - across lookup types. - Callable variables are slightly more complex than variables which only require straight lookups. Here are some things to keep in mind: @@ -448,11 +443,6 @@ If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every ``django.contrib.auth.context_processors.PermWrapper``, representing the permissions that the currently logged-in user has. -.. versionchanged:: 1.3 - Prior to version 1.3, ``PermWrapper`` was located in - ``django.contrib.auth.context_processors``. - - django.core.context_processors.debug ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -491,8 +481,6 @@ django.core.context_processors.static .. function:: django.core.context_processors.static -.. versionadded:: 1.3 - If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every ``RequestContext`` will contain a variable ``STATIC_URL``, providing the value of the :setting:`STATIC_URL` setting. diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 072eebf69f..514953d666 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -156,8 +156,6 @@ In this syntax, each value gets interpreted as a literal string, and there's no way to specify variable values. Or literal commas. Or spaces. Did we mention you shouldn't use this syntax in any new projects? -.. versionadded:: 1.3 - By default, when you use the ``as`` keyword with the cycle tag, the usage of ``{% cycle %}`` that declares the cycle will itself output the first value in the cycle. This could be a problem if you want to @@ -676,9 +674,6 @@ including it. This example produces the output ``"Hello, John"``: {{ greeting }}, {{ person|default:"friend" }}! -.. versionchanged:: 1.3 - Additional context and exclusive context. - You can pass additional context to the template using keyword arguments:: {% include "name_snippet.html" with person="Jane" greeting="Hello" %} @@ -710,8 +705,6 @@ registered in ``somelibrary`` and ``otherlibrary`` located in package {% load somelibrary package.otherlibrary %} -.. versionchanged:: 1.3 - You can also selectively load individual filters or tags from a library, using the ``from`` argument. In this example, the template tags/filters named ``foo`` and ``bar`` will be loaded from ``somelibrary``:: @@ -1076,9 +1069,6 @@ which is rounded up to 88). with ^^^^ -.. versionchanged:: 1.3 - New keyword argument format and multiple variable assignments. - Caches a complex variable under a simpler name. This is useful when accessing an "expensive" method (e.g., one that hits the database) multiple times. @@ -2126,8 +2116,6 @@ For example:: If ``value`` is ``"http://www.example.org/foo?a=b&c=d"``, the output will be ``"http%3A//www.example.org/foo%3Fa%3Db%26c%3Dd"``. -.. versionadded:: 1.3 - An optional argument containing the characters which should not be escaped can be provided. diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index ef03d5479c..88372af149 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -860,8 +860,6 @@ How to log a user out Login and logout signals ------------------------ -.. versionadded:: 1.3 - The auth framework uses two :doc:`signals ` that can be used for notification when a user logs in or out. @@ -960,8 +958,6 @@ The login_required decorator context variable which stores the redirect path will use the value of ``redirect_field_name`` as its key rather than ``"next"`` (the default). - .. versionadded:: 1.3 - :func:`~django.contrib.auth.decorators.login_required` also takes an optional ``login_url`` parameter. Example:: @@ -1189,9 +1185,6 @@ includes a few other useful built-in views located in that can be used to reset the password, and sending that link to the user's registered email address. - .. versionchanged:: 1.3 - The ``from_email`` argument was added. - .. versionchanged:: 1.4 Users flagged with an unusable password (see :meth:`~django.contrib.auth.models.User.set_unusable_password()` @@ -1672,10 +1665,6 @@ The currently logged-in user's permissions are stored in the template variable :class:`django.contrib.auth.context_processors.PermWrapper`, which is a template-friendly proxy of permissions. -.. versionchanged:: 1.3 - Prior to version 1.3, ``PermWrapper`` was located in - ``django.core.context_processors``. - In the ``{{ perms }}`` object, single-attribute lookup is a proxy to :meth:`User.has_module_perms `. This example would display ``True`` if the logged-in user had any permissions @@ -1951,8 +1940,6 @@ for example, to control anonymous access. Authorization for inactive users ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionchanged:: 1.3 - An inactive user is a one that is authenticated but has its attribute ``is_active`` set to ``False``. However this does not mean they are not authorized to do anything. For example they are allowed to activate their diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index f13238e342..77d2de7fe0 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -51,13 +51,6 @@ Your cache preference goes in the :setting:`CACHES` setting in your settings file. Here's an explanation of all available values for :setting:`CACHES`. -.. versionchanged:: 1.3 - The settings used to configure caching changed in Django 1.3. In - Django 1.2 and earlier, you used a single string-based - :setting:`CACHE_BACKEND` setting to configure caches. This has - been replaced with the new dictionary-based :setting:`CACHES` - setting. - .. _memcached: Memcached @@ -83,9 +76,6 @@ two most common are `python-memcached`_ and `pylibmc`_. .. _`python-memcached`: ftp://ftp.tummy.com/pub/python-memcached/ .. _`pylibmc`: http://sendapatch.se/projects/pylibmc/ -.. versionchanged:: 1.3 - Support for ``pylibmc`` was added. - To use Memcached with Django: * Set :setting:`BACKEND ` to @@ -785,8 +775,6 @@ nonexistent cache key.:: Cache key prefixing ------------------- -.. versionadded:: 1.3 - If you are sharing a cache instance between servers, or between your production and development environments, it's possible for data cached by one server to be used by another server. If the format of cached @@ -807,8 +795,6 @@ collisions in cache values. Cache versioning ---------------- -.. versionadded:: 1.3 - When you change running code that uses cached values, you may need to purge any existing cached values. The easiest way to do this is to flush the entire cache, but this can lead to the loss of cache values @@ -856,8 +842,6 @@ keys unaffected. Continuing our previous example:: Cache key transformation ------------------------ -.. versionadded:: 1.3 - As described in the previous two sections, the cache key provided by a user is not used verbatim -- it is combined with the cache prefix and key version to provide a final cache key. By default, the three parts @@ -878,8 +862,6 @@ be used instead of the default key combining function. Cache key warnings ------------------ -.. versionadded:: 1.3 - Memcached, the most commonly-used production cache backend, does not allow cache keys longer than 250 characters or containing whitespace or control characters, and using such keys will cause an exception. To encourage @@ -966,10 +948,6 @@ mechanism should take into account when building its cache key. For example, if the contents of a Web page depend on a user's language preference, the page is said to "vary on language." -.. versionchanged:: 1.3 - In Django 1.3 the full request path -- including the query -- is used - to create the cache keys, instead of only the path component in Django 1.2. - By default, Django's cache system creates its cache keys using the requested path and query -- e.g., ``"/stories/2005/?order_by=author"``. This means every request to that URL will use the same cached version, regardless of user-agent diff --git a/docs/topics/class-based-views/generic-display.txt b/docs/topics/class-based-views/generic-display.txt index 0d4cb6244d..10279c0f63 100644 --- a/docs/topics/class-based-views/generic-display.txt +++ b/docs/topics/class-based-views/generic-display.txt @@ -4,11 +4,6 @@ Class-based generic views ========================= -.. note:: - Prior to Django 1.3, generic views were implemented as functions. The - function-based implementation has been removed in favor of the - class-based approach described here. - Writing Web applications can be monotonous, because we repeat certain patterns again and again. Django tries to take away some of that monotony at the model and template layers, but Web developers also experience this boredom at the view diff --git a/docs/topics/class-based-views/index.txt b/docs/topics/class-based-views/index.txt index 2d3e00ab4c..a738221892 100644 --- a/docs/topics/class-based-views/index.txt +++ b/docs/topics/class-based-views/index.txt @@ -2,8 +2,6 @@ Class-based views ================= -.. versionadded:: 1.3 - A view is a callable which takes a request and returns a response. This can be more than just a function, and Django provides an example of some classes which can be used as views. These allow you diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index f07769fb8a..f349c23626 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -2,8 +2,6 @@ Using mixins with class-based views =================================== -.. versionadded:: 1.3 - .. caution:: This is an advanced topic. A working knowledge of :doc:`Django's diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 5385b2a72d..dd160656c7 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -633,8 +633,6 @@ issue the query:: >>> Entry.objects.filter(authors__name=F('blog__name')) -.. versionadded:: 1.3 - For date and date/time fields, you can add or subtract a :class:`~datetime.timedelta` object. The following would return all entries that were modified more than 3 days after they were published:: @@ -876,7 +874,6 @@ it. For example:: # This will delete the Blog and all of its Entry objects. b.delete() -.. versionadded:: 1.3 This cascade behavior is customizable via the :attr:`~django.db.models.ForeignKey.on_delete` argument to the :class:`~django.db.models.ForeignKey`. diff --git a/docs/topics/db/sql.txt b/docs/topics/db/sql.txt index 19daffd464..310dcb5ae6 100644 --- a/docs/topics/db/sql.txt +++ b/docs/topics/db/sql.txt @@ -242,7 +242,7 @@ By default, the Python DB API will return results without their field names, which means you end up with a ``list`` of values, rather than a ``dict``. At a small performance cost, you can return results as a ``dict`` by using something like this:: - + def dictfetchall(cursor): "Returns all rows from a cursor as a dict" desc = cursor.description @@ -256,7 +256,7 @@ Here is an example of the difference between the two:: >>> cursor.execute("SELECT id, parent_id from test LIMIT 2"); >>> cursor.fetchall() ((54360982L, None), (54360880L, None)) - + >>> cursor.execute("SELECT id, parent_id from test LIMIT 2"); >>> dictfetchall(cursor) [{'parent_id': None, 'id': 54360982L}, {'parent_id': None, 'id': 54360880L}] @@ -273,11 +273,6 @@ transaction containing those calls is closed correctly. See :ref:`the notes on the requirements of Django's transaction handling ` for more details. -.. versionchanged:: 1.3 - -Prior to Django 1.3, it was necessary to manually mark a transaction -as dirty using ``transaction.set_dirty()`` when using raw SQL calls. - Connections and cursors ----------------------- diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 9928354664..4a52c5af35 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -66,9 +66,6 @@ database cursor (which is mapped to its own database connection internally). Controlling transaction management in views =========================================== -.. versionchanged:: 1.3 - Transaction management context managers are new in Django 1.3. - For most people, implicit request-based transactions work wonderfully. However, if you need more fine-grained control over how transactions are managed, you can use a set of functions in ``django.db.transaction`` to control transactions on a @@ -195,8 +192,6 @@ managers, too. Requirements for transaction handling ===================================== -.. versionadded:: 1.3 - Django requires that every transaction that is opened is closed before the completion of a request. If you are using :func:`autocommit` (the default commit mode) or :func:`commit_on_success`, this will be done diff --git a/docs/topics/email.txt b/docs/topics/email.txt index 0cc476e02c..b3d7254e7f 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -119,8 +119,6 @@ The "From:" header of the email will be the value of the This method exists for convenience and readability. -.. versionchanged:: 1.3 - If ``html_message`` is provided, the resulting email will be a :mimetype:`multipart/alternative` email with ``message`` as the :mimetype:`text/plain` content type and ``html_message`` as the @@ -236,9 +234,6 @@ following parameters (in the given order, if positional arguments are used). All parameters are optional and can be set at any time prior to calling the ``send()`` method. -.. versionchanged:: 1.3 - The ``cc`` argument was added. - * ``subject``: The subject line of the email. * ``body``: The body text. This should be a plain text message. diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 2a83172e17..7c1771b758 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -35,19 +35,9 @@ display two blank forms:: >>> ArticleFormSet = formset_factory(ArticleForm, extra=2) -.. versionchanged:: 1.3 - -Prior to Django 1.3, formset instances were not iterable. To render -the formset you iterated over the ``forms`` attribute:: - - >>> formset = ArticleFormSet() - >>> for form in formset.forms: - ... print(form.as_table()) - -Iterating over ``formset.forms`` will render the forms in the order -they were created. The default formset iterator also renders the forms -in this order, but you can change this order by providing an alternate -implementation for the :meth:`__iter__()` method. +Iterating over the ``formset`` will render the forms in the order they were +created. You can change this order by providing an alternate implementation for +the :meth:`__iter__()` method. Formsets can also be indexed into, which returns the corresponding form. If you override ``__iter__``, you will need to also override ``__getitem__`` to have diff --git a/docs/topics/forms/media.txt b/docs/topics/forms/media.txt index 29a7829799..98e70e5e77 100644 --- a/docs/topics/forms/media.txt +++ b/docs/topics/forms/media.txt @@ -195,8 +195,6 @@ return values for dynamic media properties. 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 diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index fe92bc59a9..a8347e52a0 100644 --- a/docs/topics/http/middleware.txt +++ b/docs/topics/http/middleware.txt @@ -117,8 +117,6 @@ middleware is always called on every response. ``process_template_response`` ----------------------------- -.. versionadded:: 1.3 - .. method:: process_template_response(self, request, response) ``request`` is an :class:`~django.http.HttpRequest` object. ``response`` is a diff --git a/docs/topics/http/shortcuts.txt b/docs/topics/http/shortcuts.txt index 10be353e80..0dc38b1459 100644 --- a/docs/topics/http/shortcuts.txt +++ b/docs/topics/http/shortcuts.txt @@ -17,8 +17,6 @@ introduce controlled coupling for convenience's sake. .. function:: render(request, template_name[, dictionary][, context_instance][, content_type][, status][, current_app]) - .. versionadded:: 1.3 - Combines a given template with a given context dictionary and returns an :class:`~django.http.HttpResponse` object with that rendered text. diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 69089af8e9..99afa13279 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -980,13 +980,6 @@ A :class:`ResolverMatch` object can also be assigned to a triple:: func, args, kwargs = resolve('/some/path/') -.. versionchanged:: 1.3 - Triple-assignment exists for backwards-compatibility. Prior to - Django 1.3, :func:`~django.core.urlresolvers.resolve` returned a - triple containing (view function, arguments, keyword arguments); - the :class:`ResolverMatch` object (as well as the namespace and pattern - information it provides) is not available in earlier Django releases. - One possible use of :func:`~django.core.urlresolvers.resolve` would be to test whether a view would raise a ``Http404`` error before redirecting to it:: diff --git a/docs/topics/i18n/formatting.txt b/docs/topics/i18n/formatting.txt index b09164769e..fc3f37de32 100644 --- a/docs/topics/i18n/formatting.txt +++ b/docs/topics/i18n/formatting.txt @@ -80,8 +80,6 @@ Template tags localize ~~~~~~~~ -.. versionadded:: 1.3 - Enables or disables localization of template variables in the contained block. @@ -116,8 +114,6 @@ Template filters localize ~~~~~~~~ -.. versionadded:: 1.3 - Forces localization of a single value. For example:: @@ -136,8 +132,6 @@ tag. unlocalize ~~~~~~~~~~ -.. versionadded:: 1.3 - Forces a single value to be printed without localization. For example:: diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index a7f48fe1fd..aaf728b1af 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -134,8 +134,6 @@ translations wouldn't be able to reorder placeholder text. Comments for translators ------------------------ -.. versionadded:: 1.3 - If you would like to give translators hints about a translatable string, you can add a comment prefixed with the ``Translators`` keyword on the line preceding the string, e.g.:: @@ -255,8 +253,6 @@ cardinality of the elements at play. Contextual markers ------------------ -.. versionadded:: 1.3 - Sometimes words have several meanings, such as ``"May"`` in English, which refers to a month name and to a verb. To enable translators to translate these words correctly in different contexts, you can use the @@ -436,8 +432,6 @@ Localized names of languages .. function:: get_language_info -.. versionadded:: 1.3 - The ``get_language_info()`` function provides detailed information about languages:: @@ -535,9 +529,6 @@ using the ``context`` keyword: ``blocktrans`` template tag --------------------------- -.. versionchanged:: 1.3 - New keyword argument format. - Contrarily to the :ttag:`trans` tag, the ``blocktrans`` tag allows you to mark complex sentences consisting of literals and variable content for translation by making use of placeholders:: @@ -664,8 +655,6 @@ string, so they don't need to be aware of translations. translator might translate the string ``"yes,no"`` as ``"ja,nein"`` (keeping the comma intact). -.. versionadded:: 1.3 - You can also retrieve information about any of the available languages using provided template tags and filters. To get information about a single language, use the ``{% get_language_info %}`` tag:: @@ -787,10 +776,6 @@ directories listed in :setting:`LOCALE_PATHS` have the highest precedence with the ones appearing first having higher precedence than the ones appearing later. -.. versionchanged:: 1.3 - Directories listed in :setting:`LOCALE_PATHS` weren't included in the - lookup algorithm until version 1.3. - Using the JavaScript translation catalog ---------------------------------------- diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index 28baf87522..94236babd6 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -2,8 +2,6 @@ Logging ======= -.. versionadded:: 1.3 - .. module:: django.utils.log :synopsis: Logging tools for Django applications diff --git a/docs/topics/signals.txt b/docs/topics/signals.txt index db1bcb03df..1078d0372c 100644 --- a/docs/topics/signals.txt +++ b/docs/topics/signals.txt @@ -132,10 +132,6 @@ Now, our ``my_callback`` function will be called each time a request finishes. Note that ``receiver`` can also take a list of signals to connect a function to. -.. versionadded:: 1.3 - -The ``receiver`` decorator was added in Django 1.3. - .. versionchanged:: 1.5 The ability to pass a list of signals was added. diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index c4c73733f5..7afdbe88cc 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -73,8 +73,6 @@ module defines tests in class-based approach. .. admonition:: unittest2 - .. versionchanged:: 1.3 - Python 2.7 introduced some major changes to the unittest library, adding some extremely useful features. To ensure that every Django project can benefit from these new features, Django ships with a @@ -436,8 +434,6 @@ two databases. Controlling creation order for test databases ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.3 - By default, Django will always create the ``default`` database first. However, no guarantees are made on the creation order of any other databases in your test setup. @@ -1001,8 +997,6 @@ Specifically, a ``Response`` object has the following attributes: The HTTP status of the response, as an integer. See :rfc:`2616#section-10` for a full list of HTTP status codes. - .. versionadded:: 1.3 - .. attribute:: templates A list of ``Template`` instances used to render the final content, in @@ -1089,8 +1083,6 @@ The request factory .. class:: RequestFactory -.. versionadded:: 1.3 - The :class:`~django.test.client.RequestFactory` shares the same API as the test client. However, instead of behaving like a browser, the RequestFactory provides a way to generate a request instance that can @@ -1327,8 +1319,6 @@ This means, instead of instantiating a ``Client`` in each test:: Customizing the test client ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.3 - .. attribute:: TestCase.client_class If you want to use a different ``Client`` class (for example, a subclass @@ -1708,8 +1698,6 @@ your test suite. .. method:: TestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True) - .. versionadded:: 1.3 - Asserts that a queryset ``qs`` returns a particular list of values ``values``. The comparison of the contents of ``qs`` and ``values`` is performed using @@ -1730,8 +1718,6 @@ your test suite. .. method:: TestCase.assertNumQueries(num, func, *args, **kwargs) - .. versionadded:: 1.3 - Asserts that when ``func`` is called with ``*args`` and ``**kwargs`` that ``num`` database queries are executed. @@ -1854,8 +1840,6 @@ Skipping tests .. currentmodule:: django.test -.. versionadded:: 1.3 - The unittest library provides the :func:`@skipIf ` and :func:`@skipUnless ` decorators to allow you to skip tests if you know ahead of time that those tests are going to fail under certain From 486e67598f8d8e83890ef3c90399d660d8e9eee6 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 21 Sep 2012 13:12:51 +0200 Subject: [PATCH 022/265] Fixed #10853 -- Skipped some sessions tests with dummy cache backend --- django/contrib/sessions/tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index 9aa602f416..fc2d8753d7 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -12,6 +12,7 @@ from django.contrib.sessions.backends.file import SessionStore as FileSession from django.contrib.sessions.backends.signed_cookies import SessionStore as CookieSession from django.contrib.sessions.models import Session from django.contrib.sessions.middleware import SessionMiddleware +from django.core.cache import DEFAULT_CACHE_ALIAS from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.http import HttpResponse from django.test import TestCase, RequestFactory @@ -133,6 +134,9 @@ class SessionTestsMixin(object): self.assertTrue(self.session.modified) def test_save(self): + if (hasattr(self.session, '_cache') and + 'DummyCache' in settings.CACHES[DEFAULT_CACHE_ALIAS]['BACKEND']): + raise unittest.SkipTest("Session saving tests require a real cache backend") self.session.save() self.assertTrue(self.session.exists(self.session.session_key)) @@ -296,6 +300,8 @@ class CacheDBSessionTests(SessionTestsMixin, TestCase): backend = CacheDBSession + @unittest.skipIf('DummyCache' in settings.CACHES[DEFAULT_CACHE_ALIAS]['BACKEND'], + "Session saving tests require a real cache backend") def test_exists_searches_cache_first(self): self.session.save() with self.assertNumQueries(0): From 2b1ae4dbd2b0389e1e412d68262cd2a9b8209a70 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Fri, 21 Sep 2012 16:22:50 -0700 Subject: [PATCH 023/265] Fixed #19008 typo in signals docs --- docs/ref/signals.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 1312c64570..0db540370d 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -46,7 +46,7 @@ pre_init .. ^^^^^^^ this :module: hack keeps Sphinx from prepending the module. -Whenever you instantiate a Django model,, this signal is sent at the beginning +Whenever you instantiate a Django model, this signal is sent at the beginning of the model's :meth:`~django.db.models.Model.__init__` method. Arguments sent with this signal: From 69ff1b7390e140f332a5aa55c44a091c838923fb Mon Sep 17 00:00:00 2001 From: Dan Loewenherz Date: Fri, 7 Sep 2012 12:42:06 -0400 Subject: [PATCH 024/265] Fixed #16835 -- add groups to auth.user admin list_filter --- django/contrib/auth/admin.py | 2 +- docs/releases/1.5.txt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py index ccf940d16d..5c08b0615f 100644 --- a/django/contrib/auth/admin.py +++ b/django/contrib/auth/admin.py @@ -54,7 +54,7 @@ class UserAdmin(admin.ModelAdmin): add_form = UserCreationForm change_password_form = AdminPasswordChangeForm list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') - list_filter = ('is_staff', 'is_superuser', 'is_active') + list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups') search_fields = ('username', 'first_name', 'last_name', 'email') ordering = ('username',) filter_horizontal = ('user_permissions',) diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 26b6ad1bfa..e2eac09237 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -116,6 +116,8 @@ Django 1.5 also includes several smaller improvements worth noting: * The :ref:`receiver ` decorator is now able to connect to more than one signal by supplying a list of signals. +* In the admin, you can now filter users by groups which they are members of. + * :meth:`QuerySet.bulk_create() ` now has a batch_size argument. By default the batch_size is unlimited except for SQLite where From 59afc18f3735abfd7ab4b1a904000a547daed4ca Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 22 Sep 2012 11:38:19 +0200 Subject: [PATCH 025/265] Made geo3d tests independent from each other --- django/contrib/gis/tests/geo3d/tests.py | 134 +++++++++++++++--------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/django/contrib/gis/tests/geo3d/tests.py b/django/contrib/gis/tests/geo3d/tests.py index dfd0e496ff..0aba38f5ca 100644 --- a/django/contrib/gis/tests/geo3d/tests.py +++ b/django/contrib/gis/tests/geo3d/tests.py @@ -1,16 +1,17 @@ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import os import re -from django.utils.unittest import TestCase from django.contrib.gis.db.models import Union, Extent3D from django.contrib.gis.geos import GEOSGeometry, Point, Polygon from django.contrib.gis.utils import LayerMapping, LayerMapError +from django.test import TestCase from .models import (City3D, Interstate2D, Interstate3D, InterstateProj2D, InterstateProj3D, Point2D, Point3D, MultiPoint3D, Polygon2D, Polygon3D) + data_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data')) city_file = os.path.join(data_path, 'cities', 'cities.shp') vrt_file = os.path.join(data_path, 'test_vrt', 'test_vrt.vrt') @@ -46,12 +47,11 @@ interstate_data = ( # Bounding box polygon for inner-loop of Houston (in projected coordinate # system 32140), with elevation values from the National Elevation Dataset # (see above). -bbox_wkt = 'POLYGON((941527.97 4225693.20,962596.48 4226349.75,963152.57 4209023.95,942051.75 4208366.38,941527.97 4225693.20))' -bbox_z = (21.71, 13.21, 9.12, 16.40, 21.71) -def gen_bbox(): - bbox_2d = GEOSGeometry(bbox_wkt, srid=32140) - bbox_3d = Polygon(tuple((x, y, z) for (x, y), z in zip(bbox_2d[0].coords, bbox_z)), srid=32140) - return bbox_2d, bbox_3d +bbox_data = ( + 'POLYGON((941527.97 4225693.20,962596.48 4226349.75,963152.57 4209023.95,942051.75 4208366.38,941527.97 4225693.20))', + (21.71, 13.21, 9.12, 16.40, 21.71) +) + class Geo3DTest(TestCase): """ @@ -63,20 +63,7 @@ class Geo3DTest(TestCase): http://postgis.refractions.net/documentation/manual-1.4/ch08.html#PostGIS_3D_Functions """ - def test01_3d(self): - "Test the creation of 3D models." - # 3D models for the rest of the tests will be populated in here. - # For each 3D data set create model (and 2D version if necessary), - # retrieve, and assert geometry is in 3D and contains the expected - # 3D values. - for name, pnt_data in city_data: - x, y, z = pnt_data - pnt = Point(x, y, z, srid=4326) - City3D.objects.create(name=name, point=pnt) - city = City3D.objects.get(name=name) - self.assertTrue(city.point.hasz) - self.assertEqual(z, city.point.z) - + def _load_interstate_data(self): # Interstate (2D / 3D and Geographic/Projected variants) for name, line, exp_z in interstate_data: line_3d = GEOSGeometry(line, srid=4269) @@ -90,26 +77,51 @@ class Geo3DTest(TestCase): Interstate2D.objects.create(name=name, line=line_2d) InterstateProj2D.objects.create(name=name, line=line_2d) - # Retrieving and making sure it's 3D and has expected - # Z values -- shouldn't change because of coordinate system. + def _load_city_data(self): + for name, pnt_data in city_data: + City3D.objects.create(name=name, point=Point(*pnt_data, srid=4326)) + + def _load_polygon_data(self): + bbox_wkt, bbox_z = bbox_data + bbox_2d = GEOSGeometry(bbox_wkt, srid=32140) + bbox_3d = Polygon(tuple((x, y, z) for (x, y), z in zip(bbox_2d[0].coords, bbox_z)), srid=32140) + Polygon2D.objects.create(name='2D BBox', poly=bbox_2d) + Polygon3D.objects.create(name='3D BBox', poly=bbox_3d) + + def test_3d_hasz(self): + """ + Make sure data is 3D and has expected Z values -- shouldn't change + because of coordinate system. + """ + self._load_interstate_data() + for name, line, exp_z in interstate_data: interstate = Interstate3D.objects.get(name=name) interstate_proj = InterstateProj3D.objects.get(name=name) for i in [interstate, interstate_proj]: self.assertTrue(i.line.hasz) self.assertEqual(exp_z, tuple(i.line.z)) - # Creating 3D Polygon. - bbox2d, bbox3d = gen_bbox() - Polygon2D.objects.create(name='2D BBox', poly=bbox2d) - Polygon3D.objects.create(name='3D BBox', poly=bbox3d) + self._load_city_data() + for name, pnt_data in city_data: + city = City3D.objects.get(name=name) + z = pnt_data[2] + self.assertTrue(city.point.hasz) + self.assertEqual(z, city.point.z) + + def test_3d_polygons(self): + """ + Test the creation of polygon 3D models. + """ + self._load_polygon_data() p3d = Polygon3D.objects.get(name='3D BBox') self.assertTrue(p3d.poly.hasz) - self.assertEqual(bbox3d, p3d.poly) - - def test01a_3d_layermapping(self): - "Testing LayerMapping on 3D models." - from .models import Point2D, Point3D + self.assertIsInstance(p3d.poly, Polygon) + self.assertEqual(p3d.poly.srid, 32140) + def test_3d_layermapping(self): + """ + Testing LayerMapping on 3D models. + """ point_mapping = {'point' : 'POINT'} mpoint_mapping = {'mpoint' : 'MULTIPOINT'} @@ -134,34 +146,46 @@ class Geo3DTest(TestCase): lm.save() self.assertEqual(3, MultiPoint3D.objects.count()) - def test02a_kml(self): - "Test GeoQuerySet.kml() with Z values." + def test_kml(self): + """ + Test GeoQuerySet.kml() with Z values. + """ + self._load_city_data() h = City3D.objects.kml(precision=6).get(name='Houston') # KML should be 3D. # `SELECT ST_AsKML(point, 6) FROM geo3d_city3d WHERE name = 'Houston';` ref_kml_regex = re.compile(r'^-95.363\d+,29.763\d+,18$') self.assertTrue(ref_kml_regex.match(h.kml)) - def test02b_geojson(self): - "Test GeoQuerySet.geojson() with Z values." + def test_geojson(self): + """ + Test GeoQuerySet.geojson() with Z values. + """ + self._load_city_data() h = City3D.objects.geojson(precision=6).get(name='Houston') # GeoJSON should be 3D # `SELECT ST_AsGeoJSON(point, 6) FROM geo3d_city3d WHERE name='Houston';` ref_json_regex = re.compile(r'^{"type":"Point","coordinates":\[-95.363151,29.763374,18(\.0+)?\]}$') self.assertTrue(ref_json_regex.match(h.geojson)) - def test03a_union(self): - "Testing the Union aggregate of 3D models." + def test_union(self): + """ + Testing the Union aggregate of 3D models. + """ # PostGIS query that returned the reference EWKT for this test: # `SELECT ST_AsText(ST_Union(point)) FROM geo3d_city3d;` + self._load_city_data() ref_ewkt = 'SRID=4326;MULTIPOINT(-123.305196 48.462611 15,-104.609252 38.255001 1433,-97.521157 34.464642 380,-96.801611 32.782057 147,-95.363151 29.763374 18,-95.23506 38.971823 251,-87.650175 41.850385 181,174.783117 -41.315268 14)' ref_union = GEOSGeometry(ref_ewkt) union = City3D.objects.aggregate(Union('point'))['point__union'] self.assertTrue(union.hasz) self.assertEqual(ref_union, union) - def test03b_extent(self): - "Testing the Extent3D aggregate for 3D models." + def test_extent(self): + """ + Testing the Extent3D aggregate for 3D models. + """ + self._load_city_data() # `SELECT ST_Extent3D(point) FROM geo3d_city3d;` ref_extent3d = (-123.305196, -41.315268, 14,174.783117, 48.462611, 1433) extent1 = City3D.objects.aggregate(Extent3D('point'))['point__extent3d'] @@ -174,8 +198,11 @@ class Geo3DTest(TestCase): for e3d in [extent1, extent2]: check_extent3d(e3d) - def test04_perimeter(self): - "Testing GeoQuerySet.perimeter() on 3D fields." + def test_perimeter(self): + """ + Testing GeoQuerySet.perimeter() on 3D fields. + """ + self._load_polygon_data() # Reference query for values below: # `SELECT ST_Perimeter3D(poly), ST_Perimeter2D(poly) FROM geo3d_polygon3d;` ref_perim_3d = 76859.2620451 @@ -188,12 +215,15 @@ class Geo3DTest(TestCase): Polygon3D.objects.perimeter().get(name='3D BBox').perimeter.m, tol) - def test05_length(self): - "Testing GeoQuerySet.length() on 3D fields." + def test_length(self): + """ + Testing GeoQuerySet.length() on 3D fields. + """ # ST_Length_Spheroid Z-aware, and thus does not need to use # a separate function internally. # `SELECT ST_Length_Spheroid(line, 'SPHEROID["GRS 1980",6378137,298.257222101]') # FROM geo3d_interstate[2d|3d];` + self._load_interstate_data() tol = 3 ref_length_2d = 4368.1721949481 ref_length_3d = 4368.62547052088 @@ -217,16 +247,22 @@ class Geo3DTest(TestCase): InterstateProj3D.objects.length().get(name='I-45').length.m, tol) - def test06_scale(self): - "Testing GeoQuerySet.scale() on Z values." + def test_scale(self): + """ + Testing GeoQuerySet.scale() on Z values. + """ + self._load_city_data() # Mapping of City name to reference Z values. zscales = (-3, 4, 23) for zscale in zscales: for city in City3D.objects.scale(1.0, 1.0, zscale): self.assertEqual(city_dict[city.name][2] * zscale, city.scale.z) - def test07_translate(self): - "Testing GeoQuerySet.translate() on Z values." + def test_translate(self): + """ + Testing GeoQuerySet.translate() on Z values. + """ + self._load_city_data() ztranslations = (5.23, 23, -17) for ztrans in ztranslations: for city in City3D.objects.translate(0, 0, ztrans): From baa33cd8faa16737524b1ac355802a10dd63571c Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 22 Sep 2012 11:45:51 +0200 Subject: [PATCH 026/265] Fixed #16218 -- date_list order in generic CBVs. Thanks nnrcschmdt for the report and bpeschier for the initial version of the patch. --- django/views/generic/dates.py | 6 +-- .../class-based-views/mixins-date-based.txt | 12 +++-- docs/releases/1.5.txt | 15 ++++++ tests/regressiontests/generic_views/dates.py | 50 +++++++++++++------ 4 files changed, 61 insertions(+), 22 deletions(-) diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py index 52e13a4533..e1b0eb99fe 100644 --- a/django/views/generic/dates.py +++ b/django/views/generic/dates.py @@ -377,7 +377,7 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): """ return self.date_list_period - def get_date_list(self, queryset, date_type=None): + def get_date_list(self, queryset, date_type=None, ordering='ASC'): """ Get a date list by calling `queryset.dates()`, checking along the way for empty lists that aren't allowed. @@ -387,7 +387,7 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): if date_type is None: date_type = self.get_date_list_period() - date_list = queryset.dates(date_field, date_type)[::-1] + date_list = queryset.dates(date_field, date_type, ordering) if date_list is not None and not date_list and not allow_empty: name = force_text(queryset.model._meta.verbose_name_plural) raise Http404(_("No %(verbose_name_plural)s available") % @@ -409,7 +409,7 @@ class BaseArchiveIndexView(BaseDateListView): Return (date_list, items, extra_context) for this request. """ qs = self.get_dated_queryset(ordering='-%s' % self.get_date_field()) - date_list = self.get_date_list(qs) + date_list = self.get_date_list(qs, ordering='DESC') if not date_list: qs = qs.none() diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index 01181ebb6c..561e525e70 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -318,12 +318,16 @@ BaseDateListView Returns the aggregation period for ``date_list``. Returns :attr:`~BaseDateListView.date_list_period` by default. - .. method:: get_date_list(queryset, date_type=None) + .. method:: get_date_list(queryset, date_type=None, ordering='ASC') 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. + :meth:`~BaseDateListView.get_date_list_period` is used. ``date_type`` + and ``ordering`` are simply passed to + :meth:`QuerySet.dates()`. + + .. versionchanged:: 1.5 + The ``ordering`` parameter was added, and the default order was + changed to ascending. diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index e2eac09237..528a44c5a1 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -152,6 +152,21 @@ year|date:"Y" }}``. ``next_year`` and ``previous_year`` were also added in the context. They are calculated according to ``allow_empty`` and ``allow_future``. +Context in year and month archive class-based views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:class:`~django.views.generic.dates.YearArchiveView` and +:class:`~django.views.generic.dates.MonthArchiveView` were documented to +provide a ``date_list`` sorted in ascending order in the context, like their +function-based predecessors, but it actually was in descending order. In 1.5, +the documented order was restored. You may want to add (or remove) the +``reversed`` keyword when you're iterating on ``date_list`` in a template:: + + {% for date in date_list reversed %} + +:class:`~django.views.generic.dates.ArchiveIndexView` still provides a +``date_list`` in descending order. + Context in TemplateView ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/generic_views/dates.py b/tests/regressiontests/generic_views/dates.py index c2fa71b376..0c565daf9f 100644 --- a/tests/regressiontests/generic_views/dates.py +++ b/tests/regressiontests/generic_views/dates.py @@ -23,29 +23,30 @@ requires_tz_support = skipUnless(TZ_SUPPORT, "time zone, but your operating system isn't able to do that.") +def _make_books(n, base_date): + for i in range(n): + b = Book.objects.create( + name='Book %d' % i, + slug='book-%d' % i, + pages=100+i, + pubdate=base_date - datetime.timedelta(days=i)) + class ArchiveIndexViewTests(TestCase): fixtures = ['generic-views-test-data.json'] urls = 'regressiontests.generic_views.urls' - def _make_books(self, n, base_date): - for i in range(n): - b = Book.objects.create( - name='Book %d' % i, - slug='book-%d' % i, - pages=100+i, - pubdate=base_date - datetime.timedelta(days=1)) def test_archive_view(self): res = self.client.get('/dates/books/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) self.assertEqual(list(res.context['latest']), list(Book.objects.all())) self.assertTemplateUsed(res, 'generic_views/book_archive.html') def test_archive_view_context_object_name(self): res = self.client.get('/dates/books/context_object_name/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) self.assertEqual(list(res.context['thingies']), list(Book.objects.all())) self.assertFalse('latest' in res.context) self.assertTemplateUsed(res, 'generic_views/book_archive.html') @@ -65,14 +66,14 @@ class ArchiveIndexViewTests(TestCase): def test_archive_view_template(self): res = self.client.get('/dates/books/template_name/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) self.assertEqual(list(res.context['latest']), list(Book.objects.all())) self.assertTemplateUsed(res, 'generic_views/list.html') def test_archive_view_template_suffix(self): res = self.client.get('/dates/books/template_name_suffix/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) self.assertEqual(list(res.context['latest']), list(Book.objects.all())) self.assertTemplateUsed(res, 'generic_views/book_detail.html') @@ -82,13 +83,13 @@ class ArchiveIndexViewTests(TestCase): def test_archive_view_by_month(self): res = self.client.get('/dates/books/by_month/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'month')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'month', 'DESC'))) def test_paginated_archive_view(self): - self._make_books(20, base_date=datetime.date.today()) + _make_books(20, base_date=datetime.date.today()) res = self.client.get('/dates/books/paginated/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) self.assertEqual(list(res.context['latest']), list(Book.objects.all()[0:10])) self.assertTemplateUsed(res, 'generic_views/book_archive.html') @@ -99,7 +100,7 @@ class ArchiveIndexViewTests(TestCase): def test_paginated_archive_view_does_not_load_entire_table(self): # Regression test for #18087 - self._make_books(20, base_date=datetime.date.today()) + _make_books(20, base_date=datetime.date.today()) # 1 query for years list + 1 query for books with self.assertNumQueries(2): self.client.get('/dates/books/') @@ -124,6 +125,13 @@ class ArchiveIndexViewTests(TestCase): res = self.client.get('/dates/booksignings/') self.assertEqual(res.status_code, 200) + def test_date_list_order(self): + """date_list should be sorted descending in index""" + _make_books(5, base_date=datetime.date(2011, 12, 25)) + res = self.client.get('/dates/books/') + self.assertEqual(res.status_code, 200) + self.assertEqual(list(res.context['date_list']), list(reversed(sorted(res.context['date_list'])))) + class YearArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] @@ -202,6 +210,12 @@ class YearArchiveViewTests(TestCase): res = self.client.get('/dates/booksignings/2008/') self.assertEqual(res.status_code, 200) + def test_date_list_order(self): + """date_list should be sorted ascending in year view""" + _make_books(10, base_date=datetime.date(2011, 12, 25)) + res = self.client.get('/dates/books/2011/') + self.assertEqual(list(res.context['date_list']), list(sorted(res.context['date_list']))) + class MonthArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] @@ -322,6 +336,12 @@ class MonthArchiveViewTests(TestCase): res = self.client.get('/dates/booksignings/2008/apr/') self.assertEqual(res.status_code, 200) + def test_date_list_order(self): + """date_list should be sorted ascending in month view""" + _make_books(10, base_date=datetime.date(2011, 12, 25)) + res = self.client.get('/dates/books/2011/dec/') + self.assertEqual(list(res.context['date_list']), list(sorted(res.context['date_list']))) + class WeekArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] From 822cfce3df53301d9f9f4c14bd8a0cb2a1956e2e Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 22 Sep 2012 12:02:21 +0200 Subject: [PATCH 027/265] Fixed #18951 -- Formatting of microseconds. Thanks olofom at gmail com for the report. --- django/utils/dateformat.py | 4 ++-- docs/ref/templates/builtins.txt | 2 +- tests/regressiontests/utils/dateformat.py | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index 6a91a370e5..b2586ba1ff 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -110,8 +110,8 @@ class TimeFormat(Formatter): return '%02d' % self.data.second def u(self): - "Microseconds" - return self.data.microsecond + "Microseconds; i.e. '000000' to '999999'" + return '%06d' %self.data.microsecond class DateFormat(TimeFormat): diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 514953d666..07ac284905 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -1251,7 +1251,7 @@ S English ordinal suffix for day of the ``'st'``, ``'nd'``, month, 2 characters. t Number of days in the given month. ``28`` to ``31`` T Time zone of this machine. ``'EST'``, ``'MDT'`` -u Microseconds. ``0`` to ``999999`` +u Microseconds. ``000000`` to ``999999`` U Seconds since the Unix Epoch (January 1 1970 00:00:00 UTC). w Day of the week, digits without ``'0'`` (Sunday) to ``'6'`` (Saturday) diff --git a/tests/regressiontests/utils/dateformat.py b/tests/regressiontests/utils/dateformat.py index 0f18bb2a4d..0f4fd67f6f 100644 --- a/tests/regressiontests/utils/dateformat.py +++ b/tests/regressiontests/utils/dateformat.py @@ -72,6 +72,11 @@ class DateFormatTests(unittest.TestCase): self.assertEqual(dateformat.format(my_birthday, 'a'), 'p.m.') + def test_microsecond(self): + # Regression test for #18951 + dt = datetime(2009, 5, 16, microsecond=123) + self.assertEqual(dateformat.format(dt, 'u'), '000123') + def test_date_formats(self): my_birthday = datetime(1979, 7, 8, 22, 00) timestamp = datetime(2008, 5, 19, 11, 45, 23, 123456) From 2aaa467a2ab57d5616d384a70e2b6f8217ece63e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 22 Sep 2012 07:08:40 -0400 Subject: [PATCH 028/265] Fixed #18057 - Documented that caches are not cleared after each test; thanks guettli for the suggestion. --- docs/topics/testing.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index 7afdbe88cc..117dfbe591 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -508,6 +508,13 @@ file, all Django tests run with :setting:`DEBUG`\=False. This is to ensure that the observed output of your code matches what will be seen in a production setting. +Caches are not cleared after each test, and running "manage.py test fooapp" can +insert data from the tests into the cache of a live system if you run your +tests in production because, unlike databases, a separate "test cache" is not +used. This behavior `may change`_ in the future. + +.. _may change: https://code.djangoproject.com/ticket/11505 + Understanding the test output ----------------------------- From 0ab8c58ca896643ea0bb0830a96274160d14cc69 Mon Sep 17 00:00:00 2001 From: Brian Galey Date: Sat, 22 Sep 2012 15:10:42 +0200 Subject: [PATCH 029/265] Fixed #18968 -- Only use separate GML regex for SpatiaLite < 3.0 --- django/contrib/gis/tests/geoapp/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index cd3cec3074..7fc870f64b 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -520,8 +520,8 @@ class GeoQuerySetTest(TestCase): if oracle: # No precision parameter for Oracle :-/ gml_regex = re.compile(r'^-104.60925\d+,38.25500\d+ ') - elif spatialite: - # Spatialite has extra colon in SrsName + elif spatialite and connection.ops.spatial_version < (3, 0, 0): + # Spatialite before 3.0 has extra colon in SrsName gml_regex = re.compile(r'^-104.609251\d+,38.255001') else: gml_regex = re.compile(r'^-104\.60925\d+,38\.255001') From 8599f64e54adfb32ee6550ed7a6ec9944034d978 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 22 Sep 2012 15:17:13 +0200 Subject: [PATCH 030/265] Fixed #18861 -- Triggered message validation with locmem email backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Bruno Renié for the report and the initial patch. --- django/core/mail/backends/locmem.py | 2 ++ tests/regressiontests/mail/tests.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/django/core/mail/backends/locmem.py b/django/core/mail/backends/locmem.py index 642bfc49fb..6826d09ee5 100644 --- a/django/core/mail/backends/locmem.py +++ b/django/core/mail/backends/locmem.py @@ -20,5 +20,7 @@ class EmailBackend(BaseEmailBackend): def send_messages(self, messages): """Redirect messages to the dummy outbox""" + for message in messages: # .message() triggers header validation + message.message() mail.outbox.extend(messages) return len(messages) diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index 3e9ae84650..33898cc1d5 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -498,6 +498,11 @@ class LocmemBackendTests(BaseEmailBackendTests, TestCase): connection2.send_messages([email]) self.assertEqual(len(mail.outbox), 2) + def test_validate_multiline_headers(self): + # Ticket #18861 - Validate emails when using the locmem backend + with self.assertRaises(BadHeaderError): + send_mail('Subject\nMultiline', 'Content', 'from@example.com', ['to@example.com']) + class FileBackendTests(BaseEmailBackendTests, TestCase): email_backend = 'django.core.mail.backends.filebased.EmailBackend' From 3174b5f2f5bb0b0a6b775a1a50464b6bf2a4b067 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 23 Sep 2012 13:25:47 +0200 Subject: [PATCH 031/265] Fixed #18982 - Caught TypeError in DateField.clean Thanks gwahl at fusionbox com. --- django/forms/fields.py | 2 +- tests/regressiontests/forms/tests/fields.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/django/forms/fields.py b/django/forms/fields.py index 124e4f669a..0075325288 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -341,7 +341,7 @@ class BaseTemporalField(Field): for format in self.input_formats: try: return self.strptime(value, format) - except ValueError: + except (ValueError, TypeError): continue raise ValidationError(self.error_messages['invalid']) diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index 197ce1abd9..989acbc496 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -356,6 +356,11 @@ class FieldsTests(SimpleTestCase): self.assertEqual(datetime.date(2006, 10, 25), f.clean(' 25 October 2006 ')) self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, ' ') + def test_datefield_5(self): + # Test null bytes (#18982) + f = DateField() + self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, 'a\x00b') + # TimeField ################################################################### def test_timefield_1(self): From 8cdc84726e13da4c796db614765658835d4786a1 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 23 Sep 2012 19:55:52 +0200 Subject: [PATCH 032/265] [py3] Added buffer/memoryview compatibility Even if buffer and memoryview are not strictly identical, it should be safe to consider them equivalent for GIS support. Thanks Aymeric Augustin for the review. --- django/contrib/gis/__init__.py | 6 +++++ django/contrib/gis/db/models/proxy.py | 3 ++- django/contrib/gis/db/models/query.py | 3 ++- django/contrib/gis/gdal/geometries.py | 8 ++++--- django/contrib/gis/geos/factory.py | 3 ++- django/contrib/gis/geos/geometry.py | 7 +++--- django/contrib/gis/geos/prototypes/io.py | 7 +++--- django/contrib/gis/geos/tests/test_geos.py | 9 ++++---- django/contrib/gis/geos/tests/test_io.py | 26 ++++++++++++---------- 9 files changed, 44 insertions(+), 28 deletions(-) diff --git a/django/contrib/gis/__init__.py b/django/contrib/gis/__init__.py index e69de29bb2..c996fdfdc2 100644 --- a/django/contrib/gis/__init__.py +++ b/django/contrib/gis/__init__.py @@ -0,0 +1,6 @@ +from django.utils import six + +if six.PY3: + memoryview = memoryview +else: + memoryview = buffer diff --git a/django/contrib/gis/db/models/proxy.py b/django/contrib/gis/db/models/proxy.py index 413610fc5c..1fdc5036ba 100644 --- a/django/contrib/gis/db/models/proxy.py +++ b/django/contrib/gis/db/models/proxy.py @@ -5,6 +5,7 @@ corresponding to geographic model fields. Thanks to Robert Coup for providing this functionality (see #4322). """ +from django.contrib.gis import memoryview from django.utils import six class GeometryProxy(object): @@ -54,7 +55,7 @@ class GeometryProxy(object): if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'): # Assigning the SRID to the geometry. if value.srid is None: value.srid = self._field.srid - elif value is None or isinstance(value, six.string_types + (buffer,)): + elif value is None or isinstance(value, six.string_types + (memoryview,)): # Set with None, WKT, HEX, or WKB pass else: diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index d87e151aea..b994252002 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -1,6 +1,7 @@ from django.db import connections from django.db.models.query import QuerySet, ValuesQuerySet, ValuesListQuerySet +from django.contrib.gis import memoryview from django.contrib.gis.db.models import aggregates from django.contrib.gis.db.models.fields import get_srid_info, PointField, LineStringField from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery @@ -676,7 +677,7 @@ class GeoQuerySet(QuerySet): if not backend.geography: if not isinstance(geo_field, PointField): raise ValueError('Spherical distance calculation only supported on PointFields.') - if not str(Geometry(buffer(params[0].ewkb)).geom_type) == 'Point': + if not str(Geometry(memoryview(params[0].ewkb)).geom_type) == 'Point': raise ValueError('Spherical distance calculation only supported with Point Geometry parameters') # The `function` procedure argument needs to be set differently for # geodetic distance calculations. diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py index 373ece777d..1985062c56 100644 --- a/django/contrib/gis/gdal/geometries.py +++ b/django/contrib/gis/gdal/geometries.py @@ -43,6 +43,8 @@ import sys from binascii import a2b_hex, b2a_hex from ctypes import byref, string_at, c_char_p, c_double, c_ubyte, c_void_p +from django.contrib.gis import memoryview + # Getting GDAL prerequisites from django.contrib.gis.gdal.base import GDALBase from django.contrib.gis.gdal.envelope import Envelope, OGREnvelope @@ -76,7 +78,7 @@ class OGRGeometry(GDALBase): # If HEX, unpack input to to a binary buffer. if str_instance and hex_regex.match(geom_input): - geom_input = buffer(a2b_hex(geom_input.upper())) + geom_input = memoryview(a2b_hex(geom_input.upper())) str_instance = False # Constructing the geometry, @@ -106,7 +108,7 @@ class OGRGeometry(GDALBase): # (e.g., 'Point', 'POLYGON'). ogr_t = OGRGeomType(geom_input) g = capi.create_geom(OGRGeomType(geom_input).num) - elif isinstance(geom_input, buffer): + elif isinstance(geom_input, memoryview): # WKB was passed in g = capi.from_wkb(str(geom_input), None, byref(c_void_p()), len(geom_input)) elif isinstance(geom_input, OGRGeomType): @@ -354,7 +356,7 @@ class OGRGeometry(GDALBase): buf = (c_ubyte * sz)() wkb = capi.to_wkb(self.ptr, byteorder, byref(buf)) # Returning a buffer of the string at the pointer. - return buffer(string_at(buf, sz)) + return memoryview(string_at(buf, sz)) @property def wkt(self): diff --git a/django/contrib/gis/geos/factory.py b/django/contrib/gis/geos/factory.py index fbd7d5a3e9..42b0310f0a 100644 --- a/django/contrib/gis/geos/factory.py +++ b/django/contrib/gis/geos/factory.py @@ -1,3 +1,4 @@ +from django.contrib.gis import memoryview from django.contrib.gis.geos.geometry import GEOSGeometry, wkt_regex, hex_regex from django.utils import six @@ -18,7 +19,7 @@ def fromfile(file_h): if wkt_regex.match(buf) or hex_regex.match(buf): return GEOSGeometry(buf) else: - return GEOSGeometry(buffer(buf)) + return GEOSGeometry(memoryview(buf)) def fromstr(string, **kwargs): "Given a string value, returns a GEOSGeometry object." diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index 4e5409de1d..41bf4dfa1f 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -5,6 +5,7 @@ # Python, ctypes and types dependencies. from ctypes import addressof, byref, c_double +from django.contrib.gis import memoryview # super-class for mutable list behavior from django.contrib.gis.geos.mutable_list import ListMixin @@ -75,7 +76,7 @@ class GEOSGeometry(GEOSBase, ListMixin): elif isinstance(geo_input, GEOM_PTR): # When the input is a pointer to a geomtry (GEOM_PTR). g = geo_input - elif isinstance(geo_input, buffer): + elif isinstance(geo_input, memoryview): # When the input is a buffer (WKB). g = wkb_r().read(geo_input) elif isinstance(geo_input, GEOSGeometry): @@ -139,12 +140,12 @@ class GEOSGeometry(GEOSBase, ListMixin): def __getstate__(self): # The pickled state is simply a tuple of the WKB (in string form) # and the SRID. - return str(self.wkb), self.srid + return bytes(self.wkb), self.srid def __setstate__(self, state): # Instantiating from the tuple state that was pickled. wkb, srid = state - ptr = wkb_r().read(buffer(wkb)) + ptr = wkb_r().read(memoryview(wkb)) if not ptr: raise GEOSException('Invalid Geometry loaded from pickled state.') self.ptr = ptr self._post_init(srid) diff --git a/django/contrib/gis/geos/prototypes/io.py b/django/contrib/gis/geos/prototypes/io.py index 053b9948a2..4a774f9589 100644 --- a/django/contrib/gis/geos/prototypes/io.py +++ b/django/contrib/gis/geos/prototypes/io.py @@ -1,5 +1,6 @@ import threading from ctypes import byref, c_char_p, c_int, c_char, c_size_t, Structure, POINTER +from django.contrib.gis import memoryview from django.contrib.gis.geos.base import GEOSBase from django.contrib.gis.geos.libgeos import GEOM_PTR from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_string, check_sized_string @@ -130,8 +131,8 @@ class _WKBReader(IOBase): def read(self, wkb): "Returns a _pointer_ to C GEOS Geometry object from the given WKB." - if isinstance(wkb, buffer): - wkb_s = str(wkb) + if isinstance(wkb, memoryview): + wkb_s = bytes(wkb) return wkb_reader_read(self.ptr, wkb_s, len(wkb_s)) elif isinstance(wkb, six.string_types): return wkb_reader_read_hex(self.ptr, wkb, len(wkb)) @@ -155,7 +156,7 @@ class WKBWriter(IOBase): def write(self, geom): "Returns the WKB representation of the given geometry." - return buffer(wkb_writer_write(self.ptr, geom.ptr, byref(c_size_t()))) + return memoryview(wkb_writer_write(self.ptr, geom.ptr, byref(c_size_t()))) def write_hex(self, geom): "Returns the HEXEWKB representation of the given geometry." diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index 7300ab9c63..2e873b4cd9 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -2,6 +2,7 @@ import ctypes import json import random +from django.contrib.gis import memoryview from django.contrib.gis.geos import (GEOSException, GEOSIndexError, GEOSGeometry, GeometryCollection, Point, MultiPoint, Polygon, MultiPolygon, LinearRing, LineString, MultiLineString, fromfile, fromstr, geos_version_info) @@ -118,9 +119,9 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.fail('Should have raised GEOSException.') # Same for EWKB. - self.assertEqual(buffer(a2b_hex(hexewkb_2d)), pnt_2d.ewkb) + self.assertEqual(memoryview(a2b_hex(hexewkb_2d)), pnt_2d.ewkb) if GEOS_PREPARE: - self.assertEqual(buffer(a2b_hex(hexewkb_3d)), pnt_3d.ewkb) + self.assertEqual(memoryview(a2b_hex(hexewkb_3d)), pnt_3d.ewkb) else: try: ewkb = pnt_3d.ewkb @@ -150,7 +151,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): pass # Bad WKB - self.assertRaises(GEOSException, GEOSGeometry, buffer('0')) + self.assertRaises(GEOSException, GEOSGeometry, memoryview(b'0')) print("\nEND - expecting GEOS_ERROR; safe to ignore.\n") @@ -182,7 +183,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): "Testing creation from WKB." from binascii import a2b_hex for g in self.geometries.hex_wkt: - wkb = buffer(a2b_hex(g.hex)) + wkb = memoryview(a2b_hex(g.hex)) geom_h = GEOSGeometry(wkb) # we need to do this so decimal places get normalised geom_t = fromstr(g.wkt) diff --git a/django/contrib/gis/geos/tests/test_io.py b/django/contrib/gis/geos/tests/test_io.py index ebf178a807..1e604e506b 100644 --- a/django/contrib/gis/geos/tests/test_io.py +++ b/django/contrib/gis/geos/tests/test_io.py @@ -1,5 +1,7 @@ import binascii import unittest + +from django.contrib.gis import memoryview from django.contrib.gis.geos import GEOSGeometry, WKTReader, WKTWriter, WKBReader, WKBWriter, geos_version_info from django.utils import six @@ -20,7 +22,7 @@ class GEOSIOTest(unittest.TestCase): # Should only accept six.string_types objects. self.assertRaises(TypeError, wkt_r.read, 1) - self.assertRaises(TypeError, wkt_r.read, buffer('foo')) + self.assertRaises(TypeError, wkt_r.read, memoryview(b'foo')) def test02_wktwriter(self): # Creating a WKTWriter instance, testing its ptr property. @@ -29,14 +31,14 @@ class GEOSIOTest(unittest.TestCase): ref = GEOSGeometry('POINT (5 23)') ref_wkt = 'POINT (5.0000000000000000 23.0000000000000000)' - self.assertEqual(ref_wkt, wkt_w.write(ref)) + self.assertEqual(ref_wkt, wkt_w.write(ref).decode()) def test03_wkbreader(self): # Creating a WKBReader instance wkb_r = WKBReader() - hex = '000000000140140000000000004037000000000000' - wkb = buffer(binascii.a2b_hex(hex)) + hex = b'000000000140140000000000004037000000000000' + wkb = memoryview(binascii.a2b_hex(hex)) ref = GEOSGeometry(hex) # read() should return a GEOSGeometry on either a hex string or @@ -56,10 +58,10 @@ class GEOSIOTest(unittest.TestCase): # Representations of 'POINT (5 23)' in hex -- one normal and # the other with the byte order changed. g = GEOSGeometry('POINT (5 23)') - hex1 = '010100000000000000000014400000000000003740' - wkb1 = buffer(binascii.a2b_hex(hex1)) - hex2 = '000000000140140000000000004037000000000000' - wkb2 = buffer(binascii.a2b_hex(hex2)) + hex1 = b'010100000000000000000014400000000000003740' + wkb1 = memoryview(binascii.a2b_hex(hex1)) + hex2 = b'000000000140140000000000004037000000000000' + wkb2 = memoryview(binascii.a2b_hex(hex2)) self.assertEqual(hex1, wkb_w.write_hex(g)) self.assertEqual(wkb1, wkb_w.write(g)) @@ -81,10 +83,10 @@ class GEOSIOTest(unittest.TestCase): g = GEOSGeometry('POINT (5 23 17)') g.srid = 4326 - hex3d = '0101000080000000000000144000000000000037400000000000003140' - wkb3d = buffer(binascii.a2b_hex(hex3d)) - hex3d_srid = '01010000A0E6100000000000000000144000000000000037400000000000003140' - wkb3d_srid = buffer(binascii.a2b_hex(hex3d_srid)) + hex3d = b'0101000080000000000000144000000000000037400000000000003140' + wkb3d = memoryview(binascii.a2b_hex(hex3d)) + hex3d_srid = b'01010000A0E6100000000000000000144000000000000037400000000000003140' + wkb3d_srid = memoryview(binascii.a2b_hex(hex3d_srid)) # Ensuring bad output dimensions are not accepted for bad_outdim in (-1, 0, 1, 4, 423, 'foo', None): From 5330cd50cdb0674cd08cbfb40af9ac51267fbbec Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 23 Sep 2012 19:59:27 +0200 Subject: [PATCH 033/265] [py3] Fixed GEOS/GDAL tests --- django/contrib/gis/gdal/__init__.py | 2 +- django/contrib/gis/gdal/datasource.py | 5 +-- django/contrib/gis/gdal/driver.py | 3 +- django/contrib/gis/gdal/envelope.py | 2 +- django/contrib/gis/gdal/feature.py | 6 ++-- django/contrib/gis/gdal/geometries.py | 17 ++++------ django/contrib/gis/gdal/layer.py | 5 +-- django/contrib/gis/gdal/libgdal.py | 6 ++-- .../contrib/gis/gdal/prototypes/errcheck.py | 11 ++++--- django/contrib/gis/gdal/srs.py | 23 +++++++------ django/contrib/gis/gdal/tests/test_ds.py | 4 +-- django/contrib/gis/gdal/tests/test_geom.py | 4 +-- django/contrib/gis/geometry/test_data.py | 2 +- django/contrib/gis/geos/factory.py | 17 +++++++--- django/contrib/gis/geos/geometry.py | 24 +++++++------- django/contrib/gis/geos/libgeos.py | 2 ++ django/contrib/gis/geos/prototypes/io.py | 12 ++++--- django/contrib/gis/geos/tests/test_geos.py | 32 +++++++++---------- django/contrib/gis/geos/tests/test_io.py | 9 ++++-- 19 files changed, 106 insertions(+), 80 deletions(-) diff --git a/django/contrib/gis/gdal/__init__.py b/django/contrib/gis/gdal/__init__.py index adff96b47a..f477f05982 100644 --- a/django/contrib/gis/gdal/__init__.py +++ b/django/contrib/gis/gdal/__init__.py @@ -41,7 +41,7 @@ try: from django.contrib.gis.gdal.srs import SpatialReference, CoordTransform from django.contrib.gis.gdal.geometries import OGRGeometry HAS_GDAL = True -except: +except ImportError: HAS_GDAL = False try: diff --git a/django/contrib/gis/gdal/datasource.py b/django/contrib/gis/gdal/datasource.py index 4ceddc6c72..fa3d86afba 100644 --- a/django/contrib/gis/gdal/datasource.py +++ b/django/contrib/gis/gdal/datasource.py @@ -45,6 +45,7 @@ from django.contrib.gis.gdal.layer import Layer # Getting the ctypes prototypes for the DataSource. from django.contrib.gis.gdal.prototypes import ds as capi +from django.utils.encoding import force_bytes from django.utils import six from django.utils.six.moves import xrange @@ -73,7 +74,7 @@ class DataSource(GDALBase): ds_driver = Driver.ptr_type() try: # OGROpen will auto-detect the data source type. - ds = capi.open_ds(ds_input, self._write, byref(ds_driver)) + ds = capi.open_ds(force_bytes(ds_input), self._write, byref(ds_driver)) except OGRException: # Making the error message more clear rather than something # like "Invalid pointer returned from OGROpen". @@ -102,7 +103,7 @@ class DataSource(GDALBase): def __getitem__(self, index): "Allows use of the index [] operator to get a layer at the index." if isinstance(index, six.string_types): - l = capi.get_layer_by_name(self.ptr, index) + l = capi.get_layer_by_name(self.ptr, force_bytes(index)) if not l: raise OGRIndexError('invalid OGR Layer name given: "%s"' % index) elif isinstance(index, int): if index < 0 or index >= self.layer_count: diff --git a/django/contrib/gis/gdal/driver.py b/django/contrib/gis/gdal/driver.py index de4dc61c63..55a5d77d66 100644 --- a/django/contrib/gis/gdal/driver.py +++ b/django/contrib/gis/gdal/driver.py @@ -5,6 +5,7 @@ from django.contrib.gis.gdal.error import OGRException from django.contrib.gis.gdal.prototypes import ds as capi from django.utils import six +from django.utils.encoding import force_bytes # For more information, see the OGR C API source code: # http://www.gdal.org/ogr/ogr__api_8h.html @@ -36,7 +37,7 @@ class Driver(GDALBase): name = dr_input # Attempting to get the OGR driver by the string name. - dr = capi.get_driver_by_name(name) + dr = capi.get_driver_by_name(force_bytes(name)) elif isinstance(dr_input, int): self._register() dr = capi.get_driver(dr_input) diff --git a/django/contrib/gis/gdal/envelope.py b/django/contrib/gis/gdal/envelope.py index ab0940e476..f145526af0 100644 --- a/django/contrib/gis/gdal/envelope.py +++ b/django/contrib/gis/gdal/envelope.py @@ -52,7 +52,7 @@ class Envelope(object): elif len(args) == 4: # Individual parameters passed in. # Thanks to ww for the help - self._from_sequence(map(float, args)) + self._from_sequence([float(a) for a in args]) else: raise OGRException('Incorrect number (%d) of arguments.' % len(args)) diff --git a/django/contrib/gis/gdal/feature.py b/django/contrib/gis/gdal/feature.py index 292004873d..b8737cd1a0 100644 --- a/django/contrib/gis/gdal/feature.py +++ b/django/contrib/gis/gdal/feature.py @@ -7,6 +7,7 @@ from django.contrib.gis.gdal.geometries import OGRGeometry, OGRGeomType # ctypes function prototypes from django.contrib.gis.gdal.prototypes import ds as capi, geom as geom_api +from django.utils.encoding import force_bytes from django.utils import six from django.utils.six.moves import xrange @@ -107,6 +108,7 @@ class Feature(GDALBase): def index(self, field_name): "Returns the index of the given field name." - i = capi.get_field_index(self.ptr, field_name) - if i < 0: raise OGRIndexError('invalid OFT field name given: "%s"' % field_name) + i = capi.get_field_index(self.ptr, force_bytes(field_name)) + if i < 0: + raise OGRIndexError('invalid OFT field name given: "%s"' % field_name) return i diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py index 1985062c56..eb67059245 100644 --- a/django/contrib/gis/gdal/geometries.py +++ b/django/contrib/gis/gdal/geometries.py @@ -78,16 +78,11 @@ class OGRGeometry(GDALBase): # If HEX, unpack input to to a binary buffer. if str_instance and hex_regex.match(geom_input): - geom_input = memoryview(a2b_hex(geom_input.upper())) + geom_input = memoryview(a2b_hex(geom_input.upper().encode())) str_instance = False # Constructing the geometry, if str_instance: - # Checking if unicode - if isinstance(geom_input, six.text_type): - # Encoding to ASCII, WKT or HEX doesn't need any more. - geom_input = geom_input.encode('ascii') - wkt_m = wkt_regex.match(geom_input) json_m = json_regex.match(geom_input) if wkt_m: @@ -98,11 +93,11 @@ class OGRGeometry(GDALBase): # OGR_G_CreateFromWkt doesn't work with LINEARRING WKT. # See http://trac.osgeo.org/gdal/ticket/1992. g = capi.create_geom(OGRGeomType(wkt_m.group('type')).num) - capi.import_wkt(g, byref(c_char_p(wkt_m.group('wkt')))) + capi.import_wkt(g, byref(c_char_p(wkt_m.group('wkt').encode()))) else: - g = capi.from_wkt(byref(c_char_p(wkt_m.group('wkt'))), None, byref(c_void_p())) + g = capi.from_wkt(byref(c_char_p(wkt_m.group('wkt').encode())), None, byref(c_void_p())) elif json_m: - g = capi.from_json(geom_input) + g = capi.from_json(geom_input.encode()) else: # Seeing if the input is a valid short-hand string # (e.g., 'Point', 'POLYGON'). @@ -110,7 +105,7 @@ class OGRGeometry(GDALBase): g = capi.create_geom(OGRGeomType(geom_input).num) elif isinstance(geom_input, memoryview): # WKB was passed in - g = capi.from_wkb(str(geom_input), None, byref(c_void_p()), len(geom_input)) + g = capi.from_wkb(bytes(geom_input), None, byref(c_void_p()), len(geom_input)) elif isinstance(geom_input, OGRGeomType): # OGRGeomType was passed in, an empty geometry will be created. g = capi.create_geom(geom_input.num) @@ -143,7 +138,7 @@ class OGRGeometry(GDALBase): srs = srs.wkt else: srs = None - return str(self.wkb), srs + return bytes(self.wkb), srs def __setstate__(self, state): wkb, srs = state diff --git a/django/contrib/gis/gdal/layer.py b/django/contrib/gis/gdal/layer.py index 2357fbb88a..d7bf6969ca 100644 --- a/django/contrib/gis/gdal/layer.py +++ b/django/contrib/gis/gdal/layer.py @@ -14,6 +14,7 @@ from django.contrib.gis.gdal.srs import SpatialReference # GDAL ctypes function prototypes. from django.contrib.gis.gdal.prototypes import ds as capi, geom as geom_api, srs as srs_api +from django.utils.encoding import force_bytes from django.utils import six from django.utils.six.moves import xrange @@ -38,7 +39,7 @@ class Layer(GDALBase): self._ds = ds self._ldefn = capi.get_layer_defn(self._ptr) # Does the Layer support random reading? - self._random_read = self.test_capability('RandomRead') + self._random_read = self.test_capability(b'RandomRead') def __getitem__(self, index): "Gets the Feature at the specified index." @@ -212,4 +213,4 @@ class Layer(GDALBase): 'FastFeatureCount', 'FastGetExtent', 'CreateField', 'Transactions', 'DeleteFeature', and 'FastSetNextByIndex'. """ - return bool(capi.test_capability(self.ptr, capability)) + return bool(capi.test_capability(self.ptr, force_bytes(capability))) diff --git a/django/contrib/gis/gdal/libgdal.py b/django/contrib/gis/gdal/libgdal.py index 27a5b8172e..0d2889d704 100644 --- a/django/contrib/gis/gdal/libgdal.py +++ b/django/contrib/gis/gdal/libgdal.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import os import re from ctypes import c_char_p, CDLL @@ -65,7 +67,7 @@ _version_info.restype = c_char_p def gdal_version(): "Returns only the GDAL version number information." - return _version_info('RELEASE_NAME') + return _version_info(b'RELEASE_NAME') def gdal_full_version(): "Returns the full GDAL version information." @@ -86,7 +88,7 @@ def gdal_release_date(date=False): version_regex = re.compile(r'^(?P\d+)\.(?P\d+)(\.(?P\d+))?') def gdal_version_info(): - ver = gdal_version() + ver = gdal_version().decode() m = version_regex.match(ver) if not m: raise OGRException('Could not parse GDAL version string "%s"' % ver) return dict([(key, m.group(key)) for key in ('major', 'minor', 'subminor')]) diff --git a/django/contrib/gis/gdal/prototypes/errcheck.py b/django/contrib/gis/gdal/prototypes/errcheck.py index d8ff1c7dcf..9103022896 100644 --- a/django/contrib/gis/gdal/prototypes/errcheck.py +++ b/django/contrib/gis/gdal/prototypes/errcheck.py @@ -30,9 +30,10 @@ def check_const_string(result, func, cargs, offset=None): if offset: check_err(result) ptr = ptr_byref(cargs, offset) - return ptr.value + return ptr.value.decode() else: - return result + if result is not None: + return result.decode() def check_string(result, func, cargs, offset=-1, str_result=False): """ @@ -47,13 +48,13 @@ def check_string(result, func, cargs, offset=-1, str_result=False): # For routines that return a string. ptr = result if not ptr: s = None - else: s = string_at(result) + else: s = string_at(result).decode() else: # Error-code return specified. check_err(result) ptr = ptr_byref(cargs, offset) # Getting the string value - s = ptr.value + s = ptr.value.decode() # Correctly freeing the allocated memory beind GDAL pointer # w/the VSIFree routine. if ptr: lgdal.VSIFree(ptr) @@ -125,4 +126,4 @@ def check_str_arg(result, func, cargs): """ dbl = result ptr = cargs[-1]._obj - return dbl, ptr.value + return dbl, ptr.value.decode() diff --git a/django/contrib/gis/gdal/srs.py b/django/contrib/gis/gdal/srs.py index cdeaaca690..1a110b0114 100644 --- a/django/contrib/gis/gdal/srs.py +++ b/django/contrib/gis/gdal/srs.py @@ -34,6 +34,8 @@ from django.contrib.gis.gdal.error import SRSException from django.contrib.gis.gdal.prototypes import srs as capi from django.utils import six +from django.utils.encoding import force_bytes, force_text + #### Spatial Reference class. #### class SpatialReference(GDALBase): @@ -51,7 +53,6 @@ class SpatialReference(GDALBase): EPSG code, a PROJ.4 string, and/or a projection "well known" shorthand string (one of 'WGS84', 'WGS72', 'NAD27', 'NAD83'). """ - buf = c_char_p('') srs_type = 'user' if isinstance(srs_input, six.string_types): @@ -79,6 +80,7 @@ class SpatialReference(GDALBase): srs = srs_input else: # Creating a new SRS pointer, using the string buffer. + buf = c_char_p(b'') srs = capi.new_srs(buf) # If the pointer is NULL, throw an exception. @@ -137,15 +139,16 @@ class SpatialReference(GDALBase): """ if not isinstance(target, six.string_types) or not isinstance(index, int): raise TypeError - return capi.get_attr_value(self.ptr, target, index) + value = capi.get_attr_value(self.ptr, force_bytes(target), index) + return force_text(value, 'ascii', strings_only=True) def auth_name(self, target): "Returns the authority name for the given string target node." - return capi.get_auth_name(self.ptr, target) + return capi.get_auth_name(self.ptr, force_bytes(target)) def auth_code(self, target): "Returns the authority code for the given string target node." - return capi.get_auth_code(self.ptr, target) + return capi.get_auth_code(self.ptr, force_bytes(target)) def clone(self): "Returns a clone of this SpatialReference object." @@ -219,12 +222,14 @@ class SpatialReference(GDALBase): and will automatically determines whether to return the linear or angular units. """ + units, name = None, None if self.projected or self.local: - return capi.linear_units(self.ptr, byref(c_char_p())) + units, name = capi.linear_units(self.ptr, byref(c_char_p())) elif self.geographic: - return capi.angular_units(self.ptr, byref(c_char_p())) - else: - return (None, None) + units, name = capi.angular_units(self.ptr, byref(c_char_p())) + if name is not None: + name.decode() + return (units, name) #### Spheroid/Ellipsoid Properties #### @property @@ -283,7 +288,7 @@ class SpatialReference(GDALBase): def import_user_input(self, user_input): "Imports the Spatial Reference from the given user input string." - capi.from_user_input(self.ptr, user_input) + capi.from_user_input(self.ptr, force_bytes(user_input)) def import_wkt(self, wkt): "Imports the Spatial Reference from OGC WKT (string)" diff --git a/django/contrib/gis/gdal/tests/test_ds.py b/django/contrib/gis/gdal/tests/test_ds.py index 22394a2888..094f65b468 100644 --- a/django/contrib/gis/gdal/tests/test_ds.py +++ b/django/contrib/gis/gdal/tests/test_ds.py @@ -9,7 +9,7 @@ ds_list = (TestDS('test_point', nfeat=5, nfld=3, geom='POINT', gtype=1, driver=' fields={'dbl' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, extent=(-1.35011,0.166623,-0.524093,0.824508), # Got extent from QGIS srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]', - field_values={'dbl' : [float(i) for i in range(1, 6)], 'int' : range(1, 6), 'str' : [str(i) for i in range(1, 6)]}, + field_values={'dbl' : [float(i) for i in range(1, 6)], 'int' : list(range(1, 6)), 'str' : [str(i) for i in range(1, 6)]}, fids=range(5)), TestDS('test_vrt', ext='vrt', nfeat=3, nfld=3, geom='POINT', gtype='Point25D', driver='VRT', fields={'POINT_X' : OFTString, 'POINT_Y' : OFTString, 'NUM' : OFTString}, # VRT uses CSV, which all types are OFTString. @@ -200,7 +200,7 @@ class DataSourceTest(unittest.TestCase): # Setting the spatial filter with a tuple/list with the extent of # a buffer centering around Pueblo. - self.assertRaises(ValueError, lyr._set_spatial_filter, range(5)) + self.assertRaises(ValueError, lyr._set_spatial_filter, list(range(5))) filter_extent = (-105.609252, 37.255001, -103.609252, 39.255001) lyr.spatial_filter = (-105.609252, 37.255001, -103.609252, 39.255001) self.assertEqual(OGRGeometry.from_bbox(filter_extent), lyr.spatial_filter) diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py index dda22036e3..b22bb62109 100644 --- a/django/contrib/gis/gdal/tests/test_geom.py +++ b/django/contrib/gis/gdal/tests/test_geom.py @@ -92,7 +92,7 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): "Testing HEX input/output." for g in self.geometries.hex_wkt: geom1 = OGRGeometry(g.wkt) - self.assertEqual(g.hex, geom1.hex) + self.assertEqual(g.hex.encode(), geom1.hex) # Constructing w/HEX geom2 = OGRGeometry(g.hex) self.assertEqual(geom1, geom2) @@ -102,7 +102,7 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): for g in self.geometries.hex_wkt: geom1 = OGRGeometry(g.wkt) wkb = geom1.wkb - self.assertEqual(b2a_hex(wkb).upper(), g.hex) + self.assertEqual(b2a_hex(wkb).upper(), g.hex.encode()) # Constructing w/WKB. geom2 = OGRGeometry(wkb) self.assertEqual(geom1, geom2) diff --git a/django/contrib/gis/geometry/test_data.py b/django/contrib/gis/geometry/test_data.py index 505f0e4f4b..833e62f224 100644 --- a/django/contrib/gis/geometry/test_data.py +++ b/django/contrib/gis/geometry/test_data.py @@ -101,6 +101,6 @@ class TestDataMixin(object): if GEOMETRIES is None: # Load up the test geometry data from fixture into global. gzf = gzip.GzipFile(os.path.join(TEST_DATA, 'geometries.json.gz')) - geometries = json.loads(gzf.read()) + geometries = json.loads(gzf.read().decode()) GEOMETRIES = TestGeomSet(**strconvert(geometries)) return GEOMETRIES diff --git a/django/contrib/gis/geos/factory.py b/django/contrib/gis/geos/factory.py index 42b0310f0a..2e5fa4f331 100644 --- a/django/contrib/gis/geos/factory.py +++ b/django/contrib/gis/geos/factory.py @@ -3,6 +3,7 @@ from django.contrib.gis.geos.geometry import GEOSGeometry, wkt_regex, hex_regex from django.utils import six + def fromfile(file_h): """ Given a string file name, returns a GEOSGeometry. The file may contain WKB, @@ -15,11 +16,19 @@ def fromfile(file_h): else: buf = file_h.read() - # If we get WKB need to wrap in buffer(), so run through regexes. - if wkt_regex.match(buf) or hex_regex.match(buf): - return GEOSGeometry(buf) + # If we get WKB need to wrap in memoryview(), so run through regexes. + if isinstance(buf, bytes): + try: + decoded = buf.decode() + if wkt_regex.match(decoded) or hex_regex.match(decoded): + return GEOSGeometry(decoded) + except UnicodeDecodeError: + pass else: - return GEOSGeometry(memoryview(buf)) + return GEOSGeometry(buf) + + return GEOSGeometry(memoryview(buf)) + def fromstr(string, **kwargs): "Given a string value, returns a GEOSGeometry object." diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index 41bf4dfa1f..6dbb6b2cb3 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -2,6 +2,8 @@ This module contains the 'base' GEOSGeometry object -- all GEOS Geometries inherit from this object. """ +from __future__ import unicode_literals + # Python, ctypes and types dependencies. from ctypes import addressof, byref, c_double @@ -29,6 +31,8 @@ from django.contrib.gis.geos.prototypes.io import wkt_r, wkt_w, wkb_r, wkb_w, ew from django.contrib.gis.geometry.regex import hex_regex, wkt_regex, json_regex from django.utils import six +from django.utils.encoding import force_bytes, force_text + class GEOSGeometry(GEOSBase, ListMixin): "A class that, generally, encapsulates a GEOS geometry." @@ -55,19 +59,17 @@ class GEOSGeometry(GEOSBase, ListMixin): The `srid` keyword is used to specify the Source Reference Identifier (SRID) number for this Geometry. If not set, the SRID will be None. """ + if isinstance(geo_input, bytes): + geo_input = force_text(geo_input) if isinstance(geo_input, six.string_types): - if isinstance(geo_input, six.text_type): - # Encoding to ASCII, WKT or HEXEWKB doesn't need any more. - geo_input = geo_input.encode('ascii') - wkt_m = wkt_regex.match(geo_input) if wkt_m: # Handling WKT input. if wkt_m.group('srid'): srid = int(wkt_m.group('srid')) - g = wkt_r().read(wkt_m.group('wkt')) + g = wkt_r().read(force_bytes(wkt_m.group('wkt'))) elif hex_regex.match(geo_input): # Handling HEXEWKB input. - g = wkb_r().read(geo_input) + g = wkb_r().read(force_bytes(geo_input)) elif gdal.HAS_GDAL and json_regex.match(geo_input): # Handling GeoJSON input. g = wkb_r().read(gdal.OGRGeometry(geo_input).wkb) @@ -217,7 +219,7 @@ class GEOSGeometry(GEOSBase, ListMixin): @property def geom_type(self): "Returns a string representing the Geometry type, e.g. 'Polygon'" - return capi.geos_type(self.ptr) + return capi.geos_type(self.ptr).decode() @property def geom_typeid(self): @@ -284,7 +286,7 @@ class GEOSGeometry(GEOSBase, ListMixin): """ if not GEOS_PREPARE: raise GEOSException('Upgrade GEOS to 3.1 to get validity reason.') - return capi.geos_isvalidreason(self.ptr) + return capi.geos_isvalidreason(self.ptr).decode() #### Binary predicates. #### def contains(self, other): @@ -338,7 +340,7 @@ class GEOSGeometry(GEOSBase, ListMixin): """ if not isinstance(pattern, six.string_types) or len(pattern) > 9: raise GEOSException('invalid intersection matrix pattern') - return capi.geos_relatepattern(self.ptr, other.ptr, pattern) + return capi.geos_relatepattern(self.ptr, other.ptr, force_bytes(pattern)) def touches(self, other): """ @@ -380,7 +382,7 @@ class GEOSGeometry(GEOSBase, ListMixin): @property def wkt(self): "Returns the WKT (Well-Known Text) representation of this Geometry." - return wkt_w().write(self) + return wkt_w().write(self).decode() @property def hex(self): @@ -590,7 +592,7 @@ class GEOSGeometry(GEOSBase, ListMixin): def relate(self, other): "Returns the DE-9IM intersection matrix for this Geometry and the other." - return capi.geos_relate(self.ptr, other.ptr) + return capi.geos_relate(self.ptr, other.ptr).decode() def simplify(self, tolerance=0.0, preserve_topology=False): """ diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index aed6cf366c..b31a7955a2 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -57,6 +57,7 @@ lgeos = CDLL(lib_path) # typedef void (*GEOSMessageHandler)(const char *fmt, ...); NOTICEFUNC = CFUNCTYPE(None, c_char_p, c_char_p) def notice_h(fmt, lst, output_h=sys.stdout): + fmt, lst = fmt.decode(), lst.decode() try: warn_msg = fmt % lst except: @@ -66,6 +67,7 @@ notice_h = NOTICEFUNC(notice_h) ERRORFUNC = CFUNCTYPE(None, c_char_p, c_char_p) def error_h(fmt, lst, output_h=sys.stderr): + fmt, lst = fmt.decode(), lst.decode() try: err_msg = fmt % lst except: diff --git a/django/contrib/gis/geos/prototypes/io.py b/django/contrib/gis/geos/prototypes/io.py index 4a774f9589..1eeab60a4b 100644 --- a/django/contrib/gis/geos/prototypes/io.py +++ b/django/contrib/gis/geos/prototypes/io.py @@ -8,6 +8,7 @@ from django.contrib.gis.geos.prototypes.geom import c_uchar_p, geos_char_p from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc from django.utils import six +from django.utils.encoding import force_bytes ### The WKB/WKT Reader/Writer structures and pointers ### class WKTReader_st(Structure): pass @@ -121,8 +122,9 @@ class _WKTReader(IOBase): ptr_type = WKT_READ_PTR def read(self, wkt): - if not isinstance(wkt, six.string_types): raise TypeError - return wkt_reader_read(self.ptr, wkt) + if not isinstance(wkt, (bytes, six.string_types)): + raise TypeError + return wkt_reader_read(self.ptr, force_bytes(wkt)) class _WKBReader(IOBase): _constructor = wkb_reader_create @@ -134,7 +136,7 @@ class _WKBReader(IOBase): if isinstance(wkb, memoryview): wkb_s = bytes(wkb) return wkb_reader_read(self.ptr, wkb_s, len(wkb_s)) - elif isinstance(wkb, six.string_types): + elif isinstance(wkb, (bytes, six.string_types)): return wkb_reader_read_hex(self.ptr, wkb, len(wkb)) else: raise TypeError @@ -189,8 +191,8 @@ class WKBWriter(IOBase): return bool(ord(wkb_writer_get_include_srid(self.ptr))) def _set_include_srid(self, include): - if bool(include): flag = chr(1) - else: flag = chr(0) + if bool(include): flag = b'\x01' + else: flag = b'\x00' wkb_writer_set_include_srid(self.ptr, flag) srid = property(_get_include_srid, _set_include_srid) diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index 2e873b4cd9..c8d3e43a0e 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -1,6 +1,10 @@ +from __future__ import unicode_literals + import ctypes import json import random +from binascii import a2b_hex, b2a_hex +from io import BytesIO from django.contrib.gis import memoryview from django.contrib.gis.geos import (GEOSException, GEOSIndexError, GEOSGeometry, @@ -10,6 +14,7 @@ from django.contrib.gis.geos.base import gdal, numpy, GEOSBase from django.contrib.gis.geos.libgeos import GEOS_PREPARE from django.contrib.gis.geometry.test_data import TestDataMixin +from django.utils.encoding import force_bytes from django.utils import six from django.utils.six.moves import xrange from django.utils import unittest @@ -65,7 +70,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): # result in a TypeError when trying to assign it to the `ptr` property. # Thus, memmory addresses (integers) and pointers of the incorrect type # (in `bad_ptrs`) will not be allowed. - bad_ptrs = (5, ctypes.c_char_p('foobar')) + bad_ptrs = (5, ctypes.c_char_p(b'foobar')) for bad_ptr in bad_ptrs: # Equivalent to `fg.ptr = bad_ptr` self.assertRaises(TypeError, fg1._set_ptr, bad_ptr) @@ -81,18 +86,16 @@ class GEOSTest(unittest.TestCase, TestDataMixin): "Testing HEX output." for g in self.geometries.hex_wkt: geom = fromstr(g.wkt) - self.assertEqual(g.hex, geom.hex) + self.assertEqual(g.hex, geom.hex.decode()) def test_hexewkb(self): "Testing (HEX)EWKB output." - from binascii import a2b_hex - # For testing HEX(EWKB). - ogc_hex = '01010000000000000000000000000000000000F03F' + ogc_hex = b'01010000000000000000000000000000000000F03F' # `SELECT ST_AsHEXEWKB(ST_GeomFromText('POINT(0 1)', 4326));` - hexewkb_2d = '0101000020E61000000000000000000000000000000000F03F' + hexewkb_2d = b'0101000020E61000000000000000000000000000000000F03F' # `SELECT ST_AsHEXEWKB(ST_GeomFromEWKT('SRID=4326;POINT(0 1 2)'));` - hexewkb_3d = '01010000A0E61000000000000000000000000000000000F03F0000000000000040' + hexewkb_3d = b'01010000A0E61000000000000000000000000000000000F03F0000000000000040' pnt_2d = Point(0, 1, srid=4326) pnt_3d = Point(0, 1, 2, srid=4326) @@ -165,11 +168,10 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_wkb(self): "Testing WKB output." - from binascii import b2a_hex for g in self.geometries.hex_wkt: geom = fromstr(g.wkt) wkb = geom.wkb - self.assertEqual(b2a_hex(wkb).upper(), g.hex) + self.assertEqual(b2a_hex(wkb).decode().upper(), g.hex) def test_create_hex(self): "Testing creation from HEX." @@ -181,9 +183,8 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_create_wkb(self): "Testing creation from WKB." - from binascii import a2b_hex for g in self.geometries.hex_wkt: - wkb = memoryview(a2b_hex(g.hex)) + wkb = memoryview(a2b_hex(g.hex.encode())) geom_h = GEOSGeometry(wkb) # we need to do this so decimal places get normalised geom_t = fromstr(g.wkt) @@ -213,13 +214,12 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_fromfile(self): "Testing the fromfile() factory." - from io import BytesIO ref_pnt = GEOSGeometry('POINT(5 23)') wkt_f = BytesIO() - wkt_f.write(ref_pnt.wkt) + wkt_f.write(force_bytes(ref_pnt.wkt)) wkb_f = BytesIO() - wkb_f.write(str(ref_pnt.wkb)) + wkb_f.write(bytes(ref_pnt.wkb)) # Other tests use `fromfile()` on string filenames so those # aren't tested here. @@ -440,8 +440,8 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(r.geom_typeid, 2) # Testing polygon construction. - self.assertRaises(TypeError, Polygon.__init__, 0, [1, 2, 3]) - self.assertRaises(TypeError, Polygon.__init__, 'foo') + self.assertRaises(TypeError, Polygon, 0, [1, 2, 3]) + self.assertRaises(TypeError, Polygon, 'foo') # Polygon(shell, (hole1, ... holeN)) rings = tuple(r for r in poly) diff --git a/django/contrib/gis/geos/tests/test_io.py b/django/contrib/gis/geos/tests/test_io.py index 1e604e506b..45a9a220b1 100644 --- a/django/contrib/gis/geos/tests/test_io.py +++ b/django/contrib/gis/geos/tests/test_io.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import binascii import unittest @@ -5,6 +7,7 @@ from django.contrib.gis import memoryview from django.contrib.gis.geos import GEOSGeometry, WKTReader, WKTWriter, WKBReader, WKBWriter, geos_version_info from django.utils import six + class GEOSIOTest(unittest.TestCase): def test01_wktreader(self): @@ -14,8 +17,8 @@ class GEOSIOTest(unittest.TestCase): # read() should return a GEOSGeometry ref = GEOSGeometry(wkt) - g1 = wkt_r.read(wkt) - g2 = wkt_r.read(six.text_type(wkt)) + g1 = wkt_r.read(wkt.encode()) + g2 = wkt_r.read(wkt) for geom in (g1, g2): self.assertEqual(ref, geom) @@ -102,7 +105,7 @@ class GEOSIOTest(unittest.TestCase): self.assertEqual(hex3d, wkb_w.write_hex(g)) self.assertEqual(wkb3d, wkb_w.write(g)) - # Telling the WKBWriter to inlcude the srid in the representation. + # Telling the WKBWriter to include the srid in the representation. wkb_w.srid = True self.assertEqual(hex3d_srid, wkb_w.write_hex(g)) self.assertEqual(wkb3d_srid, wkb_w.write(g)) From 98b6ce60f4f4456fb00259ec118e1fed2a4dfaa4 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 23 Sep 2012 20:17:36 +0200 Subject: [PATCH 034/265] Made a version condition less confusing. Fixed #18762 (again). --- docs/intro/tutorial03.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 03d4bf68b3..d6f95008de 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -555,7 +555,7 @@ with the :ttag:`url` template tag: If ``{% url 'polls.views.detail' poll.id %}`` (with quotes) doesn't work, but ``{% url polls.views.detail poll.id %}`` (without quotes) does, that - means you're using a version of Django ≤ 1.4. In this case, add the + means you're using a version of Django < 1.5. In this case, add the following declaration at the top of your template: .. code-block:: html+django From 799786a7b64937314250bc638a60b1d4e132bec2 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 23 Sep 2012 20:19:58 +0200 Subject: [PATCH 035/265] [py3] Fixed outdated map() call in GIS sql compiler --- django/contrib/gis/db/models/sql/compiler.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py index 5c8d2647f7..233ca5a03e 100644 --- a/django/contrib/gis/db/models/sql/compiler.py +++ b/django/contrib/gis/db/models/sql/compiler.py @@ -1,3 +1,8 @@ +try: + from itertools import zip_longest +except ImportError: + from itertools import izip_longest as zip_longest + from django.utils.six.moves import zip from django.db.backends.util import truncate_name, typecast_timestamp @@ -190,7 +195,7 @@ class GeoSQLCompiler(compiler.SQLCompiler): if self.connection.ops.oracle or getattr(self.query, 'geo_values', False): # We resolve the rest of the columns if we're on Oracle or if # the `geo_values` attribute is defined. - for value, field in map(None, row[index_start:], fields): + for value, field in zip_longest(row[index_start:], fields): values.append(self.query.convert_values(value, field, self.connection)) else: values.extend(row[index_start:]) From 874908e3bba8538d1c108e1b38f9d9519c6e5416 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 23 Sep 2012 20:32:17 +0200 Subject: [PATCH 036/265] [py3] Updated PostGIS adapter --- django/contrib/gis/db/backends/postgis/adapter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/django/contrib/gis/db/backends/postgis/adapter.py b/django/contrib/gis/db/backends/postgis/adapter.py index 863ee78acd..8bb514d760 100644 --- a/django/contrib/gis/db/backends/postgis/adapter.py +++ b/django/contrib/gis/db/backends/postgis/adapter.py @@ -1,6 +1,7 @@ """ This object provides quoting for GEOS geometries into PostgreSQL/PostGIS. """ +from __future__ import unicode_literals from psycopg2 import Binary from psycopg2.extensions import ISQLQuote @@ -10,7 +11,7 @@ class PostGISAdapter(object): "Initializes on the geometry." # Getting the WKB (in string form, to allow easy pickling of # the adaptor) and the SRID from the geometry. - self.ewkb = str(geom.ewkb) + self.ewkb = bytes(geom.ewkb) self.srid = geom.srid self._adapter = Binary(self.ewkb) @@ -39,7 +40,7 @@ class PostGISAdapter(object): def getquoted(self): "Returns a properly quoted string for use in PostgreSQL/PostGIS." # psycopg will figure out whether to use E'\\000' or '\000' - return 'ST_GeomFromEWKB(%s)' % self._adapter.getquoted() + return str('ST_GeomFromEWKB(%s)' % self._adapter.getquoted().decode()) def prepare_database_save(self, unused): return self From 43c7f8c3a3cfaec001a76b1b683de3cc47eb2d2f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 23 Sep 2012 22:11:46 +0200 Subject: [PATCH 037/265] [py3] Fixed unicode string in geoapp test --- django/contrib/gis/tests/geoapp/test_regress.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/django/contrib/gis/tests/geoapp/test_regress.py b/django/contrib/gis/tests/geoapp/test_regress.py index fffd7d3cab..0e9c5c44a3 100644 --- a/django/contrib/gis/tests/geoapp/test_regress.py +++ b/django/contrib/gis/tests/geoapp/test_regress.py @@ -1,4 +1,5 @@ -from __future__ import absolute_import +# -*- encoding: utf-8 -*- +from __future__ import absolute_import, unicode_literals from datetime import datetime @@ -26,7 +27,7 @@ class GeoRegressionTests(TestCase): def test_kmz(self): "Testing `render_to_kmz` with non-ASCII data. See #11624." - name = '\xc3\x85land Islands'.decode('iso-8859-1') + name = "Åland Islands" places = [{'name' : name, 'description' : name, 'kml' : '5.0,23.0' From 54c81a1c936f3682e3405d6737958fdffa39bed9 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 23 Sep 2012 22:49:22 +0200 Subject: [PATCH 038/265] [py3] Allowed bytes in get_prep_value for a Geometry --- django/contrib/gis/db/models/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py index 17630d0899..c8b8901d59 100644 --- a/django/contrib/gis/db/models/fields.py +++ b/django/contrib/gis/db/models/fields.py @@ -160,7 +160,7 @@ class GeometryField(Field): # from the given string input. if isinstance(geom, Geometry): pass - elif isinstance(geom, six.string_types) or hasattr(geom, '__geo_interface__'): + elif isinstance(geom, (bytes, six.string_types)) or hasattr(geom, '__geo_interface__'): try: geom = Geometry(geom) except GeometryException: From 6eda8d784a128309a67540fb0a62373667958daa Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 24 Sep 2012 16:03:12 +0200 Subject: [PATCH 039/265] Enlarged exception catching when testing for GDAL presence Other import errors than ImportError can happen during import of GDAL files (e.g. OGRException). Some further auditing may be needed if we want to restrict the catched exceptions at a later stage. Thanks Ramiro Morales for raising the issue. --- django/contrib/gis/gdal/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/gis/gdal/__init__.py b/django/contrib/gis/gdal/__init__.py index f477f05982..de41df90ff 100644 --- a/django/contrib/gis/gdal/__init__.py +++ b/django/contrib/gis/gdal/__init__.py @@ -41,7 +41,7 @@ try: from django.contrib.gis.gdal.srs import SpatialReference, CoordTransform from django.contrib.gis.gdal.geometries import OGRGeometry HAS_GDAL = True -except ImportError: +except Exception: HAS_GDAL = False try: From e72e22e518a730cd28cd68c9374fa79a45e27a9c Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 24 Sep 2012 16:07:58 +0200 Subject: [PATCH 040/265] Replaced a deprecated assertEquals --- tests/regressiontests/utils/html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regressiontests/utils/html.py b/tests/regressiontests/utils/html.py index 98df80a5e2..6a93dff85e 100644 --- a/tests/regressiontests/utils/html.py +++ b/tests/regressiontests/utils/html.py @@ -154,4 +154,4 @@ class TestUtilsHtml(unittest.TestCase): ("x

y

", "a b", "x

y

"), ) for value, tags, output in items: - self.assertEquals(f(value, tags), output) + self.assertEqual(f(value, tags), output) From fc69fff9ab5bba8a6cff3eaf51f02a3204b1c015 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 24 Sep 2012 22:11:42 +0200 Subject: [PATCH 041/265] Fixed #14861 -- Moved logging config outside of Settings.__init__ Thanks donspaulding for the report and simonpercivall for the initial patch. --- django/conf/__init__.py | 30 ++++++++++--------- docs/topics/logging.txt | 30 ------------------- .../logging_tests/logconfig.py | 7 +++++ tests/regressiontests/logging_tests/tests.py | 29 ++++++++++++++++++ 4 files changed, 52 insertions(+), 44 deletions(-) create mode 100644 tests/regressiontests/logging_tests/logconfig.py diff --git a/django/conf/__init__.py b/django/conf/__init__.py index 6272f4ed5d..d636ff0b6c 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -43,13 +43,28 @@ class LazySettings(LazyObject): % (name, ENVIRONMENT_VARIABLE)) self._wrapped = Settings(settings_module) - + self._configure_logging() def __getattr__(self, name): if self._wrapped is empty: self._setup(name) return getattr(self._wrapped, name) + def _configure_logging(self): + """ + Setup logging from LOGGING_CONFIG and LOGGING settings. + """ + if self.LOGGING_CONFIG: + # First find the logging configuration function ... + logging_config_path, logging_config_func_name = self.LOGGING_CONFIG.rsplit('.', 1) + logging_config_module = importlib.import_module(logging_config_path) + logging_config_func = getattr(logging_config_module, logging_config_func_name) + + # Backwards-compatibility shim for #16288 fix + compat_patch_logging_config(self.LOGGING) + + # ... then invoke it with the logging settings + logging_config_func(self.LOGGING) def configure(self, default_settings=global_settings, **options): """ @@ -133,19 +148,6 @@ class Settings(BaseSettings): os.environ['TZ'] = self.TIME_ZONE time.tzset() - # Settings are configured, so we can set up the logger if required - if self.LOGGING_CONFIG: - # First find the logging configuration function ... - logging_config_path, logging_config_func_name = self.LOGGING_CONFIG.rsplit('.', 1) - logging_config_module = importlib.import_module(logging_config_path) - logging_config_func = getattr(logging_config_module, logging_config_func_name) - - # Backwards-compatibility shim for #16288 fix - compat_patch_logging_config(self.LOGGING) - - # ... then invoke it with the logging settings - logging_config_func(self.LOGGING) - class UserSettingsHolder(BaseSettings): """ diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index 94236babd6..a4aae0bc02 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -345,36 +345,6 @@ This logging configuration does the following things: printed to the console; ``ERROR`` and ``CRITICAL`` messages will also be output via email. -.. admonition:: Custom handlers and circular imports - - If your ``settings.py`` specifies a custom handler class and the file - defining that class also imports ``settings.py`` a circular import will - occur. - - For example, if ``settings.py`` contains the following config for - :setting:`LOGGING`:: - - LOGGING = { - 'version': 1, - 'handlers': { - 'custom_handler': { - 'level': 'INFO', - 'class': 'myproject.logconfig.MyHandler', - } - } - } - - and ``myproject/logconfig.py`` has the following line before the - ``MyHandler`` definition:: - - from django.conf import settings - - then the ``dictconfig`` module will raise an exception like the following:: - - ValueError: Unable to configure handler 'custom_handler': - Unable to configure handler 'custom_handler': - 'module' object has no attribute 'logconfig' - .. _formatter documentation: http://docs.python.org/library/logging.html#formatter-objects Custom logging configuration diff --git a/tests/regressiontests/logging_tests/logconfig.py b/tests/regressiontests/logging_tests/logconfig.py new file mode 100644 index 0000000000..8524aa2c24 --- /dev/null +++ b/tests/regressiontests/logging_tests/logconfig.py @@ -0,0 +1,7 @@ +import logging + +from django.conf import settings + +class MyHandler(logging.Handler): + def __init__(self, *args, **kwargs): + self.config = settings.LOGGING diff --git a/tests/regressiontests/logging_tests/tests.py b/tests/regressiontests/logging_tests/tests.py index f444e0ff46..a54b425f67 100644 --- a/tests/regressiontests/logging_tests/tests.py +++ b/tests/regressiontests/logging_tests/tests.py @@ -10,6 +10,8 @@ from django.test import TestCase, RequestFactory from django.test.utils import override_settings from django.utils.log import CallbackFilter, RequireDebugFalse +from ..admin_scripts.tests import AdminScriptTestCase + # logging config prior to using filter with mail_admins OLD_LOGGING = { @@ -253,3 +255,30 @@ class AdminEmailHandlerTest(TestCase): self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, expected_subject) + + +class SettingsConfigTest(AdminScriptTestCase): + """ + Test that accessing settings in a custom logging handler does not trigger + a circular import error. + """ + def setUp(self): + log_config = """{ + 'version': 1, + 'handlers': { + 'custom_handler': { + 'level': 'INFO', + 'class': 'logging_tests.logconfig.MyHandler', + } + } +}""" + self.write_settings('settings.py', sdict={'LOGGING': log_config}) + + def tearDown(self): + self.remove_settings('settings.py') + + def test_circular_dependency(self): + # validate is just an example command to trigger settings configuration + out, err = self.run_manage(['validate']) + self.assertNoOutput(err) + self.assertOutput(out, "0 errors found") From 515fd6a5de2f1bf817fac9ced07d0485d594b510 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 24 Sep 2012 22:42:58 +0200 Subject: [PATCH 042/265] Called parent __init__ in test logging handler --- tests/regressiontests/logging_tests/logconfig.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/regressiontests/logging_tests/logconfig.py b/tests/regressiontests/logging_tests/logconfig.py index 8524aa2c24..fc5ea1a0bd 100644 --- a/tests/regressiontests/logging_tests/logconfig.py +++ b/tests/regressiontests/logging_tests/logconfig.py @@ -3,5 +3,6 @@ import logging from django.conf import settings class MyHandler(logging.Handler): - def __init__(self, *args, **kwargs): + def __init__(self): + logging.Handler.__init__(self) self.config = settings.LOGGING From 29cd3d6c01d7afbcf5141430b2dd93daede22ade Mon Sep 17 00:00:00 2001 From: Andrew Badr Date: Mon, 24 Sep 2012 17:14:11 -0700 Subject: [PATCH 043/265] Fix docs for context_processors.auth Copy said it created three context variables, but only lists two. ("messages" was removed.) --- docs/ref/templates/api.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index f29d2acc12..db57d2de96 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -433,7 +433,7 @@ django.contrib.auth.context_processors.auth ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every -``RequestContext`` will contain these three variables: +``RequestContext`` will contain these variables: * ``user`` -- An ``auth.User`` instance representing the currently logged-in user (or an ``AnonymousUser`` instance, if the client isn't From f51eab796d087439eedcb768cdd312517780940e Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Mon, 24 Sep 2012 22:02:59 -0300 Subject: [PATCH 044/265] Fixed #18072 -- Made more admin links use reverse() instead of hard-coded relative URLs. Thanks kmike for the report and initial patch for the changelist->edit object view link URL. Other affected links include the delete object one and object history one (in this case the change had been implemented in commit 5a9e127, this commit adds admin-quoting of the object PK in a way similar to a222d6e.) Refs #15294. --- .../admin/templates/admin/change_form.html | 2 +- .../admin/templates/admin/submit_line.html | 6 +- .../admin/templatetags/admin_modify.py | 6 +- django/contrib/admin/util.py | 6 +- django/contrib/admin/views/main.py | 7 +- .../regressiontests/admin_changelist/tests.py | 10 ++- .../admin_custom_urls/fixtures/actions.json | 7 -- .../admin_custom_urls/tests.py | 16 +--- tests/regressiontests/admin_views/tests.py | 75 +++++++++++++------ 9 files changed, 79 insertions(+), 56 deletions(-) diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index e27875cdad..4962e732a2 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -29,7 +29,7 @@ {% if change %}{% if not is_popup %} diff --git a/django/contrib/admin/templates/admin/submit_line.html b/django/contrib/admin/templates/admin/submit_line.html index d6f854a233..8c9d22752d 100644 --- a/django/contrib/admin/templates/admin/submit_line.html +++ b/django/contrib/admin/templates/admin/submit_line.html @@ -1,8 +1,8 @@ -{% load i18n %} +{% load i18n admin_urls %}
{% if show_save %}{% endif %} -{% if show_delete_link %}{% endif %} +{% if show_delete_link %}{% endif %} {% if show_save_as_new %}{%endif%} -{% if show_save_and_add_another %}{% endif %} +{% if show_save_and_add_another %}{% endif %} {% if show_save_and_continue %}{% endif %}
diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index c190533f95..f6ac59635a 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -28,7 +28,8 @@ def submit_row(context): change = context['change'] is_popup = context['is_popup'] save_as = context['save_as'] - return { + ctx = { + 'opts': opts, 'onclick_attrib': (opts.get_ordered_objects() and change and 'onclick="submitOrderForm();"' or ''), 'show_delete_link': (not is_popup and context['has_delete_permission'] @@ -40,6 +41,9 @@ def submit_row(context): 'is_popup': is_popup, 'show_save': True } + if context.get('original') is not None: + ctx['original'] = context['original'] + return ctx @register.filter def cell_count(inline_admin_form): diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index f95fe53de1..74eef2e733 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -48,9 +48,9 @@ def prepare_lookup_value(key, value): def quote(s): """ Ensure that primary key values do not confuse the admin URLs by escaping - any '/', '_' and ':' characters. Similar to urllib.quote, except that the - quoting is slightly different so that it doesn't get automatically - unquoted by the Web browser. + any '/', '_' and ':' and similarly problematic characters. + Similar to urllib.quote, except that the quoting is slightly different so + that it doesn't get automatically unquoted by the Web browser. """ if not isinstance(s, six.string_types): return s diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 74ef095b4b..5033ba98bc 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -3,6 +3,7 @@ from functools import reduce from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured from django.core.paginator import InvalidPage +from django.core.urlresolvers import reverse from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils.datastructures import SortedDict @@ -376,4 +377,8 @@ class ChangeList(object): return qs def url_for_result(self, result): - return "%s/" % quote(getattr(result, self.pk_attname)) + pk = getattr(result, self.pk_attname) + return reverse('admin:%s_%s_change' % (self.opts.app_label, + self.opts.module_name), + args=(quote(pk),), + current_app=self.model_admin.admin_site.name) diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py index be88c9a161..2b1c1a9bcf 100644 --- a/tests/regressiontests/admin_changelist/tests.py +++ b/tests/regressiontests/admin_changelist/tests.py @@ -6,6 +6,7 @@ from django.contrib import admin from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.views.main import ChangeList, SEARCH_VAR, ALL_VAR from django.contrib.auth.models import User +from django.core.urlresolvers import reverse from django.template import Context, Template from django.test import TestCase from django.test.client import RequestFactory @@ -65,7 +66,8 @@ class ChangeListTests(TestCase): template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}') context = Context({'cl': cl}) table_output = template.render(context) - row_html = 'name(None)' % new_child.id + link = reverse('admin:admin_changelist_child_change', args=(new_child.id,)) + row_html = 'name(None)' % link self.assertFalse(table_output.find(row_html) == -1, 'Failed to find expected row element: %s' % table_output) @@ -87,7 +89,8 @@ class ChangeListTests(TestCase): template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}') context = Context({'cl': cl}) table_output = template.render(context) - row_html = 'nameParent object' % new_child.id + link = reverse('admin:admin_changelist_child_change', args=(new_child.id,)) + row_html = 'nameParent object' % link self.assertFalse(table_output.find(row_html) == -1, 'Failed to find expected row element: %s' % table_output) @@ -425,7 +428,8 @@ class ChangeListTests(TestCase): request = self._mocked_authenticated_request('/child/', superuser) response = m.changelist_view(request) for i in range(1, 10): - self.assertContains(response, '%s' % (i, i)) + link = reverse('admin:admin_changelist_child_change', args=(i,)) + self.assertContains(response, '%s' % (link, i)) list_display = m.get_list_display(request) list_display_links = m.get_list_display_links(request, list_display) diff --git a/tests/regressiontests/admin_custom_urls/fixtures/actions.json b/tests/regressiontests/admin_custom_urls/fixtures/actions.json index a63cf8135c..7c6341d71d 100644 --- a/tests/regressiontests/admin_custom_urls/fixtures/actions.json +++ b/tests/regressiontests/admin_custom_urls/fixtures/actions.json @@ -40,12 +40,5 @@ "fields": { "description": "An action with a name suspected of being a XSS attempt" } - }, - { - "pk": "The name of an action", - "model": "admin_custom_urls.action", - "fields": { - "description": "A generic action" - } } ] diff --git a/tests/regressiontests/admin_custom_urls/tests.py b/tests/regressiontests/admin_custom_urls/tests.py index 64ff9f6692..3e9cf28965 100644 --- a/tests/regressiontests/admin_custom_urls/tests.py +++ b/tests/regressiontests/admin_custom_urls/tests.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, unicode_literals +from django.contrib.admin.util import quote from django.core.urlresolvers import reverse from django.template.response import TemplateResponse from django.test import TestCase @@ -67,7 +68,7 @@ class AdminCustomUrlsTest(TestCase): # Ditto, but use reverse() to build the URL url = reverse('admin:%s_action_change' % Action._meta.app_label, - args=('add',)) + args=(quote('add'),)) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Change action') @@ -75,19 +76,8 @@ class AdminCustomUrlsTest(TestCase): # Should correctly get the change_view for the model instance with the # funny-looking PK (the one wth a 'path/to/html/document.html' value) url = reverse('admin:%s_action_change' % Action._meta.app_label, - args=("path/to/html/document.html",)) + args=(quote("path/to/html/document.html"),)) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Change action') self.assertContains(response, 'value="path/to/html/document.html"') - - def testChangeViewHistoryButton(self): - url = reverse('admin:%s_action_change' % Action._meta.app_label, - args=('The name of an action',)) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - expected_link = reverse('admin:%s_action_history' % - Action._meta.app_label, - args=('The name of an action',)) - self.assertContains(response, 'Horizontal', msg_prefix=fail_msg, html=True) - self.assertContains(response, 'Vertical', msg_prefix=fail_msg, html=True) + self.assertContains(response, 'Horizontal' % link1, msg_prefix=fail_msg, html=True) + self.assertContains(response, 'Vertical' % link2, msg_prefix=fail_msg, html=True) def testNamedGroupFieldChoicesFilter(self): """ @@ -1371,9 +1381,12 @@ class AdminViewStringPrimaryKeyTest(TestCase): self.assertEqual(response.status_code, 200) def test_changelist_to_changeform_link(self): - "The link from the changelist referring to the changeform of the object should be quoted" - response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/') - should_contain = """%s""" % (escape(quote(self.pk)), escape(self.pk)) + "Link to the changeform of the object in changelist should use reverse() and be quoted -- #18072" + prefix = '/test_admin/admin/admin_views/modelwithstringprimarykey/' + response = self.client.get(prefix) + # this URL now comes through reverse(), thus iri_to_uri encoding + pk_final_url = escape(iri_to_uri(quote(self.pk))) + should_contain = """%s""" % (prefix, pk_final_url, escape(self.pk)) self.assertContains(response, should_contain) def test_recentactions_link(self): @@ -1441,6 +1454,18 @@ class AdminViewStringPrimaryKeyTest(TestCase): should_contain = '/%s/" class="viewsitelink">' % model.pk self.assertContains(response, should_contain) + def test_change_view_history_link(self): + """Object history button link should work and contain the pk value quoted.""" + url = reverse('admin:%s_modelwithstringprimarykey_change' % + ModelWithStringPrimaryKey._meta.app_label, + args=(quote(self.pk),)) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + expected_link = reverse('admin:%s_modelwithstringprimarykey_history' % + ModelWithStringPrimaryKey._meta.app_label, + args=(quote(self.pk),)) + self.assertContains(response, '%d' % (story1.id, story1.id), 1) - self.assertContains(response, '%d' % (story2.id, story2.id), 1) + self.assertContains(response, '%d' % (link1, story1.id), 1) + self.assertContains(response, '%d' % (link2, story2.id), 1) @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) From 5a1bf7eccb100ea4f4f61ceba9381a11e3e0afc5 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 25 Sep 2012 17:08:07 +0200 Subject: [PATCH 045/265] Fix little typo in cache documentation --- docs/topics/cache.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index e80ac85bd8..2f95c33dd5 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -286,7 +286,7 @@ cache is multi-process and thread-safe. To use it, set The cache :setting:`LOCATION ` is used to identify individual memory stores. If you only have one locmem cache, you can omit the -:setting:`LOCATION `; however, if you have more that one local +:setting:`LOCATION `; however, if you have more than one local memory cache, you will need to assign a name to at least one of them in order to keep them separate. From 1f84b042f1c3fab0f806de814d668917dd86236a Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 25 Sep 2012 18:38:47 +0200 Subject: [PATCH 046/265] Fixed #19020 -- Do not depend on dict order in formtools tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks metzen for the report and initial patch, and Łukasz Rekucki for an inspirational patch on his randomhash_fixes branch. --- django/contrib/formtools/tests/__init__.py | 12 +++++------- .../contrib/formtools/tests/wizard/cookiestorage.py | 5 ++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py index 15941332ed..a21ffde533 100644 --- a/django/contrib/formtools/tests/__init__.py +++ b/django/contrib/formtools/tests/__init__.py @@ -12,6 +12,7 @@ from django.conf import settings from django.contrib.formtools import preview, utils from django.contrib.formtools.wizard import FormWizard from django.test import TestCase +from django.test.html import parse_html from django.test.utils import override_settings from django.utils import unittest @@ -218,7 +219,6 @@ class DummyRequest(http.HttpRequest): ) class WizardTests(TestCase): urls = 'django.contrib.formtools.tests.urls' - input_re = re.compile('name="([^"]+)" value="([^"]+)"') wizard_step_data = ( { '0-name': 'Pony', @@ -409,14 +409,13 @@ class WizardTests(TestCase): """ Pull the appropriate field data from the context to pass to the next wizard step """ - previous_fields = response.context['previous_fields'] + previous_fields = parse_html(response.context['previous_fields']) fields = {'wizard_step': response.context['step0']} - def grab(m): - fields[m.group(1)] = m.group(2) - return '' + for input_field in previous_fields: + input_attrs = dict(input_field.attributes) + fields[input_attrs["name"]] = input_attrs["value"] - self.input_re.sub(grab, previous_fields) return fields def check_wizard_step(self, response, step_no): @@ -428,7 +427,6 @@ class WizardTests(TestCase): """ step_count = len(self.wizard_step_data) - self.assertEqual(response.status_code, 200) self.assertContains(response, 'Step %d of %d' % (step_no, step_count)) data = self.grab_field_data(response) diff --git a/django/contrib/formtools/tests/wizard/cookiestorage.py b/django/contrib/formtools/tests/wizard/cookiestorage.py index 495d3afd03..d450f47861 100644 --- a/django/contrib/formtools/tests/wizard/cookiestorage.py +++ b/django/contrib/formtools/tests/wizard/cookiestorage.py @@ -1,3 +1,5 @@ +import json + from django.test import TestCase from django.core import signing from django.core.exceptions import SuspiciousOperation @@ -41,4 +43,5 @@ class TestCookieStorage(TestStorage, TestCase): storage.init_data() storage.update_response(response) unsigned_cookie_data = cookie_signer.unsign(response.cookies[storage.prefix].value) - self.assertEqual(unsigned_cookie_data, '{"step_files":{},"step":null,"extra_data":{},"step_data":{}}') + self.assertEqual(json.loads(unsigned_cookie_data), + {"step_files": {}, "step": None, "extra_data": {}, "step_data": {}}) From 70a0de37d132e5f1514fb939875f69649f103124 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 26 Sep 2012 18:48:09 +0800 Subject: [PATCH 047/265] Fixed #3011 -- Added swappable auth.User models. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks to the many people that contributed to the development and review of this patch, including (but not limited to) Jacob Kaplan-Moss, Anssi Kääriäinen, Ramiro Morales, Preston Holmes, Josh Ourisman, Thomas Sutton, and Roger Barnes, as well as the many, many people who have contributed to the design discussion around this ticket over many years. Squashed commit of the following: commit d84749a0f034a0a6906d20df047086b1219040d0 Merge: 531e771 7c11b1a Author: Russell Keith-Magee Date: Wed Sep 26 18:37:04 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit 531e7715da545f930c49919a19e954d41c59b446 Merge: 29d1abb 1f84b04 Author: Russell Keith-Magee Date: Wed Sep 26 07:09:23 2012 +0800 Merged recent trunk changes. commit 29d1abbe351fd5da855fe5ce09e24227d90ddc91 Merge: 8a527dd 54c81a1 Author: Russell Keith-Magee Date: Mon Sep 24 07:49:46 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit 8a527dda13c9bec955b1f7e8db5822d1d9b32a01 Author: Russell Keith-Magee Date: Mon Sep 24 07:48:05 2012 +0800 Ensure sequences are reset correctly in the presence of swapped models. commit e2b6e22f298eb986d74d28b8d9906f37f5ff8eb8 Author: Russell Keith-Magee Date: Sun Sep 23 17:53:05 2012 +0800 Modifications to the handling and docs for auth forms. commit 98aba856b534620aea9091f824b442b47d2fdb3c Author: Russell Keith-Magee Date: Sun Sep 23 15:28:57 2012 +0800 Improved error handling and docs for get_user_model() commit 0229209c844f06dfeb33b0b8eeec000c127695b6 Merge: 6494bf9 8599f64 Author: Russell Keith-Magee Date: Sun Sep 23 14:50:11 2012 +0800 Merged recent Django trunk changes. commit 6494bf91f2ddaaabec3ec017f2e3131937c35517 Author: Russell Keith-Magee Date: Mon Sep 17 21:38:44 2012 +0800 Improved validation of swappable model settings. commit 5a04cde342cc860384eb844cfda5af55204564ad Author: Russell Keith-Magee Date: Mon Sep 17 07:15:14 2012 +0800 Removed some unused imports. commit ffd535e4136dc54f084b6ac467e81444696e1c8a Author: Russell Keith-Magee Date: Sun Sep 16 20:31:28 2012 +0800 Corrected attribute access on for get_by_natural_key commit 913e1ac84c3d9c7c58a9b3bdbbb15ebccd8a8c0a Author: Russell Keith-Magee Date: Sun Sep 16 20:12:34 2012 +0800 Added test for proxy model safeguards on swappable models. commit 280bf19e94d0d534d0e51bae485c1842558f4ff4 Merge: dbb3900 935a863 Author: Russell Keith-Magee Date: Sun Sep 16 18:16:49 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit dbb3900775a99df8b6cb1d7063cf364eab55621a Author: Russell Keith-Magee Date: Sun Sep 16 18:09:27 2012 +0800 Fixes for Python 3 compatibility. commit dfd72131d8664615e245aa0f95b82604ba6b3821 Author: Russell Keith-Magee Date: Sun Sep 16 15:54:30 2012 +0800 Added protection against proxying swapped models. commit abcb027190e53613e7f1734e77ee185b2587de31 Author: Russell Keith-Magee Date: Sun Sep 16 15:11:10 2012 +0800 Cleanup and documentation of AbstractUser base class. commit a9491a87763e307f0eb0dc246f54ac865a6ffb34 Merge: fd8bb4e 08bcb4a Author: Russell Keith-Magee Date: Sun Sep 16 14:46:49 2012 +0800 Merge commit '08bcb4aec1ed154cefc631b8510ee13e9af0c19d' into t3011 commit fd8bb4e3e498a92d7a8b340f0684d5f088aa4c92 Author: Russell Keith-Magee Date: Sun Sep 16 14:20:14 2012 +0800 Documentation improvements coming from community review. commit b550a6d06d016ab6a0198c4cb2dffe9cceabe8a5 Author: Russell Keith-Magee Date: Sun Sep 16 13:52:47 2012 +0800 Refactored skipIfCustomUser into the contrib.auth tests. commit 52a02f11107c3f0d711742b8ca65b75175b79d6a Author: Russell Keith-Magee Date: Sun Sep 16 13:46:10 2012 +0800 Refactored common 'get' pattern into manager method. commit b441a6bbc7d6065175715cb09316b9f13268171b Author: Russell Keith-Magee Date: Sun Sep 16 13:41:33 2012 +0800 Added note about backwards incompatible change to admin login messages. commit 08bcb4aec1ed154cefc631b8510ee13e9af0c19d Author: Anssi Kääriäinen Date: Sat Sep 15 18:30:33 2012 +0300 Splitted User to AbstractUser and User commit d9f5e5addbad5e1a01f67e7358e4f5091c3cad81 Author: Anssi Kääriäinen Date: Sat Sep 15 18:30:02 2012 +0300 Reworked REQUIRED_FIELDS + create_user() interaction commit 579f152e4a6e06671e1ac1e59e2b43cf4d764bf4 Merge: 9184972 93e6733 Author: Russell Keith-Magee Date: Sat Sep 15 20:18:37 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit 918497218c58227f5032873ff97261627b2ceab2 Author: Russell Keith-Magee Date: Sat Sep 15 20:18:19 2012 +0800 Deprecate AUTH_PROFILE_MODULE and get_profile(). commit 334cdfc1bb6a6794791497cdefda843bca2ea57a Author: Russell Keith-Magee Date: Sat Sep 15 20:00:12 2012 +0800 Added release notes for new swappable User feature. commit 5d7bb22e8d913b51aba1c3360e7af8b01b6c0ab6 Author: Russell Keith-Magee Date: Sat Sep 15 19:59:49 2012 +0800 Ensure swapped models can't be queried. commit 57ac6e3d32605a67581e875b37ec5b2284711a32 Merge: f2ec915 abfba3b Author: Russell Keith-Magee Date: Sat Sep 15 14:31:54 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit f2ec915b20f81c8afeaa3df25f80689712f720f8 Merge: 1952656 5e99a3d Author: Russell Keith-Magee Date: Sun Sep 9 08:29:51 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit 19526563b54fa300785c49cfb625c0c6158ced67 Merge: 2c5e833 c4aa26a Author: Russell Keith-Magee Date: Sun Sep 9 08:22:26 2012 +0800 Merge recent changes from master. commit 2c5e833a30bef4305d55eacc0703533152f5c427 Author: Russell Keith-Magee Date: Sun Sep 9 07:53:46 2012 +0800 Corrected admin_views tests following removal of the email fallback on admin logins. commit 20d1892491839d6ef21f37db4ca136935c2076bf Author: Russell Keith-Magee Date: Sun Sep 9 01:00:37 2012 +0800 Added conditional skips for all tests dependent on the default User model commit 40ea8b888284775481fc1eaadeff267dbd7e3dfa Author: Russell Keith-Magee Date: Sat Sep 8 23:47:02 2012 +0800 Added documentation for REQUIRED_FIELDS in custom auth. commit e6aaf659708cf6491f5485d3edfa616cb9214cc0 Author: Russell Keith-Magee Date: Sat Sep 8 23:20:02 2012 +0800 Added first draft of custom User docs. Thanks to Greg Turner for the initial text. commit 75118bd242eec87649da2859e8c50a199a8a1dca Author: Thomas Sutton Date: Mon Aug 20 11:17:26 2012 +0800 Admin app should not allow username discovery The admin app login form should not allow users to discover the username associated with an email address. commit d088b3af58dad7449fc58493193a327725c57c22 Author: Thomas Sutton Date: Mon Aug 20 10:32:13 2012 +0800 Admin app login form should use swapped user model commit 7e82e83d67ee0871a72e1a3a723afdd214fcefc3 Merge: e29c010 39aa890 Author: Russell Keith-Magee Date: Fri Sep 7 23:45:03 2012 +0800 Merged master changes. commit e29c010beb96ca07697c4e3e0c0d5d3ffdc4c0a3 Merge: 8e3fd70 30bdf22 Author: Russell Keith-Magee Date: Mon Aug 20 13:12:57 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit 8e3fd703d02c31a4c3ac9f51f5011d03c0bd47f6 Merge: 507bb50 26e0ba0 Author: Russell Keith-Magee Date: Mon Aug 20 13:09:09 2012 +0800 Merged recent changes from trunk. commit 507bb50a9291bfcdcfa1198f9fea21d4e3b1e762 Author: Russell Keith-Magee Date: Mon Jun 4 20:41:37 2012 +0800 Modified auth app so that login with alternate auth app is possible. commit dabe3628362ab7a4a6c9686dd874803baa997eaa Author: Russell Keith-Magee Date: Mon Jun 4 20:10:51 2012 +0800 Modified auth management commands to handle custom user definitions. commit 7cc0baf89d490c92ef3f1dc909b8090191a1294b Author: Russell Keith-Magee Date: Mon Jun 4 14:17:28 2012 +0800 Added model Meta option for swappable models, and made auth.User a swappable model --- django/conf/global_settings.py | 2 + django/contrib/admin/forms.py | 15 +- django/contrib/admin/models.py | 6 +- django/contrib/admin/sites.py | 34 +- .../contrib/admin/templates/admin/base.html | 2 +- .../contrib/admin/templates/admin/login.html | 2 +- django/contrib/admin/views/decorators.py | 1 + django/contrib/auth/__init__.py | 23 +- django/contrib/auth/backends.py | 24 +- django/contrib/auth/fixtures/custom_user.json | 14 + django/contrib/auth/forms.py | 15 +- django/contrib/auth/management/__init__.py | 32 +- .../management/commands/changepassword.py | 16 +- .../management/commands/createsuperuser.py | 122 +++--- django/contrib/auth/models.py | 203 ++++++---- django/contrib/auth/tests/__init__.py | 37 +- django/contrib/auth/tests/auth_backends.py | 5 + django/contrib/auth/tests/basic.py | 43 ++- .../contrib/auth/tests/context_processors.py | 3 +- django/contrib/auth/tests/custom_user.py | 75 ++++ django/contrib/auth/tests/decorators.py | 4 +- django/contrib/auth/tests/forms.py | 8 +- django/contrib/auth/tests/management.py | 100 ++++- django/contrib/auth/tests/models.py | 6 + django/contrib/auth/tests/remote_user.py | 4 + django/contrib/auth/tests/signals.py | 2 + django/contrib/auth/tests/tokens.py | 2 + django/contrib/auth/tests/utils.py | 9 + django/contrib/auth/tests/views.py | 30 ++ django/contrib/auth/tokens.py | 1 + django/contrib/auth/views.py | 19 +- django/contrib/comments/models.py | 17 +- django/core/exceptions.py | 13 +- django/core/management/commands/sqlall.py | 1 + django/core/management/commands/syncdb.py | 2 +- django/core/management/commands/validate.py | 1 + django/core/management/sql.py | 12 +- django/core/management/validation.py | 27 +- django/core/validators.py | 33 +- django/db/backends/__init__.py | 10 +- django/db/backends/creation.py | 16 +- django/db/models/base.py | 20 +- django/db/models/fields/related.py | 35 +- django/db/models/loading.py | 1 + django/db/models/manager.py | 9 +- django/db/models/options.py | 21 +- django/test/__init__.py | 3 +- django/test/testcases.py | 2 + django/test/utils.py | 7 +- docs/internals/deprecation.txt | 3 + docs/ref/settings.txt | 27 +- docs/releases/1.5.txt | 42 ++ docs/topics/auth.txt | 359 ++++++++++++++++++ .../invalid_models/invalid_models/models.py | 110 +++++- tests/modeltests/invalid_models/tests.py | 13 +- tests/modeltests/proxy_models/tests.py | 38 +- tests/regressiontests/admin_views/tests.py | 102 ++--- 57 files changed, 1426 insertions(+), 357 deletions(-) create mode 100644 django/contrib/auth/fixtures/custom_user.json create mode 100644 django/contrib/auth/tests/custom_user.py create mode 100644 django/contrib/auth/tests/utils.py diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 13f7991b57..4d5dc49ee0 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -488,6 +488,8 @@ PROFANITIES_LIST = () # AUTHENTICATION # ################## +AUTH_USER_MODEL = 'auth.User' + AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) LOGIN_URL = '/accounts/login/' diff --git a/django/contrib/admin/forms.py b/django/contrib/admin/forms.py index 398af075b1..f1e7076ece 100644 --- a/django/contrib/admin/forms.py +++ b/django/contrib/admin/forms.py @@ -4,12 +4,12 @@ from django import forms from django.contrib.auth import authenticate from django.contrib.auth.forms import AuthenticationForm -from django.contrib.auth.models import User -from django.utils.translation import ugettext_lazy, ugettext as _ +from django.utils.translation import ugettext_lazy ERROR_MESSAGE = ugettext_lazy("Please enter the correct username and password " "for a staff account. Note that both fields are case-sensitive.") + class AdminAuthenticationForm(AuthenticationForm): """ A custom authentication form used in the admin app. @@ -26,17 +26,6 @@ class AdminAuthenticationForm(AuthenticationForm): if username and password: self.user_cache = authenticate(username=username, password=password) if self.user_cache is None: - if '@' in username: - # Mistakenly entered e-mail address instead of username? Look it up. - try: - user = User.objects.get(email=username) - except (User.DoesNotExist, User.MultipleObjectsReturned): - # Nothing to do here, moving along. - pass - else: - if user.check_password(password): - message = _("Your e-mail address is not your username." - " Try '%s' instead.") % user.username raise forms.ValidationError(message) elif not self.user_cache.is_active or not self.user_cache.is_staff: raise forms.ValidationError(message) diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py index 2b12edd4e2..e1d3b40d01 100644 --- a/django/contrib/admin/models.py +++ b/django/contrib/admin/models.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals from django.db import models +from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.contrib.auth.models import User from django.contrib.admin.util import quote from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_text @@ -12,15 +12,17 @@ ADDITION = 1 CHANGE = 2 DELETION = 3 + class LogEntryManager(models.Manager): def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''): e = self.model(None, None, user_id, content_type_id, smart_text(object_id), object_repr[:200], action_flag, change_message) e.save() + @python_2_unicode_compatible class LogEntry(models.Model): action_time = models.DateTimeField(_('action time'), auto_now=True) - user = models.ForeignKey(User) + user = models.ForeignKey(settings.AUTH_USER_MODEL) content_type = models.ForeignKey(ContentType, blank=True, null=True) object_id = models.TextField(_('object id'), blank=True, null=True) object_repr = models.CharField(_('object repr'), max_length=200) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 05773ceac0..e375bc608f 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -9,7 +9,6 @@ from django.db.models.base import ModelBase from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse, NoReverseMatch from django.template.response import TemplateResponse -from django.utils.safestring import mark_safe from django.utils import six from django.utils.text import capfirst from django.utils.translation import ugettext as _ @@ -18,12 +17,15 @@ from django.conf import settings LOGIN_FORM_KEY = 'this_is_the_login_form' + class AlreadyRegistered(Exception): pass + class NotRegistered(Exception): pass + class AdminSite(object): """ An AdminSite object encapsulates an instance of the Django admin application, ready @@ -41,7 +43,7 @@ class AdminSite(object): password_change_done_template = None def __init__(self, name='admin', app_name='admin'): - self._registry = {} # model_class class -> admin_class instance + self._registry = {} # model_class class -> admin_class instance self.name = name self.app_name = app_name self._actions = {'delete_selected': actions.delete_selected} @@ -80,20 +82,23 @@ class AdminSite(object): if model in self._registry: raise AlreadyRegistered('The model %s is already registered' % model.__name__) - # If we got **options then dynamically construct a subclass of - # admin_class with those **options. - if options: - # For reasons I don't quite understand, without a __module__ - # the created class appears to "live" in the wrong place, - # which causes issues later on. - options['__module__'] = __name__ - admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) + # Ignore the registration if the model has been + # swapped out. + if not model._meta.swapped: + # If we got **options then dynamically construct a subclass of + # admin_class with those **options. + if options: + # For reasons I don't quite understand, without a __module__ + # the created class appears to "live" in the wrong place, + # which causes issues later on. + options['__module__'] = __name__ + admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) - # Validate (which might be a no-op) - validate(admin_class, model) + # Validate (which might be a no-op) + validate(admin_class, model) - # Instantiate the admin class to save in the registry - self._registry[model] = admin_class(model, self) + # Instantiate the admin class to save in the registry + self._registry[model] = admin_class(model, self) def unregister(self, model_or_iterable): """ @@ -319,6 +324,7 @@ class AdminSite(object): REDIRECT_FIELD_NAME: request.get_full_path(), } context.update(extra_context or {}) + defaults = { 'extra_context': context, 'current_app': self.name, diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index caa26744d4..3d2a07eba2 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -26,7 +26,7 @@ {% if user.is_active and user.is_staff %}
{% trans 'Welcome,' %} - {% filter force_escape %}{% firstof user.first_name user.username %}{% endfilter %}. + {% filter force_escape %}{% firstof user.get_short_name user.username %}{% endfilter %}. {% block userlinks %} {% url 'django-admindocs-docroot' as docsroot %} {% if docsroot %} diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index 06fe4c8160..4690363891 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -30,7 +30,7 @@
{% csrf_token %}
{% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %} - {{ form.username }} + {{ form.username }}
{% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %} diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py index b5313a162e..e19265fc83 100644 --- a/django/contrib/admin/views/decorators.py +++ b/django/contrib/admin/views/decorators.py @@ -4,6 +4,7 @@ from django.contrib.admin.forms import AdminAuthenticationForm from django.contrib.auth.views import login from django.contrib.auth import REDIRECT_FIELD_NAME + def staff_member_required(view_func): """ Decorator for views that checks that the user is logged in and is a staff diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 0b3ccf7d8c..1050d1d1bb 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -6,9 +6,10 @@ SESSION_KEY = '_auth_user_id' BACKEND_SESSION_KEY = '_auth_user_backend' REDIRECT_FIELD_NAME = 'next' + def load_backend(path): i = path.rfind('.') - module, attr = path[:i], path[i+1:] + module, attr = path[:i], path[i + 1:] try: mod = import_module(module) except ImportError as e: @@ -21,6 +22,7 @@ def load_backend(path): raise ImproperlyConfigured('Module "%s" does not define a "%s" authentication backend' % (module, attr)) return cls() + def get_backends(): from django.conf import settings backends = [] @@ -30,6 +32,7 @@ def get_backends(): raise ImproperlyConfigured('No authentication backends have been defined. Does AUTHENTICATION_BACKENDS contain anything?') return backends + def authenticate(**credentials): """ If the given credentials are valid, return a User object. @@ -46,6 +49,7 @@ def authenticate(**credentials): user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) return user + def login(request, user): """ Persist a user id and a backend in the request. This way a user doesn't @@ -69,6 +73,7 @@ def login(request, user): request.user = user user_logged_in.send(sender=user.__class__, request=request, user=user) + def logout(request): """ Removes the authenticated user's ID from the request and flushes their @@ -86,6 +91,22 @@ def logout(request): from django.contrib.auth.models import AnonymousUser request.user = AnonymousUser() + +def get_user_model(): + "Return the User model that is active in this project" + from django.conf import settings + from django.db.models import get_model + + try: + app_label, model_name = settings.AUTH_USER_MODEL.split('.') + except ValueError: + raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'") + user_model = get_model(app_label, model_name) + if user_model is None: + raise ImproperlyConfigured("AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.AUTH_USER_MODEL) + return user_model + + def get_user(request): from django.contrib.auth.models import AnonymousUser try: diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index 9088e2fbf6..d103f32eb5 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals - -from django.contrib.auth.models import User, Permission +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission class ModelBackend(object): @@ -12,10 +12,11 @@ class ModelBackend(object): # configurable. def authenticate(self, username=None, password=None): try: - user = User.objects.get(username=username) + UserModel = get_user_model() + user = UserModel.objects.get_by_natural_key(username) if user.check_password(password): return user - except User.DoesNotExist: + except UserModel.DoesNotExist: return None def get_group_permissions(self, user_obj, obj=None): @@ -60,8 +61,9 @@ class ModelBackend(object): def get_user(self, user_id): try: - return User.objects.get(pk=user_id) - except User.DoesNotExist: + UserModel = get_user_model() + return UserModel.objects.get(pk=user_id) + except UserModel.DoesNotExist: return None @@ -94,17 +96,21 @@ class RemoteUserBackend(ModelBackend): user = None username = self.clean_username(remote_user) + UserModel = get_user_model() + # Note that this could be accomplished in one try-except clause, but # instead we use get_or_create when creating unknown users since it has # built-in safeguards for multiple threads. if self.create_unknown_user: - user, created = User.objects.get_or_create(username=username) + user, created = UserModel.objects.get_or_create(**{ + getattr(UserModel, 'USERNAME_FIELD', 'username'): username + }) if created: user = self.configure_user(user) else: try: - user = User.objects.get(username=username) - except User.DoesNotExist: + user = UserModel.objects.get_by_natural_key(username) + except UserModel.DoesNotExist: pass return user diff --git a/django/contrib/auth/fixtures/custom_user.json b/django/contrib/auth/fixtures/custom_user.json new file mode 100644 index 0000000000..770bea6541 --- /dev/null +++ b/django/contrib/auth/fixtures/custom_user.json @@ -0,0 +1,14 @@ +[ + { + "pk": "1", + "model": "auth.customuser", + "fields": { + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "last_login": "2006-12-17 07:03:31", + "email": "staffmember@example.com", + "is_active": true, + "is_admin": false, + "date_of_birth": "1976-11-08" + } + } +] \ No newline at end of file diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 08488237c7..a430f042e9 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -7,9 +7,10 @@ from django.utils.datastructures import SortedDict from django.utils.html import format_html, format_html_join from django.utils.http import int_to_base36 from django.utils.safestring import mark_safe +from django.utils.text import capfirst from django.utils.translation import ugettext, ugettext_lazy as _ -from django.contrib.auth import authenticate +from django.contrib.auth import authenticate, get_user_model from django.contrib.auth.models import User from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher from django.contrib.auth.tokens import default_token_generator @@ -135,7 +136,7 @@ class AuthenticationForm(forms.Form): Base class for authenticating users. Extend this to get a form that accepts username/password logins. """ - username = forms.CharField(label=_("Username"), max_length=30) + username = forms.CharField(max_length=30) password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) error_messages = { @@ -157,6 +158,11 @@ class AuthenticationForm(forms.Form): self.user_cache = None super(AuthenticationForm, self).__init__(*args, **kwargs) + # Set the label for the "username" field. + UserModel = get_user_model() + username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username')) + self.fields['username'].label = capfirst(username_field.verbose_name) + def clean(self): username = self.cleaned_data.get('username') password = self.cleaned_data.get('password') @@ -198,9 +204,10 @@ class PasswordResetForm(forms.Form): """ Validates that an active user exists with the given email address. """ + UserModel = get_user_model() email = self.cleaned_data["email"] - self.users_cache = User.objects.filter(email__iexact=email, - is_active=True) + self.users_cache = UserModel.objects.filter(email__iexact=email, + is_active=True) if not len(self.users_cache): raise forms.ValidationError(self.error_messages['unknown']) if any((user.password == UNUSABLE_PASSWORD) diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index 23a053d985..2ada789cae 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -6,9 +6,10 @@ from __future__ import unicode_literals import getpass import locale import unicodedata -from django.contrib.auth import models as auth_app + +from django.contrib.auth import models as auth_app, get_user_model +from django.core import exceptions from django.db.models import get_models, signals -from django.contrib.auth.models import User from django.utils import six from django.utils.six.moves import input @@ -64,7 +65,9 @@ def create_permissions(app, created_models, verbosity, **kwargs): def create_superuser(app, created_models, verbosity, db, **kwargs): from django.core.management import call_command - if auth_app.User in created_models and kwargs.get('interactive', True): + UserModel = get_user_model() + + if UserModel in created_models and kwargs.get('interactive', True): msg = ("\nYou just installed Django's auth system, which means you " "don't have any superusers defined.\nWould you like to create one " "now? (yes/no): ") @@ -113,28 +116,35 @@ def get_default_username(check_db=True): :returns: The username, or an empty string if no username can be determined. """ - from django.contrib.auth.management.commands.createsuperuser import ( - RE_VALID_USERNAME) + # If the User model has been swapped out, we can't make any assumptions + # about the default user name. + if auth_app.User._meta.swapped: + return '' + default_username = get_system_username() try: default_username = unicodedata.normalize('NFKD', default_username)\ .encode('ascii', 'ignore').decode('ascii').replace(' ', '').lower() except UnicodeDecodeError: return '' - if not RE_VALID_USERNAME.match(default_username): + + # Run the username validator + try: + auth_app.User._meta.get_field('username').run_validators(default_username) + except exceptions.ValidationError: return '' + # Don't return the default username if it is already taken. if check_db and default_username: try: - User.objects.get(username=default_username) - except User.DoesNotExist: + auth_app.User.objects.get(username=default_username) + except auth_app.User.DoesNotExist: pass else: return '' return default_username - signals.post_syncdb.connect(create_permissions, - dispatch_uid = "django.contrib.auth.management.create_permissions") + dispatch_uid="django.contrib.auth.management.create_permissions") signals.post_syncdb.connect(create_superuser, - sender=auth_app, dispatch_uid = "django.contrib.auth.management.create_superuser") + sender=auth_app, dispatch_uid="django.contrib.auth.management.create_superuser") diff --git a/django/contrib/auth/management/commands/changepassword.py b/django/contrib/auth/management/commands/changepassword.py index d125dfe5b6..1a2387442c 100644 --- a/django/contrib/auth/management/commands/changepassword.py +++ b/django/contrib/auth/management/commands/changepassword.py @@ -1,8 +1,8 @@ import getpass from optparse import make_option +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import User from django.db import DEFAULT_DB_ALIAS @@ -30,12 +30,16 @@ class Command(BaseCommand): else: username = getpass.getuser() + UserModel = get_user_model() + try: - u = User.objects.using(options.get('database')).get(username=username) - except User.DoesNotExist: + u = UserModel.objects.using(options.get('database')).get(**{ + getattr(UserModel, 'USERNAME_FIELD', 'username'): username + }) + except UserModel.DoesNotExist: raise CommandError("user '%s' does not exist" % username) - self.stdout.write("Changing password for user '%s'\n" % u.username) + self.stdout.write("Changing password for user '%s'\n" % u) MAX_TRIES = 3 count = 0 @@ -48,9 +52,9 @@ class Command(BaseCommand): count = count + 1 if count == MAX_TRIES: - raise CommandError("Aborting password change for user '%s' after %s attempts" % (username, count)) + raise CommandError("Aborting password change for user '%s' after %s attempts" % (u, count)) u.set_password(p1) u.save() - return "Password changed successfully for user '%s'" % u.username + return "Password changed successfully for user '%s'" % u diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index 6e0d0bc754..c5f6469548 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -3,109 +3,114 @@ Management utility to create superusers. """ import getpass -import re import sys from optparse import make_option -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.contrib.auth.management import get_default_username from django.core import exceptions from django.core.management.base import BaseCommand, CommandError from django.db import DEFAULT_DB_ALIAS from django.utils.six.moves import input -from django.utils.translation import ugettext as _ - -RE_VALID_USERNAME = re.compile('[\w.@+-]+$') - -EMAIL_RE = re.compile( - r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom - r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' # quoted-string - r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain - - -def is_valid_email(value): - if not EMAIL_RE.search(value): - raise exceptions.ValidationError(_('Enter a valid e-mail address.')) +from django.utils.text import capfirst class Command(BaseCommand): option_list = BaseCommand.option_list + ( make_option('--username', dest='username', default=None, help='Specifies the username for the superuser.'), - make_option('--email', dest='email', default=None, - help='Specifies the email address for the superuser.'), make_option('--noinput', action='store_false', dest='interactive', default=True, help=('Tells Django to NOT prompt the user for input of any kind. ' - 'You must use --username and --email with --noinput, and ' - 'superusers created with --noinput will not be able to log ' - 'in until they\'re given a valid password.')), + 'You must use --username with --noinput, along with an option for ' + 'any other required field. Superusers created with --noinput will ' + ' not be able to log in until they\'re given a valid password.')), make_option('--database', action='store', dest='database', default=DEFAULT_DB_ALIAS, help='Specifies the database to use. Default is "default".'), + ) + tuple( + make_option('--%s' % field, dest=field, default=None, + help='Specifies the %s for the superuser.' % field) + for field in get_user_model().REQUIRED_FIELDS ) + help = 'Used to create a superuser.' def handle(self, *args, **options): username = options.get('username', None) - email = options.get('email', None) interactive = options.get('interactive') verbosity = int(options.get('verbosity', 1)) database = options.get('database') - # Do quick and dirty validation if --noinput - if not interactive: - if not username or not email: - raise CommandError("You must use --username and --email with --noinput.") - if not RE_VALID_USERNAME.match(username): - raise CommandError("Invalid username. Use only letters, digits, and underscores") - try: - is_valid_email(email) - except exceptions.ValidationError: - raise CommandError("Invalid email address.") + UserModel = get_user_model() + + username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username')) + other_fields = UserModel.REQUIRED_FIELDS # If not provided, create the user with an unusable password password = None + other_data = {} - # Prompt for username/email/password. Enclose this whole thing in a - # try/except to trap for a keyboard interrupt and exit gracefully. - if interactive: + # Do quick and dirty validation if --noinput + if not interactive: + try: + if not username: + raise CommandError("You must use --username with --noinput.") + username = username_field.clean(username, None) + + for field_name in other_fields: + if options.get(field_name): + field = UserModel._meta.get_field(field_name) + other_data[field_name] = field.clean(options[field_name], None) + else: + raise CommandError("You must use --%s with --noinput." % field_name) + except exceptions.ValidationError as e: + raise CommandError('; '.join(e.messages)) + + else: + # Prompt for username/password, and any other required fields. + # Enclose this whole thing in a try/except to trap for a + # keyboard interrupt and exit gracefully. default_username = get_default_username() try: # Get a username - while 1: + while username is None: + username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username')) if not username: - input_msg = 'Username' + input_msg = capfirst(username_field.verbose_name) if default_username: input_msg += ' (leave blank to use %r)' % default_username - username = input(input_msg + ': ') - if default_username and username == '': + raw_value = input(input_msg + ': ') + if default_username and raw_value == '': username = default_username - if not RE_VALID_USERNAME.match(username): - self.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.") + try: + username = username_field.clean(raw_value, None) + except exceptions.ValidationError as e: + self.stderr.write("Error: %s" % '; '.join(e.messages)) username = None continue try: - User.objects.using(database).get(username=username) - except User.DoesNotExist: - break + UserModel.objects.using(database).get(**{ + getattr(UserModel, 'USERNAME_FIELD', 'username'): username + }) + except UserModel.DoesNotExist: + pass else: self.stderr.write("Error: That username is already taken.") username = None - # Get an email - while 1: - if not email: - email = input('E-mail address: ') - try: - is_valid_email(email) - except exceptions.ValidationError: - self.stderr.write("Error: That e-mail address is invalid.") - email = None - else: - break + for field_name in other_fields: + field = UserModel._meta.get_field(field_name) + other_data[field_name] = options.get(field_name) + while other_data[field_name] is None: + raw_value = input(capfirst(field.verbose_name + ': ')) + try: + other_data[field_name] = field.clean(raw_value, None) + except exceptions.ValidationError as e: + self.stderr.write("Error: %s" % '; '.join(e.messages)) + other_data[field_name] = None # Get a password - while 1: + while password is None: if not password: password = getpass.getpass() password2 = getpass.getpass('Password (again): ') @@ -117,12 +122,11 @@ class Command(BaseCommand): self.stderr.write("Error: Blank passwords aren't allowed.") password = None continue - break + except KeyboardInterrupt: self.stderr.write("\nOperation cancelled.") sys.exit(1) - User.objects.db_manager(database).create_superuser(username, email, password) + UserModel.objects.db_manager(database).create_superuser(username=username, password=password, **other_data) if verbosity >= 1: - self.stdout.write("Superuser created successfully.") - + self.stdout.write("Superuser created successfully.") diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 98eb44ea05..abcc7ceafc 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -1,7 +1,10 @@ from __future__ import unicode_literals +import re +import warnings from django.core.exceptions import ImproperlyConfigured from django.core.mail import send_mail +from django.core import validators from django.db import models from django.db.models.manager import EmptyManager from django.utils.crypto import get_random_string @@ -96,6 +99,7 @@ class GroupManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) + @python_2_unicode_compatible class Group(models.Model): """ @@ -131,7 +135,7 @@ class Group(models.Model): return (self.name,) -class UserManager(models.Manager): +class BaseUserManager(models.Manager): @classmethod def normalize_email(cls, email): @@ -148,30 +152,6 @@ class UserManager(models.Manager): email = '@'.join([email_name, domain_part.lower()]) return email - def create_user(self, username, email=None, password=None): - """ - Creates and saves a User with the given username, email and password. - """ - now = timezone.now() - if not username: - raise ValueError('The given username must be set') - email = UserManager.normalize_email(email) - user = self.model(username=username, email=email, - is_staff=False, is_active=True, is_superuser=False, - last_login=now, date_joined=now) - - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, username, email, password): - u = self.create_user(username, email, password) - u.is_staff = True - u.is_active = True - u.is_superuser = True - u.save(using=self._db) - return u - def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyz' 'ABCDEFGHJKLMNPQRSTUVWXYZ' @@ -185,7 +165,34 @@ class UserManager(models.Manager): return get_random_string(length, allowed_chars) def get_by_natural_key(self, username): - return self.get(username=username) + return self.get(**{getattr(self.model, 'USERNAME_FIELD', 'username'): username}) + + +class UserManager(BaseUserManager): + + def create_user(self, username, email=None, password=None, **extra_fields): + """ + Creates and saves a User with the given username, email and password. + """ + now = timezone.now() + if not username: + raise ValueError('The given username must be set') + email = UserManager.normalize_email(email) + user = self.model(username=username, email=email, + is_staff=False, is_active=True, is_superuser=False, + last_login=now, date_joined=now, **extra_fields) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, username, email, password, **extra_fields): + u = self.create_user(username, email, password, **extra_fields) + u.is_staff = True + u.is_active = True + u.is_superuser = True + u.save(using=self._db) + return u # A few helper functions for common logic between User and AnonymousUser. @@ -201,8 +208,6 @@ def _user_get_all_permissions(user, obj): def _user_has_perm(user, perm, obj): - anon = user.is_anonymous() - active = user.is_active for backend in auth.get_backends(): if hasattr(backend, "has_perm"): if obj is not None: @@ -215,8 +220,6 @@ def _user_has_perm(user, perm, obj): def _user_has_module_perms(user, app_label): - anon = user.is_anonymous() - active = user.is_active for backend in auth.get_backends(): if hasattr(backend, "has_module_perms"): if backend.has_module_perms(user, app_label): @@ -224,53 +227,14 @@ def _user_has_module_perms(user, app_label): return False -@python_2_unicode_compatible -class User(models.Model): - """ - Users within the Django authentication system are represented by this - model. - - Username and password are required. Other fields are optional. - """ - username = models.CharField(_('username'), max_length=30, unique=True, - help_text=_('Required. 30 characters or fewer. Letters, numbers and ' - '@/./+/-/_ characters')) - first_name = models.CharField(_('first name'), max_length=30, blank=True) - last_name = models.CharField(_('last name'), max_length=30, blank=True) - email = models.EmailField(_('e-mail address'), blank=True) +class AbstractBaseUser(models.Model): password = models.CharField(_('password'), max_length=128) - is_staff = models.BooleanField(_('staff status'), default=False, - help_text=_('Designates whether the user can log into this admin ' - 'site.')) - is_active = models.BooleanField(_('active'), default=True, - help_text=_('Designates whether this user should be treated as ' - 'active. Unselect this instead of deleting accounts.')) - is_superuser = models.BooleanField(_('superuser status'), default=False, - help_text=_('Designates that this user has all permissions without ' - 'explicitly assigning them.')) last_login = models.DateTimeField(_('last login'), default=timezone.now) - date_joined = models.DateTimeField(_('date joined'), default=timezone.now) - groups = models.ManyToManyField(Group, verbose_name=_('groups'), - blank=True, help_text=_('The groups this user belongs to. A user will ' - 'get all permissions granted to each of ' - 'his/her group.')) - user_permissions = models.ManyToManyField(Permission, - verbose_name=_('user permissions'), blank=True, - help_text='Specific permissions for this user.') - objects = UserManager() + + REQUIRED_FIELDS = [] class Meta: - verbose_name = _('user') - verbose_name_plural = _('users') - - def __str__(self): - return self.username - - def natural_key(self): - return (self.username,) - - def get_absolute_url(self): - return "/users/%s/" % urlquote(self.username) + abstract = True def is_anonymous(self): """ @@ -286,13 +250,6 @@ class User(models.Model): """ return True - def get_full_name(self): - """ - Returns the first_name plus the last_name, with a space in between. - """ - full_name = '%s %s' % (self.first_name, self.last_name) - return full_name.strip() - def set_password(self, raw_password): self.password = make_password(raw_password) @@ -313,6 +270,77 @@ class User(models.Model): def has_usable_password(self): return is_password_usable(self.password) + def get_full_name(self): + raise NotImplementedError() + + def get_short_name(self): + raise NotImplementedError() + + +@python_2_unicode_compatible +class AbstractUser(AbstractBaseUser): + """ + An abstract base class implementing a fully featured User model with + admin-compliant permissions. + + Username, password and email are required. Other fields are optional. + """ + username = models.CharField(_('username'), max_length=30, unique=True, + help_text=_('Required. 30 characters or fewer. Letters, numbers and ' + '@/./+/-/_ characters'), + validators=[ + validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid') + ]) + first_name = models.CharField(_('first name'), max_length=30, blank=True) + last_name = models.CharField(_('last name'), max_length=30, blank=True) + email = models.EmailField(_('email address'), blank=True) + is_staff = models.BooleanField(_('staff status'), default=False, + help_text=_('Designates whether the user can log into this admin ' + 'site.')) + is_active = models.BooleanField(_('active'), default=True, + help_text=_('Designates whether this user should be treated as ' + 'active. Unselect this instead of deleting accounts.')) + is_superuser = models.BooleanField(_('superuser status'), default=False, + help_text=_('Designates that this user has all permissions without ' + 'explicitly assigning them.')) + date_joined = models.DateTimeField(_('date joined'), default=timezone.now) + groups = models.ManyToManyField(Group, verbose_name=_('groups'), + blank=True, help_text=_('The groups this user belongs to. A user will ' + 'get all permissions granted to each of ' + 'his/her group.')) + user_permissions = models.ManyToManyField(Permission, + verbose_name=_('user permissions'), blank=True, + help_text='Specific permissions for this user.') + + objects = UserManager() + + REQUIRED_FIELDS = ['email'] + + class Meta: + verbose_name = _('user') + verbose_name_plural = _('users') + abstract = True + + def __str__(self): + return self.username + + def natural_key(self): + return (self.username,) + + def get_absolute_url(self): + return "/users/%s/" % urlquote(self.username) + + def get_full_name(self): + """ + Returns the first_name plus the last_name, with a space in between. + """ + full_name = '%s %s' % (self.first_name, self.last_name) + return full_name.strip() + + def get_short_name(self): + "Returns the short name for the user." + return self.first_name + def get_group_permissions(self, obj=None): """ Returns a list of permission strings that this user has through his/her @@ -381,6 +409,8 @@ class User(models.Model): Returns site-specific profile for this user. Raises SiteProfileNotAvailable if this site does not allow profiles. """ + warnings.warn("The use of AUTH_PROFILE_MODULE to define user profiles has been deprecated.", + PendingDeprecationWarning) if not hasattr(self, '_profile_cache'): from django.conf import settings if not getattr(settings, 'AUTH_PROFILE_MODULE', False): @@ -407,6 +437,17 @@ class User(models.Model): return self._profile_cache +class User(AbstractUser): + """ + Users within the Django authentication system are represented by this + model. + + Username, password and email are required. Other fields are optional. + """ + class Meta: + swappable = 'AUTH_USER_MODEL' + + @python_2_unicode_compatible class AnonymousUser(object): id = None @@ -431,7 +472,7 @@ class AnonymousUser(object): return not self.__eq__(other) def __hash__(self): - return 1 # instances always return the same hash value + return 1 # instances always return the same hash value def save(self): raise NotImplementedError diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 16eaa5c5b4..094a595238 100644 --- a/django/contrib/auth/tests/__init__.py +++ b/django/contrib/auth/tests/__init__.py @@ -1,26 +1,15 @@ -from django.contrib.auth.tests.auth_backends import (BackendTest, - RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest, - InActiveUserBackendTest) -from django.contrib.auth.tests.basic import BasicTestCase -from django.contrib.auth.tests.context_processors import AuthContextProcessorTests -from django.contrib.auth.tests.decorators import LoginRequiredTestCase -from django.contrib.auth.tests.forms import (UserCreationFormTest, - AuthenticationFormTest, SetPasswordFormTest, PasswordChangeFormTest, - UserChangeFormTest, PasswordResetFormTest) -from django.contrib.auth.tests.remote_user import (RemoteUserTest, - RemoteUserNoCreateTest, RemoteUserCustomTest) -from django.contrib.auth.tests.management import ( - GetDefaultUsernameTestCase, - ChangepasswordManagementCommandTestCase, -) -from django.contrib.auth.tests.models import (ProfileTestCase, NaturalKeysTestCase, - LoadDataWithoutNaturalKeysTestCase, LoadDataWithNaturalKeysTestCase, - UserManagerTestCase) -from django.contrib.auth.tests.hashers import TestUtilsHashPass -from django.contrib.auth.tests.signals import SignalTestCase -from django.contrib.auth.tests.tokens import TokenGeneratorTest -from django.contrib.auth.tests.views import (AuthViewNamedURLTests, - PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest, - LoginURLSettings) +from django.contrib.auth.tests.custom_user import * +from django.contrib.auth.tests.auth_backends import * +from django.contrib.auth.tests.basic import * +from django.contrib.auth.tests.context_processors import * +from django.contrib.auth.tests.decorators import * +from django.contrib.auth.tests.forms import * +from django.contrib.auth.tests.remote_user import * +from django.contrib.auth.tests.management import * +from django.contrib.auth.tests.models import * +from django.contrib.auth.tests.hashers import * +from django.contrib.auth.tests.signals import * +from django.contrib.auth.tests.tokens import * +from django.contrib.auth.tests.views import * # The password for the fixture data users is 'password' diff --git a/django/contrib/auth/tests/auth_backends.py b/django/contrib/auth/tests/auth_backends.py index 9a4d8f9b3a..a6be985412 100644 --- a/django/contrib/auth/tests/auth_backends.py +++ b/django/contrib/auth/tests/auth_backends.py @@ -2,12 +2,14 @@ from __future__ import unicode_literals from django.conf import settings from django.contrib.auth.models import User, Group, Permission, AnonymousUser +from django.contrib.auth.tests.utils import skipIfCustomUser from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.test.utils import override_settings +@skipIfCustomUser class BackendTest(TestCase): backend = 'django.contrib.auth.backends.ModelBackend' @@ -151,6 +153,7 @@ class SimpleRowlevelBackend(object): return ['none'] +@skipIfCustomUser class RowlevelBackendTest(TestCase): """ Tests for auth backend that supports object level permissions @@ -223,6 +226,7 @@ class AnonymousUserBackendTest(TestCase): self.assertEqual(self.user1.get_all_permissions(TestObj()), set(['anon'])) +@skipIfCustomUser @override_settings(AUTHENTICATION_BACKENDS=[]) class NoBackendsTest(TestCase): """ @@ -235,6 +239,7 @@ class NoBackendsTest(TestCase): self.assertRaises(ImproperlyConfigured, self.user.has_perm, ('perm', TestObj(),)) +@skipIfCustomUser class InActiveUserBackendTest(TestCase): """ Tests for a inactive user diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py index 710754b8f1..ed1d0674fc 100644 --- a/django/contrib/auth/tests/basic.py +++ b/django/contrib/auth/tests/basic.py @@ -1,13 +1,18 @@ import locale -import traceback +from django.contrib.auth import get_user_model from django.contrib.auth.management.commands import createsuperuser from django.contrib.auth.models import User, AnonymousUser +from django.contrib.auth.tests.custom_user import CustomUser +from django.contrib.auth.tests.utils import skipIfCustomUser +from django.core.exceptions import ImproperlyConfigured from django.core.management import call_command from django.test import TestCase +from django.test.utils import override_settings from django.utils.six import StringIO +@skipIfCustomUser class BasicTestCase(TestCase): def test_user(self): "Check that users can be created and can set their password" @@ -33,7 +38,7 @@ class BasicTestCase(TestCase): self.assertFalse(u.is_superuser) # Check API-based user creation with no password - u2 = User.objects.create_user('testuser2', 'test2@example.com') + User.objects.create_user('testuser2', 'test2@example.com') self.assertFalse(u.has_usable_password()) def test_user_no_email(self): @@ -98,7 +103,6 @@ class BasicTestCase(TestCase): self.assertEqual(u.email, 'joe2@somewhere.org') self.assertFalse(u.has_usable_password()) - new_io = StringIO() call_command("createsuperuser", interactive=False, @@ -124,15 +128,21 @@ class BasicTestCase(TestCase): # Temporarily replace getpass to allow interactive code to be used # non-interactively - class mock_getpass: pass + class mock_getpass: + pass mock_getpass.getpass = staticmethod(lambda p=None: "nopasswd") createsuperuser.getpass = mock_getpass # Call the command in this new environment new_io = StringIO() - call_command("createsuperuser", interactive=True, username="nolocale@somewhere.org", email="nolocale@somewhere.org", stdout=new_io) + call_command("createsuperuser", + interactive=True, + username="nolocale@somewhere.org", + email="nolocale@somewhere.org", + stdout=new_io + ) - except TypeError as e: + except TypeError: self.fail("createsuperuser fails if the OS provides no information about the current locale") finally: @@ -143,3 +153,24 @@ class BasicTestCase(TestCase): # If we were successful, a user should have been created u = User.objects.get(username="nolocale@somewhere.org") self.assertEqual(u.email, 'nolocale@somewhere.org') + + def test_get_user_model(self): + "The current user model can be retrieved" + self.assertEqual(get_user_model(), User) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user(self): + "The current user model can be swapped out for another" + self.assertEqual(get_user_model(), CustomUser) + + @override_settings(AUTH_USER_MODEL='badsetting') + def test_swappable_user_bad_setting(self): + "The alternate user setting must point to something in the format app.model" + with self.assertRaises(ImproperlyConfigured): + get_user_model() + + @override_settings(AUTH_USER_MODEL='thismodel.doesntexist') + def test_swappable_user_nonexistent_model(self): + "The current user model must point to an installed model" + with self.assertRaises(ImproperlyConfigured): + get_user_model() diff --git a/django/contrib/auth/tests/context_processors.py b/django/contrib/auth/tests/context_processors.py index 6c824e831b..4e914133d0 100644 --- a/django/contrib/auth/tests/context_processors.py +++ b/django/contrib/auth/tests/context_processors.py @@ -2,12 +2,13 @@ import os from django.conf import global_settings from django.contrib.auth import authenticate +from django.contrib.auth.tests.utils import skipIfCustomUser from django.db.models import Q -from django.template import context from django.test import TestCase from django.test.utils import override_settings +@skipIfCustomUser @override_settings( TEMPLATE_DIRS=( os.path.join(os.path.dirname(__file__), 'templates'), diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py new file mode 100644 index 0000000000..3e7fa097b5 --- /dev/null +++ b/django/contrib/auth/tests/custom_user.py @@ -0,0 +1,75 @@ +# The custom User uses email as the unique identifier, and requires +# that every user provide a date of birth. This lets us test +# changes in username datatype, and non-text required fields. + +from django.db import models +from django.contrib.auth.models import BaseUserManager, AbstractBaseUser + + +class CustomUserManager(BaseUserManager): + def create_user(self, email, date_of_birth, password=None): + """ + Creates and saves a User with the given email and password. + """ + if not email: + raise ValueError('Users must have an email address') + + user = self.model( + email=CustomUserManager.normalize_email(email), + date_of_birth=date_of_birth, + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, username, password, date_of_birth): + u = self.create_user(username, password=password, date_of_birth=date_of_birth) + u.is_admin = True + u.save(using=self._db) + return u + + +class CustomUser(AbstractBaseUser): + email = models.EmailField(verbose_name='email address', max_length=255, unique=True) + is_active = models.BooleanField(default=True) + is_admin = models.BooleanField(default=False) + date_of_birth = models.DateField() + + objects = CustomUserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['date_of_birth'] + + class Meta: + app_label = 'auth' + + def get_full_name(self): + return self.email + + def get_short_name(self): + return self.email + + def __unicode__(self): + return self.email + + # Maybe required? + def get_group_permissions(self, obj=None): + return set() + + def get_all_permissions(self, obj=None): + return set() + + def has_perm(self, perm, obj=None): + return True + + def has_perms(self, perm_list, obj=None): + return True + + def has_module_perms(self, app_label): + return True + + # Admin required fields + @property + def is_staff(self): + return self.is_admin diff --git a/django/contrib/auth/tests/decorators.py b/django/contrib/auth/tests/decorators.py index cefc310e40..be99e7abb6 100644 --- a/django/contrib/auth/tests/decorators.py +++ b/django/contrib/auth/tests/decorators.py @@ -1,7 +1,9 @@ -from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.auth.tests.views import AuthViewsTestCase +from django.contrib.auth.tests.utils import skipIfCustomUser + +@skipIfCustomUser class LoginRequiredTestCase(AuthViewsTestCase): """ Tests the login_required decorators diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index 74aa47e199..7c6410da0f 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -4,16 +4,17 @@ import os from django.contrib.auth.models import User from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm, PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm) +from django.contrib.auth.tests.utils import skipIfCustomUser from django.core import mail from django.forms.fields import Field, EmailField from django.test import TestCase from django.test.utils import override_settings from django.utils.encoding import force_text -from django.utils import six from django.utils import translation from django.utils.translation import ugettext as _ +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class UserCreationFormTest(TestCase): @@ -81,6 +82,7 @@ class UserCreationFormTest(TestCase): self.assertEqual(repr(u), '') +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AuthenticationFormTest(TestCase): @@ -133,6 +135,7 @@ class AuthenticationFormTest(TestCase): self.assertEqual(form.non_field_errors(), []) +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class SetPasswordFormTest(TestCase): @@ -160,6 +163,7 @@ class SetPasswordFormTest(TestCase): self.assertTrue(form.is_valid()) +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class PasswordChangeFormTest(TestCase): @@ -208,6 +212,7 @@ class PasswordChangeFormTest(TestCase): ['old_password', 'new_password1', 'new_password2']) +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class UserChangeFormTest(TestCase): @@ -261,6 +266,7 @@ class UserChangeFormTest(TestCase): form.as_table()) +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class PasswordResetFormTest(TestCase): diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index ac83086dc3..60c05a0255 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -1,13 +1,20 @@ from __future__ import unicode_literals +from datetime import date from django.contrib.auth import models, management from django.contrib.auth.management.commands import changepassword +from django.contrib.auth.models import User +from django.contrib.auth.tests import CustomUser +from django.contrib.auth.tests.utils import skipIfCustomUser +from django.core.management import call_command from django.core.management.base import CommandError from django.test import TestCase +from django.test.utils import override_settings from django.utils import six from django.utils.six import StringIO +@skipIfCustomUser class GetDefaultUsernameTestCase(TestCase): def setUp(self): @@ -36,6 +43,7 @@ class GetDefaultUsernameTestCase(TestCase): self.assertEqual(management.get_default_username(), 'julia') +@skipIfCustomUser class ChangepasswordManagementCommandTestCase(TestCase): def setUp(self): @@ -48,7 +56,7 @@ class ChangepasswordManagementCommandTestCase(TestCase): self.stderr.close() def test_that_changepassword_command_changes_joes_password(self): - " Executing the changepassword management command should change joe's password " + "Executing the changepassword management command should change joe's password" self.assertTrue(self.user.check_password('qwerty')) command = changepassword.Command() command._get_pass = lambda *args: 'not qwerty' @@ -69,3 +77,93 @@ class ChangepasswordManagementCommandTestCase(TestCase): with self.assertRaises(CommandError): command.execute("joe", stdout=self.stdout, stderr=self.stderr) + + +@skipIfCustomUser +class CreatesuperuserManagementCommandTestCase(TestCase): + + def test_createsuperuser(self): + "Check the operation of the createsuperuser management command" + # We can use the management command to create a superuser + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + username="joe", + email="joe@somewhere.org", + stdout=new_io + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = User.objects.get(username="joe") + self.assertEqual(u.email, 'joe@somewhere.org') + + # created password should be unusable + self.assertFalse(u.has_usable_password()) + + def test_verbosity_zero(self): + # We can supress output on the management command + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + username="joe2", + email="joe2@somewhere.org", + verbosity=0, + stdout=new_io + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, '') + u = User.objects.get(username="joe2") + self.assertEqual(u.email, 'joe2@somewhere.org') + self.assertFalse(u.has_usable_password()) + + def test_email_in_username(self): + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + username="joe+admin@somewhere.org", + email="joe@somewhere.org", + stdout=new_io + ) + u = User.objects.get(username="joe+admin@somewhere.org") + self.assertEqual(u.email, 'joe@somewhere.org') + self.assertFalse(u.has_usable_password()) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user(self): + "A superuser can be created when a custom User model is in use" + # We can use the management command to create a superuser + # We skip validation because the temporary substitution of the + # swappable User model messes with validation. + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + username="joe@somewhere.org", + date_of_birth="1976-04-01", + stdout=new_io, + skip_validation=True + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = CustomUser.objects.get(email="joe@somewhere.org") + self.assertEqual(u.date_of_birth, date(1976, 4, 1)) + + # created password should be unusable + self.assertFalse(u.has_usable_password()) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user_missing_required_field(self): + "A superuser can be created when a custom User model is in use" + # We can use the management command to create a superuser + # We skip validation because the temporary substitution of the + # swappable User model messes with validation. + new_io = StringIO() + with self.assertRaises(CommandError): + call_command("createsuperuser", + interactive=False, + username="joe@somewhere.org", + stdout=new_io, + stderr=new_io, + skip_validation=True + ) + + self.assertEqual(CustomUser.objects.count(), 0) diff --git a/django/contrib/auth/tests/models.py b/django/contrib/auth/tests/models.py index e4efee4339..252a0887c8 100644 --- a/django/contrib/auth/tests/models.py +++ b/django/contrib/auth/tests/models.py @@ -1,11 +1,13 @@ from django.conf import settings from django.contrib.auth.models import (Group, User, SiteProfileNotAvailable, UserManager) +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.test.utils import override_settings from django.utils import six +@skipIfCustomUser @override_settings(USE_TZ=False, AUTH_PROFILE_MODULE='') class ProfileTestCase(TestCase): @@ -31,6 +33,7 @@ class ProfileTestCase(TestCase): user.get_profile() +@skipIfCustomUser @override_settings(USE_TZ=False) class NaturalKeysTestCase(TestCase): fixtures = ['authtestdata.json'] @@ -45,6 +48,7 @@ class NaturalKeysTestCase(TestCase): self.assertEqual(Group.objects.get_by_natural_key('users'), users_group) +@skipIfCustomUser @override_settings(USE_TZ=False) class LoadDataWithoutNaturalKeysTestCase(TestCase): fixtures = ['regular.json'] @@ -55,6 +59,7 @@ class LoadDataWithoutNaturalKeysTestCase(TestCase): self.assertEqual(group, user.groups.get()) +@skipIfCustomUser @override_settings(USE_TZ=False) class LoadDataWithNaturalKeysTestCase(TestCase): fixtures = ['natural.json'] @@ -65,6 +70,7 @@ class LoadDataWithNaturalKeysTestCase(TestCase): self.assertEqual(group, user.groups.get()) +@skipIfCustomUser class UserManagerTestCase(TestCase): def test_create_user(self): diff --git a/django/contrib/auth/tests/remote_user.py b/django/contrib/auth/tests/remote_user.py index fa324781d2..9b0f6f8be3 100644 --- a/django/contrib/auth/tests/remote_user.py +++ b/django/contrib/auth/tests/remote_user.py @@ -3,10 +3,12 @@ from datetime import datetime from django.conf import settings from django.contrib.auth.backends import RemoteUserBackend from django.contrib.auth.models import User +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.utils import timezone +@skipIfCustomUser class RemoteUserTest(TestCase): urls = 'django.contrib.auth.tests.urls' @@ -106,6 +108,7 @@ class RemoteUserNoCreateBackend(RemoteUserBackend): create_unknown_user = False +@skipIfCustomUser class RemoteUserNoCreateTest(RemoteUserTest): """ Contains the same tests as RemoteUserTest, but using a custom auth backend @@ -142,6 +145,7 @@ class CustomRemoteUserBackend(RemoteUserBackend): return user +@skipIfCustomUser class RemoteUserCustomTest(RemoteUserTest): """ Tests a custom RemoteUserBackend subclass that overrides the clean_username diff --git a/django/contrib/auth/tests/signals.py b/django/contrib/auth/tests/signals.py index e570280ada..c597aa9ed0 100644 --- a/django/contrib/auth/tests/signals.py +++ b/django/contrib/auth/tests/signals.py @@ -1,10 +1,12 @@ from django.contrib.auth import signals from django.contrib.auth.models import User +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class SignalTestCase(TestCase): urls = 'django.contrib.auth.tests.urls' diff --git a/django/contrib/auth/tests/tokens.py b/django/contrib/auth/tests/tokens.py index 44117a4f84..e8aeb46326 100644 --- a/django/contrib/auth/tests/tokens.py +++ b/django/contrib/auth/tests/tokens.py @@ -4,10 +4,12 @@ from datetime import date, timedelta from django.conf import settings from django.contrib.auth.models import User from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.utils import unittest +@skipIfCustomUser class TokenGeneratorTest(TestCase): def test_make_token(self): diff --git a/django/contrib/auth/tests/utils.py b/django/contrib/auth/tests/utils.py new file mode 100644 index 0000000000..6bb3d9994f --- /dev/null +++ b/django/contrib/auth/tests/utils.py @@ -0,0 +1,9 @@ +from django.conf import settings +from django.utils.unittest import skipIf + + +def skipIfCustomUser(test_func): + """ + Skip a test if a custom user model is in use. + """ + return skipIf(settings.AUTH_USER_MODEL != 'auth.User', 'Custom user model in use')(test_func) diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index e3402b13b9..5727dc289f 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -16,6 +16,7 @@ from django.test.utils import override_settings from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm, SetPasswordForm, PasswordResetForm) +from django.contrib.auth.tests.utils import skipIfCustomUser @override_settings( @@ -50,6 +51,7 @@ class AuthViewsTestCase(TestCase): return self.assertContains(response, escape(force_text(text)), **kwargs) +@skipIfCustomUser class AuthViewNamedURLTests(AuthViewsTestCase): urls = 'django.contrib.auth.urls' @@ -75,6 +77,7 @@ class AuthViewNamedURLTests(AuthViewsTestCase): self.fail("Reversal of url named '%s' failed with NoReverseMatch" % name) +@skipIfCustomUser class PasswordResetTest(AuthViewsTestCase): def test_email_not_found(self): @@ -172,6 +175,30 @@ class PasswordResetTest(AuthViewsTestCase): self.assertContainsEscaped(response, SetPasswordForm.error_messages['password_mismatch']) +@override_settings(AUTH_USER_MODEL='auth.CustomUser') +class CustomUserPasswordResetTest(AuthViewsTestCase): + fixtures = ['custom_user.json'] + + def _test_confirm_start(self): + # Start by creating the email + response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + return self._read_signup_email(mail.outbox[0]) + + def _read_signup_email(self, email): + urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body) + self.assertTrue(urlmatch is not None, "No URL found in sent email") + return urlmatch.group(), urlmatch.groups()[0] + + def test_confirm_valid_custom_user(self): + url, path = self._test_confirm_start() + response = self.client.get(path) + # redirect to a 'complete' page: + self.assertContains(response, "Please enter your new password") + + +@skipIfCustomUser class ChangePasswordTest(AuthViewsTestCase): def fail_login(self, password='password'): @@ -231,6 +258,7 @@ class ChangePasswordTest(AuthViewsTestCase): self.assertTrue(response['Location'].endswith('/login/?next=/password_change/done/')) +@skipIfCustomUser class LoginTest(AuthViewsTestCase): def test_current_site_in_context_after_login(self): @@ -289,6 +317,7 @@ class LoginTest(AuthViewsTestCase): "%s should be allowed" % good_url) +@skipIfCustomUser class LoginURLSettings(AuthViewsTestCase): def setUp(self): @@ -347,6 +376,7 @@ class LoginURLSettings(AuthViewsTestCase): querystring.urlencode('/'))) +@skipIfCustomUser class LogoutTest(AuthViewsTestCase): def confirm_logged_out(self): diff --git a/django/contrib/auth/tokens.py b/django/contrib/auth/tokens.py index 9b2eda83d4..930c70012b 100644 --- a/django/contrib/auth/tokens.py +++ b/django/contrib/auth/tokens.py @@ -4,6 +4,7 @@ from django.utils.http import int_to_base36, base36_to_int from django.utils.crypto import constant_time_compare, salted_hmac from django.utils import six + class PasswordResetTokenGenerator(object): """ Strategy object used to generate and check tokens for the password diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 024be5e46d..747b5c0991 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -15,10 +15,9 @@ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect # Avoid shadowing the login() and logout() views below. -from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout +from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout, get_user_model from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm, PasswordChangeForm -from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.models import get_current_site @@ -74,6 +73,7 @@ def login(request, template_name='registration/login.html', return TemplateResponse(request, template_name, context, current_app=current_app) + def logout(request, next_page=None, template_name='registration/logged_out.html', redirect_field_name=REDIRECT_FIELD_NAME, @@ -104,6 +104,7 @@ def logout(request, next_page=None, # Redirect to this page until the session has been cleared. return HttpResponseRedirect(next_page or request.path) + def logout_then_login(request, login_url=None, current_app=None, extra_context=None): """ Logs out the user if he is logged in. Then redirects to the log-in page. @@ -113,6 +114,7 @@ def logout_then_login(request, login_url=None, current_app=None, extra_context=N 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, redirect_field_name=REDIRECT_FIELD_NAME): """ @@ -128,6 +130,7 @@ def redirect_to_login(next, login_url=None, return HttpResponseRedirect(urlunparse(login_url_parts)) + # 4 views for password reset: # - password_reset sends the mail # - password_reset_done shows a success message for the above @@ -173,6 +176,7 @@ def password_reset(request, is_admin_site=False, return TemplateResponse(request, template_name, context, current_app=current_app) + def password_reset_done(request, template_name='registration/password_reset_done.html', current_app=None, extra_context=None): @@ -182,6 +186,7 @@ def password_reset_done(request, return TemplateResponse(request, template_name, context, current_app=current_app) + # Doesn't need csrf_protect since no-one can guess the URL @sensitive_post_parameters() @never_cache @@ -195,13 +200,14 @@ def password_reset_confirm(request, uidb36=None, token=None, View that checks the hash in a password reset link and presents a form for entering a new password. """ - assert uidb36 is not None and token is not None # checked by URLconf + UserModel = get_user_model() + assert uidb36 is not None and token is not None # checked by URLconf if post_reset_redirect is None: post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete') try: uid_int = base36_to_int(uidb36) - user = User.objects.get(id=uid_int) - except (ValueError, OverflowError, User.DoesNotExist): + user = UserModel.objects.get(id=uid_int) + except (ValueError, OverflowError, UserModel.DoesNotExist): user = None if user is not None and token_generator.check_token(user, token): @@ -225,6 +231,7 @@ def password_reset_confirm(request, uidb36=None, token=None, return TemplateResponse(request, template_name, context, current_app=current_app) + def password_reset_complete(request, template_name='registration/password_reset_complete.html', current_app=None, extra_context=None): @@ -236,6 +243,7 @@ def password_reset_complete(request, return TemplateResponse(request, template_name, context, current_app=current_app) + @sensitive_post_parameters() @csrf_protect @login_required @@ -261,6 +269,7 @@ def password_change(request, return TemplateResponse(request, template_name, context, current_app=current_app) + @login_required def password_change_done(request, template_name='registration/password_change_done.html', diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py index b043b4187a..a39c2622dd 100644 --- a/django/contrib/comments/models.py +++ b/django/contrib/comments/models.py @@ -1,16 +1,16 @@ -from django.contrib.auth.models import User +from django.conf import settings from django.contrib.comments.managers import CommentManager from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site -from django.db import models from django.core import urlresolvers +from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils import timezone -from django.conf import settings from django.utils.encoding import python_2_unicode_compatible -COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000) +COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000) + class BaseCommentAbstractModel(models.Model): """ @@ -40,6 +40,7 @@ class BaseCommentAbstractModel(models.Model): args=(self.content_type_id, self.object_pk) ) + @python_2_unicode_compatible class Comment(BaseCommentAbstractModel): """ @@ -49,7 +50,7 @@ class Comment(BaseCommentAbstractModel): # Who posted this comment? If ``user`` is set then it was an authenticated # user; otherwise at least user_name should have been set and the comment # was posted by a non-authenticated user. - user = models.ForeignKey(User, verbose_name=_('user'), + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), blank=True, null=True, related_name="%(class)s_comments") user_name = models.CharField(_("user's name"), max_length=50, blank=True) user_email = models.EmailField(_("user's email address"), blank=True) @@ -117,6 +118,7 @@ class Comment(BaseCommentAbstractModel): def _get_name(self): return self.userinfo["name"] + def _set_name(self, val): if self.user_id: raise AttributeError(_("This comment was posted by an authenticated "\ @@ -126,6 +128,7 @@ class Comment(BaseCommentAbstractModel): def _get_email(self): return self.userinfo["email"] + def _set_email(self, val): if self.user_id: raise AttributeError(_("This comment was posted by an authenticated "\ @@ -135,6 +138,7 @@ class Comment(BaseCommentAbstractModel): def _get_url(self): return self.userinfo["url"] + def _set_url(self, val): self.user_url = val url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment") @@ -155,6 +159,7 @@ class Comment(BaseCommentAbstractModel): } return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d + @python_2_unicode_compatible class CommentFlag(models.Model): """ @@ -169,7 +174,7 @@ class CommentFlag(models.Model): design users are only allowed to flag a comment with a given flag once; if you want rating look elsewhere. """ - user = models.ForeignKey(User, verbose_name=_('user'), related_name="comment_flags") + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name="comment_flags") comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags") flag = models.CharField(_('flag'), max_length=30, db_index=True) flag_date = models.DateTimeField(_('date'), default=None) diff --git a/django/core/exceptions.py b/django/core/exceptions.py index f0f14cffda..233af40f88 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -3,42 +3,54 @@ Global Django exception and warning classes. """ from functools import reduce + class DjangoRuntimeWarning(RuntimeWarning): pass + class ObjectDoesNotExist(Exception): "The requested object does not exist" silent_variable_failure = True + class MultipleObjectsReturned(Exception): "The query returned multiple objects when only one was expected." pass + class SuspiciousOperation(Exception): "The user did something suspicious" pass + class PermissionDenied(Exception): "The user did not have permission to do that" pass + class ViewDoesNotExist(Exception): "The requested view does not exist" pass + class MiddlewareNotUsed(Exception): "This middleware is not used in this server configuration" pass + class ImproperlyConfigured(Exception): "Django is somehow improperly configured" pass + class FieldError(Exception): """Some kind of problem with a model field.""" pass + NON_FIELD_ERRORS = '__all__' + + class ValidationError(Exception): """An error while validating data.""" def __init__(self, message, code=None, params=None): @@ -85,4 +97,3 @@ class ValidationError(Exception): else: error_dict[NON_FIELD_ERRORS] = self.messages return error_dict - diff --git a/django/core/management/commands/sqlall.py b/django/core/management/commands/sqlall.py index 6d0735a6f9..0e2c05ba82 100644 --- a/django/core/management/commands/sqlall.py +++ b/django/core/management/commands/sqlall.py @@ -6,6 +6,7 @@ from django.core.management.base import AppCommand from django.core.management.sql import sql_all from django.db import connections, DEFAULT_DB_ALIAS + class Command(AppCommand): help = "Prints the CREATE TABLE, custom SQL and CREATE INDEX SQL statements for the given model module name(s)." diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index cceec07be8..4ce2910fb5 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -68,6 +68,7 @@ class Command(NoArgsCommand): if router.allow_syncdb(db, m)]) for app in models.get_apps() ] + def model_installed(model): opts = model._meta converter = connection.introspection.table_name_converter @@ -101,7 +102,6 @@ class Command(NoArgsCommand): cursor.execute(statement) tables.append(connection.introspection.table_name_converter(model._meta.db_table)) - transaction.commit_unless_managed(using=db) # Send the post_syncdb signal, so individual apps can do whatever they need diff --git a/django/core/management/commands/validate.py b/django/core/management/commands/validate.py index 760d41c5bf..0dec3ea8b9 100644 --- a/django/core/management/commands/validate.py +++ b/django/core/management/commands/validate.py @@ -1,5 +1,6 @@ from django.core.management.base import NoArgsCommand + class Command(NoArgsCommand): help = "Validates all installed models." diff --git a/django/core/management/sql.py b/django/core/management/sql.py index b02a548314..ac16a5b358 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -9,6 +9,7 @@ from django.core.management.base import CommandError from django.db import models from django.db.models import get_models + def sql_create(app, style, connection): "Returns a list of the CREATE TABLE SQL statements for the given app." @@ -55,6 +56,7 @@ def sql_create(app, style, connection): return final_output + def sql_delete(app, style, connection): "Returns a list of the DROP TABLE SQL statements for the given app." @@ -83,7 +85,7 @@ def sql_delete(app, style, connection): opts = model._meta for f in opts.local_fields: if f.rel and f.rel.to not in to_delete: - references_to_delete.setdefault(f.rel.to, []).append( (model, f) ) + references_to_delete.setdefault(f.rel.to, []).append((model, f)) to_delete.add(model) @@ -97,7 +99,8 @@ def sql_delete(app, style, connection): cursor.close() connection.close() - return output[::-1] # Reverse it, to deal with table dependencies. + return output[::-1] # Reverse it, to deal with table dependencies. + def sql_flush(style, connection, only_django=False, reset_sequences=True): """ @@ -114,6 +117,7 @@ def sql_flush(style, connection, only_django=False, reset_sequences=True): statements = connection.ops.sql_flush(style, tables, seqs) return statements + def sql_custom(app, style, connection): "Returns a list of the custom table modifying SQL statements for the given app." output = [] @@ -125,6 +129,7 @@ def sql_custom(app, style, connection): return output + def sql_indexes(app, style, connection): "Returns a list of the CREATE INDEX SQL statements for all models in the given app." output = [] @@ -132,10 +137,12 @@ def sql_indexes(app, style, connection): output.extend(connection.creation.sql_indexes_for_model(model, style)) return output + def sql_all(app, style, connection): "Returns a list of CREATE TABLE SQL, initial-data inserts, and CREATE INDEX SQL for the given module." return sql_create(app, style, connection) + sql_custom(app, style, connection) + sql_indexes(app, style, connection) + def _split_statements(content): comment_re = re.compile(r"^((?:'[^']*'|[^'])*?)--.*$") statements = [] @@ -150,6 +157,7 @@ def _split_statements(content): statement = "" return statements + def custom_sql_for_model(model, style, connection): opts = model._meta app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql')) diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 6cd66f3a6a..fa3edb4430 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -5,6 +5,7 @@ from django.utils.encoding import force_str from django.utils.itercompat import is_iterable from django.utils import six + class ModelErrorCollection: def __init__(self, outfile=sys.stdout): self.errors = [] @@ -15,6 +16,7 @@ class ModelErrorCollection: self.errors.append((context, error)) self.outfile.write(self.style.ERROR(force_str("%s: %s\n" % (context, error)))) + def get_validation_errors(outfile, app=None): """ Validates all models that are part of the specified app. If no app name is provided, @@ -56,7 +58,7 @@ def get_validation_errors(outfile, app=None): e.add(opts, '"%s": CharFields require a "max_length" attribute that is a positive integer.' % f.name) if isinstance(f, models.DecimalField): decimalp_ok, mdigits_ok = False, False - decimalp_msg ='"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.' + decimalp_msg = '"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.' try: decimal_places = int(f.decimal_places) if decimal_places < 0: @@ -123,6 +125,10 @@ def get_validation_errors(outfile, app=None): if isinstance(f.rel.to, six.string_types): continue + # Make sure the model we're related hasn't been swapped out + if f.rel.to._meta.swapped: + e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable)) + # Make sure the related field specified by a ForeignKey is unique if not f.rel.to._meta.get_field(f.rel.field_name).unique: e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.rel.field_name, f.rel.to.__name__)) @@ -165,6 +171,10 @@ def get_validation_errors(outfile, app=None): if isinstance(f.rel.to, six.string_types): continue + # Make sure the model we're related hasn't been swapped out + if f.rel.to._meta.swapped: + e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable)) + # Check that the field is not set to unique. ManyToManyFields do not support unique. if f.unique: e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name) @@ -176,7 +186,7 @@ def get_validation_errors(outfile, app=None): seen_from, seen_to, seen_self = False, False, 0 for inter_field in f.rel.through._meta.fields: rel_to = getattr(inter_field.rel, 'to', None) - if from_model == to_model: # relation to self + if from_model == to_model: # relation to self if rel_to == from_model: seen_self += 1 if seen_self > 2: @@ -275,10 +285,21 @@ def get_validation_errors(outfile, app=None): if r.get_accessor_name() == rel_query_name: e.add(opts, "Reverse query name for m2m field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + # Check swappable attribute. + if opts.swapped: + try: + app_label, model_name = opts.swapped.split('.') + except ValueError: + e.add(opts, "%s is not of the form 'app_label.app_name'." % opts.swappable) + continue + if not models.get_model(app_label, model_name): + e.add(opts, "Model has been swapped out for '%s' which has not been installed or is abstract." % opts.swapped) + # Check ordering attribute. if opts.ordering: for field_name in opts.ordering: - if field_name == '?': continue + if field_name == '?': + continue if field_name.startswith('-'): field_name = field_name[1:] if opts.order_with_respect_to and field_name == '_order': diff --git a/django/core/validators.py b/django/core/validators.py index 317e3880bf..cf12f8c9fc 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -15,6 +15,7 @@ from django.utils import six # These values, if given to validate(), will trigger the self.required check. EMPTY_VALUES = (None, '', [], (), {}) + class RegexValidator(object): regex = '' message = _('Enter a valid value.') @@ -39,14 +40,15 @@ class RegexValidator(object): if not self.regex.search(force_text(value)): raise ValidationError(self.message, code=self.code) + class URLValidator(RegexValidator): regex = re.compile( - r'^(?:http|ftp)s?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... - r'localhost|' #localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 - r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 - r'(?::\d+)?' # optional port + r'^(?:http|ftp)s?://' # http:// or https:// + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... + r'localhost|' # localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 + r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 + r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) def __call__(self, value): @@ -58,8 +60,8 @@ class URLValidator(RegexValidator): value = force_text(value) scheme, netloc, path, query, fragment = urlsplit(value) try: - netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE - except UnicodeError: # invalid domain part + netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE + except UnicodeError: # invalid domain part raise e url = urlunsplit((scheme, netloc, path, query, fragment)) super(URLValidator, self).__call__(url) @@ -75,6 +77,7 @@ def validate_integer(value): except (ValueError, TypeError): raise ValidationError('') + class EmailValidator(RegexValidator): def __call__(self, value): @@ -106,10 +109,12 @@ validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of le ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$') validate_ipv4_address = RegexValidator(ipv4_re, _('Enter a valid IPv4 address.'), 'invalid') + def validate_ipv6_address(value): if not is_valid_ipv6_address(value): raise ValidationError(_('Enter a valid IPv6 address.'), code='invalid') + def validate_ipv46_address(value): try: validate_ipv4_address(value) @@ -125,6 +130,7 @@ ip_address_validator_map = { 'ipv6': ([validate_ipv6_address], _('Enter a valid IPv6 address.')), } + def ip_address_validators(protocol, unpack_ipv4): """ Depending on the given parameters returns the appropriate validators for @@ -147,7 +153,7 @@ validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_ class BaseValidator(object): compare = lambda self, a, b: a is not b - clean = lambda self, x: x + clean = lambda self, x: x message = _('Ensure this value is %(limit_value)s (it is %(show_value)s).') code = 'limit_value' @@ -164,25 +170,28 @@ class BaseValidator(object): params=params, ) + class MaxValueValidator(BaseValidator): compare = lambda self, a, b: a > b message = _('Ensure this value is less than or equal to %(limit_value)s.') code = 'max_value' + class MinValueValidator(BaseValidator): compare = lambda self, a, b: a < b message = _('Ensure this value is greater than or equal to %(limit_value)s.') code = 'min_value' + class MinLengthValidator(BaseValidator): compare = lambda self, a, b: a < b - clean = lambda self, x: len(x) + clean = lambda self, x: len(x) message = _('Ensure this value has at least %(limit_value)d characters (it has %(show_value)d).') code = 'min_length' + class MaxLengthValidator(BaseValidator): compare = lambda self, a, b: a > b - clean = lambda self, x: len(x) + clean = lambda self, x: len(x) message = _('Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).') code = 'max_length' - diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 9b0f495749..02d2a16a46 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -319,6 +319,7 @@ class BaseDatabaseWrapper(object): def make_debug_cursor(self, cursor): return util.CursorDebugWrapper(cursor, self) + class BaseDatabaseFeatures(object): allows_group_by_pk = False # True if django.db.backend.utils.typecast_timestamp is used on values @@ -776,7 +777,7 @@ class BaseDatabaseOperations(object): The `style` argument is a Style object as returned by either color_style() or no_style() in django.core.management.color. """ - return [] # No sequence reset required by default. + return [] # No sequence reset required by default. def start_transaction_sql(self): """ @@ -915,6 +916,7 @@ class BaseDatabaseOperations(object): conn = ' %s ' % connector return conn.join(sub_expressions) + class BaseDatabaseIntrospection(object): """ This class encapsulates all backend-specific introspection utilities @@ -1010,12 +1012,14 @@ class BaseDatabaseIntrospection(object): for model in models.get_models(app): if not model._meta.managed: continue + if model._meta.swapped: + continue if not router.allow_syncdb(self.connection.alias, model): continue for f in model._meta.local_fields: if isinstance(f, models.AutoField): sequence_list.append({'table': model._meta.db_table, 'column': f.column}) - break # Only one AutoField is allowed per model, so don't bother continuing. + break # Only one AutoField is allowed per model, so don't bother continuing. for f in model._meta.local_many_to_many: # If this is an m2m using an intermediate table, @@ -1052,6 +1056,7 @@ class BaseDatabaseIntrospection(object): """ raise NotImplementedError + class BaseDatabaseClient(object): """ This class encapsulates all backend-specific methods for opening a @@ -1068,6 +1073,7 @@ class BaseDatabaseClient(object): def runshell(self): raise NotImplementedError() + class BaseDatabaseValidation(object): """ This class encapsualtes all backend-specific model validation. diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 52d5ac0547..3262a8922f 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -40,7 +40,7 @@ class BaseDatabaseCreation(object): (list_of_sql, pending_references_dict) """ opts = model._meta - if not opts.managed or opts.proxy: + if not opts.managed or opts.proxy or opts.swapped: return [], {} final_output = [] table_output = [] @@ -92,9 +92,9 @@ class BaseDatabaseCreation(object): full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' ('] - for i, line in enumerate(table_output): # Combine and add commas. + for i, line in enumerate(table_output): # Combine and add commas. full_statement.append( - ' %s%s' % (line, i < len(table_output)-1 and ',' or '')) + ' %s%s' % (line, i < len(table_output) - 1 and ',' or '')) full_statement.append(')') if opts.db_tablespace: tablespace_sql = self.connection.ops.tablespace_sql( @@ -143,11 +143,11 @@ class BaseDatabaseCreation(object): """ from django.db.backends.util import truncate_name - if not model._meta.managed or model._meta.proxy: + opts = model._meta + if not opts.managed or opts.proxy or opts.swapped: return [] qn = self.connection.ops.quote_name final_output = [] - opts = model._meta if model in pending_references: for rel_class, f in pending_references[model]: rel_opts = rel_class._meta @@ -172,7 +172,7 @@ class BaseDatabaseCreation(object): """ Returns the CREATE INDEX SQL statements for a single model. """ - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] output = [] for f in model._meta.local_fields: @@ -211,7 +211,7 @@ class BaseDatabaseCreation(object): Return the DROP TABLE and restraint dropping statements for a single model. """ - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] # Drop the table now qn = self.connection.ops.quote_name @@ -228,7 +228,7 @@ class BaseDatabaseCreation(object): def sql_remove_table_constraints(self, model, references_to_delete, style): from django.db.backends.util import truncate_name - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] output = [] qn = self.connection.ops.quote_name diff --git a/django/db/models/base.py b/django/db/models/base.py index 62024c8ee4..a1f9e2f26e 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -5,7 +5,7 @@ import sys from functools import update_wrapper from django.utils.six.moves import zip -import django.db.models.manager # Imported to register signal handler. +import django.db.models.manager # Imported to register signal handler. from django.conf import settings from django.core.exceptions import (ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS) @@ -108,6 +108,11 @@ class ModelBase(type): is_proxy = new_class._meta.proxy + # If the model is a proxy, ensure that the base class + # hasn't been swapped out. + if is_proxy and base_meta and base_meta.swapped: + raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped)) + if getattr(new_class, '_default_manager', None): if not is_proxy: # Multi-table inheritance doesn't inherit default manager from @@ -262,6 +267,7 @@ class ModelBase(type): if opts.order_with_respect_to: cls.get_next_in_order = curry(cls._get_next_or_previous_in_order, is_next=True) cls.get_previous_in_order = curry(cls._get_next_or_previous_in_order, is_next=False) + # defer creating accessors on the foreign class until we are # certain it has been created def make_foreign_order_accessors(field, model, cls): @@ -292,6 +298,7 @@ class ModelBase(type): signals.class_prepared.send(sender=cls) + class ModelState(object): """ A class for storing instance state @@ -303,6 +310,7 @@ class ModelState(object): # This impacts validation only; it has no effect on the actual save. self.adding = True + class Model(six.with_metaclass(ModelBase, object)): _deferred = False @@ -632,7 +640,6 @@ class Model(six.with_metaclass(ModelBase, object)): signals.post_save.send(sender=origin, instance=self, created=(not record_exists), update_fields=update_fields, raw=raw, using=using) - save_base.alters_data = True def delete(self, using=None): @@ -656,7 +663,7 @@ class Model(six.with_metaclass(ModelBase, object)): order = not is_next and '-' or '' param = force_text(getattr(self, field.attname)) q = Q(**{'%s__%s' % (field.name, op): param}) - q = q|Q(**{field.name: param, 'pk__%s' % op: self.pk}) + q = q | Q(**{field.name: param, 'pk__%s' % op: self.pk}) qs = self.__class__._default_manager.using(self._state.db).filter(**kwargs).filter(q).order_by('%s%s' % (order, field.name), '%spk' % order) try: return qs[0] @@ -849,7 +856,7 @@ class Model(six.with_metaclass(ModelBase, object)): field = opts.get_field(field_name) field_label = capfirst(field.verbose_name) # Insert the error into the error dict, very sneaky - return field.error_messages['unique'] % { + return field.error_messages['unique'] % { 'model_name': six.text_type(model_name), 'field_label': six.text_type(field_label) } @@ -857,7 +864,7 @@ class Model(six.with_metaclass(ModelBase, object)): else: field_labels = [capfirst(opts.get_field(f).verbose_name) for f in unique_check] field_labels = get_text_list(field_labels, _('and')) - return _("%(model_name)s with this %(field_label)s already exists.") % { + return _("%(model_name)s with this %(field_label)s already exists.") % { 'model_name': six.text_type(model_name), 'field_label': six.text_type(field_labels) } @@ -921,7 +928,6 @@ class Model(six.with_metaclass(ModelBase, object)): raise ValidationError(errors) - ############################################ # HELPER FUNCTIONS (CURRIED MODEL METHODS) # ############################################ @@ -963,6 +969,7 @@ def get_absolute_url(opts, func, self, *args, **kwargs): class Empty(object): pass + def model_unpickle(model, attrs): """ Used to unpickle Model subclasses with deferred fields. @@ -971,6 +978,7 @@ def model_unpickle(model, attrs): return cls.__new__(cls) model_unpickle.__safe_for_unpickle__ = True + def unpickle_inner_exception(klass, exception_name): # Get the exception class from the class it is attached to: exception = getattr(klass, exception_name) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 08cc0a747f..c065162aa0 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -21,6 +21,7 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self' pending_lookups = {} + def add_lazy_relation(cls, field, relation, operation): """ Adds a lookup on ``cls`` when a related field is defined using a string, @@ -77,6 +78,7 @@ def add_lazy_relation(cls, field, relation, operation): value = (cls, field, operation) pending_lookups.setdefault(key, []).append(value) + def do_pending_lookups(sender, **kwargs): """ Handle any pending relations to the sending model. Sent from class_prepared. @@ -87,6 +89,7 @@ def do_pending_lookups(sender, **kwargs): signals.class_prepared.connect(do_pending_lookups) + #HACK class RelatedField(object): def contribute_to_class(self, cls, name): @@ -220,6 +223,7 @@ class RelatedField(object): # "related_name" option. return self.rel.related_name or self.opts.object_name.lower() + class SingleRelatedObjectDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -306,6 +310,7 @@ class SingleRelatedObjectDescriptor(object): setattr(instance, self.cache_name, value) setattr(value, self.related.field.get_cache_name(), instance) + class ReverseSingleRelatedObjectDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -430,6 +435,7 @@ class ReverseSingleRelatedObjectDescriptor(object): if value is not None and not self.field.rel.multiple: setattr(value, self.field.related.get_cache_name(), instance) + class ForeignRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -660,7 +666,7 @@ def create_many_related_manager(superclass, rel): for obj in objs: if isinstance(obj, self.model): if not router.allow_relation(obj, self.instance): - raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % + raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % (obj, self.instance._state.db, obj._state.db)) new_ids.add(obj.pk) elif isinstance(obj, Model): @@ -752,6 +758,7 @@ def create_many_related_manager(superclass, rel): return ManyRelatedManager + class ManyRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -860,12 +867,13 @@ class ReverseManyRelatedObjectsDescriptor(object): manager.clear() manager.add(*value) + class ManyToOneRel(object): def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): try: to._meta - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT self.to, self.field_name = to, field_name self.related_name = related_name @@ -891,6 +899,7 @@ class ManyToOneRel(object): self.field_name) return data[0] + class OneToOneRel(ManyToOneRel): def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): @@ -900,6 +909,7 @@ class OneToOneRel(ManyToOneRel): ) self.multiple = False + class ManyToManyRel(object): def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True, through=None): @@ -924,16 +934,18 @@ class ManyToManyRel(object): """ return self.to._meta.pk + class ForeignKey(RelatedField, Field): empty_strings_allowed = False default_error_messages = { 'invalid': _('Model %(model)s with pk %(pk)r does not exist.') } description = _("Foreign Key (type determined by related field)") + def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): try: to_name = to._meta.object_name.lower() - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) else: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) @@ -1050,6 +1062,7 @@ class ForeignKey(RelatedField, Field): return IntegerField().db_type(connection=connection) return rel_field.db_type(connection=connection) + class OneToOneField(ForeignKey): """ A OneToOneField is essentially the same as a ForeignKey, with the exception @@ -1058,6 +1071,7 @@ class OneToOneField(ForeignKey): rather than returning a list. """ description = _("One-to-one relationship") + def __init__(self, to, to_field=None, **kwargs): kwargs['unique'] = True super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) @@ -1077,12 +1091,14 @@ class OneToOneField(ForeignKey): else: setattr(instance, self.attname, data) + def create_many_to_many_intermediary_model(field, klass): from django.db import models managed = True if isinstance(field.rel.to, six.string_types) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT: to_model = field.rel.to to = to_model.split('.')[-1] + def set_managed(field, model, cls): field.rel.through._meta.managed = model._meta.managed or cls._meta.managed add_lazy_relation(klass, field, to_model, set_managed) @@ -1119,12 +1135,14 @@ def create_many_to_many_intermediary_model(field, klass): to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace) }) + class ManyToManyField(RelatedField, Field): description = _("Many-to-many relationship") + def __init__(self, to, **kwargs): try: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ManyToManyField must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) # Python 2.6 and earlier require dictionary keys to be of str type, # not unicode and class names must be ASCII (in Python 2.x), so we @@ -1135,7 +1153,7 @@ class ManyToManyField(RelatedField, Field): kwargs['rel'] = ManyToManyRel(to, related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), - symmetrical=kwargs.pop('symmetrical', to==RECURSIVE_RELATIONSHIP_CONSTANT), + symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT), through=kwargs.pop('through', None)) self.db_table = kwargs.pop('db_table', None) @@ -1166,7 +1184,7 @@ class ManyToManyField(RelatedField, Field): if hasattr(self, cache_attr): return getattr(self, cache_attr) for f in self.rel.through._meta.fields: - if hasattr(f,'rel') and f.rel and f.rel.to == related.model: + if hasattr(f, 'rel') and f.rel and f.rel.to == related.model: setattr(self, cache_attr, getattr(f, attr)) return getattr(self, cache_attr) @@ -1177,7 +1195,7 @@ class ManyToManyField(RelatedField, Field): return getattr(self, cache_attr) found = False for f in self.rel.through._meta.fields: - if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: + if hasattr(f, 'rel') and f.rel and f.rel.to == related.parent_model: if related.model == related.parent_model: # If this is an m2m-intermediate to self, # the first foreign key you find will be @@ -1222,7 +1240,8 @@ class ManyToManyField(RelatedField, Field): # The intermediate m2m model is not auto created if: # 1) There is a manually specified intermediate, or # 2) The class owning the m2m field is abstract. - if not self.rel.through and not cls._meta.abstract: + # 3) The class owning the m2m field has been swapped out. + if not self.rel.through and not cls._meta.abstract and not cls._meta.swapped: self.rel.through = create_many_to_many_intermediary_model(self, cls) # Add the descriptor for the m2m relation diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 7a9cb2cb41..8a0e796f4b 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -14,6 +14,7 @@ import os __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models', 'load_app', 'app_cache_ready') + class AppCache(object): """ A cache that stores installed applications and their models. Used to diff --git a/django/db/models/manager.py b/django/db/models/manager.py index e1bbf6ebc5..522a8a2306 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -13,7 +13,7 @@ def ensure_default_manager(sender, **kwargs): _default_manager if it's not a subclass of Manager). """ cls = sender - if cls._meta.abstract: + if cls._meta.abstract or cls._meta.swapped: return if not getattr(cls, '_default_manager', None): # Create the default manager, if needed. @@ -42,6 +42,7 @@ def ensure_default_manager(sender, **kwargs): signals.class_prepared.connect(ensure_default_manager) + class Manager(object): # Tracks each time a Manager instance is created. Used to retain order. creation_counter = 0 @@ -56,7 +57,9 @@ class Manager(object): def contribute_to_class(self, model, name): # TODO: Use weakref because of possible memory leak / circular reference. self.model = model - setattr(model, name, ManagerDescriptor(self)) + # Only contribute the manager if the model is concrete + if not model._meta.abstract and not model._meta.swapped: + setattr(model, name, ManagerDescriptor(self)) if not getattr(model, '_default_manager', None) or self.creation_counter < model._default_manager.creation_counter: model._default_manager = self if model._meta.abstract or (self._inherited and not self.model._meta.proxy): @@ -208,6 +211,7 @@ class Manager(object): def raw(self, raw_query, params=None, *args, **kwargs): return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self._db, *args, **kwargs) + class ManagerDescriptor(object): # This class ensures managers aren't accessible via model instances. # For example, Poll.objects works, but poll_obj.objects raises AttributeError. @@ -219,6 +223,7 @@ class ManagerDescriptor(object): raise AttributeError("Manager isn't accessible via %s instances" % type.__name__) return self.manager + class EmptyManager(Manager): def get_query_set(self): return self.get_empty_query_set() diff --git a/django/db/models/options.py b/django/db/models/options.py index 6814ce27ff..d2de96ea5c 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -21,7 +21,8 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]| DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering', 'unique_together', 'permissions', 'get_latest_by', 'order_with_respect_to', 'app_label', 'db_tablespace', - 'abstract', 'managed', 'proxy', 'auto_created') + 'abstract', 'managed', 'proxy', 'swappable', 'auto_created') + @python_2_unicode_compatible class Options(object): @@ -32,8 +33,8 @@ class Options(object): self.verbose_name_plural = None self.db_table = '' self.ordering = [] - self.unique_together = [] - self.permissions = [] + self.unique_together = [] + self.permissions = [] self.object_name, self.app_label = None, app_label self.get_latest_by = None self.order_with_respect_to = None @@ -55,6 +56,7 @@ class Options(object): # in the end of the proxy_for_model chain. In particular, for # concrete models, the concrete_model is always the class itself. self.concrete_model = None + self.swappable = None self.parents = SortedDict() self.duplicate_targets = {} self.auto_created = False @@ -218,6 +220,19 @@ class Options(object): return raw verbose_name_raw = property(verbose_name_raw) + def _swapped(self): + """ + Has this model been swapped out for another? If so, return the model + name of the replacement; otherwise, return None. + """ + if self.swappable: + model_label = '%s.%s' % (self.app_label, self.object_name) + swapped_for = getattr(settings, self.swappable, None) + if swapped_for not in (None, model_label): + return swapped_for + return None + swapped = property(_swapped) + def _fields(self): """ The getter for self.fields. This returns the list of field objects diff --git a/django/test/__init__.py b/django/test/__init__.py index 21a4841a6b..7a4987508e 100644 --- a/django/test/__init__.py +++ b/django/test/__init__.py @@ -5,5 +5,6 @@ Django Unit Test and Doctest framework. from django.test.client import Client, RequestFactory from django.test.testcases import (TestCase, TransactionTestCase, SimpleTestCase, LiveServerTestCase, skipIfDBFeature, - skipUnlessDBFeature) + skipUnlessDBFeature +) from django.test.utils import Approximate diff --git a/django/test/testcases.py b/django/test/testcases.py index d37be58a71..2b1ef912b6 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -44,6 +44,7 @@ from django.utils import unittest as ut2 from django.utils.encoding import force_text from django.utils import six from django.utils.unittest.util import safe_repr +from django.utils.unittest import skipIf from django.views.static import serve __all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase', @@ -53,6 +54,7 @@ normalize_long_ints = lambda s: re.sub(r'(?` for details. -.. setting:: AUTH_PROFILE_MODULE +.. setting:: AUTH_USER_MODEL -AUTH_PROFILE_MODULE -------------------- +AUTH_USER_MODEL +--------------- -Default: Not defined +Default: 'auth.User' -The site-specific user profile model used by this site. See -:ref:`auth-profiles`. +The model to use to represent a User. See :ref:`auth-custom-user`. .. setting:: CACHES @@ -2209,6 +2208,22 @@ ADMIN_MEDIA_PREFIX integration. See the :doc:`Django 1.4 release notes` for more information. +.. setting:: AUTH_PROFILE_MODULE + +AUTH_PROFILE_MODULE +------------------- + +.. deprecated:: 1.5 + With the introduction of :ref:`custom User models `, + the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile + model is no longer supported. See the + :doc:`Django 1.5 release notes` for more information. + +Default: Not defined + +The site-specific user profile model used by this site. See +:ref:`auth-profiles`. + .. setting:: IGNORABLE_404_ENDS IGNORABLE_404_ENDS diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 528a44c5a1..df8d89c185 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -34,6 +34,23 @@ release featuring 2.7 support. What's new in Django 1.5 ======================== +Configurable User model +~~~~~~~~~~~~~~~~~~~~~~~ + +In Django 1.5, you can now use your own model as the store for user-related +data. If your project needs a username with more than 30 characters, or if +you want to store usernames in a format other than first name/last name, or +you want to put custom profile information onto your User object, you can +now do so. + +If you have a third-party reusable application that references the User model, +you may need to make some changes to the way you reference User instances. You +should also document any specific features of the User model that your +application relies upon. + +See the :ref:`documentation on custom User models ` for +more details. + Support for saving a subset of model's fields ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -277,6 +294,18 @@ Session not saved on 500 responses Django's session middleware will skip saving the session data if the response's status code is 500. +Email checks on failed admin login +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Prior to Django 1.5, if you attempted to log into the admin interface and +mistakenly used your email address instead of your username, the admin +interface would provide a warning advising that your email address was +not your username. In Django 1.5, the introduction of +:ref:`custom User models ` has required the removal of this +warning. This doesn't change the login behavior of the admin site; it only +affects the warning message that is displayed under one particular mode of +login failure. + Changes in tests execution ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -394,3 +423,16 @@ 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. + +:setting:`AUTH_PROFILE_MODULE` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With the introduction of :ref:`custom User models `, there is +no longer any need for a built-in mechanism to store user profile data. + +You can still define user profiles models that have a one-to-one relation with +the User model - in fact, for many applications needing to associate data with +a User account, this will be an appropriate design pattern to follow. However, +the :setting:`AUTH_PROFILE_MODULE` setting, and the +:meth:`~django.contrib.auth.models.User.get_profile()` method for accessing +the user profile model, should not be used any longer. diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index 88372af149..a767b5a93f 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -250,6 +250,12 @@ Methods .. method:: models.User.get_profile() + .. deprecated:: 1.5 + With the introduction of :ref:`custom User models `, + the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile + model is no longer supported. See the + :doc:`Django 1.5 release notes` for more information. + Returns a site-specific profile for this user. Raises :exc:`django.contrib.auth.models.SiteProfileNotAvailable` if the current site doesn't allow profiles, or @@ -582,6 +588,12 @@ correct path and environment for you. Storing additional information about users ------------------------------------------ +.. deprecated:: 1.5 + With the introduction of :ref:`custom User models `, + the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile + model is no longer supported. See the + :doc:`Django 1.5 release notes` for more information. + If you'd like to store additional information related to your users, Django provides a method to specify a site-specific related model -- termed a "user profile" -- for this purpose. @@ -1345,6 +1357,9 @@ Helper functions URL to redirect to after log out. Overrides ``next`` if the given ``GET`` parameter is passed. + +.. _built-in-auth-forms: + Built-in forms -------------- @@ -1735,6 +1750,350 @@ Fields group.permissions.remove(permission, permission, ...) group.permissions.clear() +.. _auth-custom-user: + +Customizing the User model +========================== + +.. versionadded:: 1.5 + +Some kinds of projects may have authentication requirements for which Django's +built-in :class:`~django.contrib.auth.models.User` model is not always +appropriate. For instance, on some sites it makes more sense to use an email +address as your identification token instead of a username. + +Django allows you to override the default User model by providing a value for +the :setting:`AUTH_USER_MODEL` setting that references a custom model:: + + AUTH_USER_MODEL = 'myapp.MyUser' + +This dotted pair describes the name of the Django app, and the name of the Django +model that you wish to use as your User model. + +.. admonition:: Warning + + Changing :setting:`AUTH_USER_MODEL` has a big effect on your database + structure. It changes the tables that are available, and it will affect the + construction of foreign keys and many-to-many relationships. If you intend + to set :setting:`AUTH_USER_MODEL`, you should set it before running + ``manage.py syncdb`` for the first time. + + If you have an existing project and you want to migrate to using a custom + User model, you may need to look into using a migration tool like South_ + to ease the transition. + +.. _South: http://south.aeracode.org + +Referencing the User model +-------------------------- + +If you reference :class:`~django.contrib.auth.models.User` directly (for +example, by referring to it in a foreign key), your code will not work in +projects where the :setting:`AUTH_USER_MODEL` setting has been changed to a +different User model. + +Instead of referring to :class:`~django.contrib.auth.models.User` directly, +you should reference the user model using +:func:`django.contrib.auth.get_user_model()`. This method will return the +currently active User model -- the custom User model if one is specified, or +:class:`~django.contrib.auth.User` otherwise. + +In relations to the User model, you should specify the custom model using +the :setting:`AUTH_USER_MODEL` setting. For example:: + + from django.conf import settings + from django.db import models + + class Article(models.Model) + author = models.ForeignKey(settings.AUTH_USER_MODEL) + +Specifying a custom User model +------------------------------ + +.. admonition:: Model design considerations + + Think carefully before handling information not directly related to + authentication in your custom User Model. + + It may be better to store app-specific user information in a model + that has a relation with the User model. That allows each app to specify + its own user data requirements without risking conflicts with other + apps. On the other hand, queries to retrieve this related information + will involve a database join, which may have an effect on performance. + +Django expects your custom User model to meet some minimum requirements. + +1. Your model must have a single unique field that can be used for + identification purposes. This can be a username, an email address, + or any other unique attribute. + +2. Your model must provide a way to address the user in a "short" and + "long" form. The most common interpretation of this would be to use + the user's given name as the "short" identifier, and the user's full + name as the "long" identifier. However, there are no constraints on + what these two methods return - if you want, they can return exactly + the same value. + +The easiest way to construct a compliant custom User model is to inherit from +:class:`~django.contrib.auth.models.AbstractBaseUser`. +:class:`~django.contrib.auth.models.AbstractBaseUser` provides the core +implementation of a `User` model, including hashed passwords and tokenized +password resets. You must then provide some key implementation details: + +.. attribute:: User.USERNAME_FIELD + + A string describing the name of the field on the User model that is + used as the unique identifier. This will usually be a username of + some kind, but it can also be an email address, or any other unique + identifier. In the following example, the field `identifier` is used + as the identifying field:: + + class MyUser(AbstractBaseUser): + identfier = models.CharField(max_length=40, unique=True, db_index=True) + ... + USERNAME_FIELD = 'identifier' + +.. attribute:: User.REQUIRED_FIELDS + + A list of the field names that *must* be provided when creating + a user. For example, here is the partial definition for a User model + that defines two required fields - a date of birth and height:: + + class MyUser(AbstractBaseUser): + ... + date_of_birth = models.DateField() + height = models.FloatField() + ... + REQUIRED_FIELDS = ['date_of_birth', 'height'] + +.. method:: User.get_full_name(): + + A longer formal identifier for the user. A common interpretation + would be the full name name of the user, but it can be any string that + identifies the user. + +.. method:: User.get_short_name(): + + A short, informal identifier for the user. A common interpretation + would be the first name of the user, but it can be any string that + identifies the user in an informal way. It may also return the same + value as :meth:`django.contrib.auth.User.get_full_name()`. + +You should also define a custom manager for your User model. If your User +model defines `username` and `email` fields the same as Django's default User, +you can just install Django's +:class:`~django.contrib.auth.models.UserManager`; however, if your User model +defines different fields, you will need to define a custom manager that +extends :class:`~django.contrib.auth.models.BaseUserManager` providing two +additional methods: + +.. method:: UserManager.create_user(username, password=None, **other_fields) + + The prototype of `create_user()` should accept all required fields + as arguments. For example, if your user model defines `username`, + and `date_of_birth` as required fields, then create_user should be + defined as:: + + def create_user(self, username, date_of_birth, password=None): + # create user here + +.. method:: UserManager.create_superuser(username, password, **other_fields) + + The prototype of `create_superuser()` should accept all required fields + as arguments. For example, if your user model defines `username`, + and `date_of_birth` as required fields, then create_user should be + defined as:: + + def create_superuser(self, username, date_of_birth, password): + # create superuser here + + Unlike `create_user()`, `create_superuser()` *must* require the caller + to provider a password. + +Extending Django's default User +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're entirely happy with Django's :class:`~django.contrib.auth.models.User` +model and you just want to add some additional profile information, you can +simply subclass :class:`~django.contrib.auth.models.AbstractUser` and add your +custom profile fields. + +Custom users and the built-in auth forms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As you may expect, built-in Django's :ref:`forms <_built-in-auth-forms>` +and :ref:`views ` make certain assumptions about +the user model that they are working with. + +If your user model doesn't follow the same assumptions, it may be necessary to define +a replacement form, and pass that form in as part of the configuration of the +auth views. + +* :class:`~django.contrib.auth.forms.UserCreationForm` + + Depends on the :class:`~django.contrib.auth.models.User` model. + Must be re-written for any custom user model. + +* :class:`~django.contrib.auth.forms.UserChangeForm` + + Depends on the :class:`~django.contrib.auth.models.User` model. + Must be re-written for any custom user model. + +* :class:`~django.contrib.auth.forms.AuthenticationForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`, + and will adapt to use the field defined in `USERNAME_FIELD`. + +* :class:`~django.contrib.auth.forms.PasswordResetForm` + + Assumes that the user model has an integer primary key, has a field named + `email` that can be used to identify the user, and a boolean field + named `is_active` to prevent password resets for inactive users. + +* :class:`~django.contrib.auth.forms.SetPasswordForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + +* :class:`~django.contrib.auth.forms.PasswordChangeForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + +* :class:`~django.contrib.auth.forms.AdminPasswordChangeForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + + +Custom users and django.contrib.admin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want your custom User model to also work with Admin, your User model must +define some additional attributes and methods. These methods allow the admin to +control access of the User to admin content: + +.. attribute:: User.is_staff + + Returns True if the user is allowed to have access to the admin site. + +.. attribute:: User.is_active + + Returns True if the user account is currently active. + +.. method:: User.has_perm(perm, obj=None): + + Returns True if the user has the named permission. If `obj` is + provided, the permission needs to be checked against a specific object + instance. + +.. method:: User.has_module_perms(app_label): + + Returns True if the user has permission to access models in + the given app. + + +Custom users and Proxy models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One limitation of custom User models is that installing a custom User model +will break any proxy model extending :class:`~django.contrib.auth.models.User`. +Proxy models must be based on a concrete base class; by defining a custom User +model, you remove the ability of Django to reliably identify the base class. + +If your project uses proxy models, you must either modify the proxy to extend +the User model that is currently in use in your project, or merge your proxy's +behavior into your User subclass. + +A full example +-------------- + +Here is an example of a full models.py for an admin-compliant custom +user app. This user model uses an email address as the username, and has a +required date of birth; it provides no permission checking, beyond a simple +`admin` flag on the user account. This model would be compatible with all +the built-in auth forms and views, except for the User creation forms. + +This code would all live in a ``models.py`` file for a custom +authentication app:: + + from django.db import models + from django.contrib.auth.models import ( + BaseUserManager, AbstractBaseUser + ) + + + class MyUserManager(BaseUserManager): + def create_user(self, email, date_of_birth, password=None): + """ + Creates and saves a User with the given email, date of + birth and password. + """ + if not email: + raise ValueError('Users must have an email address') + + user = self.model( + email=MyUserManager.normalize_email(email), + date_of_birth=date_of_birth, + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, username, date_of_birth, password): + """ + Creates and saves a superuser with the given email, date of + birth and password. + """ + u = self.create_user(username, + password=password, + date_of_birth=date_of_birth + ) + u.is_admin = True + u.save(using=self._db) + return u + + + class MyUser(AbstractBaseUser): + email = models.EmailField( + verbose_name='email address', + max_length=255 + ) + date_of_birth = models.DateField() + is_active = models.BooleanField(default=True) + is_admin = models.BooleanField(default=False) + + objects = MyUserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['date_of_birth'] + + def get_full_name(self): + # The user is identified by their email address + return self.email + + def get_short_name(self): + # The user is identified by their email address + return self.email + + def __unicode__(self): + return self.email + + def has_perm(self, perm, obj=None): + "Does the user have a specific permission?" + # Simplest possible answer: Yes, always + return True + + def has_module_perms(self, app_label): + "Does the user have permissions to view the app `app_label`?" + # Simplest possible answer: Yes, always + return True + + @property + def is_staff(self): + "Is the user a member of staff?" + # Simplest possible answer: All admins are staff + return self.is_admin + + .. _authentication-backends: Other authentication sources diff --git a/tests/modeltests/invalid_models/invalid_models/models.py b/tests/modeltests/invalid_models/invalid_models/models.py index b2ba253c5d..3f95d314e3 100644 --- a/tests/modeltests/invalid_models/invalid_models/models.py +++ b/tests/modeltests/invalid_models/invalid_models/models.py @@ -21,11 +21,12 @@ class FieldErrors(models.Model): decimalfield5 = models.DecimalField(max_digits=10, decimal_places=10) filefield = models.FileField() choices = models.CharField(max_length=10, choices='bad') - choices2 = models.CharField(max_length=10, choices=[(1,2,3),(1,2,3)]) + choices2 = models.CharField(max_length=10, choices=[(1, 2, 3), (1, 2, 3)]) index = models.CharField(max_length=10, db_index='bad') field_ = models.CharField(max_length=10) nullbool = models.BooleanField(null=True) + class Target(models.Model): tgt_safe = models.CharField(max_length=10) clash1 = models.CharField(max_length=10) @@ -33,12 +34,14 @@ class Target(models.Model): clash1_set = models.CharField(max_length=10) + class Clash1(models.Model): src_safe = models.CharField(max_length=10) foreign = models.ForeignKey(Target) m2m = models.ManyToManyField(Target) + class Clash2(models.Model): src_safe = models.CharField(max_length=10) @@ -48,6 +51,7 @@ class Clash2(models.Model): m2m_1 = models.ManyToManyField(Target, related_name='id') m2m_2 = models.ManyToManyField(Target, related_name='src_safe') + class Target2(models.Model): clash3 = models.CharField(max_length=10) foreign_tgt = models.ForeignKey(Target) @@ -56,6 +60,7 @@ class Target2(models.Model): m2m_tgt = models.ManyToManyField(Target) clashm2m_set = models.ManyToManyField(Target) + class Clash3(models.Model): src_safe = models.CharField(max_length=10) @@ -65,12 +70,15 @@ class Clash3(models.Model): m2m_1 = models.ManyToManyField(Target2, related_name='foreign_tgt') m2m_2 = models.ManyToManyField(Target2, related_name='m2m_tgt') + class ClashForeign(models.Model): foreign = models.ForeignKey(Target2) + class ClashM2M(models.Model): m2m = models.ManyToManyField(Target2) + class SelfClashForeign(models.Model): src_safe = models.CharField(max_length=10) selfclashforeign = models.CharField(max_length=10) @@ -79,6 +87,7 @@ class SelfClashForeign(models.Model): foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id') foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe') + class ValidM2M(models.Model): src_safe = models.CharField(max_length=10) validm2m = models.CharField(max_length=10) @@ -94,6 +103,7 @@ class ValidM2M(models.Model): m2m_3 = models.ManyToManyField('self') m2m_4 = models.ManyToManyField('self') + class SelfClashM2M(models.Model): src_safe = models.CharField(max_length=10) selfclashm2m = models.CharField(max_length=10) @@ -108,120 +118,148 @@ class SelfClashM2M(models.Model): m2m_3 = models.ManyToManyField('self', symmetrical=False) m2m_4 = models.ManyToManyField('self', symmetrical=False) + class Model(models.Model): "But it's valid to call a model Model." - year = models.PositiveIntegerField() #1960 - make = models.CharField(max_length=10) #Aston Martin - name = models.CharField(max_length=10) #DB 4 GT + year = models.PositiveIntegerField() # 1960 + make = models.CharField(max_length=10) # Aston Martin + name = models.CharField(max_length=10) # DB 4 GT + class Car(models.Model): colour = models.CharField(max_length=5) model = models.ForeignKey(Model) + class MissingRelations(models.Model): rel1 = models.ForeignKey("Rel1") rel2 = models.ManyToManyField("Rel2") + class MissingManualM2MModel(models.Model): name = models.CharField(max_length=5) missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") + class Person(models.Model): name = models.CharField(max_length=5) + class Group(models.Model): name = models.CharField(max_length=5) primary = models.ManyToManyField(Person, through="Membership", related_name="primary") secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary") + class GroupTwo(models.Model): name = models.CharField(max_length=5) primary = models.ManyToManyField(Person, through="Membership") secondary = models.ManyToManyField(Group, through="MembershipMissingFK") + class Membership(models.Model): person = models.ForeignKey(Person) group = models.ForeignKey(Group) not_default_or_null = models.CharField(max_length=5) + class MembershipMissingFK(models.Model): person = models.ForeignKey(Person) + class PersonSelfRefM2M(models.Model): name = models.CharField(max_length=5) friends = models.ManyToManyField('self', through="Relationship") too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") + class PersonSelfRefM2MExplicit(models.Model): name = models.CharField(max_length=5) friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True) + class Relationship(models.Model): first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") date_added = models.DateTimeField() + class ExplicitRelationship(models.Model): first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set") second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set") date_added = models.DateTimeField() + class RelationshipTripleFK(models.Model): first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") date_added = models.DateTimeField() + class RelationshipDoubleFK(models.Model): first = models.ForeignKey(Person, related_name="first_related_name") second = models.ForeignKey(Person, related_name="second_related_name") third = models.ForeignKey(Group, related_name="rel_to_set") date_added = models.DateTimeField() + class AbstractModel(models.Model): name = models.CharField(max_length=10) + class Meta: abstract = True + class AbstractRelationModel(models.Model): fk1 = models.ForeignKey('AbstractModel') fk2 = models.ManyToManyField('AbstractModel') + class UniqueM2M(models.Model): """ Model to test for unique ManyToManyFields, which are invalid. """ unique_people = models.ManyToManyField(Person, unique=True) + class NonUniqueFKTarget1(models.Model): """ Model to test for non-unique FK target in yet-to-be-defined model: expect an error """ tgt = models.ForeignKey('FKTarget', to_field='bad') + class UniqueFKTarget1(models.Model): """ Model to test for unique FK target in yet-to-be-defined model: expect no error """ tgt = models.ForeignKey('FKTarget', to_field='good') + class FKTarget(models.Model): bad = models.IntegerField() good = models.IntegerField(unique=True) + class NonUniqueFKTarget2(models.Model): """ Model to test for non-unique FK target in previously seen model: expect an error """ tgt = models.ForeignKey(FKTarget, to_field='bad') + class UniqueFKTarget2(models.Model): """ Model to test for unique FK target in previously seen model: expect no error """ tgt = models.ForeignKey(FKTarget, to_field='good') + class NonExistingOrderingWithSingleUnderscore(models.Model): class Meta: ordering = ("does_not_exist",) + class InvalidSetNull(models.Model): fk = models.ForeignKey('self', on_delete=models.SET_NULL) + class InvalidSetDefault(models.Model): fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT) + class UnicodeForeignKeys(models.Model): """Foreign keys which can translate to ascii should be OK, but fail if they're not.""" @@ -232,9 +270,11 @@ class UnicodeForeignKeys(models.Model): # when adding the errors in core/management/validation.py #bad = models.ForeignKey('★') + class PrimaryKeyNull(models.Model): my_pk_field = models.IntegerField(primary_key=True, null=True) + class OrderByPKModel(models.Model): """ Model to test that ordering by pk passes validation. @@ -245,6 +285,62 @@ class OrderByPKModel(models.Model): class Meta: ordering = ('pk',) + +class SwappableModel(models.Model): + """A model that can be, but isn't swapped out. + + References to this model *shoudln't* raise any validation error. + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPABLE_MODEL' + + +class SwappedModel(models.Model): + """A model that is swapped out. + + References to this model *should* raise a validation error. + Requires TEST_SWAPPED_MODEL to be defined in the test environment; + this is guaranteed by the test runner using @override_settings. + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPED_MODEL' + + +class BadSwappableValue(models.Model): + """A model that can be swapped out; during testing, the swappable + value is not of the format app.model + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPED_MODEL_BAD_VALUE' + + +class BadSwappableModel(models.Model): + """A model that can be swapped out; during testing, the swappable + value references an unknown model. + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPED_MODEL_BAD_MODEL' + + +class HardReferenceModel(models.Model): + fk_1 = models.ForeignKey(SwappableModel, related_name='fk_hardref1') + fk_2 = models.ForeignKey('invalid_models.SwappableModel', related_name='fk_hardref2') + fk_3 = models.ForeignKey(SwappedModel, related_name='fk_hardref3') + fk_4 = models.ForeignKey('invalid_models.SwappedModel', related_name='fk_hardref4') + m2m_1 = models.ManyToManyField(SwappableModel, related_name='m2m_hardref1') + m2m_2 = models.ManyToManyField('invalid_models.SwappableModel', related_name='m2m_hardref2') + m2m_3 = models.ManyToManyField(SwappedModel, related_name='m2m_hardref3') + m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4') + + model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer. invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer. invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer. @@ -353,6 +449,12 @@ invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have invalid_models.nonexistingorderingwithsingleunderscore: "ordering" refers to "does_not_exist", a field that doesn't exist. invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null. invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value. +invalid_models.hardreferencemodel: 'fk_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'fk_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'm2m_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'm2m_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.badswappablevalue: TEST_SWAPPED_MODEL_BAD_VALUE is not of the form 'app_label.app_name'. +invalid_models.badswappablemodel: Model has been swapped out for 'not_an_app.Target' which has not been installed or is abstract. """ if not connection.features.interprets_empty_strings_as_nulls: diff --git a/tests/modeltests/invalid_models/tests.py b/tests/modeltests/invalid_models/tests.py index e1fc68743e..6050a20880 100644 --- a/tests/modeltests/invalid_models/tests.py +++ b/tests/modeltests/invalid_models/tests.py @@ -4,6 +4,7 @@ import sys from django.core.management.validation import get_validation_errors from django.db.models.loading import cache, load_app +from django.test.utils import override_settings from django.utils import unittest from django.utils.six import StringIO @@ -31,14 +32,22 @@ class InvalidModelTestCase(unittest.TestCase): cache._get_models_cache = {} sys.stdout = self.old_stdout + # Technically, this isn't an override -- TEST_SWAPPED_MODEL must be + # set to *something* in order for the test to work. However, it's + # easier to set this up as an override than to require every developer + # to specify a value in their test settings. + @override_settings( + TEST_SWAPPED_MODEL='invalid_models.Target', + TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model', + TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target', + ) def test_invalid_models(self): - try: module = load_app("modeltests.invalid_models.invalid_models") except Exception: self.fail('Unable to load invalid model module') - count = get_validation_errors(self.stdout, module) + get_validation_errors(self.stdout, module) self.stdout.seek(0) error_log = self.stdout.read() actual = error_log.split('\n') diff --git a/tests/modeltests/proxy_models/tests.py b/tests/modeltests/proxy_models/tests.py index 7ec86e9b22..d1c95467ee 100644 --- a/tests/modeltests/proxy_models/tests.py +++ b/tests/modeltests/proxy_models/tests.py @@ -1,10 +1,13 @@ from __future__ import absolute_import, unicode_literals +import copy +from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core import management from django.core.exceptions import FieldError from django.db import models, DEFAULT_DB_ALIAS from django.db.models import signals +from django.db.models.loading import cache from django.test import TestCase @@ -13,6 +16,7 @@ from .models import (MyPerson, Person, StatusPerson, LowerStatusPerson, Country, State, StateProxy, TrackerUser, BaseUser, Bug, ProxyTrackerUser, Improvement, ProxyProxyBug, ProxyBug, ProxyImprovement) + class ProxyModelTests(TestCase): def test_same_manager_queries(self): """ @@ -91,7 +95,7 @@ class ProxyModelTests(TestCase): ) self.assertRaises(Person.MultipleObjectsReturned, MyPersonProxy.objects.get, - id__lt=max_id+1 + id__lt=max_id + 1 ) self.assertRaises(Person.DoesNotExist, StatusPerson.objects.get, @@ -104,7 +108,7 @@ class ProxyModelTests(TestCase): self.assertRaises(Person.MultipleObjectsReturned, StatusPerson.objects.get, - id__lt=max_id+1 + id__lt=max_id + 1 ) def test_abc(self): @@ -138,10 +142,40 @@ class ProxyModelTests(TestCase): def build_new_fields(): class NoNewFields(Person): newfield = models.BooleanField() + class Meta: proxy = True self.assertRaises(FieldError, build_new_fields) + def test_swappable(self): + try: + # This test adds dummy applications to the app cache. These + # need to be removed in order to prevent bad interactions + # with the flush operation in other tests. + old_app_models = copy.deepcopy(cache.app_models) + old_app_store = copy.deepcopy(cache.app_store) + + settings.TEST_SWAPPABLE_MODEL = 'proxy_models.AlternateModel' + + class SwappableModel(models.Model): + + class Meta: + swappable = 'TEST_SWAPPABLE_MODEL' + + class AlternateModel(models.Model): + pass + + # You can't proxy a swapped model + with self.assertRaises(TypeError): + class ProxyModel(SwappableModel): + + class Meta: + proxy = True + finally: + del settings.TEST_SWAPPABLE_MODEL + cache.app_models = old_app_models + cache.app_store = old_app_store + def test_myperson_manager(self): Person.objects.create(name="fred") Person.objects.create(name="wilma") diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 8151c8c854..284ea94226 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -52,6 +52,7 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount, ERROR_MESSAGE = "Please enter the correct username and password \ for a staff account. Note that both fields are case-sensitive." + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewBasicTest(TestCase): fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', @@ -141,7 +142,7 @@ class AdminViewBasicTest(TestCase): "article_set-MAX_NUM_FORMS": "0", } response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testPopupAddPost(self): """ @@ -205,7 +206,7 @@ class AdminViewBasicTest(TestCase): A smoke test to ensure POST on edit_view works. """ response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, self.inline_post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testEditSaveAs(self): """ @@ -221,7 +222,7 @@ class AdminViewBasicTest(TestCase): "article_set-5-section": "1", }) response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testChangeListSortingCallable(self): """ @@ -308,7 +309,7 @@ class AdminViewBasicTest(TestCase): self.assertContentBefore(response, link2, link1) # Test we can override with query string - response = self.client.get('/test_admin/admin/admin_views/language/', {'o':'-1'}) + response = self.client.get('/test_admin/admin/admin_views/language/', {'o': '-1'}) self.assertContentBefore(response, link1, link2) def testChangeListSortingOverrideModelAdmin(self): @@ -358,13 +359,13 @@ class AdminViewBasicTest(TestCase): kinds of 'ordering' fields: field names, method on the model admin and model itself, and other callables. See #17252. """ - models = [(AdminOrderedField, 'adminorderedfield' ), + models = [(AdminOrderedField, 'adminorderedfield'), (AdminOrderedModelMethod, 'adminorderedmodelmethod'), (AdminOrderedAdminMethod, 'adminorderedadminmethod'), - (AdminOrderedCallable, 'adminorderedcallable' )] + (AdminOrderedCallable, 'adminorderedcallable')] for model, url in models: - a1 = model.objects.create(stuff='The Last Item', order=3) - a2 = model.objects.create(stuff='The First Item', order=1) + a1 = model.objects.create(stuff='The Last Item', order=3) + a2 = model.objects.create(stuff='The First Item', order=1) a3 = model.objects.create(stuff='The Middle Item', order=2) response = self.client.get('/test_admin/admin/admin_views/%s/' % url, {}) self.assertEqual(response.status_code, 200) @@ -671,7 +672,6 @@ class AdminJavaScriptTest(TestCase): '' ) - def test_js_minified_only_if_debug_is_false(self): """ Ensure that the minified versions of the JS files are only used when @@ -709,7 +709,7 @@ class AdminJavaScriptTest(TestCase): @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class SaveAsTests(TestCase): urls = "regressiontests.admin_views.urls" - fixtures = ['admin-views-users.xml','admin-views-person.xml'] + fixtures = ['admin-views-users.xml', 'admin-views-person.xml'] def setUp(self): self.client.login(username='super', password='secret') @@ -719,7 +719,7 @@ class SaveAsTests(TestCase): def test_save_as_duplication(self): """Ensure save as actually creates a new person""" - post_data = {'_saveasnew':'', 'name':'John M', 'gender':1, 'age': 42} + post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 1, 'age': 42} response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data) self.assertEqual(len(Person.objects.filter(name='John M')), 1) self.assertEqual(len(Person.objects.filter(id=1)), 1) @@ -732,10 +732,11 @@ class SaveAsTests(TestCase): """ response = self.client.get('/test_admin/admin/admin_views/person/1/') self.assertTrue(response.context['save_as']) - post_data = {'_saveasnew':'', 'name':'John M', 'gender':3, 'alive':'checked'} + post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 3, 'alive': 'checked'} response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data) self.assertEqual(response.context['form_url'], '/test_admin/admin/admin_views/person/add/') + class CustomModelAdminTest(AdminViewBasicTest): urls = "regressiontests.admin_views.urls" urlbit = "admin2" @@ -791,11 +792,13 @@ class CustomModelAdminTest(AdminViewBasicTest): response = self.client.get('/test_admin/%s/my_view/' % self.urlbit) self.assertEqual(response.content, b"Django is a magical pony!") + def get_perm(Model, perm): """Return the permission object, for the Model""" ct = ContentType.objects.get_for_model(Model) return Permission.objects.get(content_type=ct, codename=perm) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewPermissionsTest(TestCase): """Tests for Admin Views Permissions.""" @@ -898,7 +901,7 @@ class AdminViewPermissionsTest(TestCase): response = self.client.get('/test_admin/admin/') self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.super_email_login) - self.assertContains(login, "Your e-mail address is not your username") + self.assertContains(login, ERROR_MESSAGE) # only correct passwords get a username hint login = self.client.post('/test_admin/admin/', self.super_email_bad_login) self.assertContains(login, ERROR_MESSAGE) @@ -959,7 +962,7 @@ class AdminViewPermissionsTest(TestCase): def testAddView(self): """Test add view restricts access and actually adds items.""" - add_dict = {'title' : 'Døm ikke', + add_dict = {'title': 'Døm ikke', 'content': '

great article

', 'date_0': '2008-03-18', 'date_1': '10:54:39', 'section': 1} @@ -1014,7 +1017,7 @@ class AdminViewPermissionsTest(TestCase): def testChangeView(self): """Change view should restrict access and allow users to edit items.""" - change_dict = {'title' : 'Ikke fordømt', + change_dict = {'title': 'Ikke fordømt', 'content': '

edited article

', 'date_0': '2008-03-18', 'date_1': '10:54:39', 'section': 1} @@ -1346,6 +1349,7 @@ class AdminViewDeletedObjectsTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(3)) self.assertContains(response, should_contain) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewStringPrimaryKeyTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -1400,7 +1404,7 @@ class AdminViewStringPrimaryKeyTest(TestCase): response = self.client.get('/test_admin/admin/') should_contain = """%s""" % (escape(quote(self.pk)), escape(self.pk)) self.assertContains(response, should_contain) - should_contain = "Model with string primary key" # capitalized in Recent Actions + should_contain = "Model with string primary key" # capitalized in Recent Actions self.assertContains(response, should_contain) logentry = LogEntry.objects.get(content_type__name__iexact=should_contain) # http://code.djangoproject.com/ticket/10275 @@ -1522,7 +1526,7 @@ class SecureViewTests(TestCase): def test_secure_view_shows_login_if_not_logged_in(self): "Ensure that we see the login form" - response = self.client.get('/test_admin/admin/secure-view/' ) + response = self.client.get('/test_admin/admin/secure-view/') self.assertTemplateUsed(response, 'admin/login.html') def test_secure_view_login_successfully_redirects_to_original_url(self): @@ -1556,7 +1560,7 @@ class SecureViewTests(TestCase): response = self.client.get('/test_admin/admin/secure-view/') self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login) - self.assertContains(login, "Your e-mail address is not your username") + self.assertContains(login, ERROR_MESSAGE) # only correct passwords get a username hint login = self.client.post('/test_admin/admin/secure-view/', self.super_email_bad_login) self.assertContains(login, ERROR_MESSAGE) @@ -1626,6 +1630,7 @@ class SecureViewTests(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], 'http://example.com/users/super/') + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewUnicodeTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -1668,7 +1673,7 @@ class AdminViewUnicodeTest(TestCase): } response = self.client.post('/test_admin/admin/admin_views/book/1/', post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testUnicodeDelete(self): """ @@ -2035,7 +2040,7 @@ class AdminViewListEditable(TestCase): story1 = Story.objects.create(title='The adventures of Guido', content='Once upon a time in Djangoland...') story2 = Story.objects.create(title='Crouching Tiger, Hidden Python', content='The Python was sneaking into...') response = self.client.get('/test_admin/admin/admin_views/story/') - self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. + self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. self.assertContains(response, 'id="id_form-1-id"', 1) self.assertContains(response, '
\n\n
' % (story2.id, story1.id), html=True) self.assertContains(response, '%d' % story1.id, 1) @@ -2051,7 +2056,7 @@ class AdminViewListEditable(TestCase): link1 = reverse('admin:admin_views_otherstory_change', args=(story1.pk,)) link2 = reverse('admin:admin_views_otherstory_change', args=(story2.pk,)) response = self.client.get('/test_admin/admin/admin_views/otherstory/') - self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. + self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. self.assertContains(response, 'id="id_form-1-id"', 1) self.assertContains(response, '
\n\n
' % (story2.id, story1.id), html=True) self.assertContains(response, '%d' % (link1, story1.id), 1) @@ -2109,7 +2114,7 @@ class AdminSearchTest(TestCase): @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminInheritedInlinesTest(TestCase): urls = "regressiontests.admin_views.urls" - fixtures = ['admin-views-users.xml',] + fixtures = ['admin-views-users.xml'] def setUp(self): self.client.login(username='super', password='secret') @@ -2146,7 +2151,7 @@ class AdminInheritedInlinesTest(TestCase): } response = self.client.post('/test_admin/admin/admin_views/persona/add/', post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere self.assertEqual(Persona.objects.count(), 1) self.assertEqual(FooAccount.objects.count(), 1) self.assertEqual(BarAccount.objects.count(), 1) @@ -2193,6 +2198,7 @@ class AdminInheritedInlinesTest(TestCase): self.assertEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user) self.assertEqual(Persona.objects.all()[0].accounts.count(), 2) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminActionsTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -2208,7 +2214,7 @@ class AdminActionsTest(TestCase): "Tests a custom action defined in a ModelAdmin method" action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'mail_admin', + 'action': 'mail_admin', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2219,12 +2225,12 @@ class AdminActionsTest(TestCase): "Tests the default delete action defined as a ModelAdmin method" action_data = { ACTION_CHECKBOX_NAME: [1, 2], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } delete_confirmation_data = { ACTION_CHECKBOX_NAME: [1, 2], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'post': 'yes', } confirmation = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2248,12 +2254,12 @@ class AdminActionsTest(TestCase): subscriber.save() action_data = { ACTION_CHECKBOX_NAME: [9999, 2], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) self.assertTemplateUsed(response, 'admin/delete_selected_confirmation.html') - self.assertContains(response, 'value="9999"') # Instead of 9,999 + self.assertContains(response, 'value="9999"') # Instead of 9,999 self.assertContains(response, 'value="2"') settings.USE_THOUSAND_SEPARATOR = self.old_USE_THOUSAND_SEPARATOR settings.USE_L10N = self.old_USE_L10N @@ -2270,7 +2276,7 @@ class AdminActionsTest(TestCase): action_data = { ACTION_CHECKBOX_NAME: [q1.pk, q2.pk], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } @@ -2284,7 +2290,7 @@ class AdminActionsTest(TestCase): "Tests a custom action defined in a function" action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'external_mail', + 'action': 'external_mail', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) @@ -2295,7 +2301,7 @@ class AdminActionsTest(TestCase): "Tests a custom action defined in a function" action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'redirect_to', + 'action': 'redirect_to', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) @@ -2309,7 +2315,7 @@ class AdminActionsTest(TestCase): """ action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'external_mail', + 'action': 'external_mail', 'index': 0, } url = '/test_admin/admin/admin_views/externalsubscriber/?o=1' @@ -2374,7 +2380,7 @@ class AdminActionsTest(TestCase): """ action_data = { ACTION_CHECKBOX_NAME: [], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2388,7 +2394,7 @@ class AdminActionsTest(TestCase): """ action_data = { ACTION_CHECKBOX_NAME: [1, 2], - 'action' : '', + 'action': '', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2432,7 +2438,7 @@ class TestCustomChangeList(TestCase): # Insert some data post_data = {"name": "First Gadget"} response = self.client.post('/test_admin/%s/admin_views/gadget/add/' % self.urlbit, post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere # Hit the page once to get messages out of the queue message list response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit) # Ensure that that data is still not visible on the page @@ -2460,6 +2466,7 @@ class TestInlineNotEditable(TestCase): response = self.client.get('/test_admin/admin/admin_views/parent/add/') self.assertEqual(response.status_code, 200) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminCustomQuerysetTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -2516,6 +2523,7 @@ class AdminCustomQuerysetTest(TestCase): # Message should contain non-ugly model name. Instance representation is set by model's __unicode__() self.assertContains(response, '
  • The cover letter "John Doe II" was changed successfully.
  • ', html=True) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminInlineFileUploadTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -2656,7 +2664,7 @@ class AdminInlineTests(TestCase): result = self.client.login(username='super', password='secret') self.assertEqual(result, True) - self.collector = Collector(pk=1,name='John Fowles') + self.collector = Collector(pk=1, name='John Fowles') self.collector.save() def tearDown(self): @@ -2982,14 +2990,14 @@ class PrePopulatedTest(TestCase): self.assertNotContains(response, "field['dependency_ids'].push('#id_title');") self.assertNotContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',") - @override_settings(USE_THOUSAND_SEPARATOR = True, USE_L10N = True) + @override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True) def test_prepopulated_maxlength_localized(self): """ Regression test for #15938: if USE_THOUSAND_SEPARATOR is set, make sure that maxLength (in the JavaScript) is rendered without separators. """ response = self.client.get('/test_admin/admin/admin_views/prepopulatedpostlargeslug/add/') - self.assertContains(response, "maxLength: 1000") # instead of 1,000 + self.assertContains(response, "maxLength: 1000") # instead of 1,000 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -3035,8 +3043,8 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase): self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-name').send_keys(' now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog text... ') slug1 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-slug1').get_attribute('value') slug2 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-slug2').get_attribute('value') - self.assertEqual(slug1, 'now-you-have-another-stacked-inline-very-loooooooo') # 50 characters maximum for slug1 field - self.assertEqual(slug2, 'option-two-now-you-have-another-stacked-inline-very-looooooo') # 60 characters maximum for slug2 field + self.assertEqual(slug1, 'now-you-have-another-stacked-inline-very-loooooooo') # 50 characters maximum for slug1 field + self.assertEqual(slug2, 'option-two-now-you-have-another-stacked-inline-very-looooooo') # 60 characters maximum for slug2 field # Tabular inlines ---------------------------------------------------- # Initial inline @@ -3087,7 +3095,7 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase): slug2='option-one-here-stacked-inline', ) RelatedPrepopulated.objects.get( - name=' now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooo', # 75 characters in name field + name=' now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooo', # 75 characters in name field pubdate='1999-01-25', status='option two', slug1='now-you-have-another-stacked-inline-very-loooooooo', @@ -3112,6 +3120,7 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase): class SeleniumPrePopulatedChromeTests(SeleniumPrePopulatedFirefoxTests): webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver' + class SeleniumPrePopulatedIETests(SeleniumPrePopulatedFirefoxTests): webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver' @@ -3172,7 +3181,7 @@ class ReadonlyTest(TestCase): p = Post.objects.get() self.assertEqual(p.posted, datetime.date.today()) - data["posted"] = "10-8-1990" # some date that's not today + data["posted"] = "10-8-1990" # some date that's not today response = self.client.post('/test_admin/admin/admin_views/post/add/', data) self.assertEqual(response.status_code, 302) self.assertEqual(Post.objects.count(), 2) @@ -3214,7 +3223,7 @@ class RawIdFieldsTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/sketch/add/') # Find the link m = re.search(br']* id="lookup_id_inquisition"', response.content) - self.assertTrue(m) # Got a match + self.assertTrue(m) # Got a match popup_url = m.groups()[0].decode().replace("&", "&") # Handle relative links @@ -3224,6 +3233,7 @@ class RawIdFieldsTest(TestCase): self.assertContains(response2, "Spain") self.assertNotContains(response2, "England") + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class UserAdminTest(TestCase): """ @@ -3378,7 +3388,7 @@ class CSSTest(TestCase): self.assertContains(response, 'class="form-row field-awesomeness_level"') self.assertContains(response, 'class="form-row field-coolness"') self.assertContains(response, 'class="form-row field-value"') - self.assertContains(response, 'class="form-row"') # The lambda function + self.assertContains(response, 'class="form-row"') # The lambda function # The tabular inline self.assertContains(response, '') @@ -3390,6 +3400,7 @@ try: except ImportError: docutils = None + @unittest.skipUnless(docutils, "no docutils installed.") @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminDocsTest(TestCase): @@ -3448,7 +3459,7 @@ class ValidXHTMLTests(TestCase): @override_settings( TEMPLATE_CONTEXT_PROCESSORS=filter( - lambda t:t!='django.core.context_processors.i18n', + lambda t: t != 'django.core.context_processors.i18n', global_settings.TEMPLATE_CONTEXT_PROCESSORS), USE_I18N=False, ) @@ -3585,6 +3596,7 @@ class DateHierarchyTests(TestCase): self.assert_non_localized_year(response, 2003) self.assert_non_localized_year(response, 2005) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminCustomSaveRelatedTests(TestCase): """ From 2c8267bf3db608b99c04ae903c424b60cafaaf93 Mon Sep 17 00:00:00 2001 From: Adrien Lemaire Date: Wed, 26 Sep 2012 14:14:51 +0200 Subject: [PATCH 048/265] Fixed #17899 -- Rewrote [Ee]-mail to [Ee]mail --- django/conf/global_settings.py | 2 +- django/contrib/admin/templates/admin/500.html | 2 +- .../registration/password_reset_done.html | 2 +- .../registration/password_reset_email.html | 2 +- .../registration/password_reset_form.html | 4 +-- django/contrib/auth/forms.py | 6 ++-- django/contrib/auth/tests/forms.py | 2 +- .../registration/password_reset_done.html | 2 +- django/core/validators.py | 2 +- django/db/models/fields/__init__.py | 2 +- django/forms/fields.py | 2 +- docs/index.txt | 2 +- docs/internals/deprecation.txt | 2 +- docs/ref/contrib/syndication.txt | 4 +-- docs/ref/forms/api.txt | 10 +++---- docs/ref/forms/fields.txt | 2 +- docs/ref/forms/validation.txt | 4 +-- docs/topics/testing.txt | 2 +- tests/modeltests/test_client/models.py | 4 +-- tests/regressiontests/admin_views/tests.py | 8 +++--- tests/regressiontests/forms/tests/extra.py | 2 +- tests/regressiontests/forms/tests/fields.py | 28 +++++++++---------- .../test_client_regress/tests.py | 4 +-- tests/regressiontests/test_utils/tests.py | 2 +- 24 files changed, 51 insertions(+), 51 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 4d5dc49ee0..708e9c9f70 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -144,7 +144,7 @@ DEFAULT_CHARSET = 'utf-8' # Encoding of files read from disk (template and initial SQL files). FILE_CHARSET = 'utf-8' -# E-mail address that error messages come from. +# Email address that error messages come from. SERVER_EMAIL = 'root@localhost' # Whether to send broken-link emails. diff --git a/django/contrib/admin/templates/admin/500.html b/django/contrib/admin/templates/admin/500.html index 9a3b636346..4842faa656 100644 --- a/django/contrib/admin/templates/admin/500.html +++ b/django/contrib/admin/templates/admin/500.html @@ -12,6 +12,6 @@ {% block content %}

    {% trans 'Server Error (500)' %}

    -

    {% trans "There's been an error. It's been reported to the site administrators via e-mail and should be fixed shortly. Thanks for your patience." %}

    +

    {% trans "There's been an error. It's been reported to the site administrators via email and should be fixed shortly. Thanks for your patience." %}

    {% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_done.html b/django/contrib/admin/templates/registration/password_reset_done.html index 3c9796e63c..7584c8393a 100644 --- a/django/contrib/admin/templates/registration/password_reset_done.html +++ b/django/contrib/admin/templates/registration/password_reset_done.html @@ -14,6 +14,6 @@

    {% trans 'Password reset successful' %}

    -

    {% trans "We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly." %}

    +

    {% trans "We've emailed you instructions for setting your password to the email address you submitted. You should be receiving it shortly." %}

    {% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_email.html b/django/contrib/admin/templates/registration/password_reset_email.html index 4f002fe5bb..0eef4a7f9d 100644 --- a/django/contrib/admin/templates/registration/password_reset_email.html +++ b/django/contrib/admin/templates/registration/password_reset_email.html @@ -1,5 +1,5 @@ {% load i18n %}{% autoescape off %} -{% blocktrans %}You're receiving this e-mail because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} +{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html index ca9ff115bc..c9998a1a3b 100644 --- a/django/contrib/admin/templates/registration/password_reset_form.html +++ b/django/contrib/admin/templates/registration/password_reset_form.html @@ -14,11 +14,11 @@

    {% trans "Password reset" %}

    -

    {% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}

    +

    {% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}

    {% csrf_token %} {{ form.email.errors }} -

    {{ form.email }}

    +

    {{ form.email }}

    {% endblock %} diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index a430f042e9..c114c18afe 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -193,12 +193,12 @@ class AuthenticationForm(forms.Form): class PasswordResetForm(forms.Form): error_messages = { - 'unknown': _("That e-mail address doesn't have an associated " + 'unknown': _("That email address doesn't have an associated " "user account. Are you sure you've registered?"), - 'unusable': _("The user account associated with this e-mail " + 'unusable': _("The user account associated with this email " "address cannot reset the password."), } - email = forms.EmailField(label=_("E-mail"), max_length=75) + email = forms.EmailField(label=_("Email"), max_length=75) def clean_email(self): """ diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index 7c6410da0f..6be6249711 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -344,4 +344,4 @@ class PasswordResetFormTest(TestCase): form = PasswordResetForm(data) self.assertFalse(form.is_valid()) self.assertEqual(form["email"].errors, - [_("The user account associated with this e-mail address cannot reset the password.")]) + [_("The user account associated with this email address cannot reset the password.")]) diff --git a/django/contrib/auth/tests/templates/registration/password_reset_done.html b/django/contrib/auth/tests/templates/registration/password_reset_done.html index d56b10f0d5..c3d1d0c7b0 100644 --- a/django/contrib/auth/tests/templates/registration/password_reset_done.html +++ b/django/contrib/auth/tests/templates/registration/password_reset_done.html @@ -1 +1 @@ -E-mail sent \ No newline at end of file +Email sent \ No newline at end of file diff --git a/django/core/validators.py b/django/core/validators.py index cf12f8c9fc..c7bda682ac 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -101,7 +101,7 @@ email_re = re.compile( r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$)' # domain r'|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) # literal form, ipv4 address (SMTP 4.1.3) -validate_email = EmailValidator(email_re, _('Enter a valid e-mail address.'), 'invalid') +validate_email = EmailValidator(email_re, _('Enter a valid email address.'), 'invalid') slug_re = re.compile(r'^[-a-zA-Z0-9_]+$') validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), 'invalid') diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 58ae3413f3..94abfd784c 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -905,7 +905,7 @@ class DecimalField(Field): class EmailField(CharField): default_validators = [validators.validate_email] - description = _("E-mail address") + description = _("Email address") def __init__(self, *args, **kwargs): # max_length should be overridden to 254 characters to be fully diff --git a/django/forms/fields.py b/django/forms/fields.py index 0075325288..4438812a37 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -461,7 +461,7 @@ class RegexField(CharField): class EmailField(CharField): default_error_messages = { - 'invalid': _('Enter a valid e-mail address.'), + 'invalid': _('Enter a valid email address.'), } default_validators = [validators.validate_email] diff --git a/docs/index.txt b/docs/index.txt index 8b29c95fa2..ce84f79d43 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -241,7 +241,7 @@ applications: * :doc:`Authentication ` * :doc:`Caching ` * :doc:`Logging ` -* :doc:`Sending e-mails ` +* :doc:`Sending emails ` * :doc:`Syndication feeds (RSS/Atom) ` * :doc:`Comments `, :doc:`comment moderation ` and :doc:`custom comments ` * :doc:`Pagination ` diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 4e341c6953..6387c87d1d 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -23,7 +23,7 @@ these changes. * The :mod:`django.contrib.gis.db.backend` module will be removed in favor of the specific backends. -* ``SMTPConnection`` will be removed in favor of a generic E-mail backend API. +* ``SMTPConnection`` will be removed in favor of a generic Email backend API. * The many to many SQL generation functions on the database backends will be removed. diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index 5653397748..27b8fc0875 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -455,7 +455,7 @@ This example illustrates all possible attributes and methods for a author_name = 'Sally Smith' # Hard-coded author name. - # AUTHOR E-MAIL --One of the following three is optional. The framework + # AUTHOR EMAIL --One of the following three is optional. The framework # looks for them in this order. def author_email(self, obj): @@ -635,7 +635,7 @@ This example illustrates all possible attributes and methods for a item_author_name = 'Sally Smith' # Hard-coded author name. - # ITEM AUTHOR E-MAIL --One of the following three is optional. The + # ITEM AUTHOR EMAIL --One of the following three is optional. The # framework looks for them in this order. # # If you specify this, you must specify item_author_name. diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 2323425277..dffef314b7 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -105,7 +105,7 @@ Access the :attr:`~Form.errors` attribute to get a dictionary of error messages:: >>> f.errors - {'sender': [u'Enter a valid e-mail address.'], 'subject': [u'This field is required.']} + {'sender': [u'Enter a valid email address.'], 'subject': [u'This field is required.']} In this dictionary, the keys are the field names, and the values are lists of Unicode strings representing the error messages. The error messages are stored @@ -538,18 +538,18 @@ method you're using:: >>> print(f.as_table()) Subject:
    • This field is required.
    Message: - Sender:
    • Enter a valid e-mail address.
    + Sender:
    • Enter a valid email address.
    Cc myself: >>> print(f.as_ul())
    • This field is required.
    Subject:
  • Message:
  • -
    • Enter a valid e-mail address.
    Sender:
  • +
    • Enter a valid email address.
    Sender:
  • Cc myself:
  • >>> print(f.as_p())

    • This field is required.

    Subject:

    Message:

    -

    • Enter a valid e-mail address.

    +

    • Enter a valid email address.

    Sender:

    Cc myself:

    @@ -572,7 +572,7 @@ pass that in at construction time::
    This field is required.

    Subject:

    Message:

    -
    Enter a valid e-mail address.
    +
    Enter a valid email address.

    Sender:

    Cc myself:

    diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 9f3dc68b4d..82a3ea9ab3 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -28,7 +28,7 @@ exception or returns the clean value:: >>> f.clean('invalid email address') Traceback (most recent call last): ... - ValidationError: [u'Enter a valid e-mail address.'] + ValidationError: [u'Enter a valid email address.'] Core field arguments -------------------- diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt index 1af32da875..e89bce748f 100644 --- a/docs/ref/forms/validation.txt +++ b/docs/ref/forms/validation.txt @@ -185,7 +185,7 @@ a look at Django's ``EmailField``:: class EmailField(CharField): default_error_messages = { - 'invalid': _('Enter a valid e-mail address.'), + 'invalid': _('Enter a valid email address.'), } default_validators = [validators.validate_email] @@ -198,7 +198,7 @@ on field definition so:: is equivalent to:: email = forms.CharField(validators=[validators.validate_email], - error_messages={'invalid': _('Enter a valid e-mail address.')}) + error_messages={'invalid': _('Enter a valid email address.')}) Form field default cleaning diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index 117dfbe591..2bc8410745 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -1622,7 +1622,7 @@ your test suite. "a@a.com" as a valid email address, but rejects "aaa" with a reasonable error message:: - self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': [u'Enter a valid e-mail address.']}) + self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': [u'Enter a valid email address.']}) .. method:: TestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py index 1d9c999f21..0f3cba7e88 100644 --- a/tests/modeltests/test_client/models.py +++ b/tests/modeltests/test_client/models.py @@ -215,7 +215,7 @@ class ClientTest(TestCase): self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "Invalid POST Template") - self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.') + self.assertFormError(response, 'form', 'email', 'Enter a valid email address.') def test_valid_form_with_template(self): "POST valid data to a form using multiple templates" @@ -263,7 +263,7 @@ class ClientTest(TestCase): self.assertTemplateUsed(response, 'base.html') self.assertTemplateNotUsed(response, "Invalid POST Template") - self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.') + self.assertFormError(response, 'form', 'email', 'Enter a valid email address.') def test_unknown_page(self): "GET an invalid URL" diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 284ea94226..72dc6a3f97 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -897,7 +897,7 @@ class AdminViewPermissionsTest(TestCase): self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') - # Test if user enters e-mail address + # Test if user enters email address response = self.client.get('/test_admin/admin/') self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.super_email_login) @@ -907,7 +907,7 @@ class AdminViewPermissionsTest(TestCase): self.assertContains(login, ERROR_MESSAGE) new_user = User(username='jondoe', password='secret', email='super@example.com') new_user.save() - # check to ensure if there are multiple e-mail addresses a user doesn't get a 500 + # check to ensure if there are multiple email addresses a user doesn't get a 500 login = self.client.post('/test_admin/admin/', self.super_email_login) self.assertContains(login, ERROR_MESSAGE) @@ -1556,7 +1556,7 @@ class SecureViewTests(TestCase): # make sure the view removes test cookie self.assertEqual(self.client.session.test_cookie_worked(), False) - # Test if user enters e-mail address + # Test if user enters email address response = self.client.get('/test_admin/admin/secure-view/') self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login) @@ -1566,7 +1566,7 @@ class SecureViewTests(TestCase): self.assertContains(login, ERROR_MESSAGE) new_user = User(username='jondoe', password='secret', email='super@example.com') new_user.save() - # check to ensure if there are multiple e-mail addresses a user doesn't get a 500 + # check to ensure if there are multiple email addresses a user doesn't get a 500 login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login) self.assertContains(login, ERROR_MESSAGE) diff --git a/tests/regressiontests/forms/tests/extra.py b/tests/regressiontests/forms/tests/extra.py index 2ab5d40942..44d6778aa2 100644 --- a/tests/regressiontests/forms/tests/extra.py +++ b/tests/regressiontests/forms/tests/extra.py @@ -613,7 +613,7 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): data = dict(email='invalid') f = CommentForm(data, auto_id=False, error_class=DivErrorList) self.assertHTMLEqual(f.as_p(), """

    Name:

    -
    Enter a valid e-mail address.
    +
    Enter a valid email address.

    Email:

    This field is required.

    Comment:

    """) diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index 989acbc496..8695256d64 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -507,16 +507,16 @@ class FieldsTests(SimpleTestCase): self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) self.assertEqual('person@example.com', f.clean('person@example.com')) - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@bar') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@invalid-.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@-invalid.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@inv-.alid-.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@inv-.-alid.com') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@bar') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@invalid-.com') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@-invalid.com') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@inv-.alid-.com') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@inv-.-alid.com') self.assertEqual('example@valid-----hyphens.com', f.clean('example@valid-----hyphens.com')) self.assertEqual('example@valid-with-hyphens.com', f.clean('example@valid-with-hyphens.com')) - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@.com') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@.com') self.assertEqual('local@domain.with.idn.xyz\xe4\xf6\xfc\xdfabc.part.com', f.clean('local@domain.with.idn.xyzäöüßabc.part.com')) def test_email_regexp_for_performance(self): @@ -525,7 +525,7 @@ class FieldsTests(SimpleTestCase): # if the security fix isn't in place. self.assertRaisesMessage( ValidationError, - "'Enter a valid e-mail address.'", + "'Enter a valid email address.'", f.clean, 'viewx3dtextx26qx3d@yahoo.comx26latlngx3d15854521645943074058' ) @@ -536,9 +536,9 @@ class FieldsTests(SimpleTestCase): self.assertEqual('', f.clean(None)) self.assertEqual('person@example.com', f.clean('person@example.com')) self.assertEqual('example@example.com', f.clean(' example@example.com \t \t ')) - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@bar') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@bar') def test_emailfield_3(self): f = EmailField(min_length=10, max_length=15) @@ -926,7 +926,7 @@ class FieldsTests(SimpleTestCase): f = ComboField(fields=[CharField(max_length=20), EmailField()]) self.assertEqual('test@example.com', f.clean('test@example.com')) self.assertRaisesMessage(ValidationError, "'Ensure this value has at most 20 characters (it has 28).'", f.clean, 'longemailaddress@example.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'not an e-mail') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'not an email') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) @@ -934,7 +934,7 @@ class FieldsTests(SimpleTestCase): f = ComboField(fields=[CharField(max_length=20), EmailField()], required=False) self.assertEqual('test@example.com', f.clean('test@example.com')) self.assertRaisesMessage(ValidationError, "'Ensure this value has at most 20 characters (it has 28).'", f.clean, 'longemailaddress@example.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'not an e-mail') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'not an email') self.assertEqual('', f.clean('')) self.assertEqual('', f.clean(None)) diff --git a/tests/regressiontests/test_client_regress/tests.py b/tests/regressiontests/test_client_regress/tests.py index 9deb8a4755..c741903c34 100644 --- a/tests/regressiontests/test_client_regress/tests.py +++ b/tests/regressiontests/test_client_regress/tests.py @@ -499,11 +499,11 @@ class AssertFormErrorTests(TestCase): try: self.assertFormError(response, 'form', 'email', 'Some error.') except AssertionError as e: - self.assertIn(str_prefix("The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [%(_)s'Enter a valid e-mail address.'])"), str(e)) + self.assertIn(str_prefix("The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [%(_)s'Enter a valid email address.'])"), str(e)) try: self.assertFormError(response, 'form', 'email', 'Some error.', msg_prefix='abc') except AssertionError as e: - self.assertIn(str_prefix("abc: The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [%(_)s'Enter a valid e-mail address.'])"), str(e)) + self.assertIn(str_prefix("abc: The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [%(_)s'Enter a valid email address.'])"), str(e)) def test_unknown_nonfield_error(self): """ diff --git a/tests/regressiontests/test_utils/tests.py b/tests/regressiontests/test_utils/tests.py index 468af77f44..12c639cee1 100644 --- a/tests/regressiontests/test_utils/tests.py +++ b/tests/regressiontests/test_utils/tests.py @@ -476,7 +476,7 @@ class AssertRaisesMsgTest(SimpleTestCase): class AssertFieldOutputTests(SimpleTestCase): def test_assert_field_output(self): - error_invalid = ['Enter a valid e-mail address.'] + error_invalid = ['Enter a valid email address.'] self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': error_invalid}) self.assertRaises(AssertionError, self.assertFieldOutput, EmailField, {'a@a.com': 'a@a.com'}, {'aaa': error_invalid + ['Another error']}) self.assertRaises(AssertionError, self.assertFieldOutput, EmailField, {'a@a.com': 'Wrong output'}, {'aaa': error_invalid}) From bb7da7844ff9f11286509c22a2549bbd4553d58d Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 26 Sep 2012 15:07:11 +0200 Subject: [PATCH 049/265] Fixed #18845 -- Do not swallow AttributeErrors when running commands --- django/core/management/__init__.py | 6 ++++-- tests/regressiontests/admin_scripts/tests.py | 21 ++++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index b40570efc9..c61ab2b663 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -103,10 +103,12 @@ def get_commands(): _commands = dict([(name, 'django.core') for name in find_commands(__path__[0])]) # Find the installed apps + from django.conf import settings try: - from django.conf import settings apps = settings.INSTALLED_APPS - except (AttributeError, ImproperlyConfigured): + except ImproperlyConfigured: + # Still useful for commands that do not require functional settings, + # like startproject or help 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 6028eac846..6f524bea29 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -982,13 +982,11 @@ class ManageMultipleSettings(AdminScriptTestCase): self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:NoArgsCommand") + class ManageSettingsWithImportError(AdminScriptTestCase): """Tests for manage.py when using the default settings.py file with an import error. Ticket #14130. """ - def setUp(self): - self.write_settings_with_import_error('settings.py') - def tearDown(self): self.remove_settings('settings.py') @@ -1004,12 +1002,27 @@ class ManageSettingsWithImportError(AdminScriptTestCase): settings_file.write('# The next line will cause an import error:\nimport foo42bar\n') def test_builtin_command(self): - "import error: manage.py builtin commands shows useful diagnostic info when settings with import errors is provided" + """ + import error: manage.py builtin commands shows useful diagnostic info + when settings with import errors is provided + """ + self.write_settings_with_import_error('settings.py') args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) self.assertOutput(err, "No module named foo42bar") + def test_builtin_command_with_attribute_error(self): + """ + manage.py builtin commands does not swallow attribute errors from bad settings (#18845) + """ + self.write_settings('settings.py', sdict={'BAD_VAR': 'INSTALLED_APPS.crash'}) + args = ['collectstatic', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "AttributeError: 'list' object has no attribute 'crash'") + + class ManageValidate(AdminScriptTestCase): def tearDown(self): self.remove_settings('settings.py') From b3ee80a0cf0e60876f03b797d2bdc69505dbdfcb Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 26 Sep 2012 21:10:17 +0200 Subject: [PATCH 050/265] Fixed parse_http_date docstring and moved related tests Refs #18675. --- django/utils/http.py | 3 +- .../conditional_processing/models.py | 29 ------------------- tests/regressiontests/utils/http.py | 29 +++++++++++++++++++ tests/regressiontests/utils/tests.py | 2 +- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/django/utils/http.py b/django/utils/http.py index d3c70f1209..1c3b0039b5 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -118,8 +118,7 @@ def parse_http_date(date): The three formats allowed by the RFC are accepted, even if only the first one is still in widespread use. - Returns an floating point number expressed in seconds since the epoch, in - UTC. + Returns an integer expressed in seconds since the epoch, in UTC. """ # emails.Util.parsedate does the job for RFC1123 dates; unfortunately # RFC2616 makes it mandatory to support RFC850 dates too. So we roll diff --git a/tests/regressiontests/conditional_processing/models.py b/tests/regressiontests/conditional_processing/models.py index d0838e153d..b47fdf6fb5 100644 --- a/tests/regressiontests/conditional_processing/models.py +++ b/tests/regressiontests/conditional_processing/models.py @@ -4,8 +4,6 @@ from __future__ import unicode_literals from datetime import datetime from django.test import TestCase -from django.utils import unittest -from django.utils.http import parse_etags, quote_etag, parse_http_date FULL_RESPONSE = 'Test conditional get response' @@ -129,30 +127,3 @@ class ConditionalGet(TestCase): self.client.defaults['HTTP_IF_NONE_MATCH'] = r'"\"' response = self.client.get('/condition/etag/') self.assertFullResponse(response, check_last_modified=False) - - -class ETagProcessing(unittest.TestCase): - def testParsing(self): - etags = parse_etags(r'"", "etag", "e\"t\"ag", "e\\tag", W/"weak"') - self.assertEqual(etags, ['', 'etag', 'e"t"ag', r'e\tag', 'weak']) - - def testQuoting(self): - quoted_etag = quote_etag(r'e\t"ag') - self.assertEqual(quoted_etag, r'"e\\t\"ag"') - - -class HttpDateProcessing(unittest.TestCase): - def testParsingRfc1123(self): - parsed = parse_http_date('Sun, 06 Nov 1994 08:49:37 GMT') - self.assertEqual(datetime.utcfromtimestamp(parsed), - datetime(1994, 11, 6, 8, 49, 37)) - - def testParsingRfc850(self): - parsed = parse_http_date('Sunday, 06-Nov-94 08:49:37 GMT') - self.assertEqual(datetime.utcfromtimestamp(parsed), - datetime(1994, 11, 6, 8, 49, 37)) - - def testParsingAsctime(self): - parsed = parse_http_date('Sun Nov 6 08:49:37 1994') - self.assertEqual(datetime.utcfromtimestamp(parsed), - datetime(1994, 11, 6, 8, 49, 37)) diff --git a/tests/regressiontests/utils/http.py b/tests/regressiontests/utils/http.py index f22e05496d..6d3bc025af 100644 --- a/tests/regressiontests/utils/http.py +++ b/tests/regressiontests/utils/http.py @@ -1,3 +1,4 @@ +from datetime import datetime import sys from django.http import HttpResponse, utils @@ -7,6 +8,7 @@ from django.utils import http from django.utils import six from django.utils import unittest + class TestUtilsHttp(unittest.TestCase): def test_same_origin_true(self): @@ -132,3 +134,30 @@ class TestUtilsHttp(unittest.TestCase): for n, b36 in [(0, '0'), (1, '1'), (42, '16'), (818469960, 'django')]: self.assertEqual(http.int_to_base36(n), b36) self.assertEqual(http.base36_to_int(b36), n) + + +class ETagProcessingTests(unittest.TestCase): + def testParsing(self): + etags = http.parse_etags(r'"", "etag", "e\"t\"ag", "e\\tag", W/"weak"') + self.assertEqual(etags, ['', 'etag', 'e"t"ag', r'e\tag', 'weak']) + + def testQuoting(self): + quoted_etag = http.quote_etag(r'e\t"ag') + self.assertEqual(quoted_etag, r'"e\\t\"ag"') + + +class HttpDateProcessingTests(unittest.TestCase): + def testParsingRfc1123(self): + parsed = http.parse_http_date('Sun, 06 Nov 1994 08:49:37 GMT') + self.assertEqual(datetime.utcfromtimestamp(parsed), + datetime(1994, 11, 6, 8, 49, 37)) + + def testParsingRfc850(self): + parsed = http.parse_http_date('Sunday, 06-Nov-94 08:49:37 GMT') + self.assertEqual(datetime.utcfromtimestamp(parsed), + datetime(1994, 11, 6, 8, 49, 37)) + + def testParsingAsctime(self): + parsed = http.parse_http_date('Sun Nov 6 08:49:37 1994') + self.assertEqual(datetime.utcfromtimestamp(parsed), + datetime(1994, 11, 6, 8, 49, 37)) diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index 47af07561a..11dd7c320e 100644 --- a/tests/regressiontests/utils/tests.py +++ b/tests/regressiontests/utils/tests.py @@ -17,7 +17,7 @@ from .encoding import TestEncodingUtils from .feedgenerator import FeedgeneratorTest from .functional import FunctionalTestCase from .html import TestUtilsHtml -from .http import TestUtilsHttp +from .http import TestUtilsHttp, ETagProcessingTests, HttpDateProcessingTests from .ipv6 import TestUtilsIPv6 from .jslex import JsToCForGettextTest, JsTokensTest from .module_loading import CustomLoader, DefaultLoader, EggLoader From 3cbe686af6b6a3f23a607b2eb7497c55d88e8345 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 26 Sep 2012 21:12:56 +0200 Subject: [PATCH 051/265] Fixed #18675 -- Fixed was_modified_since with floating-point mtime Thanks Simon Charette for the patch. --- django/views/static.py | 2 +- tests/regressiontests/views/tests/__init__.py | 2 +- tests/regressiontests/views/tests/static.py | 16 +++++++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/django/views/static.py b/django/views/static.py index 2ff22ce13f..7dd44c5772 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -138,7 +138,7 @@ def was_modified_since(header=None, mtime=0, size=0): header_len = matches.group(3) if header_len and int(header_len) != size: raise ValueError - if mtime > header_mtime: + if int(mtime) > header_mtime: raise ValueError except (AttributeError, ValueError, OverflowError): return True diff --git a/tests/regressiontests/views/tests/__init__.py b/tests/regressiontests/views/tests/__init__.py index 63db5da2a3..12d0c59014 100644 --- a/tests/regressiontests/views/tests/__init__.py +++ b/tests/regressiontests/views/tests/__init__.py @@ -7,4 +7,4 @@ from .defaults import DefaultsTests from .i18n import JsI18NTests, I18NTests, JsI18NTestsMultiPackage from .shortcuts import ShortcutTests from .specials import URLHandling -from .static import StaticHelperTest, StaticTests +from .static import StaticHelperTest, StaticUtilsTests, StaticTests diff --git a/tests/regressiontests/views/tests/static.py b/tests/regressiontests/views/tests/static.py index 9d87ade137..38cf38ce46 100644 --- a/tests/regressiontests/views/tests/static.py +++ b/tests/regressiontests/views/tests/static.py @@ -2,11 +2,14 @@ from __future__ import absolute_import import mimetypes from os import path +import unittest from django.conf import settings from django.conf.urls.static import static -from django.test import TestCase from django.http import HttpResponseNotModified +from django.test import TestCase +from django.utils.http import http_date +from django.views.static import was_modified_since from .. import urls from ..urls import media_dir @@ -105,3 +108,14 @@ class StaticHelperTest(StaticTests): def tearDown(self): super(StaticHelperTest, self).tearDown() urls.urlpatterns = self._old_views_urlpatterns + + +class StaticUtilsTests(unittest.TestCase): + def test_was_modified_since_fp(self): + """ + Test that a floating point mtime does not disturb was_modified_since. + (#18675) + """ + mtime = 1343416141.107817 + header = http_date(mtime) + self.assertFalse(was_modified_since(header, mtime)) From 50d573d2c0b3e17cbf1aa240b03b52e4ad0c32cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Thu, 27 Sep 2012 15:36:30 +0300 Subject: [PATCH 052/265] Fixed #18979 -- Avoid endless loop caused by "val in PermLookupDict" Fixed by defining __iter__ which raises TypeError. This was done to PermWrapper earlier. --- django/contrib/auth/context_processors.py | 5 +++ .../contrib/auth/tests/context_processors.py | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/django/contrib/auth/context_processors.py b/django/contrib/auth/context_processors.py index 1b6c2eedd0..77face01a7 100644 --- a/django/contrib/auth/context_processors.py +++ b/django/contrib/auth/context_processors.py @@ -11,6 +11,11 @@ class PermLookupDict(object): def __getitem__(self, perm_name): return self.user.has_perm("%s.%s" % (self.module_name, perm_name)) + def __iter__(self): + # To fix 'item in perms.someapp' and __getitem__ iteraction we need to + # define __iter__. See #18979 for details. + raise TypeError("PermLookupDict is not iterable.") + def __bool__(self): return self.user.has_module_perms(self.module_name) __nonzero__ = __bool__ # Python 2 diff --git a/django/contrib/auth/tests/context_processors.py b/django/contrib/auth/tests/context_processors.py index 4e914133d0..8d87e0ae15 100644 --- a/django/contrib/auth/tests/context_processors.py +++ b/django/contrib/auth/tests/context_processors.py @@ -3,11 +3,55 @@ import os from django.conf import global_settings from django.contrib.auth import authenticate from django.contrib.auth.tests.utils import skipIfCustomUser +from django.contrib.auth.context_processors import PermWrapper, PermLookupDict from django.db.models import Q from django.test import TestCase from django.test.utils import override_settings +class MockUser(object): + def has_module_perm(self, perm): + if perm == 'mockapp.someapp': + return True + return False + + def has_perm(self, perm): + if perm == 'someperm': + return True + return False + + +class PermWrapperTests(TestCase): + """ + Test some details of the PermWrapper implementation. + """ + class EQLimiterObject(object): + """ + This object makes sure __eq__ will not be called endlessly. + """ + def __init__(self): + self.eq_calls = 0 + + def __eq__(self, other): + if self.eq_calls > 0: + return True + self.eq_calls += 1 + return False + + def test_permwrapper_in(self): + """ + Test that 'something' in PermWrapper doesn't end up in endless loop. + """ + perms = PermWrapper(MockUser()) + with self.assertRaises(TypeError): + self.EQLimiterObject() in perms + + def test_permlookupdict_in(self): + pldict = PermLookupDict(MockUser(), 'mockapp') + with self.assertRaises(TypeError): + self.EQLimiterObject() in pldict + + @skipIfCustomUser @override_settings( TEMPLATE_DIRS=( From b946db5241b924c72c1079ce30d9b368e2b82f07 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Thu, 27 Sep 2012 15:06:58 +0200 Subject: [PATCH 053/265] Fixed #15695 -- Added `ResolverMatch` to the request object. --- django/core/handlers/base.py | 7 ++++--- docs/ref/request-response.txt | 11 +++++++++++ docs/releases/1.5.txt | 3 +++ .../urlpatterns_reverse/namespace_urls.py | 1 + tests/regressiontests/urlpatterns_reverse/tests.py | 5 +++++ tests/regressiontests/urlpatterns_reverse/views.py | 5 +++++ 6 files changed, 29 insertions(+), 3 deletions(-) diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 791382bac0..39d109405b 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -95,14 +95,15 @@ class BaseHandler(object): break if response is None: - if hasattr(request, "urlconf"): + if hasattr(request, 'urlconf'): # Reset url resolver with a custom urlconf. urlconf = request.urlconf urlresolvers.set_urlconf(urlconf) resolver = urlresolvers.RegexURLResolver(r'^/', urlconf) - callback, callback_args, callback_kwargs = resolver.resolve( - request.path_info) + resolver_match = resolver.resolve(request.path_info) + callback, callback_args, callback_kwargs = resolver_match + request.resolver_match = resolver_match # Apply view middleware for middleware_method in self._view_middleware: diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 21e99de10d..50301b8567 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -192,6 +192,17 @@ All attributes should be considered read-only, unless stated otherwise below. URLconf for the current request, overriding the :setting:`ROOT_URLCONF` setting. See :ref:`how-django-processes-a-request` for details. +.. attribute:: HttpRequest.resolver_match + + .. versionadded:: 1.5 + + An instance of :class:`~django.core.urlresolvers.ResolverMatch` representing + the resolved url. This attribute is only set after url resolving took place, + which means it's available in all views but not in middleware methods which + are executed before url resolving takes place (like ``process_request``, you + can use ``process_view`` instead). + + Methods ------- diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 26b6ad1bfa..f1fcd923b1 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -127,6 +127,9 @@ Django 1.5 also includes several smaller improvements worth noting: configuration duplication. More information can be found in the :func:`~django.contrib.auth.decorators.login_required` documentation. +* An instance of :class:`~django.core.urlresolvers.ResolverMatch` is stored on + the request as ``resolver_match``. + Backwards incompatible changes in 1.5 ===================================== diff --git a/tests/regressiontests/urlpatterns_reverse/namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py index fa892a4346..ab2e77af24 100644 --- a/tests/regressiontests/urlpatterns_reverse/namespace_urls.py +++ b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py @@ -28,6 +28,7 @@ otherobj2 = URLObject('nodefault', 'other-ns2') urlpatterns = patterns('regressiontests.urlpatterns_reverse.views', url(r'^normal/$', 'empty_view', name='normal-view'), url(r'^normal/(?P\d+)/(?P\d+)/$', 'empty_view', name='normal-view'), + url(r'^resolver_match/$', 'pass_resolver_match_view', name='test-resolver-match'), url(r'^\+\\\$\*/$', 'empty_view', name='special-view'), diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py index 0ea5ffe380..234897d267 100644 --- a/tests/regressiontests/urlpatterns_reverse/tests.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -512,6 +512,11 @@ class ResolverMatchTests(TestCase): self.assertEqual(match[1], args) self.assertEqual(match[2], kwargs) + def test_resolver_match_on_request(self): + response = self.client.get('/resolver_match/') + resolver_match = response.resolver_match + self.assertEqual(resolver_match.url_name, 'test-resolver-match') + class ErroneousViewTests(TestCase): urls = 'regressiontests.urlpatterns_reverse.erroneous_urls' diff --git a/tests/regressiontests/urlpatterns_reverse/views.py b/tests/regressiontests/urlpatterns_reverse/views.py index f631acf3ec..88d169a118 100644 --- a/tests/regressiontests/urlpatterns_reverse/views.py +++ b/tests/regressiontests/urlpatterns_reverse/views.py @@ -19,6 +19,11 @@ def defaults_view(request, arg1, arg2): def erroneous_view(request): import non_existent +def pass_resolver_match_view(request, *args, **kwargs): + response = HttpResponse('') + response.resolver_match = request.resolver_match + return response + uncallable = "Can I be a view? Pleeeease?" class ViewClass(object): From c9c9a5642512155e9693bf5fa6221fd26adeccca Mon Sep 17 00:00:00 2001 From: Eduardo Cereto Carvalho Date: Thu, 27 Sep 2012 15:55:27 +0200 Subject: [PATCH 054/265] Added complementary regression test for commit c1729510 Also fixed #18660. --- tests/regressiontests/admin_inlines/models.py | 3 +++ tests/regressiontests/admin_inlines/tests.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/tests/regressiontests/admin_inlines/models.py b/tests/regressiontests/admin_inlines/models.py index 5b703a7481..b004d5f85a 100644 --- a/tests/regressiontests/admin_inlines/models.py +++ b/tests/regressiontests/admin_inlines/models.py @@ -57,6 +57,9 @@ class Inner(models.Model): holder = models.ForeignKey(Holder) readonly = models.CharField("Inner readonly label", max_length=1) + def get_absolute_url(self): + return '/inner/' + class Holder2(models.Model): dummy = models.IntegerField() diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py index 57f45ab0ff..5bb6077bff 100644 --- a/tests/regressiontests/admin_inlines/tests.py +++ b/tests/regressiontests/admin_inlines/tests.py @@ -148,6 +148,18 @@ class TestInline(TestCase): '', html=True) + @override_settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True) + def test_localize_pk_shortcut(self): + """ + Ensure that the "View on Site" link is correct for locales that use + thousand separators + """ + holder = Holder.objects.create(pk=123456789, dummy=42) + inner = Inner.objects.create(pk=987654321, holder=holder, dummy=42, readonly='') + response = self.client.get('/admin/admin_inlines/holder/%i/' % holder.id) + inner_shortcut = 'r/%s/%s/'%(ContentType.objects.get_for_model(inner).pk, inner.pk) + self.assertContains(response, inner_shortcut) + def test_custom_pk_shortcut(self): """ Ensure that the "View on Site" link is correct for models with a From 01362745ba72286309ff1955219a5ffc32c760b0 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Thu, 27 Sep 2012 10:24:34 -0700 Subject: [PATCH 055/265] Fixed a small oversight in auth tests Thanks to Vinicius Ruan Cainelli for the catch closes #392 --- django/contrib/auth/tests/basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py index ed1d0674fc..bc7344f753 100644 --- a/django/contrib/auth/tests/basic.py +++ b/django/contrib/auth/tests/basic.py @@ -38,8 +38,8 @@ class BasicTestCase(TestCase): self.assertFalse(u.is_superuser) # Check API-based user creation with no password - User.objects.create_user('testuser2', 'test2@example.com') - self.assertFalse(u.has_usable_password()) + u2 = User.objects.create_user('testuser2', 'test2@example.com') + self.assertFalse(u2.has_usable_password()) def test_user_no_email(self): "Check that users can be created without an email" From 373932fa6b9137a7e760d81dc66d49fc10ff2942 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sun, 23 Sep 2012 22:48:13 -0700 Subject: [PATCH 056/265] fixed #10809 -- add a mod_wsgi authentication handler Thanks to baumer1122 for the suggestion and initial patch and David Fischer for the contributions and long term patch maintenance and docs. --- django/contrib/auth/handlers/modwsgi.py | 43 ++++++++ django/contrib/auth/tests/__init__.py | 1 + django/contrib/auth/tests/handlers.py | 45 ++++++++ docs/howto/apache-auth.txt | 45 -------- docs/howto/deployment/wsgi/apache-auth.txt | 122 +++++++++++++++++++++ docs/howto/deployment/wsgi/index.txt | 1 + docs/howto/deployment/wsgi/modwsgi.txt | 7 ++ docs/howto/index.txt | 1 - docs/releases/1.5.txt | 3 + 9 files changed, 222 insertions(+), 46 deletions(-) create mode 100644 django/contrib/auth/handlers/modwsgi.py create mode 100644 django/contrib/auth/tests/handlers.py delete mode 100644 docs/howto/apache-auth.txt create mode 100644 docs/howto/deployment/wsgi/apache-auth.txt diff --git a/django/contrib/auth/handlers/modwsgi.py b/django/contrib/auth/handlers/modwsgi.py new file mode 100644 index 0000000000..0e543ef368 --- /dev/null +++ b/django/contrib/auth/handlers/modwsgi.py @@ -0,0 +1,43 @@ +from django.contrib.auth.models import User +from django import db +from django.utils.encoding import force_bytes + + +def check_password(environ, username, password): + """ + Authenticates against Django's auth database + + mod_wsgi docs specify None, True, False as return value depending + on whether the user exists and authenticates. + """ + + # db connection state is managed similarly to the wsgi handler + # as mod_wsgi may call these functions outside of a request/response cycle + db.reset_queries() + + try: + try: + user = User.objects.get(username=username, is_active=True) + except User.DoesNotExist: + return None + return user.check_password(password) + finally: + db.close_connection() + + +def groups_for_user(environ, username): + """ + Authorizes a user based on groups + """ + + db.reset_queries() + + try: + try: + user = User.objects.get(username=username, is_active=True) + except User.DoesNotExist: + return [] + + return [force_bytes(group.name) for group in user.groups.all()] + finally: + db.close_connection() diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 094a595238..b3007ea484 100644 --- a/django/contrib/auth/tests/__init__.py +++ b/django/contrib/auth/tests/__init__.py @@ -7,6 +7,7 @@ from django.contrib.auth.tests.forms import * from django.contrib.auth.tests.remote_user import * from django.contrib.auth.tests.management import * from django.contrib.auth.tests.models import * +from django.contrib.auth.tests.handlers import * from django.contrib.auth.tests.hashers import * from django.contrib.auth.tests.signals import * from django.contrib.auth.tests.tokens import * diff --git a/django/contrib/auth/tests/handlers.py b/django/contrib/auth/tests/handlers.py new file mode 100644 index 0000000000..f061042ce3 --- /dev/null +++ b/django/contrib/auth/tests/handlers.py @@ -0,0 +1,45 @@ +from __future__ import unicode_literals + +from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user +from django.contrib.auth.models import User, Group +from django.test import TestCase + + +class ModWsgiHandlerTestCase(TestCase): + """ + Tests for the mod_wsgi authentication handler + """ + + def setUp(self): + user1 = User.objects.create_user('test', 'test@example.com', 'test') + User.objects.create_user('test1', 'test1@example.com', 'test1') + + group = Group.objects.create(name='test_group') + user1.groups.add(group) + + def test_check_password(self): + """ + Verify that check_password returns the correct values as per + http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider + """ + + # User not in database + self.assertTrue(check_password({}, 'unknown', '') is None) + + # Valid user with correct password + self.assertTrue(check_password({}, 'test', 'test')) + + # Valid user with incorrect password + self.assertFalse(check_password({}, 'test', 'incorrect')) + + def test_groups_for_user(self): + """ + Check that groups_for_user returns correct values as per + http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Group_Authorisation + """ + + # User not in database + self.assertEqual(groups_for_user({}, 'unknown'), []) + + self.assertEqual(groups_for_user({}, 'test'), [b'test_group']) + self.assertEqual(groups_for_user({}, 'test1'), []) diff --git a/docs/howto/apache-auth.txt b/docs/howto/apache-auth.txt deleted file mode 100644 index 719fbc1769..0000000000 --- a/docs/howto/apache-auth.txt +++ /dev/null @@ -1,45 +0,0 @@ -========================================================= -Authenticating against Django's user database from Apache -========================================================= - -Since keeping multiple authentication databases in sync is a common problem when -dealing with Apache, you can configuring Apache to authenticate against Django's -:doc:`authentication system
    ` directly. This requires Apache -version >= 2.2 and mod_wsgi >= 2.0. For example, you could: - -* Serve static/media files directly from Apache only to authenticated users. - -* Authenticate access to a Subversion_ repository against Django users with - a certain permission. - -* Allow certain users to connect to a WebDAV share created with mod_dav_. - -.. _Subversion: http://subversion.tigris.org/ -.. _mod_dav: http://httpd.apache.org/docs/2.2/mod/mod_dav.html - -Configuring Apache -================== - -To check against Django's authorization database from a Apache configuration -file, you'll need to set 'wsgi' as the value of ``AuthBasicProvider`` or -``AuthDigestProvider`` directive and then use the ``WSGIAuthUserScript`` -directive to set the path to your authentification script: - -.. code-block:: apache - - - AuthType Basic - AuthName "example.com" - AuthBasicProvider wsgi - WSGIAuthUserScript /usr/local/wsgi/scripts/auth.wsgi - Require valid-user - - -Your auth.wsgi script will have to implement either a -``check_password(environ, user, password)`` function (for ``AuthBasicProvider``) -or a ``get_realm_hash(environ, user, realm)`` function (for ``AuthDigestProvider``). - -See the `mod_wsgi documentation`_ for more details about the implementation -of such a solution. - -.. _mod_wsgi documentation: http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider diff --git a/docs/howto/deployment/wsgi/apache-auth.txt b/docs/howto/deployment/wsgi/apache-auth.txt new file mode 100644 index 0000000000..36e3d0233c --- /dev/null +++ b/docs/howto/deployment/wsgi/apache-auth.txt @@ -0,0 +1,122 @@ +========================================================= +Authenticating against Django's user database from Apache +========================================================= + +Since keeping multiple authentication databases in sync is a common problem when +dealing with Apache, you can configure Apache to authenticate against Django's +:doc:`authentication system
    ` directly. This requires Apache +version >= 2.2 and mod_wsgi >= 2.0. For example, you could: + +* Serve static/media files directly from Apache only to authenticated users. + +* Authenticate access to a Subversion_ repository against Django users with + a certain permission. + +* Allow certain users to connect to a WebDAV share created with mod_dav_. + +.. _Subversion: http://subversion.tigris.org/ +.. _mod_dav: http://httpd.apache.org/docs/2.2/mod/mod_dav.html + +Authentication with mod_wsgi +============================ + +Make sure that mod_wsgi is installed and activated and that you have +followed the steps to setup +:doc:`Apache with mod_wsgi ` + +Next, edit your Apache configuration to add a location that you want +only authenticated users to be able to view: + +.. code-block:: apache + + WSGIScriptAlias / /path/to/mysite/config/mysite.wsgi + + WSGIProcessGroup %{GLOBAL} + WSGIApplicationGroup django + + + AuthType Basic + AuthName "Top Secret" + Require valid-user + AuthBasicProvider wsgi + WSGIAuthUserScript /path/to/mysite/config/mysite.wsgi + + +The ``WSGIAuthUserScript`` directive tells mod_wsgi to execute the +``check_password`` function in specified wsgi script, passing the user name and +password that it receives from the prompt. In this example, the +``WSGIAuthUserScript`` is the same as the ``WSGIScriptAlias`` that defines your +application :doc:`that is created by django-admin.py startproject +`. + +.. admonition:: Using Apache 2.2 with authentication + + Make sure that ``mod_auth_basic`` and ``mod_authz_user`` are loaded. + + These might be compiled statically into Apache, or you might need to use + LoadModule to load them dynamically in your ``httpd.conf``: + + .. code-block:: apache + + LoadModule auth_basic_module modules/mod_auth_basic.so + LoadModule authz_user_module modules/mod_authz_user.so + +Finally, edit your WSGI script ``mysite.wsgi`` to tie Apache's +authentication to your site's authentication mechanisms by importing the +check_user function: + +.. code-block:: python + + import os + import sys + + os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings' + + from django.contrib.auth.handlers.modwsgi import check_user + + from django.core.handlers.wsgi import WSGIHandler + application = WSGIHandler() + + +Requests beginning with ``/secret/`` will now require a user to authenticate. + +The mod_wsgi `access control mechanisms documentation`_ provides additional +details and information about alternative methods of authentication. + +.. _access control mechanisms documentation: http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms + +Authorization with mod_wsgi and Django groups +--------------------------------------------- + +mod_wsgi also provides functionality to restrict a particular location to +members of a group. + +In this case, the Apache configuration should look like this: + +.. code-block:: apache + + WSGIScriptAlias / /path/to/mysite/config/mysite.wsgi + + WSGIProcessGroup %{GLOBAL} + WSGIApplicationGroup django + + + AuthType Basic + AuthName "Top Secret" + AuthBasicProvider wsgi + WSGIAuthUserScript /path/to/mysite/config/mysite.wsgi + WSGIAuthGroupScript /path/to/mysite/config/mysite.wsgi + Require group secret-agents + Require valid-user + + +To support the ``WSGIAuthGroupScript`` directive, the same WSGI script +``mysite.wsgi`` must also import the ``groups_for_user`` function which +returns a list groups the given user belongs to. + +.. code-block:: python + + from django.contrib.auth.handlers.modwsgi import check_user, groups_for_user + +Requests for ``/secret/`` will now also require user to be a member of the +"secret-agents" group. diff --git a/docs/howto/deployment/wsgi/index.txt b/docs/howto/deployment/wsgi/index.txt index ecb302cee3..769d406b1b 100644 --- a/docs/howto/deployment/wsgi/index.txt +++ b/docs/howto/deployment/wsgi/index.txt @@ -16,6 +16,7 @@ documentation for the following WSGI servers: :maxdepth: 1 modwsgi + apache-auth gunicorn uwsgi diff --git a/docs/howto/deployment/wsgi/modwsgi.txt b/docs/howto/deployment/wsgi/modwsgi.txt index 8398f12eb7..b525255dbd 100644 --- a/docs/howto/deployment/wsgi/modwsgi.txt +++ b/docs/howto/deployment/wsgi/modwsgi.txt @@ -177,6 +177,13 @@ other approaches: 3. Copy the admin static files so that they live within your Apache document root. +Authenticating against Django's user database from Apache +========================================================= + +Django provides a handler to allow Apache to authenticate users directly +against Django's authentication backends. See the :doc:`mod_wsgi authentication +documentation `. + If you get a UnicodeEncodeError =============================== diff --git a/docs/howto/index.txt b/docs/howto/index.txt index 737ee71da4..d39222be26 100644 --- a/docs/howto/index.txt +++ b/docs/howto/index.txt @@ -9,7 +9,6 @@ you quickly accomplish common tasks. .. toctree:: :maxdepth: 1 - apache-auth auth-remote-user custom-management-commands custom-model-fields diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index df8d89c185..fddd03d421 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -146,6 +146,9 @@ Django 1.5 also includes several smaller improvements worth noting: configuration duplication. More information can be found in the :func:`~django.contrib.auth.decorators.login_required` documentation. +* Django now provides a mod_wsgi :doc:`auth handler + ` + Backwards incompatible changes in 1.5 ===================================== From 1df58968a4b2247aff91db40f1325f079ba3cdce Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Thu, 27 Sep 2012 13:19:04 -0700 Subject: [PATCH 057/265] Added a note regarding interaction between GitHub and Trac Plugin --- docs/internals/contributing/committing-code.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/internals/contributing/committing-code.txt b/docs/internals/contributing/committing-code.txt index d36bc78fe1..67dda02f8b 100644 --- a/docs/internals/contributing/committing-code.txt +++ b/docs/internals/contributing/committing-code.txt @@ -187,7 +187,15 @@ Django's Git repository: For the curious, we're using a `Trac plugin`_ for this. - .. _Trac plugin: https://github.com/aaugustin/trac-github +.. note:: + + Note that the Trac integration doesn't know anything about pull requests. + So if you try to close a pull request with the phrase "closes #400" in your + commit message, GitHub will close the pull request, but the Trac plugin + will also close the same numbered ticket in Trac. + + +.. _Trac plugin: https://github.com/aaugustin/trac-github * If your commit references a ticket in the Django `ticket tracker`_ but does *not* close the ticket, include the phrase "Refs #xxxxx", where "xxxxx" From 84fa9099c6a760104d69a87d3cc2cba192f1ebf2 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 27 Sep 2012 17:33:52 -0400 Subject: [PATCH 058/265] Fixed two broken links introduced in recent commits. --- docs/index.txt | 1 - docs/topics/auth.txt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index ce84f79d43..5055edf7e7 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -181,7 +181,6 @@ testing of Django applications: :doc:`Overview ` | :doc:`WSGI servers ` | :doc:`FastCGI/SCGI/AJP ` | - :doc:`Apache authentication ` | :doc:`Handling static files ` | :doc:`Tracking code errors by email ` diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index a767b5a93f..1d320df9c1 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -1921,7 +1921,7 @@ custom profile fields. Custom users and the built-in auth forms ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -As you may expect, built-in Django's :ref:`forms <_built-in-auth-forms>` +As you may expect, built-in Django's :ref:`forms ` and :ref:`views ` make certain assumptions about the user model that they are working with. From d08096317ab598b7a350d61b9b8396a3be7b8c79 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 27 Sep 2012 17:17:21 -0400 Subject: [PATCH 059/265] Fixed #11460 - Added a FAQ regarding missing rows in the admin. --- docs/faq/admin.txt | 13 ++++++++++++- docs/ref/contrib/admin/index.txt | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/faq/admin.txt b/docs/faq/admin.txt index ea6aa2e74e..872ad254c9 100644 --- a/docs/faq/admin.txt +++ b/docs/faq/admin.txt @@ -68,6 +68,18 @@ For example, if your ``list_filter`` includes ``sites``, and there's only one site in your database, it won't display a "Site" filter. In that case, filtering by site would be meaningless. +Some objects aren't appearing in the admin. +------------------------------------------- + +Inconsistent row counts may be caused by missing foreign key values or a +foreign key field incorrectly set to :attr:`null=False +`. If you have a record with a +:class:`~django.db.models.ForeignKey` pointing to a non-existent object and +that foreign key is included is +:attr:`~django.contrib.admin.ModelAdmin.list_display`, the record will not be +shown in the admin changelist because the Django model is declaring an +integrity constraint that is not implemented at the database level. + How can I customize the functionality of the admin interface? ------------------------------------------------------------- @@ -104,4 +116,3 @@ 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/ - diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 2aabc55908..06751df879 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -60,6 +60,8 @@ Other topics For information about serving the static files (images, JavaScript, and CSS) associated with the admin in production, see :ref:`serving-files`. + Having problems? Try :doc:`/faq/admin`. + ``ModelAdmin`` objects ====================== From e44bedd13f974321c0c5deece9f5ac3da02e64c0 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 27 Sep 2012 20:25:31 -0400 Subject: [PATCH 060/265] Fixed a typo in runserver docs --- docs/ref/django-admin.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 467e32c86d..93e8fd9856 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -635,7 +635,7 @@ A hostname containing ASCII-only characters can also be used. If the :doc:`staticfiles` contrib app is enabled (default in new projects) the :djadmin:`runserver` command will be overriden -with an own :djadmin:`runserver` command. +with its own :ref:`runserver` command. .. django-admin-option:: --noreload From 751a7d0c32746dc6774f1b561db523b25365148a Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 27 Sep 2012 20:16:38 -0600 Subject: [PATCH 061/265] Fixed #18518 -- Add warning re mod_wsgi and wsgi.py environ handling. --- django/conf/project_template/project_name/wsgi.py | 4 ++++ docs/howto/deployment/wsgi/modwsgi.txt | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/django/conf/project_template/project_name/wsgi.py b/django/conf/project_template/project_name/wsgi.py index b083a0e699..f768265b23 100644 --- a/django/conf/project_template/project_name/wsgi.py +++ b/django/conf/project_template/project_name/wsgi.py @@ -15,6 +15,10 @@ framework. """ import os +# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks +# if running multiple sites in the same mod_wsgi process. To fix this, use +# mod_wsgi daemon mode with each site in its own daemon process, or use +# os.environ["DJANGO_SETTINGS_MODULE"] = "{{ project_name }}.settings" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") # This application object is used by any WSGI server configured to use this diff --git a/docs/howto/deployment/wsgi/modwsgi.txt b/docs/howto/deployment/wsgi/modwsgi.txt index b525255dbd..01399aa5a6 100644 --- a/docs/howto/deployment/wsgi/modwsgi.txt +++ b/docs/howto/deployment/wsgi/modwsgi.txt @@ -56,6 +56,15 @@ for you; otherwise, you'll need to create it. See the :doc:`WSGI overview documentation` for the default contents you should put in this file, and what else you can add to it. +.. warning:: + + If multiple Django sites are run in a single mod_wsgi process, all of them + will use the settings of whichever one happens to run first. This can be + solved with a minor edit to ``wsgi.py`` (see comment in the file for + details), or by :ref:`using mod_wsgi daemon mode` and ensuring + that each site runs in its own daemon process. + + Using a virtualenv ================== @@ -71,6 +80,8 @@ Make sure you give the correct path to your virtualenv, and replace .. _virtualenv: http://www.virtualenv.org +.. _daemon-mode: + Using mod_wsgi daemon mode ========================== From b8244c654c85034bd4209a93a7802eb306c32e2b Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Thu, 27 Sep 2012 20:34:45 -0700 Subject: [PATCH 062/265] Fixed #18881 -- Made the context option in {% trans %} and {% blocktrans %} accept literals wrapped in single quotes. Thanks to lanyjie for the report. --- django/utils/translation/trans_real.py | 4 ++-- tests/regressiontests/i18n/commands/extraction.py | 15 +++++++++++++++ .../i18n/commands/templates/test.html | 5 +++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 9fd33a7ea8..6911c2c723 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -437,8 +437,8 @@ def blankout(src, char): return dot_re.sub(char, src) context_re = re.compile(r"""^\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?'))\s*""") -inline_re = re.compile(r"""^\s*trans\s+((?:"[^"]*?")|(?:'[^']*?'))(\s+.*context\s+(?:"[^"]*?")|(?:'[^']*?'))?\s*""") -block_re = re.compile(r"""^\s*blocktrans(\s+.*context\s+(?:"[^"]*?")|(?:'[^']*?'))?(?:\s+|$)""") +inline_re = re.compile(r"""^\s*trans\s+((?:"[^"]*?")|(?:'[^']*?'))(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?\s*""") +block_re = re.compile(r"""^\s*blocktrans(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?(?:\s+|$)""") endblock_re = re.compile(r"""^\s*endblocktrans$""") plural_re = re.compile(r"""^\s*plural$""") constant_re = re.compile(r"""_\(((?:".*?")|(?:'.*?'))\)""") diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index 29d9e277ff..ca2c3cc026 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -156,6 +156,21 @@ class BasicExtractorTests(ExtractorTests): self.assertTrue('msgctxt "Special blocktrans context #4"' in po_contents) self.assertTrue("Translatable literal #8d" in po_contents) + def test_context_in_single_quotes(self): + os.chdir(self.test_dir) + management.call_command('makemessages', locale=LOCALE, verbosity=0) + self.assertTrue(os.path.exists(self.PO_FILE)) + with open(self.PO_FILE, 'r') as fp: + po_contents = fp.read() + # {% trans %} + self.assertTrue('msgctxt "Context wrapped in double quotes"' in po_contents) + self.assertTrue('msgctxt "Context wrapped in single quotes"' in po_contents) + + # {% blocktrans %} + self.assertTrue('msgctxt "Special blocktrans context wrapped in double quotes"' in po_contents) + self.assertTrue('msgctxt "Special blocktrans context wrapped in single quotes"' in po_contents) + + class JavascriptExtractorTests(ExtractorTests): PO_FILE='locale/%s/LC_MESSAGES/djangojs.po' % LOCALE diff --git a/tests/regressiontests/i18n/commands/templates/test.html b/tests/regressiontests/i18n/commands/templates/test.html index 5789346984..e7d7f3ca53 100644 --- a/tests/regressiontests/i18n/commands/templates/test.html +++ b/tests/regressiontests/i18n/commands/templates/test.html @@ -77,3 +77,8 @@ continued here.{% endcomment %} {% trans "Shouldn't double escape this sequence %% either" context "ctx1" %} {% trans "Looks like a str fmt spec %s but shouldn't be interpreted as such" %} {% trans "Looks like a str fmt spec % o but shouldn't be interpreted as such" %} + +{% trans "Translatable literal with context wrapped in single quotes" context 'Context wrapped in single quotes' as var %} +{% trans "Translatable literal with context wrapped in double quotes" context "Context wrapped in double quotes" as var %} +{% blocktrans context 'Special blocktrans context wrapped in single quotes' %}Translatable literal with context wrapped in single quotes{% endblocktrans %} +{% blocktrans context "Special blocktrans context wrapped in double quotes" %}Translatable literal with context wrapped in double quotes{% endblocktrans %} \ No newline at end of file From 07ffe7814394815d116cdbe814ec6a523b345df1 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 28 Sep 2012 14:35:26 +0200 Subject: [PATCH 063/265] Used get_current_site in comments feed class --- django/contrib/comments/feeds.py | 21 ++++++++----------- .../comment_tests/tests/feed_tests.py | 21 +++++++++++++++++++ tests/regressiontests/comment_tests/urls.py | 1 + 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/django/contrib/comments/feeds.py b/django/contrib/comments/feeds.py index db4b2f818e..2e0d4c3dc5 100644 --- a/django/contrib/comments/feeds.py +++ b/django/contrib/comments/feeds.py @@ -1,30 +1,27 @@ -from django.conf import settings from django.contrib.syndication.views import Feed -from django.contrib.sites.models import Site +from django.contrib.sites.models import get_current_site from django.contrib import comments from django.utils.translation import ugettext as _ class LatestCommentFeed(Feed): """Feed of latest comments on the current site.""" + def __call__(self, request, *args, **kwargs): + self.site = get_current_site(request) + return super(LatestCommentFeed, self).__call__(request, *args, **kwargs) + def title(self): - if not hasattr(self, '_site'): - self._site = Site.objects.get_current() - return _("%(site_name)s comments") % dict(site_name=self._site.name) + return _("%(site_name)s comments") % dict(site_name=self.site.name) def link(self): - if not hasattr(self, '_site'): - self._site = Site.objects.get_current() - return "http://%s/" % (self._site.domain) + return "http://%s/" % (self.site.domain) def description(self): - if not hasattr(self, '_site'): - self._site = Site.objects.get_current() - return _("Latest comments on %(site_name)s") % dict(site_name=self._site.name) + return _("Latest comments on %(site_name)s") % dict(site_name=self.site.name) def items(self): qs = comments.get_model().objects.filter( - site__pk = settings.SITE_ID, + site__pk = self.site.pk, is_public = True, is_removed = False, ) diff --git a/tests/regressiontests/comment_tests/tests/feed_tests.py b/tests/regressiontests/comment_tests/tests/feed_tests.py index b15ec0c7b8..1ec316eab8 100644 --- a/tests/regressiontests/comment_tests/tests/feed_tests.py +++ b/tests/regressiontests/comment_tests/tests/feed_tests.py @@ -1,12 +1,32 @@ from __future__ import absolute_import +from django.conf import settings +from django.contrib.comments.models import Comment +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site + from . import CommentTestCase +from ..models import Article class CommentFeedTests(CommentTestCase): urls = 'regressiontests.comment_tests.urls' feed_url = '/rss/comments/' + def setUp(self): + site_2 = Site.objects.create(id=settings.SITE_ID+1, + domain="example2.com", name="example2.com") + # A comment for another site + c5 = Comment.objects.create( + content_type = ContentType.objects.get_for_model(Article), + object_pk = "1", + user_name = "Joe Somebody", + user_email = "jsomebody@example.com", + user_url = "http://example.com/~joe/", + comment = "A comment for the second site.", + site = site_2, + ) + def test_feed(self): response = self.client.get(self.feed_url) self.assertEqual(response.status_code, 200) @@ -15,3 +35,4 @@ class CommentFeedTests(CommentTestCase): self.assertContains(response, 'example.com comments') self.assertContains(response, 'http://example.com/') self.assertContains(response, '') + self.assertNotContains(response, "A comment for the second site.") diff --git a/tests/regressiontests/comment_tests/urls.py b/tests/regressiontests/comment_tests/urls.py index b2f676786f..0a7e8b5fdf 100644 --- a/tests/regressiontests/comment_tests/urls.py +++ b/tests/regressiontests/comment_tests/urls.py @@ -15,6 +15,7 @@ urlpatterns = patterns('', url(r'^flag/(\d+)/$', views.custom_flag_comment), url(r'^delete/(\d+)/$', views.custom_delete_comment), url(r'^approve/(\d+)/$', views.custom_approve_comment), + url(r'^cr/(\d+)/(.+)/$', 'django.contrib.contenttypes.views.shortcut', name='comments-url-redirect'), ) urlpatterns += patterns('', From 3fcca0e94721a734882389ce473f522d293907e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Thu, 20 Sep 2012 03:18:19 +0300 Subject: [PATCH 064/265] Added a way to check if a signal has listeners --- django/dispatch/dispatcher.py | 3 +++ .../regressiontests/dispatch/tests/test_dispatcher.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/django/dispatch/dispatcher.py b/django/dispatch/dispatcher.py index ad7302176e..8d26e58bf4 100644 --- a/django/dispatch/dispatcher.py +++ b/django/dispatch/dispatcher.py @@ -141,6 +141,9 @@ class Signal(object): del self.receivers[index] break + def has_listeners(self, sender=None): + return bool(self._live_receivers(_make_id(sender))) + def send(self, sender, **named): """ Send signal from sender to all connected receivers. diff --git a/tests/regressiontests/dispatch/tests/test_dispatcher.py b/tests/regressiontests/dispatch/tests/test_dispatcher.py index 5f7094d5fa..4e4669d34c 100644 --- a/tests/regressiontests/dispatch/tests/test_dispatcher.py +++ b/tests/regressiontests/dispatch/tests/test_dispatcher.py @@ -126,6 +126,17 @@ class DispatcherTests(unittest.TestCase): a_signal.disconnect(receiver_3) self._testIsClean(a_signal) + def test_has_listeners(self): + self.assertIs(a_signal.has_listeners(), False) + self.assertIs(a_signal.has_listeners(sender=object()), False) + receiver_1 = Callable() + a_signal.connect(receiver_1) + self.assertIs(a_signal.has_listeners(), True) + self.assertIs(a_signal.has_listeners(sender=object()), True) + a_signal.disconnect(receiver_1) + self.assertIs(a_signal.has_listeners(), False) + self.assertIs(a_signal.has_listeners(sender=object()), False) + class ReceiverTestCase(unittest.TestCase): """ From 1cd6e04cd4f768bcd4385b75de433d497d938f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Thu, 20 Sep 2012 18:51:30 +0300 Subject: [PATCH 065/265] Fixed #18676 -- Allow fast-path deletion of objects Objects can be fast-path deleted if there are no signals, and there are no further cascades. If fast-path is taken, the objects do not need to be loaded into memory before deletion. Thanks to Jeremy Dunck, Simon Charette and Alex Gaynor for reviewing the patch. --- django/contrib/admin/util.py | 7 ++ django/db/models/deletion.py | 63 +++++++++-- django/db/models/query.py | 8 ++ django/db/models/sql/compiler.py | 3 +- django/db/models/sql/subqueries.py | 32 ++++++ docs/ref/models/querysets.txt | 15 +++ docs/releases/1.5.txt | 6 ++ tests/modeltests/delete/models.py | 20 +++- tests/modeltests/delete/tests.py | 101 +++++++++++++++++- tests/regressiontests/admin_util/models.py | 3 + tests/regressiontests/admin_util/tests.py | 13 ++- tests/regressiontests/delete_regress/tests.py | 11 +- .../dispatch/tests/test_dispatcher.py | 12 +-- 13 files changed, 275 insertions(+), 19 deletions(-) diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 74eef2e733..a85045c515 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -191,6 +191,13 @@ class NestedObjects(Collector): roots.extend(self._nested(root, seen, format_callback)) return roots + def can_fast_delete(self, *args, **kwargs): + """ + We always want to load the objects into memory so that we can display + them to the user in confirm page. + """ + return False + def model_format_dict(obj): """ diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 4449b75a81..6dff4a2882 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -77,6 +77,9 @@ class Collector(object): self.data = {} self.batches = {} # {model: {field: set([instances])}} self.field_updates = {} # {model: {(field, value): set([instances])}} + # fast_deletes is a list of queryset-likes that can be deleted without + # fetching the objects into memory. + self.fast_deletes = [] # Tracks deletion-order dependency for databases without transactions # or ability to defer constraint checks. Only concrete model classes @@ -131,6 +134,43 @@ class Collector(object): model, {}).setdefault( (field, value), set()).update(objs) + def can_fast_delete(self, objs, from_field=None): + """ + Determines if the objects in the given queryset-like can be + fast-deleted. This can be done if there are no cascades, no + parents and no signal listeners for the object class. + + The 'from_field' tells where we are coming from - we need this to + determine if the objects are in fact to be deleted. Allows also + skipping parent -> child -> parent chain preventing fast delete of + the child. + """ + if from_field and from_field.rel.on_delete is not CASCADE: + return False + if not (hasattr(objs, 'model') and hasattr(objs, '_raw_delete')): + return False + model = objs.model + if (signals.pre_delete.has_listeners(model) + or signals.post_delete.has_listeners(model) + or signals.m2m_changed.has_listeners(model)): + return False + # The use of from_field comes from the need to avoid cascade back to + # parent when parent delete is cascading to child. + opts = model._meta + if any(link != from_field for link in opts.concrete_model._meta.parents.values()): + return False + # Foreign keys pointing to this model, both from m2m and other + # models. + for related in opts.get_all_related_objects( + include_hidden=True, include_proxy_eq=True): + if related.field.rel.on_delete is not DO_NOTHING: + return False + # GFK deletes + for relation in opts.many_to_many: + if not relation.rel.through: + return False + return True + def collect(self, objs, source=None, nullable=False, collect_related=True, source_attr=None, reverse_dependency=False): """ @@ -148,6 +188,9 @@ class Collector(object): models, the one case in which the cascade follows the forwards direction of an FK rather than the reverse direction.) """ + if self.can_fast_delete(objs): + self.fast_deletes.append(objs) + return new_objs = self.add(objs, source, nullable, reverse_dependency=reverse_dependency) if not new_objs: @@ -160,6 +203,10 @@ class Collector(object): concrete_model = model._meta.concrete_model for ptr in six.itervalues(concrete_model._meta.parents): if ptr: + # FIXME: This seems to be buggy and execute a query for each + # parent object fetch. We have the parent data in the obj, + # but we don't have a nice way to turn that data into parent + # object instance. parent_objs = [getattr(obj, ptr.name) for obj in new_objs] self.collect(parent_objs, source=model, source_attr=ptr.rel.related_name, @@ -170,12 +217,12 @@ class Collector(object): for related in model._meta.get_all_related_objects( include_hidden=True, include_proxy_eq=True): field = related.field - if related.model._meta.auto_created: - self.add_batch(related.model, field, new_objs) - else: - sub_objs = self.related_objects(related, new_objs) - if not sub_objs: - continue + if field.rel.on_delete == DO_NOTHING: + continue + sub_objs = self.related_objects(related, new_objs) + if self.can_fast_delete(sub_objs, from_field=field): + self.fast_deletes.append(sub_objs) + elif sub_objs: field.rel.on_delete(self, field, sub_objs, self.using) # TODO This entire block is only needed as a special case to @@ -241,6 +288,10 @@ class Collector(object): sender=model, instance=obj, using=self.using ) + # fast deletes + for qs in self.fast_deletes: + qs._raw_delete(using=self.using) + # update fields for model, instances_for_fieldvalues in six.iteritems(self.field_updates): query = sql.UpdateQuery(model) diff --git a/django/db/models/query.py b/django/db/models/query.py index 8bf08b7a93..0210a7914d 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -529,6 +529,14 @@ class QuerySet(object): self._result_cache = None delete.alters_data = True + def _raw_delete(self, using): + """ + Deletes objects found from the given queryset in single direct SQL + query. No signals are sent, and there is no protection for cascades. + """ + sql.DeleteQuery(self.model).delete_qs(self, using) + _raw_delete.alters_data = True + def update(self, **kwargs): """ Updates all elements in the current QuerySet, setting all the given diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index f06d6b11a4..f6b6bba1d9 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -934,7 +934,8 @@ class SQLDeleteCompiler(SQLCompiler): qn = self.quote_name_unless_alias result = ['DELETE FROM %s' % qn(self.query.tables[0])] where, params = self.query.where.as_sql(qn=qn, connection=self.connection) - result.append('WHERE %s' % where) + if where: + result.append('WHERE %s' % where) return ' '.join(result), tuple(params) class SQLUpdateCompiler(SQLCompiler): diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index c6995c6abb..9f3fb8ac22 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 import connections from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import DateField, FieldDoesNotExist from django.db.models.sql.constants import * @@ -46,6 +47,37 @@ class DeleteQuery(Query): pk_list[offset:offset + GET_ITERATOR_CHUNK_SIZE]), AND) self.do_query(self.model._meta.db_table, where, using=using) + def delete_qs(self, query, using): + innerq = query.query + # Make sure the inner query has at least one table in use. + innerq.get_initial_alias() + # The same for our new query. + self.get_initial_alias() + innerq_used_tables = [t for t in innerq.tables + if innerq.alias_refcount[t]] + if ((not innerq_used_tables or innerq_used_tables == self.tables) + and not len(innerq.having)): + # There is only the base table in use in the query, and there are + # no aggregate filtering going on. + self.where = innerq.where + else: + pk = query.model._meta.pk + if not connections[using].features.update_can_self_select: + # We can't do the delete using subquery. + values = list(query.values_list('pk', flat=True)) + if not values: + return + self.delete_batch(values, using) + return + else: + values = innerq + innerq.select = [(self.get_initial_alias(), pk.column)] + where = self.where_class() + where.add((Constraint(None, pk.column, pk), 'in', values), AND) + self.where = where + self.get_compiler(using).execute_sql(None) + + class UpdateQuery(Query): """ Represents an "update" SQL query. diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 749a979db6..d17d869164 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1667,6 +1667,21 @@ methods on your models. It does, however, emit the :data:`~django.db.models.signals.post_delete` signals for all deleted objects (including cascaded deletions). +.. versionadded:: 1.5 + Allow fast-path deletion of objects + +Django needs to fetch objects into memory to send signals and handle cascades. +However, if there are no cascades and no signals, then Django may take a +fast-path and delete objects without fetching into memory. For large +deletes this can result in significantly reduced memory usage. The amount of +executed queries can be reduced, too. + +ForeignKeys which are set to :attr:`~django.db.models.ForeignKey.on_delete` +DO_NOTHING do not prevent taking the fast-path in deletion. + +Note that the queries generated in object deletion is an implementation +detail subject to change. + .. _field-lookups: Field lookups diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index fddd03d421..5636a2b34b 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -149,6 +149,12 @@ Django 1.5 also includes several smaller improvements worth noting: * Django now provides a mod_wsgi :doc:`auth handler ` +* The :meth:`QuerySet.delete() ` + and :meth:`Model.delete() ` can now take + fast-path in some cases. The fast-path allows for less queries and less + objects fetched into memory. See :meth:`QuerySet.delete() + ` for details. + Backwards incompatible changes in 1.5 ===================================== diff --git a/tests/modeltests/delete/models.py b/tests/modeltests/delete/models.py index e0cec426ea..65d4e6f725 100644 --- a/tests/modeltests/delete/models.py +++ b/tests/modeltests/delete/models.py @@ -95,7 +95,7 @@ class MRNull(models.Model): class Avatar(models.Model): - pass + desc = models.TextField(null=True) class User(models.Model): @@ -108,3 +108,21 @@ class HiddenUser(models.Model): class HiddenUserProfile(models.Model): user = models.ForeignKey(HiddenUser) + +class M2MTo(models.Model): + pass + +class M2MFrom(models.Model): + m2m = models.ManyToManyField(M2MTo) + +class Parent(models.Model): + pass + +class Child(Parent): + pass + +class Base(models.Model): + pass + +class RelToBase(models.Model): + base = models.ForeignKey(Base, on_delete=models.DO_NOTHING) diff --git a/tests/modeltests/delete/tests.py b/tests/modeltests/delete/tests.py index 26f2fd52c1..2610cb4b39 100644 --- a/tests/modeltests/delete/tests.py +++ b/tests/modeltests/delete/tests.py @@ -1,11 +1,12 @@ from __future__ import absolute_import -from django.db import models, IntegrityError +from django.db import models, IntegrityError, connection from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature from django.utils.six.moves import xrange from .models import (R, RChild, S, T, U, A, M, MR, MRNull, - create_a, get_default_r, User, Avatar, HiddenUser, HiddenUserProfile) + create_a, get_default_r, User, Avatar, HiddenUser, HiddenUserProfile, + M2MTo, M2MFrom, Parent, Child, Base) class OnDeleteTests(TestCase): @@ -74,6 +75,16 @@ class OnDeleteTests(TestCase): self.assertEqual(replacement_r, a.donothing) models.signals.pre_delete.disconnect(check_do_nothing) + def test_do_nothing_qscount(self): + """ + Test that a models.DO_NOTHING relation doesn't trigger a query. + """ + b = Base.objects.create() + with self.assertNumQueries(1): + # RelToBase should not be queried. + b.delete() + self.assertEqual(Base.objects.count(), 0) + def test_inheritance_cascade_up(self): child = RChild.objects.create() child.delete() @@ -229,16 +240,34 @@ class DeletionTests(TestCase): # 1 query to delete the avatar # The important thing is that when we can defer constraint checks there # is no need to do an UPDATE on User.avatar to null it out. + + # Attach a signal to make sure we will not do fast_deletes. + calls = [] + def noop(*args, **kwargs): + calls.append('') + models.signals.post_delete.connect(noop, sender=User) + self.assertNumQueries(3, a.delete) self.assertFalse(User.objects.exists()) self.assertFalse(Avatar.objects.exists()) + self.assertEquals(len(calls), 1) + models.signals.post_delete.disconnect(noop, sender=User) @skipIfDBFeature("can_defer_constraint_checks") def test_cannot_defer_constraint_checks(self): u = User.objects.create( avatar=Avatar.objects.create() ) + # Attach a signal to make sure we will not do fast_deletes. + calls = [] + def noop(*args, **kwargs): + calls.append('') + models.signals.post_delete.connect(noop, sender=User) + a = Avatar.objects.get(pk=u.avatar_id) + # The below doesn't make sense... Why do we need to null out + # user.avatar if we are going to delete the user immediately after it, + # and there are no more cascades. # 1 query to find the users for the avatar. # 1 query to delete the user # 1 query to null out user.avatar, because we can't defer the constraint @@ -246,6 +275,8 @@ class DeletionTests(TestCase): self.assertNumQueries(4, a.delete) self.assertFalse(User.objects.exists()) self.assertFalse(Avatar.objects.exists()) + self.assertEquals(len(calls), 1) + models.signals.post_delete.disconnect(noop, sender=User) def test_hidden_related(self): r = R.objects.create() @@ -254,3 +285,69 @@ class DeletionTests(TestCase): r.delete() self.assertEqual(HiddenUserProfile.objects.count(), 0) + +class FastDeleteTests(TestCase): + + def test_fast_delete_fk(self): + u = User.objects.create( + avatar=Avatar.objects.create() + ) + a = Avatar.objects.get(pk=u.avatar_id) + # 1 query to fast-delete the user + # 1 query to delete the avatar + self.assertNumQueries(2, a.delete) + self.assertFalse(User.objects.exists()) + self.assertFalse(Avatar.objects.exists()) + + def test_fast_delete_m2m(self): + t = M2MTo.objects.create() + f = M2MFrom.objects.create() + f.m2m.add(t) + # 1 to delete f, 1 to fast-delete m2m for f + self.assertNumQueries(2, f.delete) + + def test_fast_delete_revm2m(self): + t = M2MTo.objects.create() + f = M2MFrom.objects.create() + f.m2m.add(t) + # 1 to delete t, 1 to fast-delete t's m_set + self.assertNumQueries(2, f.delete) + + def test_fast_delete_qs(self): + u1 = User.objects.create() + u2 = User.objects.create() + self.assertNumQueries(1, User.objects.filter(pk=u1.pk).delete) + self.assertEquals(User.objects.count(), 1) + self.assertTrue(User.objects.filter(pk=u2.pk).exists()) + + def test_fast_delete_joined_qs(self): + a = Avatar.objects.create(desc='a') + User.objects.create(avatar=a) + u2 = User.objects.create() + expected_queries = 1 if connection.features.update_can_self_select else 2 + self.assertNumQueries(expected_queries, + User.objects.filter(avatar__desc='a').delete) + self.assertEquals(User.objects.count(), 1) + self.assertTrue(User.objects.filter(pk=u2.pk).exists()) + + def test_fast_delete_inheritance(self): + c = Child.objects.create() + p = Parent.objects.create() + # 1 for self, 1 for parent + # However, this doesn't work as child.parent access creates a query, + # and this means we will be generating extra queries (a lot for large + # querysets). This is not a fast-delete problem. + # self.assertNumQueries(2, c.delete) + c.delete() + self.assertFalse(Child.objects.exists()) + self.assertEquals(Parent.objects.count(), 1) + self.assertEquals(Parent.objects.filter(pk=p.pk).count(), 1) + # 1 for self delete, 1 for fast delete of empty "child" qs. + self.assertNumQueries(2, p.delete) + self.assertFalse(Parent.objects.exists()) + # 1 for self delete, 1 for fast delete of empty "child" qs. + c = Child.objects.create() + p = c.parent_ptr + self.assertNumQueries(2, p.delete) + self.assertFalse(Parent.objects.exists()) + self.assertFalse(Child.objects.exists()) diff --git a/tests/regressiontests/admin_util/models.py b/tests/regressiontests/admin_util/models.py index b3504a1fa4..32a6cd6291 100644 --- a/tests/regressiontests/admin_util/models.py +++ b/tests/regressiontests/admin_util/models.py @@ -39,3 +39,6 @@ class Guest(models.Model): class Meta: verbose_name = "awesome guest" + +class EventGuide(models.Model): + event = models.ForeignKey(Event, on_delete=models.DO_NOTHING) diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py index d04740ce95..ef8a91d1db 100644 --- a/tests/regressiontests/admin_util/tests.py +++ b/tests/regressiontests/admin_util/tests.py @@ -17,7 +17,7 @@ from django.utils.formats import localize from django.utils.safestring import mark_safe from django.utils import six -from .models import Article, Count, Event, Location +from .models import Article, Count, Event, Location, EventGuide class NestedObjectsTests(TestCase): @@ -71,6 +71,17 @@ class NestedObjectsTests(TestCase): # Should not require additional queries to populate the nested graph. self.assertNumQueries(2, self._collect, 0) + def test_on_delete_do_nothing(self): + """ + Check that the nested collector doesn't query for DO_NOTHING objects. + """ + n = NestedObjects(using=DEFAULT_DB_ALIAS) + objs = [Event.objects.create()] + EventGuide.objects.create(event=objs[0]) + with self.assertNumQueries(2): + # One for Location, one for Guest, and no query for EventGuide + n.collect(objs) + class UtilTests(unittest.TestCase): def test_values_from_lookup_field(self): """ diff --git a/tests/regressiontests/delete_regress/tests.py b/tests/regressiontests/delete_regress/tests.py index 32feae2ded..ebe59bffd7 100644 --- a/tests/regressiontests/delete_regress/tests.py +++ b/tests/regressiontests/delete_regress/tests.py @@ -3,7 +3,7 @@ from __future__ import absolute_import import datetime from django.conf import settings -from django.db import backend, transaction, DEFAULT_DB_ALIAS +from django.db import backend, transaction, DEFAULT_DB_ALIAS, models from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature from .models import (Book, Award, AwardNote, Person, Child, Toy, PlayedWith, @@ -139,17 +139,24 @@ class DeleteCascadeTransactionTests(TransactionTestCase): eaten = Eaten.objects.create(food=apple, meal="lunch") apple.delete() + self.assertFalse(Food.objects.exists()) + self.assertFalse(Eaten.objects.exists()) + class LargeDeleteTests(TestCase): def test_large_deletes(self): "Regression for #13309 -- if the number of objects > chunk size, deletion still occurs" for x in range(300): track = Book.objects.create(pagecount=x+100) + # attach a signal to make sure we will not fast-delete + def noop(*args, **kwargs): + pass + models.signals.post_delete.connect(noop, sender=Book) Book.objects.all().delete() + models.signals.post_delete.disconnect(noop, sender=Book) self.assertEqual(Book.objects.count(), 0) - class ProxyDeleteTest(TestCase): """ Tests on_delete behavior for proxy models. diff --git a/tests/regressiontests/dispatch/tests/test_dispatcher.py b/tests/regressiontests/dispatch/tests/test_dispatcher.py index 4e4669d34c..5f8f92acaf 100644 --- a/tests/regressiontests/dispatch/tests/test_dispatcher.py +++ b/tests/regressiontests/dispatch/tests/test_dispatcher.py @@ -127,15 +127,15 @@ class DispatcherTests(unittest.TestCase): self._testIsClean(a_signal) def test_has_listeners(self): - self.assertIs(a_signal.has_listeners(), False) - self.assertIs(a_signal.has_listeners(sender=object()), False) + self.assertFalse(a_signal.has_listeners()) + self.assertFalse(a_signal.has_listeners(sender=object())) receiver_1 = Callable() a_signal.connect(receiver_1) - self.assertIs(a_signal.has_listeners(), True) - self.assertIs(a_signal.has_listeners(sender=object()), True) + self.assertTrue(a_signal.has_listeners()) + self.assertTrue(a_signal.has_listeners(sender=object())) a_signal.disconnect(receiver_1) - self.assertIs(a_signal.has_listeners(), False) - self.assertIs(a_signal.has_listeners(sender=object()), False) + self.assertFalse(a_signal.has_listeners()) + self.assertFalse(a_signal.has_listeners(sender=object())) class ReceiverTestCase(unittest.TestCase): From 6c2faaceb0482267cec19da0ff432984028f9d0c Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 28 Sep 2012 20:10:22 +0200 Subject: [PATCH 066/265] Made more extensive use of get_current_site Refs #15089 --- django/contrib/comments/moderation.py | 4 +- django/contrib/contenttypes/tests.py | 5 +- .../flatpages/templatetags/flatpages.py | 7 ++- django/contrib/redirects/middleware.py | 6 ++- django/views/decorators/cache.py | 2 +- docs/ref/contrib/sites.txt | 53 +++++++------------ 6 files changed, 35 insertions(+), 42 deletions(-) diff --git a/django/contrib/comments/moderation.py b/django/contrib/comments/moderation.py index 9b206a5bad..6c56d7a8a5 100644 --- a/django/contrib/comments/moderation.py +++ b/django/contrib/comments/moderation.py @@ -62,7 +62,7 @@ from django.contrib.comments import signals from django.db.models.base import ModelBase from django.template import Context, loader from django.contrib import comments -from django.contrib.sites.models import Site +from django.contrib.sites.models import get_current_site from django.utils import timezone class AlreadyModerated(Exception): @@ -240,7 +240,7 @@ class CommentModerator(object): t = loader.get_template('comments/comment_notification_email.txt') c = Context({ 'comment': comment, 'content_object': content_object }) - subject = '[%s] New comment posted on "%s"' % (Site.objects.get_current().name, + subject = '[%s] New comment posted on "%s"' % (get_current_site(request).name, content_object) message = t.render(c) send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True) diff --git a/django/contrib/contenttypes/tests.py b/django/contrib/contenttypes/tests.py index 2f92a34581..10311fae92 100644 --- a/django/contrib/contenttypes/tests.py +++ b/django/contrib/contenttypes/tests.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.views import shortcut -from django.contrib.sites.models import Site +from django.contrib.sites.models import Site, get_current_site from django.http import HttpRequest, Http404 from django.test import TestCase from django.utils.http import urlquote @@ -219,9 +219,8 @@ class ContentTypesTests(TestCase): obj = FooWithUrl.objects.create(name="john") if Site._meta.installed: - current_site = Site.objects.get_current() response = shortcut(request, user_ct.id, obj.id) - self.assertEqual("http://%s/users/john/" % current_site.domain, + self.assertEqual("http://%s/users/john/" % get_current_site(request).domain, response._headers.get("location")[1]) Site._meta.installed = False diff --git a/django/contrib/flatpages/templatetags/flatpages.py b/django/contrib/flatpages/templatetags/flatpages.py index 702d968145..a32ac7f490 100644 --- a/django/contrib/flatpages/templatetags/flatpages.py +++ b/django/contrib/flatpages/templatetags/flatpages.py @@ -1,6 +1,7 @@ from django import template from django.conf import settings from django.contrib.flatpages.models import FlatPage +from django.contrib.sites.models import get_current_site register = template.Library() @@ -19,7 +20,11 @@ class FlatpageNode(template.Node): self.user = None def render(self, context): - flatpages = FlatPage.objects.filter(sites__id=settings.SITE_ID) + if 'request' in context: + site_pk = get_current_site(context['request']).pk + else: + site_pk = settings.SITE_ID + flatpages = FlatPage.objects.filter(sites__id=site_pk) # If a prefix was specified, add a filter if self.starts_with: flatpages = flatpages.filter( diff --git a/django/contrib/redirects/middleware.py b/django/contrib/redirects/middleware.py index 8998c2ce3e..927220d44d 100644 --- a/django/contrib/redirects/middleware.py +++ b/django/contrib/redirects/middleware.py @@ -1,4 +1,5 @@ from django.contrib.redirects.models import Redirect +from django.contrib.sites.models import get_current_site from django import http from django.conf import settings @@ -7,14 +8,15 @@ class RedirectFallbackMiddleware(object): if response.status_code != 404: return response # No need to check for a redirect for non-404 responses. path = request.get_full_path() + current_site = get_current_site(request) try: - r = Redirect.objects.get(site__id__exact=settings.SITE_ID, old_path=path) + r = Redirect.objects.get(site__id__exact=current_site.id, old_path=path) except Redirect.DoesNotExist: r = None if r is None and settings.APPEND_SLASH: # Try removing the trailing slash. try: - r = Redirect.objects.get(site__id__exact=settings.SITE_ID, + r = Redirect.objects.get(site__id__exact=current_site.id, old_path=path[:path.rfind('/')]+path[path.rfind('/')+1:]) except Redirect.DoesNotExist: pass diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py index ac8b3752d7..06925c1f4a 100644 --- a/django/views/decorators/cache.py +++ b/django/views/decorators/cache.py @@ -12,7 +12,7 @@ def cache_page(*args, **kwargs): The cache is keyed by the URL and some data from the headers. Additionally there is the key prefix that is used to distinguish different cache areas in a multi-site setup. You could use the - sites.get_current().domain, for example, as that is unique across a Django + sites.get_current_site().domain, for example, as that is unique across a Django project. Additionally, all headers from the response's Vary header will be taken diff --git a/docs/ref/contrib/sites.txt b/docs/ref/contrib/sites.txt index 8bb7b27f32..790e003453 100644 --- a/docs/ref/contrib/sites.txt +++ b/docs/ref/contrib/sites.txt @@ -80,11 +80,11 @@ This accomplishes several things quite nicely: The view code that displays a given story just checks to make sure the requested story is on the current site. It looks something like this:: - from django.conf import settings + from django.contrib.sites.models import get_current_site def article_detail(request, article_id): try: - a = Article.objects.get(id=article_id, sites__id__exact=settings.SITE_ID) + a = Article.objects.get(id=article_id, sites__id__exact=get_current_site(request).id) except Article.DoesNotExist: raise Http404 # ... @@ -131,49 +131,36 @@ For example:: # Do something else. Of course, it's ugly to hard-code the site IDs like that. This sort of -hard-coding is best for hackish fixes that you need done quickly. A slightly +hard-coding is best for hackish fixes that you need done quickly. The cleaner way of accomplishing the same thing is to check the current site's domain:: - from django.conf import settings - from django.contrib.sites.models import Site + from django.contrib.sites.models import get_current_site def my_view(request): - current_site = Site.objects.get(id=settings.SITE_ID) + current_site = get_current_site(request) if current_site.domain == 'foo.com': # Do something else: # Do something else. -The idiom of retrieving the :class:`~django.contrib.sites.models.Site` object -for the value of :setting:`settings.SITE_ID ` is quite common, so -the :class:`~django.contrib.sites.models.Site` model's manager has a -``get_current()`` method. This example is equivalent to the previous one:: +This has also the advantage of checking if the sites framework is installed, and +return a :class:`RequestSite` instance if it is not. + +If you don't have access to the request object, you can use the +``get_current()`` method of the :class:`~django.contrib.sites.models.Site` +model's manager. You should then ensure that your settings file does contain +the :setting:`SITE_ID` setting. This example is equivalent to the previous one:: from django.contrib.sites.models import Site - def my_view(request): + def my_function_without_request(): current_site = Site.objects.get_current() if current_site.domain == 'foo.com': # Do something else: # Do something else. -For code which relies on getting the current domain but cannot be certain -that the sites framework will be installed for any given project, there is a -utility function :func:`~django.contrib.sites.models.get_current_site` that -takes a request object as an argument and returns either a Site instance (if -the sites framework is installed) or a RequestSite instance (if it is not). -This allows loose coupling with the sites framework and provides a usable -fallback for cases where it is not installed. - -.. function:: get_current_site(request) - - Checks if contrib.sites is installed and returns either the current - :class:`~django.contrib.sites.models.Site` object or a - :class:`~django.contrib.sites.models.RequestSite` object based on - the request. - Getting the current domain for display -------------------------------------- @@ -192,14 +179,14 @@ current site's :attr:`~django.contrib.sites.models.Site.name` and Here's an example of what the form-handling view looks like:: - from django.contrib.sites.models import Site + from django.contrib.sites.models import get_current_site from django.core.mail import send_mail def register_for_newsletter(request): # Check form values, etc., and subscribe the user. # ... - current_site = Site.objects.get_current() + current_site = get_current_site(request) send_mail('Thanks for subscribing to %s alerts' % current_site.name, 'Thanks for your subscription. We appreciate it.\n\n-The %s team.' % current_site.name, 'editor@%s' % current_site.domain, @@ -370,19 +357,19 @@ Here's how Django uses the sites framework: * In the :mod:`redirects framework `, each redirect object is associated with a particular site. When Django searches - for a redirect, it takes into account the current :setting:`SITE_ID`. + for a redirect, it takes into account the current site. * In the comments framework, each comment is associated with a particular site. When a comment is posted, its - :class:`~django.contrib.sites.models.Site` is set to the current - :setting:`SITE_ID`, and when comments are listed via the appropriate - template tag, only the comments for the current site are displayed. + :class:`~django.contrib.sites.models.Site` is set to the current site, + and when comments are listed via the appropriate template tag, only the + comments for the current site are displayed. * In the :mod:`flatpages framework `, each flatpage is associated with a particular site. When a flatpage is created, you specify its :class:`~django.contrib.sites.models.Site`, and the :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware` - checks the current :setting:`SITE_ID` in retrieving flatpages to display. + checks the current site in retrieving flatpages to display. * In the :mod:`syndication framework `, the templates for ``title`` and ``description`` automatically have access to a From fea0ca4334b8c35100c0ca1048f81b9b3573bc26 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 28 Sep 2012 09:50:02 -0400 Subject: [PATCH 067/265] Fixed #12871 - Documented creation of a comment form for authenticated users; thanks shacker for patch. --- docs/ref/contrib/comments/index.txt | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt index 4b1dd96280..1c6ff7c7ed 100644 --- a/docs/ref/contrib/comments/index.txt +++ b/docs/ref/contrib/comments/index.txt @@ -254,6 +254,56 @@ you can include a hidden form input called ``next`` in your comment form. For ex +Providing a comment form for authenticated users +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a user is already authenticated, it makes little sense to display the name, +email, and URL fields, since these can already be retrieved from their login +data and profile. In addition, some sites will only accept comments from +authenticated users. + +To provide a comment form for authenticated users, you can manually provide the +additional fields expected by the Django comments framework. For example, +assuming comments are attached to the model "object":: + + {% if user.is_authenticated %} + {% get_comment_form for object as form %} +
    + {% csrf_token %} + {{ form.comment }} + {{ form.honeypot }} + {{ form.content_type }} + {{ form.object_pk }} + {{ form.timestamp }} + {{ form.security_hash }} + + +
    + {% else %} +

    Please log in to leave a comment.

    + {% endif %} + +The honeypot, content_type, object_pk, timestamp, and security_hash fields are +fields that would have been created automatically if you had simply used +``{{ form }}`` in your template, and are referred to in `Notes on the comment +form`_ below. + +Note that we do not need to specify the user to be associated with comments +submitted by authenticated users. This is possible because the :doc:`Built-in +Comment Models` that come with Django associate +comments with authenticated users by default. + +In this example, the honeypot field will still be visible to the user; you'll +need to hide that field in your CSS:: + + #id_honeypot { + display: none; + } + +If you want to accept either anonymous or authenticated comments, replace the +contents of the "else" clause above with a standard comment form and the right +thing will happen whether a user is logged in or not. + .. _notes-on-the-comment-form: Notes on the comment form From 15d355d79dc26fb150776e6e019b2f667e4cb2ff Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 29 Sep 2012 11:14:16 +0800 Subject: [PATCH 068/265] Fixed #19041 -- Corrected the handling of default usernames in createsuperuser. --- django/contrib/auth/management/commands/createsuperuser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index c5f6469548..8130b326c5 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -78,10 +78,11 @@ class Command(BaseCommand): if not username: input_msg = capfirst(username_field.verbose_name) if default_username: - input_msg += ' (leave blank to use %r)' % default_username + input_msg += " (leave blank to use '%s')" % default_username raw_value = input(input_msg + ': ') + if default_username and raw_value == '': - username = default_username + raw_value = default_username try: username = username_field.clean(raw_value, None) except exceptions.ValidationError as e: From 2f6e00a840176f95c836f25a41cc1a7d31941ba5 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 29 Sep 2012 11:01:08 +0200 Subject: [PATCH 069/265] Fixed #11948 -- Added interpolate and project linear referencing methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks novalis for the report and the initial patch, and Anssi Kääriäinen and Justin Bronn for the review. --- django/contrib/gis/geos/geometry.py | 32 +++++++++++++++++++ .../contrib/gis/geos/prototypes/topology.py | 23 ++++++++++--- django/contrib/gis/geos/tests/test_geos.py | 21 ++++++++++++ docs/ref/contrib/gis/geos.txt | 25 +++++++++++++++ docs/releases/1.5.txt | 14 ++++++-- 5 files changed, 108 insertions(+), 7 deletions(-) diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index 6dbb6b2cb3..079308bba8 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -581,6 +581,20 @@ class GEOSGeometry(GEOSBase, ListMixin): "Return the envelope for this geometry (a polygon)." return self._topology(capi.geos_envelope(self.ptr)) + def interpolate(self, distance): + if not isinstance(self, (LineString, MultiLineString)): + raise TypeError('interpolate only works on LineString and MultiLineString geometries') + if not hasattr(capi, 'geos_interpolate'): + raise NotImplementedError('interpolate requires GEOS 3.2+') + return self._topology(capi.geos_interpolate(self.ptr, distance)) + + def interpolate_normalized(self, distance): + if not isinstance(self, (LineString, MultiLineString)): + raise TypeError('interpolate only works on LineString and MultiLineString geometries') + if not hasattr(capi, 'geos_interpolate_normalized'): + raise NotImplementedError('interpolate_normalized requires GEOS 3.2+') + return self._topology(capi.geos_interpolate_normalized(self.ptr, distance)) + def intersection(self, other): "Returns a Geometry representing the points shared by this Geometry and other." return self._topology(capi.geos_intersection(self.ptr, other.ptr)) @@ -590,6 +604,24 @@ class GEOSGeometry(GEOSBase, ListMixin): "Computes an interior point of this Geometry." return self._topology(capi.geos_pointonsurface(self.ptr)) + def project(self, point): + if not isinstance(point, Point): + raise TypeError('locate_point argument must be a Point') + if not isinstance(self, (LineString, MultiLineString)): + raise TypeError('locate_point only works on LineString and MultiLineString geometries') + if not hasattr(capi, 'geos_project'): + raise NotImplementedError('geos_project requires GEOS 3.2+') + return capi.geos_project(self.ptr, point.ptr) + + def project_normalized(self, point): + if not isinstance(point, Point): + raise TypeError('locate_point argument must be a Point') + if not isinstance(self, (LineString, MultiLineString)): + raise TypeError('locate_point only works on LineString and MultiLineString geometries') + if not hasattr(capi, 'geos_project_normalized'): + raise NotImplementedError('project_normalized requires GEOS 3.2+') + return capi.geos_project_normalized(self.ptr, point.ptr) + def relate(self, other): "Returns the DE-9IM intersection matrix for this Geometry and the other." return capi.geos_relate(self.ptr, other.ptr).decode() diff --git a/django/contrib/gis/geos/prototypes/topology.py b/django/contrib/gis/geos/prototypes/topology.py index cc5734b5e4..dfea3e98b6 100644 --- a/django/contrib/gis/geos/prototypes/topology.py +++ b/django/contrib/gis/geos/prototypes/topology.py @@ -8,18 +8,18 @@ __all__ = ['geos_boundary', 'geos_buffer', 'geos_centroid', 'geos_convexhull', 'geos_simplify', 'geos_symdifference', 'geos_union', 'geos_relate'] from ctypes import c_double, c_int -from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOS_PREPARE -from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_string +from django.contrib.gis.geos.libgeos import geos_version_info, GEOM_PTR, GEOS_PREPARE +from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_minus_one, check_string from django.contrib.gis.geos.prototypes.geom import geos_char_p from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc -def topology(func, *args): +def topology(func, *args, **kwargs): "For GEOS unary topology functions." argtypes = [GEOM_PTR] if args: argtypes += args func.argtypes = argtypes - func.restype = GEOM_PTR - func.errcheck = check_geom + func.restype = kwargs.get('restype', GEOM_PTR) + func.errcheck = kwargs.get('errcheck', check_geom) return func ### Topology Routines ### @@ -49,3 +49,16 @@ if GEOS_PREPARE: geos_cascaded_union.argtypes = [GEOM_PTR] geos_cascaded_union.restype = GEOM_PTR __all__.append('geos_cascaded_union') + +# Linear referencing routines +info = geos_version_info() +if info['version'] >= '3.2.0': + geos_project = topology(GEOSFunc('GEOSProject'), GEOM_PTR, + restype=c_double, errcheck=check_minus_one) + geos_interpolate = topology(GEOSFunc('GEOSInterpolate'), c_double) + + geos_project_normalized = topology(GEOSFunc('GEOSProjectNormalized'), + GEOM_PTR, restype=c_double, errcheck=check_minus_one) + geos_interpolate_normalized = topology(GEOSFunc('GEOSInterpolateNormalized'), c_double) + __all__.extend(['geos_project', 'geos_interpolate', + 'geos_project_normalized', 'geos_interpolate_normalized']) diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index c8d3e43a0e..e10ac80982 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -1023,6 +1023,27 @@ class GEOSTest(unittest.TestCase, TestDataMixin): print("\nEND - expecting GEOS_NOTICE; safe to ignore.\n") + @unittest.skipUnless(geos_version_info()['version'] >= '3.2.0', "geos >= 3.2.0 is required") + def test_linearref(self): + "Testing linear referencing" + + ls = fromstr('LINESTRING(0 0, 0 10, 10 10, 10 0)') + mls = fromstr('MULTILINESTRING((0 0, 0 10), (10 0, 10 10))') + + self.assertEqual(ls.project(Point(0, 20)), 10.0) + self.assertEqual(ls.project(Point(7, 6)), 24) + self.assertEqual(ls.project_normalized(Point(0, 20)), 1.0/3) + + self.assertEqual(ls.interpolate(10), Point(0, 10)) + self.assertEqual(ls.interpolate(24), Point(10, 6)) + self.assertEqual(ls.interpolate_normalized(1.0/3), Point(0, 10)) + + self.assertEqual(mls.project(Point(0, 20)), 10) + self.assertEqual(mls.project(Point(7, 6)), 16) + + self.assertEqual(mls.interpolate(9), Point(0, 9)) + self.assertEqual(mls.interpolate(17), Point(10, 7)) + def test_geos_version(self): "Testing the GEOS version regular expression." from django.contrib.gis.geos.libgeos import version_regex diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index b569a74fe3..88883784f9 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -416,11 +416,36 @@ quarter circle (defaults is 8). Returns a :class:`GEOSGeometry` representing the points making up this geometry that do not make up other. +.. method:: GEOSGeometry.interpolate(distance) +.. method:: GEOSGeometry.interpolate_normalized(distance) + +.. versionadded:: 1.5 + +Given a distance (float), returns the point (or closest point) within the +geometry (:class:`LineString` or :class:`MultiLineString`) at that distance. +The normalized version takes the distance as a float between 0 (origin) and 1 +(endpoint). + +Reverse of :meth:`GEOSGeometry.project`. + .. method:: GEOSGeometry:intersection(other) Returns a :class:`GEOSGeometry` representing the points shared by this geometry and other. +.. method:: GEOSGeometry.project(point) +.. method:: GEOSGeometry.project_normalized(point) + +.. versionadded:: 1.5 + +Returns the distance (float) from the origin of the geometry +(:class:`LineString` or :class:`MultiLineString`) to the point projected on the +geometry (that is to a point of the line the closest to the given point). +The normalized version returns the distance as a float between 0 (origin) and 1 +(endpoint). + +Reverse of :meth:`GEOSGeometry.interpolate`. + .. method:: GEOSGeometry.relate(other) Returns the DE-9IM intersection matrix (a string) representing the diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 5636a2b34b..294ceb159e 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -103,10 +103,22 @@ associated with proxy models. New ``view`` variable in class-based views context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + In all :doc:`generic class-based views ` (or any class-based view inheriting from ``ContextMixin``), the context dictionary contains a ``view`` variable that points to the ``View`` instance. +GeoDjango +~~~~~~~~~ + +* :class:`~django.contrib.gis.geos.LineString` and + :class:`~django.contrib.gis.geos.MultiLineString` GEOS objects now support the + :meth:`~django.contrib.gis.geos.GEOSGeometry.interpolate()` and + :meth:`~django.contrib.gis.geos.GEOSGeometry.project()` methods + (so-called linear referencing). + +* Support for GDAL < 1.5 has been dropped. + Minor features ~~~~~~~~~~~~~~ @@ -379,8 +391,6 @@ on the form. Miscellaneous ~~~~~~~~~~~~~ -* GeoDjango dropped support for GDAL < 1.5 - * :func:`~django.utils.http.int_to_base36` properly raises a :exc:`TypeError` instead of :exc:`ValueError` for non-integer inputs. From 82a74dce243935688f4f9cd447521d4d141c563b Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 29 Sep 2012 12:10:52 +0200 Subject: [PATCH 070/265] Used TransactionTestCase in ModWsgiHandlerTestCase Now the data created in setUp() is not discarded when the connection is closed in the handler's methods. --- django/contrib/auth/tests/handlers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/contrib/auth/tests/handlers.py b/django/contrib/auth/tests/handlers.py index f061042ce3..190fcee9fa 100644 --- a/django/contrib/auth/tests/handlers.py +++ b/django/contrib/auth/tests/handlers.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user from django.contrib.auth.models import User, Group -from django.test import TestCase +from django.test import TransactionTestCase -class ModWsgiHandlerTestCase(TestCase): +class ModWsgiHandlerTestCase(TransactionTestCase): """ Tests for the mod_wsgi authentication handler """ From ffdd6595ea2220f8e8a6fb3aacd3213b751d982f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 22 Sep 2012 11:55:37 +0200 Subject: [PATCH 071/265] Fixed #18919 -- Stopped dropping Z attribute when transforming geometries Previously, the wkb of geometries was dropping the Z attribute. Thanks luizvital for the report and tests and georger.silva@gmail.com for the tests. --- django/contrib/gis/geos/geometry.py | 41 +++++++++------------- django/contrib/gis/geos/prototypes/io.py | 14 +++----- django/contrib/gis/geos/tests/test_geos.py | 29 +++++++++++---- django/contrib/gis/tests/geo3d/tests.py | 5 ++- docs/ref/contrib/gis/geos.txt | 20 +++++++---- docs/releases/1.5.txt | 2 ++ 6 files changed, 61 insertions(+), 50 deletions(-) diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index 079308bba8..df396bdbd3 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -25,7 +25,7 @@ from django.contrib.gis.geos import prototypes as capi # These functions provide access to a thread-local instance # of their corresponding GEOS I/O class. -from django.contrib.gis.geos.prototypes.io import wkt_r, wkt_w, wkb_r, wkb_w, ewkb_w, ewkb_w3d +from django.contrib.gis.geos.prototypes.io import wkt_r, wkt_w, wkb_r, wkb_w, ewkb_w # For recognizing geometry input. from django.contrib.gis.geometry.regex import hex_regex, wkt_regex, json_regex @@ -388,28 +388,24 @@ class GEOSGeometry(GEOSBase, ListMixin): def hex(self): """ Returns the WKB of this Geometry in hexadecimal form. Please note - that the SRID and Z values are not included in this representation - because it is not a part of the OGC specification (use the `hexewkb` - property instead). + that the SRID is not included in this representation because it is not + a part of the OGC specification (use the `hexewkb` property instead). """ # A possible faster, all-python, implementation: # str(self.wkb).encode('hex') - return wkb_w().write_hex(self) + return wkb_w(self.hasz and 3 or 2).write_hex(self) @property def hexewkb(self): """ Returns the EWKB of this Geometry in hexadecimal form. This is an - extension of the WKB specification that includes SRID and Z values - that are a part of this geometry. + extension of the WKB specification that includes SRID value that are + a part of this geometry. """ - if self.hasz: - if not GEOS_PREPARE: - # See: http://trac.osgeo.org/geos/ticket/216 - raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D HEXEWKB.') - return ewkb_w3d().write_hex(self) - else: - return ewkb_w().write_hex(self) + if self.hasz and not GEOS_PREPARE: + # See: http://trac.osgeo.org/geos/ticket/216 + raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D HEXEWKB.') + return ewkb_w(self.hasz and 3 or 2).write_hex(self) @property def json(self): @@ -429,22 +425,19 @@ class GEOSGeometry(GEOSBase, ListMixin): as a Python buffer. SRID and Z values are not included, use the `ewkb` property instead. """ - return wkb_w().write(self) + return wkb_w(self.hasz and 3 or 2).write(self) @property def ewkb(self): """ Return the EWKB representation of this Geometry as a Python buffer. This is an extension of the WKB specification that includes any SRID - and Z values that are a part of this geometry. + value that are a part of this geometry. """ - if self.hasz: - if not GEOS_PREPARE: - # See: http://trac.osgeo.org/geos/ticket/216 - raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D EWKB.') - return ewkb_w3d().write(self) - else: - return ewkb_w().write(self) + if self.hasz and not GEOS_PREPARE: + # See: http://trac.osgeo.org/geos/ticket/216 + raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D EWKB.') + return ewkb_w(self.hasz and 3 or 2).write(self) @property def kml(self): @@ -516,7 +509,7 @@ class GEOSGeometry(GEOSBase, ListMixin): raise GEOSException("GDAL library is not available to transform() geometry.") # Creating an OGR Geometry, which is then transformed. - g = gdal.OGRGeometry(self.wkb, srid) + g = self.ogr g.transform(ct) # Getting a new GEOS pointer ptr = wkb_r().read(g.wkb) diff --git a/django/contrib/gis/geos/prototypes/io.py b/django/contrib/gis/geos/prototypes/io.py index 1eeab60a4b..1be7da8845 100644 --- a/django/contrib/gis/geos/prototypes/io.py +++ b/django/contrib/gis/geos/prototypes/io.py @@ -207,7 +207,6 @@ class ThreadLocalIO(threading.local): wkb_r = None wkb_w = None ewkb_w = None - ewkb_w3d = None thread_context = ThreadLocalIO() @@ -228,20 +227,15 @@ def wkb_r(): thread_context.wkb_r = _WKBReader() return thread_context.wkb_r -def wkb_w(): +def wkb_w(dim=2): if not thread_context.wkb_w: thread_context.wkb_w = WKBWriter() + thread_context.wkb_w.outdim = dim return thread_context.wkb_w -def ewkb_w(): +def ewkb_w(dim=2): if not thread_context.ewkb_w: thread_context.ewkb_w = WKBWriter() thread_context.ewkb_w.srid = True + thread_context.ewkb_w.outdim = dim return thread_context.ewkb_w - -def ewkb_w3d(): - if not thread_context.ewkb_w3d: - thread_context.ewkb_w3d = WKBWriter() - thread_context.ewkb_w3d.srid = True - thread_context.ewkb_w3d.outdim = 3 - return thread_context.ewkb_w3d diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index e10ac80982..cbe51367ae 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -92,6 +92,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): "Testing (HEX)EWKB output." # For testing HEX(EWKB). ogc_hex = b'01010000000000000000000000000000000000F03F' + ogc_hex_3d = b'01010000800000000000000000000000000000F03F0000000000000040' # `SELECT ST_AsHEXEWKB(ST_GeomFromText('POINT(0 1)', 4326));` hexewkb_2d = b'0101000020E61000000000000000000000000000000000F03F' # `SELECT ST_AsHEXEWKB(ST_GeomFromEWKT('SRID=4326;POINT(0 1 2)'));` @@ -100,9 +101,9 @@ class GEOSTest(unittest.TestCase, TestDataMixin): pnt_2d = Point(0, 1, srid=4326) pnt_3d = Point(0, 1, 2, srid=4326) - # OGC-compliant HEX will not have SRID nor Z value. + # OGC-compliant HEX will not have SRID value. self.assertEqual(ogc_hex, pnt_2d.hex) - self.assertEqual(ogc_hex, pnt_3d.hex) + self.assertEqual(ogc_hex_3d, pnt_3d.hex) # HEXEWKB should be appropriate for its dimension -- have to use an # a WKBWriter w/dimension set accordingly, else GEOS will insert @@ -830,12 +831,17 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_gdal(self): "Testing `ogr` and `srs` properties." g1 = fromstr('POINT(5 23)') - self.assertEqual(True, isinstance(g1.ogr, gdal.OGRGeometry)) - self.assertEqual(g1.srs, None) + self.assertIsInstance(g1.ogr, gdal.OGRGeometry) + self.assertIsNone(g1.srs) + + if GEOS_PREPARE: + g1_3d = fromstr('POINT(5 23 8)') + self.assertIsInstance(g1_3d.ogr, gdal.OGRGeometry) + self.assertEqual(g1_3d.ogr.z, 8) g2 = fromstr('LINESTRING(0 0, 5 5, 23 23)', srid=4326) - self.assertEqual(True, isinstance(g2.ogr, gdal.OGRGeometry)) - self.assertEqual(True, isinstance(g2.srs, gdal.SpatialReference)) + self.assertIsInstance(g2.ogr, gdal.OGRGeometry) + self.assertIsInstance(g2.srs, gdal.SpatialReference) self.assertEqual(g2.hex, g2.ogr.hex) self.assertEqual('WGS 84', g2.srs.name) @@ -848,7 +854,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertNotEqual(poly._ptr, cpy1._ptr) self.assertNotEqual(poly._ptr, cpy2._ptr) - @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required") + @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required to transform geometries") def test_transform(self): "Testing `transform` method." orig = GEOSGeometry('POINT (-104.609 38.255)', 4326) @@ -873,6 +879,15 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertAlmostEqual(trans.x, p.x, prec) self.assertAlmostEqual(trans.y, p.y, prec) + @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required to transform geometries") + def test_transform_3d(self): + p3d = GEOSGeometry('POINT (5 23 100)', 4326) + p3d.transform(2774) + if GEOS_PREPARE: + self.assertEqual(p3d.z, 100) + else: + self.assertIsNone(p3d.z) + def test_transform_noop(self): """ Testing `transform` method (SRID match) """ # transform() should no-op if source & dest SRIDs match, diff --git a/django/contrib/gis/tests/geo3d/tests.py b/django/contrib/gis/tests/geo3d/tests.py index 0aba38f5ca..f7590fe84a 100644 --- a/django/contrib/gis/tests/geo3d/tests.py +++ b/django/contrib/gis/tests/geo3d/tests.py @@ -4,7 +4,7 @@ import os import re from django.contrib.gis.db.models import Union, Extent3D -from django.contrib.gis.geos import GEOSGeometry, Point, Polygon +from django.contrib.gis.geos import GEOSGeometry, LineString, Point, Polygon from django.contrib.gis.utils import LayerMapping, LayerMapError from django.test import TestCase @@ -67,8 +67,7 @@ class Geo3DTest(TestCase): # Interstate (2D / 3D and Geographic/Projected variants) for name, line, exp_z in interstate_data: line_3d = GEOSGeometry(line, srid=4269) - # Using `hex` attribute because it omits 3D. - line_2d = GEOSGeometry(line_3d.hex, srid=4269) + line_2d = LineString([l[:2] for l in line_3d.coords], srid=4269) # Creating a geographic and projected version of the # interstate in both 2D and 3D. diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index 88883784f9..eb20b1f411 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -273,14 +273,18 @@ Essentially the SRID is prepended to the WKT representation, for example .. attribute:: GEOSGeometry.hex Returns the WKB of this Geometry in hexadecimal form. Please note -that the SRID and Z values are not included in this representation +that the SRID value is not included in this representation because it is not a part of the OGC specification (use the :attr:`GEOSGeometry.hexewkb` property instead). +.. versionchanged:: 1.5 + + Prior to Django 1.5, the Z value of the geometry was dropped. + .. attribute:: GEOSGeometry.hexewkb Returns the EWKB of this Geometry in hexadecimal form. This is an -extension of the WKB specification that includes SRID and Z values +extension of the WKB specification that includes the SRID value that are a part of this geometry. .. note:: @@ -319,16 +323,20 @@ correspondg to the GEOS geometry. .. attribute:: GEOSGeometry.wkb Returns the WKB (Well-Known Binary) representation of this Geometry -as a Python buffer. SRID and Z values are not included, use the +as a Python buffer. SRID value is not included, use the :attr:`GEOSGeometry.ewkb` property instead. +.. versionchanged:: 1.5 + + Prior to Django 1.5, the Z value of the geometry was dropped. + .. _ewkb: .. attribute:: GEOSGeometry.ewkb Return the EWKB representation of this Geometry as a Python buffer. This is an extension of the WKB specification that includes any SRID -and Z values that are a part of this geometry. +value that are a part of this geometry. .. note:: @@ -822,7 +830,7 @@ Writer Objects All writer objects have a ``write(geom)`` method that returns either the WKB or WKT of the given geometry. In addition, :class:`WKBWriter` objects also have properties that may be used to change the byte order, and or -include the SRID and 3D values (in other words, EWKB). +include the SRID value (in other words, EWKB). .. class:: WKBWriter @@ -884,7 +892,7 @@ so that the Z value is included in the WKB. Outdim Value Description ============ =========================== 2 The default, output 2D WKB. -3 Output 3D EWKB. +3 Output 3D WKB. ============ =========================== Example:: diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 294ceb159e..b769debb0b 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -117,6 +117,8 @@ GeoDjango :meth:`~django.contrib.gis.geos.GEOSGeometry.project()` methods (so-called linear referencing). +* The wkb and hex properties of `GEOSGeometry` objects preserve the Z dimension. + * Support for GDAL < 1.5 has been dropped. Minor features From 8867c276135887458c21536f53c8b4045baefefc Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 29 Sep 2012 20:04:08 +0200 Subject: [PATCH 072/265] Added link to PostGIS matrix on OSGeo Wiki --- docs/ref/contrib/gis/install.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ref/contrib/gis/install.txt b/docs/ref/contrib/gis/install.txt index b815973202..d84ffc6b52 100644 --- a/docs/ref/contrib/gis/install.txt +++ b/docs/ref/contrib/gis/install.txt @@ -69,6 +69,11 @@ Oracle GEOS 10.2, 11 XE not s SQLite GEOS, GDAL, PROJ.4, SpatiaLite 3.6.+ Requires SpatiaLite 2.3+, pysqlite2 2.5+ ================== ============================== ================== ========================================= +See also `this comparison matrix`__ on the OSGeo Wiki for +PostgreSQL/PostGIS/GEOS/GDAL possible combinations. + +__ http://trac.osgeo.org/postgis/wiki/UsersWikiPostgreSQLPostGIS + .. _geospatial_libs: Geospatial libraries From 15202baace1453e7576806f13d137ae930de6dcb Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 29 Sep 2012 16:41:55 -0400 Subject: [PATCH 073/265] Fixed #17058 - Clarified where extras/csrf_migration_helper.py is located --- docs/ref/contrib/csrf.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index 8d352ff8b2..32d8a705bc 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -72,9 +72,9 @@ To enable CSRF protection for your views, follow these steps: :func:`~django.shortcuts.render_to_response()` wrapper that takes care of this step for you. -The utility script ``extras/csrf_migration_helper.py`` can help to automate the -finding of code and templates that may need these steps. It contains full help -on how to use it. +The utility script ``extras/csrf_migration_helper.py`` (located in the Django +distribution, but not installed) can help to automate the finding of code and +templates that may need these steps. It contains full help on how to use it. .. _csrf-ajax: From a014ddfef2f606471f25c756d97b3b50fcbd9e91 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 24 Sep 2012 22:30:38 +0200 Subject: [PATCH 074/265] Combined Django DEFAULT_LOGGING with user LOGGING config Refs #18993. --- django/conf/__init__.py | 12 +++++++---- django/conf/global_settings.py | 29 ++----------------------- django/utils/log.py | 39 ++++++++++++++++++++++++++++------ docs/topics/logging.txt | 22 +++++++++++++++++++ 4 files changed, 65 insertions(+), 37 deletions(-) diff --git a/django/conf/__init__.py b/django/conf/__init__.py index d636ff0b6c..7452013671 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -55,16 +55,20 @@ class LazySettings(LazyObject): Setup logging from LOGGING_CONFIG and LOGGING settings. """ if self.LOGGING_CONFIG: + from django.utils.log import DEFAULT_LOGGING # First find the logging configuration function ... logging_config_path, logging_config_func_name = self.LOGGING_CONFIG.rsplit('.', 1) logging_config_module = importlib.import_module(logging_config_path) logging_config_func = getattr(logging_config_module, logging_config_func_name) - # Backwards-compatibility shim for #16288 fix - compat_patch_logging_config(self.LOGGING) + logging_config_func(DEFAULT_LOGGING) - # ... then invoke it with the logging settings - logging_config_func(self.LOGGING) + if self.LOGGING: + # Backwards-compatibility shim for #16288 fix + compat_patch_logging_config(self.LOGGING) + + # ... then invoke it with the logging settings + logging_config_func(self.LOGGING) def configure(self, default_settings=global_settings, **options): """ diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 708e9c9f70..f1cbb22880 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -551,33 +551,8 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.fallback.FallbackStorage' # The callable to use to configure logging LOGGING_CONFIG = 'django.utils.log.dictConfig' -# The default logging configuration. This sends an email to -# the site admins on every HTTP 500 error. All other log -# records are sent to the bit bucket. - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', - } - }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} +# Custom logging configuration. +LOGGING = {} # Default exception reporter filter class used in case none has been # specifically assigned to the HttpRequest instance. diff --git a/django/utils/log.py b/django/utils/log.py index df2089f924..c111512fe8 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -5,6 +5,7 @@ from django.conf import settings from django.core import mail from django.views.debug import ExceptionReporter, get_exception_reporter_filter + # Make sure a NullHandler is available # This was added in Python 2.7/3.2 try: @@ -23,12 +24,38 @@ except ImportError: getLogger = logging.getLogger -# Ensure the creation of the Django logger -# with a null handler. This ensures we don't get any -# 'No handlers could be found for logger "django"' messages -logger = getLogger('django') -if not logger.handlers: - logger.addHandler(NullHandler()) +# Default logging for Django. This sends an email to +# the site admins on every HTTP 500 error. All other log +# records are sent to the bit bucket. +DEFAULT_LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + } + }, + 'handlers': { + 'null': { + 'class': 'django.utils.log.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django': { + 'handlers': ['null'], + }, + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} class AdminEmailHandler(logging.Handler): diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index a4aae0bc02..a7f0a14b5b 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -192,6 +192,8 @@ There are two other logging calls available: * ``logger.exception()``: Creates an ``ERROR`` level logging message wrapping the current exception stack frame. +.. _configuring-logging: + Configuring logging =================== @@ -216,6 +218,14 @@ handlers, filters and formatters that you want in your logging setup, and the log levels and other properties that you want those components to have. +Prior to Django 1.5, the :setting:`LOGGING` setting overwrote the :ref:`default +Django logging configuration `. From Django +1.5 forward, the project's logging configuration is merged with Django's +defaults, hence you can decide if you want to add to, or replace the existing +configuration. To completely override the default configuration, set the +``disable_existing_loggers`` key to True in the :setting:`LOGGING` +dictConfig. Alternatively you can redefine some or all of the loggers. + Logging is configured as soon as settings have been loaded (either manually using :func:`~django.conf.settings.configure` or when at least one setting is accessed). Since the loading of settings is one of the first @@ -535,3 +545,15 @@ logging module. 'class': 'django.utils.log.AdminEmailHandler' } }, + +.. _default-logging-configuration: + +Django's default logging configuration +====================================== + +By default, Django configures the ``django.request`` logger so that all messages +with ``ERROR`` or ``CRITICAL`` level are sent to :class:`AdminEmailHandler`, as +long as the :setting:`DEBUG` setting is set to ``False``. + +All messages reaching the ``django`` catch-all logger are discarded +(sent to ``NullHandler``). From f0f327bbfe1caae6d11fbe20a3b5b96eed1704cf Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 26 Sep 2012 19:56:21 +0200 Subject: [PATCH 075/265] Fixed #18993 -- 'django' logger logs to console when DEBUG=True Thanks Preston Holmes for the review. --- django/utils/log.py | 25 +++++++++--- docs/releases/1.5.txt | 4 ++ docs/topics/logging.txt | 20 +++++++++- tests/regressiontests/logging_tests/tests.py | 40 +++++++++++++++----- 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/django/utils/log.py b/django/utils/log.py index c111512fe8..9e07961221 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -24,18 +24,25 @@ except ImportError: getLogger = logging.getLogger -# Default logging for Django. This sends an email to -# the site admins on every HTTP 500 error. All other log -# records are sent to the bit bucket. +# Default logging for Django. This sends an email to the site admins on every +# HTTP 500 error. Depending on DEBUG, all other log records are either sent to +# the console (DEBUG=True) or discarded by mean of the NullHandler (DEBUG=False). DEFAULT_LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'filters': { 'require_debug_false': { '()': 'django.utils.log.RequireDebugFalse', - } + }, + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, }, 'handlers': { + 'console':{ + 'level': 'INFO', + 'class': 'logging.StreamHandler', + }, 'null': { 'class': 'django.utils.log.NullHandler', }, @@ -47,12 +54,13 @@ DEFAULT_LOGGING = { }, 'loggers': { 'django': { - 'handlers': ['null'], + 'handlers': ['console'], + 'filters': ['require_debug_true'], }, 'django.request': { 'handlers': ['mail_admins'], 'level': 'ERROR', - 'propagate': True, + 'propagate': False, }, } } @@ -130,3 +138,8 @@ class CallbackFilter(logging.Filter): class RequireDebugFalse(logging.Filter): def filter(self, record): return not settings.DEBUG + + +class RequireDebugTrue(logging.Filter): + def filter(self, record): + return settings.DEBUG diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index c25858b5a6..367b4f8349 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -172,6 +172,10 @@ Django 1.5 also includes several smaller improvements worth noting: * An instance of :class:`~django.core.urlresolvers.ResolverMatch` is stored on the request as ``resolver_match``. +* By default, all logging messages reaching the `django` logger when + :setting:`DEBUG` is `True` are sent to the console (unless you redefine the + logger in your :setting:`LOGGING` setting). + Backwards incompatible changes in 1.5 ===================================== diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index a7f0a14b5b..7bd56e92ec 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -546,6 +546,13 @@ logging module. } }, +.. class:: RequireDebugTrue() + + .. versionadded:: 1.5 + + This filter is similar to :class:`RequireDebugFalse`, except that records are + passed only when :setting:`DEBUG` is `True`. + .. _default-logging-configuration: Django's default logging configuration @@ -555,5 +562,14 @@ By default, Django configures the ``django.request`` logger so that all messages with ``ERROR`` or ``CRITICAL`` level are sent to :class:`AdminEmailHandler`, as long as the :setting:`DEBUG` setting is set to ``False``. -All messages reaching the ``django`` catch-all logger are discarded -(sent to ``NullHandler``). +All messages reaching the ``django`` catch-all logger when :setting:`DEBUG` is +`True` are sent ot the console. They are simply discarded (sent to +``NullHandler``) when :setting:`DEBUG` is `False`. + +.. versionchanged:: 1.5 + + Before Django 1.5, all messages reaching the ``django`` logger were + discarded, regardless of :setting:`DEBUG`. + +See also :ref:`Configuring logging ` to learn how you can +complement or replace this default logging configuration. diff --git a/tests/regressiontests/logging_tests/tests.py b/tests/regressiontests/logging_tests/tests.py index a54b425f67..e40800efde 100644 --- a/tests/regressiontests/logging_tests/tests.py +++ b/tests/regressiontests/logging_tests/tests.py @@ -9,6 +9,7 @@ from django.core import mail from django.test import TestCase, RequestFactory from django.test.utils import override_settings from django.utils.log import CallbackFilter, RequireDebugFalse +from django.utils.six import StringIO from ..admin_scripts.tests import AdminScriptTestCase @@ -109,6 +110,28 @@ class PatchLoggingConfigTest(TestCase): self.assertEqual(config, new_config) +class DefaultLoggingTest(TestCase): + def setUp(self): + self.logger = logging.getLogger('django') + self.old_stream = self.logger.handlers[0].stream + + def tearDown(self): + self.logger.handlers[0].stream = self.old_stream + + def test_django_logger(self): + """ + The 'django' base logger only output anything when DEBUG=True. + """ + output = StringIO() + self.logger.handlers[0].stream = output + self.logger.error("Hey, this is an error.") + self.assertEqual(output.getvalue(), '') + + with self.settings(DEBUG=True): + self.logger.error("Hey, this is an error.") + self.assertEqual(output.getvalue(), 'Hey, this is an error.\n') + + class CallbackFilterTest(TestCase): def test_sense(self): f_false = CallbackFilter(lambda r: False) @@ -131,6 +154,7 @@ class CallbackFilterTest(TestCase): class AdminEmailHandlerTest(TestCase): + logger = logging.getLogger('django.request') def get_admin_email_handler(self, logger): # Inspired from regressiontests/views/views.py: send_log() @@ -156,14 +180,13 @@ class AdminEmailHandlerTest(TestCase): token1 = 'ping' token2 = 'pong' - logger = logging.getLogger('django.request') - admin_email_handler = self.get_admin_email_handler(logger) + admin_email_handler = self.get_admin_email_handler(self.logger) # Backup then override original filters orig_filters = admin_email_handler.filters try: admin_email_handler.filters = [] - logger.error(message, token1, token2) + self.logger.error(message, token1, token2) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].to, ['admin@example.com']) @@ -187,15 +210,14 @@ class AdminEmailHandlerTest(TestCase): token1 = 'ping' token2 = 'pong' - logger = logging.getLogger('django.request') - admin_email_handler = self.get_admin_email_handler(logger) + admin_email_handler = self.get_admin_email_handler(self.logger) # Backup then override original filters orig_filters = admin_email_handler.filters try: admin_email_handler.filters = [] rf = RequestFactory() request = rf.get('/') - logger.error(message, token1, token2, + self.logger.error(message, token1, token2, extra={ 'status_code': 403, 'request': request, @@ -225,8 +247,7 @@ class AdminEmailHandlerTest(TestCase): self.assertEqual(len(mail.outbox), 0) - logger = logging.getLogger('django.request') - logger.error(message) + self.logger.error(message) self.assertEqual(len(mail.outbox), 1) self.assertFalse('\n' in mail.outbox[0].subject) @@ -250,8 +271,7 @@ class AdminEmailHandlerTest(TestCase): self.assertEqual(len(mail.outbox), 0) - logger = logging.getLogger('django.request') - logger.error(message) + self.logger.error(message) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, expected_subject) From 10e505b1b37f29c9ac6567134c48677b042c2a87 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 29 Sep 2012 23:45:28 +0200 Subject: [PATCH 076/265] Moved filter at handler level Filters at logger level are only processed for messages directly logged to the specific logger, not for loggers in the parent chain. As the 'django' logger is almost always processed as an inherited logger, it makes more sense to filter messages at the 'console' handler level. --- django/utils/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/utils/log.py b/django/utils/log.py index 9e07961221..ea0122794b 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -41,6 +41,7 @@ DEFAULT_LOGGING = { 'handlers': { 'console':{ 'level': 'INFO', + 'filters': ['require_debug_true'], 'class': 'logging.StreamHandler', }, 'null': { @@ -55,7 +56,6 @@ DEFAULT_LOGGING = { 'loggers': { 'django': { 'handlers': ['console'], - 'filters': ['require_debug_true'], }, 'django.request': { 'handlers': ['mail_admins'], From dad7eec6e1c1770f5d81d5c5ed2de296c1eca969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sun, 30 Sep 2012 02:43:47 +0300 Subject: [PATCH 077/265] Corrected links to only()/defer() in Model documentation Refs #18306 --- docs/ref/models/instances.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 2fdc87df8c..92fc4ef31a 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -387,10 +387,11 @@ perform an update on all fields. Specifying ``update_fields`` will force an update. When saving a model fetched through deferred model loading -(:meth:`~Model.only()` or :meth:`~Model.defer()`) only the fields loaded from -the DB will get updated. In effect there is an automatic ``update_fields`` in -this case. If you assign or change any deferred field value, these fields will -be added to the updated fields. +(:meth:`~django.db.models.query.QuerySet.only()` or +:meth:`~django.db.models.query.QuerySet.defer()`) only the fields loaded +from the DB will get updated. In effect there is an automatic +``update_fields`` in this case. If you assign or change any deferred field +value, the field will be added to the updated fields. Deleting objects ================ From 3abf6105b6c953c6feb28708b9903f583cb28438 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sat, 29 Sep 2012 21:46:32 -0700 Subject: [PATCH 078/265] Fixed a couple errors and inconsistencies in mod_wsgi docs Fixes #19042 --- docs/howto/deployment/wsgi/apache-auth.txt | 14 +++++++------- docs/howto/deployment/wsgi/modwsgi.txt | 10 ++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/howto/deployment/wsgi/apache-auth.txt b/docs/howto/deployment/wsgi/apache-auth.txt index 36e3d0233c..d6594d194f 100644 --- a/docs/howto/deployment/wsgi/apache-auth.txt +++ b/docs/howto/deployment/wsgi/apache-auth.txt @@ -29,7 +29,7 @@ only authenticated users to be able to view: .. code-block:: apache - WSGIScriptAlias / /path/to/mysite/config/mysite.wsgi + WSGIScriptAlias / /path/to/mysite.com/mysite/wsgi.py WSGIProcessGroup %{GLOBAL} WSGIApplicationGroup django @@ -39,7 +39,7 @@ only authenticated users to be able to view: AuthName "Top Secret" Require valid-user AuthBasicProvider wsgi - WSGIAuthUserScript /path/to/mysite/config/mysite.wsgi + WSGIAuthUserScript /path/to/mysite.com/mysite/wsgi.py The ``WSGIAuthUserScript`` directive tells mod_wsgi to execute the @@ -72,7 +72,7 @@ check_user function: os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings' - from django.contrib.auth.handlers.modwsgi import check_user + from django.contrib.auth.handlers.modwsgi import check_password from django.core.handlers.wsgi import WSGIHandler application = WSGIHandler() @@ -95,7 +95,7 @@ In this case, the Apache configuration should look like this: .. code-block:: apache - WSGIScriptAlias / /path/to/mysite/config/mysite.wsgi + WSGIScriptAlias / /path/to/mysite.com/mysite/wsgi.py WSGIProcessGroup %{GLOBAL} WSGIApplicationGroup django @@ -104,8 +104,8 @@ In this case, the Apache configuration should look like this: AuthType Basic AuthName "Top Secret" AuthBasicProvider wsgi - WSGIAuthUserScript /path/to/mysite/config/mysite.wsgi - WSGIAuthGroupScript /path/to/mysite/config/mysite.wsgi + WSGIAuthUserScript /path/to/mysite.com/mysite/wsgi.py + WSGIAuthGroupScript /path/to/mysite.com/mysite/wsgi.py Require group secret-agents Require valid-user @@ -116,7 +116,7 @@ returns a list groups the given user belongs to. .. code-block:: python - from django.contrib.auth.handlers.modwsgi import check_user, groups_for_user + from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user Requests for ``/secret/`` will now also require user to be a member of the "secret-agents" group. diff --git a/docs/howto/deployment/wsgi/modwsgi.txt b/docs/howto/deployment/wsgi/modwsgi.txt index 01399aa5a6..fd467cb995 100644 --- a/docs/howto/deployment/wsgi/modwsgi.txt +++ b/docs/howto/deployment/wsgi/modwsgi.txt @@ -25,7 +25,9 @@ Basic configuration =================== Once you've got mod_wsgi installed and activated, edit your Apache server's -``httpd.conf`` file and add:: +``httpd.conf`` file and add + +.. code-block:: apache WSGIScriptAlias / /path/to/mysite.com/mysite/wsgi.py WSGIPythonPath /path/to/mysite.com @@ -70,10 +72,10 @@ Using a virtualenv If you install your project's Python dependencies inside a `virtualenv`_, you'll need to add the path to this virtualenv's ``site-packages`` directory to -your Python path as well. To do this, you can add another line to your -Apache configuration:: +your Python path as well. To do this, add an additional path to your +`WSGIPythonPath` directive with multiple paths separated by a colon:: - WSGIPythonPath /path/to/your/venv/lib/python2.X/site-packages + WSGIPythonPath /path/to/mysite.com:/path/to/your/venv/lib/python2.X/site-packages Make sure you give the correct path to your virtualenv, and replace ``python2.X`` with the correct Python version (e.g. ``python2.7``). From ab2a1773fdef2ff240b124268f5ae1118d8e27b5 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sat, 29 Sep 2012 21:53:13 -0700 Subject: [PATCH 079/265] Added a missing comma --- docs/howto/deployment/wsgi/modwsgi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/deployment/wsgi/modwsgi.txt b/docs/howto/deployment/wsgi/modwsgi.txt index fd467cb995..7f68485dff 100644 --- a/docs/howto/deployment/wsgi/modwsgi.txt +++ b/docs/howto/deployment/wsgi/modwsgi.txt @@ -73,7 +73,7 @@ Using a virtualenv If you install your project's Python dependencies inside a `virtualenv`_, you'll need to add the path to this virtualenv's ``site-packages`` directory to your Python path as well. To do this, add an additional path to your -`WSGIPythonPath` directive with multiple paths separated by a colon:: +`WSGIPythonPath` directive, with multiple paths separated by a colon:: WSGIPythonPath /path/to/mysite.com:/path/to/your/venv/lib/python2.X/site-packages From ddd7d1af203d6d260c970d8ee8749fedbce6bba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sun, 30 Sep 2012 17:50:56 +0300 Subject: [PATCH 080/265] Avoided storing ExpressionNodes in dicts --- django/db/models/sql/expressions.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py index ac8fea6da3..374509914d 100644 --- a/django/db/models/sql/expressions.py +++ b/django/db/models/sql/expressions.py @@ -6,7 +6,7 @@ class SQLEvaluator(object): def __init__(self, expression, query, allow_joins=True): self.expression = expression self.opts = query.get_meta() - self.cols = {} + self.cols = [] self.contains_aggregate = False self.expression.prepare(self, query, allow_joins) @@ -18,11 +18,15 @@ class SQLEvaluator(object): return self.expression.evaluate(self, qn, connection) def relabel_aliases(self, change_map): - for node, col in self.cols.items(): + new_cols = [] + for node, col in self.cols: if hasattr(col, "relabel_aliases"): col.relabel_aliases(change_map) + new_cols.append((node, col)) else: - self.cols[node] = (change_map.get(col[0], col[0]), col[1]) + new_cols.append((node, + (change_map.get(col[0], col[0]), col[1]))) + self.cols = new_cols ##################################################### # Vistor methods for initial expression preparation # @@ -41,7 +45,7 @@ class SQLEvaluator(object): if (len(field_list) == 1 and node.name in query.aggregate_select.keys()): self.contains_aggregate = True - self.cols[node] = query.aggregate_select[node.name] + self.cols.append((node, query.aggregate_select[node.name])) else: try: field, source, opts, join_list, last, _ = query.setup_joins( @@ -49,7 +53,7 @@ class SQLEvaluator(object): query.get_initial_alias(), False) col, _, join_list = query.trim_joins(source, join_list, last, False) - self.cols[node] = (join_list[-1], col) + self.cols.append((node, (join_list[-1], col))) except FieldDoesNotExist: raise FieldError("Cannot resolve keyword %r into field. " "Choices are: %s" % (self.name, @@ -80,7 +84,13 @@ class SQLEvaluator(object): return connection.ops.combine_expression(node.connector, expressions), expression_params def evaluate_leaf(self, node, qn, connection): - col = self.cols[node] + col = None + for n, c in self.cols: + if n is node: + col = c + break + if col is None: + raise ValueError("Given node not found") if hasattr(col, 'as_sql'): return col.as_sql(qn, connection), () else: From 28abf5f0ebc9d380f25dd278d7ef4642c4504545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sun, 30 Sep 2012 17:51:06 +0300 Subject: [PATCH 081/265] Fixed #16211 -- Added comparison and negation ops to F() expressions Work done by Walter Doekes and Trac alias knoeb. Reviewed by Simon Charette. --- django/db/backends/__init__.py | 3 + django/db/models/expressions.py | 37 +++++++++ django/utils/tree.py | 8 +- docs/releases/1.5.txt | 4 + docs/topics/db/queries.txt | 9 ++ tests/modeltests/expressions/models.py | 2 + tests/modeltests/expressions/tests.py | 109 ++++++++++++++++++++----- 7 files changed, 150 insertions(+), 22 deletions(-) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 02d2a16a46..4edde04f42 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -913,6 +913,9 @@ class BaseDatabaseOperations(object): can vary between backends (e.g., Oracle with %% and &) and between subexpression types (e.g., date expressions) """ + if connector == 'NOT': + assert len(sub_expressions) == 1 + return 'NOT (%s)' % sub_expressions[0] conn = ' %s ' % connector return conn.join(sub_expressions) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 639ef6ee10..972440b858 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -18,6 +18,17 @@ class ExpressionNode(tree.Node): AND = '&' OR = '|' + # Unary operator (needs special attention in combine_expression) + NOT = 'NOT' + + # Comparison operators + EQ = '=' + GE = '>=' + GT = '>' + LE = '<=' + LT = '<' + NE = '<>' + def __init__(self, children=None, connector=None, negated=False): if children is not None and len(children) > 1 and connector is None: raise TypeError('You have to specify a connector.') @@ -93,6 +104,32 @@ class ExpressionNode(tree.Node): def __ror__(self, other): return self._combine(other, self.OR, True) + def __invert__(self): + obj = ExpressionNode([self], connector=self.NOT, negated=True) + return obj + + def __eq__(self, other): + return self._combine(other, self.EQ, False) + + def __ge__(self, other): + return self._combine(other, self.GE, False) + + def __gt__(self, other): + return self._combine(other, self.GT, False) + + def __le__(self, other): + return self._combine(other, self.LE, False) + + def __lt__(self, other): + return self._combine(other, self.LT, False) + + def __ne__(self, other): + return self._combine(other, self.NE, False) + + def __bool__(self): + raise TypeError('Boolean operators should be avoided. Use bitwise operators.') + __nonzero__ = __bool__ + def prepare_database_save(self, unused): return self diff --git a/django/utils/tree.py b/django/utils/tree.py index 717181d2b9..6229493544 100644 --- a/django/utils/tree.py +++ b/django/utils/tree.py @@ -88,8 +88,12 @@ class Node(object): Otherwise, the whole tree is pushed down one level and a new root connector is created, connecting the existing tree and the new node. """ - if node in self.children and conn_type == self.connector: - return + # Using for loop with 'is' instead of 'if node in children' so node + # __eq__ method doesn't get called. The __eq__ method can be overriden + # by subtypes, for example the F-expression. + for child in self.children: + if node is child and conn_type == self.connector: + return if len(self.children) < 2: self.connector = conn_type if self.connector == conn_type: diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 367b4f8349..b371214994 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -176,6 +176,10 @@ Django 1.5 also includes several smaller improvements worth noting: :setting:`DEBUG` is `True` are sent to the console (unless you redefine the logger in your :setting:`LOGGING` setting). +* :ref:`F() expressions ` now support comparison operations + and inversion, expanding the types of expressions that can be passed to the + database. + Backwards incompatible changes in 1.5 ===================================== diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index dd160656c7..c724eabb8e 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -640,6 +640,15 @@ that were modified more than 3 days after they were published:: >>> from datetime import timedelta >>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3)) +.. versionadded:: 1.5 + Comparisons and negation operators for ``F()`` expressions + +Django also supports the comparison operators ``==``, ``!=``, ``<=``, ``<``, +``>``, ``>=`` and the bitwise negation operator ``~`` (boolean ``not`` operator +will raise ``TypeError``):: + + >>> Entry.objects.filter(is_heavily_quoted=~(F('n_pingbacks') < 100)) + The pk lookup shortcut ---------------------- diff --git a/tests/modeltests/expressions/models.py b/tests/modeltests/expressions/models.py index f592a0eb13..15f0d24541 100644 --- a/tests/modeltests/expressions/models.py +++ b/tests/modeltests/expressions/models.py @@ -27,6 +27,8 @@ class Company(models.Model): Employee, related_name='company_point_of_contact_set', null=True) + is_large = models.BooleanField( + blank=True) def __str__(self): return self.name diff --git a/tests/modeltests/expressions/tests.py b/tests/modeltests/expressions/tests.py index 99eb07e370..14419ec55b 100644 --- a/tests/modeltests/expressions/tests.py +++ b/tests/modeltests/expressions/tests.py @@ -11,22 +11,22 @@ from .models import Company, Employee class ExpressionsTests(TestCase): def test_filter(self): Company.objects.create( - name="Example Inc.", num_employees=2300, num_chairs=5, + name="Example Inc.", num_employees=2300, num_chairs=5, is_large=False, ceo=Employee.objects.create(firstname="Joe", lastname="Smith") ) Company.objects.create( - name="Foobar Ltd.", num_employees=3, num_chairs=4, + name="Foobar Ltd.", num_employees=3, num_chairs=4, is_large=False, ceo=Employee.objects.create(firstname="Frank", lastname="Meyer") ) Company.objects.create( - name="Test GmbH", num_employees=32, num_chairs=1, + name="Test GmbH", num_employees=32, num_chairs=1, is_large=False, ceo=Employee.objects.create(firstname="Max", lastname="Mustermann") ) company_query = Company.objects.values( - "name", "num_employees", "num_chairs" + "name", "num_employees", "num_chairs", "is_large" ).order_by( - "name", "num_employees", "num_chairs" + "name", "num_employees", "num_chairs", "is_large" ) # We can filter for companies where the number of employees is greater @@ -37,11 +37,13 @@ class ExpressionsTests(TestCase): "num_chairs": 5, "name": "Example Inc.", "num_employees": 2300, + "is_large": False }, { "num_chairs": 1, "name": "Test GmbH", - "num_employees": 32 + "num_employees": 32, + "is_large": False }, ], lambda o: o @@ -55,17 +57,20 @@ class ExpressionsTests(TestCase): { "num_chairs": 2300, "name": "Example Inc.", - "num_employees": 2300 + "num_employees": 2300, + "is_large": False }, { "num_chairs": 3, "name": "Foobar Ltd.", - "num_employees": 3 + "num_employees": 3, + "is_large": False }, { "num_chairs": 32, "name": "Test GmbH", - "num_employees": 32 + "num_employees": 32, + "is_large": False } ], lambda o: o @@ -79,17 +84,20 @@ class ExpressionsTests(TestCase): { 'num_chairs': 2302, 'name': 'Example Inc.', - 'num_employees': 2300 + 'num_employees': 2300, + 'is_large': False }, { 'num_chairs': 5, 'name': 'Foobar Ltd.', - 'num_employees': 3 + 'num_employees': 3, + 'is_large': False }, { 'num_chairs': 34, 'name': 'Test GmbH', - 'num_employees': 32 + 'num_employees': 32, + 'is_large': False } ], lambda o: o, @@ -104,17 +112,20 @@ class ExpressionsTests(TestCase): { 'num_chairs': 6900, 'name': 'Example Inc.', - 'num_employees': 2300 + 'num_employees': 2300, + 'is_large': False }, { 'num_chairs': 9, 'name': 'Foobar Ltd.', - 'num_employees': 3 + 'num_employees': 3, + 'is_large': False }, { 'num_chairs': 96, 'name': 'Test GmbH', - 'num_employees': 32 + 'num_employees': 32, + 'is_large': False } ], lambda o: o, @@ -129,21 +140,80 @@ class ExpressionsTests(TestCase): { 'num_chairs': 5294600, 'name': 'Example Inc.', - 'num_employees': 2300 + 'num_employees': 2300, + 'is_large': False }, { 'num_chairs': 15, 'name': 'Foobar Ltd.', - 'num_employees': 3 + 'num_employees': 3, + 'is_large': False }, { 'num_chairs': 1088, 'name': 'Test GmbH', - 'num_employees': 32 + 'num_employees': 32, + 'is_large': False } ], lambda o: o, ) + # The comparison operators and the bitwise unary not can be used + # to assign to boolean fields + for expression in ( + # Check boundaries + ~(F('num_employees') < 33), + ~(F('num_employees') <= 32), + (F('num_employees') > 2299), + (F('num_employees') >= 2300), + (F('num_employees') == 2300), + ((F('num_employees') + 1 != 4) & (32 != F('num_employees'))), + # Inverted argument order works too + (2299 < F('num_employees')), + (2300 <= F('num_employees')) + ): + # Test update by F-expression + company_query.update( + is_large=expression + ) + # Compare results + self.assertQuerysetEqual( + company_query, [ + { + 'num_chairs': 5294600, + 'name': 'Example Inc.', + 'num_employees': 2300, + 'is_large': True + }, + { + 'num_chairs': 15, + 'name': 'Foobar Ltd.', + 'num_employees': 3, + 'is_large': False + }, + { + 'num_chairs': 1088, + 'name': 'Test GmbH', + 'num_employees': 32, + 'is_large': False + } + ], + lambda o: o, + ) + # Reset values + company_query.update( + is_large=False + ) + + # The python boolean operators should be avoided as they yield + # unexpected results + test_gmbh = Company.objects.get(name="Test GmbH") + with self.assertRaises(TypeError): + test_gmbh.is_large = not F('is_large') + with self.assertRaises(TypeError): + test_gmbh.is_large = F('is_large') and F('is_large') + with self.assertRaises(TypeError): + test_gmbh.is_large = F('is_large') or F('is_large') # The relation of a foreign key can become copied over to an other # foreign key. @@ -202,9 +272,8 @@ class ExpressionsTests(TestCase): test_gmbh.point_of_contact = None test_gmbh.save() self.assertTrue(test_gmbh.point_of_contact is None) - def test(): + with self.assertRaises(ValueError): test_gmbh.point_of_contact = F("ceo") - self.assertRaises(ValueError, test) test_gmbh.point_of_contact = test_gmbh.ceo test_gmbh.save() From c2532825dbe2a422bbce67285637febb0ef9c9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sun, 30 Sep 2012 17:51:11 +0300 Subject: [PATCH 082/265] Splitted expressions tests into smaller methods --- tests/modeltests/expressions/tests.py | 43 +++++++++++++++++---------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/tests/modeltests/expressions/tests.py b/tests/modeltests/expressions/tests.py index 14419ec55b..5898eac7bb 100644 --- a/tests/modeltests/expressions/tests.py +++ b/tests/modeltests/expressions/tests.py @@ -9,7 +9,7 @@ from .models import Company, Employee class ExpressionsTests(TestCase): - def test_filter(self): + def setUp(self): Company.objects.create( name="Example Inc.", num_employees=2300, num_chairs=5, is_large=False, ceo=Employee.objects.create(firstname="Joe", lastname="Smith") @@ -23,6 +23,8 @@ class ExpressionsTests(TestCase): ceo=Employee.objects.create(firstname="Max", lastname="Mustermann") ) + + def test_filter(self): company_query = Company.objects.values( "name", "num_employees", "num_chairs", "is_large" ).order_by( @@ -158,6 +160,13 @@ class ExpressionsTests(TestCase): ], lambda o: o, ) + + def test_comparisons(self): + company_query = Company.objects.values( + "name", "num_employees", "num_chairs", "is_large" + ).order_by( + "name", "num_employees", "num_chairs", "is_large" + ) # The comparison operators and the bitwise unary not can be used # to assign to boolean fields for expression in ( @@ -180,19 +189,19 @@ class ExpressionsTests(TestCase): self.assertQuerysetEqual( company_query, [ { - 'num_chairs': 5294600, + 'num_chairs': 5, 'name': 'Example Inc.', 'num_employees': 2300, 'is_large': True }, { - 'num_chairs': 15, + 'num_chairs': 4, 'name': 'Foobar Ltd.', 'num_employees': 3, 'is_large': False }, { - 'num_chairs': 1088, + 'num_chairs': 1, 'name': 'Test GmbH', 'num_employees': 32, 'is_large': False @@ -230,28 +239,31 @@ class ExpressionsTests(TestCase): lambda c: six.text_type(c.point_of_contact), ) + def test_joins(self): c = Company.objects.all()[0] - c.point_of_contact = Employee.objects.create(firstname="Guido", lastname="van Rossum") + c.point_of_contact = Employee.objects.create( + firstname="Guido", lastname="van Rossum") + old_ceo = c.ceo + c.ceo = c.point_of_contact c.save() # F Expressions can also span joins self.assertQuerysetEqual( - Company.objects.filter(ceo__firstname=F("point_of_contact__firstname")), [ - "Foobar Ltd.", - "Test GmbH", + Company.objects.filter( + ceo__firstname=F("point_of_contact__firstname")), + [ + "Example Inc.", ], lambda c: c.name ) - + c.ceo = old_ceo + c.save() + # Guido is point of contanct but not CEO. For the null cases we do + # not generate a match. Company.objects.exclude( ceo__firstname=F("point_of_contact__firstname") ).update(name="foo") - self.assertEqual( - Company.objects.exclude( - ceo__firstname=F('point_of_contact__firstname') - ).get().name, - "foo", - ) + self.assertEqual(Company.objects.filter(name="foo").count(), 1) self.assertRaises(FieldError, lambda: Company.objects.exclude( @@ -259,6 +271,7 @@ class ExpressionsTests(TestCase): ).update(name=F('point_of_contact__lastname')) ) + def test_save(self): # F expressions can be used to update attributes on single objects test_gmbh = Company.objects.get(name="Test GmbH") self.assertEqual(test_gmbh.num_employees, 32) From d5a4f209c3889a76a23a19f3212ab2d7b5c62e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Sun, 30 Sep 2012 18:13:23 +0300 Subject: [PATCH 083/265] Fixed #18991 -- Allowed permission lookup by "if in" When looking permissions from PermWrapper it is now possible to use {% if "someapp.someperm" in perms %} instead of {% if perms.someapp.someperm %}. --- django/contrib/auth/context_processors.py | 11 +++++ .../contrib/auth/tests/context_processors.py | 41 +++++++++++++++---- .../auth_attrs_perm_in_perms.html | 4 ++ .../context_processors/auth_attrs_perms.html | 3 ++ django/contrib/auth/tests/urls.py | 5 +++ docs/releases/1.5.txt | 4 ++ docs/topics/auth.txt | 14 +++++++ 7 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 django/contrib/auth/tests/templates/context_processors/auth_attrs_perm_in_perms.html diff --git a/django/contrib/auth/context_processors.py b/django/contrib/auth/context_processors.py index 77face01a7..5929505359 100644 --- a/django/contrib/auth/context_processors.py +++ b/django/contrib/auth/context_processors.py @@ -32,6 +32,17 @@ class PermWrapper(object): # I am large, I contain multitudes. raise TypeError("PermWrapper is not iterable.") + def __contains__(self, perm_name): + """ + Lookup by "someapp" or "someapp.someperm" in perms. + """ + if '.' not in perm_name: + # The name refers to module. + return bool(self[perm_name]) + module_name, perm_name = perm_name.split('.', 1) + return self[module_name][perm_name] + + def auth(request): """ Returns context variables required by apps that use Django's authentication diff --git a/django/contrib/auth/tests/context_processors.py b/django/contrib/auth/tests/context_processors.py index 8d87e0ae15..32fea8ac80 100644 --- a/django/contrib/auth/tests/context_processors.py +++ b/django/contrib/auth/tests/context_processors.py @@ -3,6 +3,8 @@ import os from django.conf import global_settings from django.contrib.auth import authenticate from django.contrib.auth.tests.utils import skipIfCustomUser +from django.contrib.auth.models import User, Permission +from django.contrib.contenttypes.models import ContentType from django.contrib.auth.context_processors import PermWrapper, PermLookupDict from django.db.models import Q from django.test import TestCase @@ -10,13 +12,13 @@ from django.test.utils import override_settings class MockUser(object): - def has_module_perm(self, perm): - if perm == 'mockapp.someapp': + def has_module_perms(self, perm): + if perm == 'mockapp': return True return False def has_perm(self, perm): - if perm == 'someperm': + if perm == 'mockapp.someperm': return True return False @@ -40,13 +42,19 @@ class PermWrapperTests(TestCase): def test_permwrapper_in(self): """ - Test that 'something' in PermWrapper doesn't end up in endless loop. + Test that 'something' in PermWrapper works as expected. """ perms = PermWrapper(MockUser()) - with self.assertRaises(TypeError): - self.EQLimiterObject() in perms + # Works for modules and full permissions. + self.assertTrue('mockapp' in perms) + self.assertFalse('nonexisting' in perms) + self.assertTrue('mockapp.someperm' in perms) + self.assertFalse('mockapp.nonexisting' in perms) def test_permlookupdict_in(self): + """ + No endless loops if accessed with 'in' - refs #18979. + """ pldict = PermLookupDict(MockUser(), 'mockapp') with self.assertRaises(TypeError): self.EQLimiterObject() in pldict @@ -92,9 +100,28 @@ class AuthContextProcessorTests(TestCase): self.assertContains(response, "Session accessed") def test_perms_attrs(self): - self.client.login(username='super', password='secret') + u = User.objects.create_user(username='normal', password='secret') + u.user_permissions.add( + Permission.objects.get( + content_type=ContentType.objects.get_for_model(Permission), + codename='add_permission')) + self.client.login(username='normal', password='secret') response = self.client.get('/auth_processor_perms/') self.assertContains(response, "Has auth permissions") + self.assertContains(response, "Has auth.add_permission permissions") + self.assertNotContains(response, "nonexisting") + + def test_perm_in_perms_attrs(self): + u = User.objects.create_user(username='normal', password='secret') + u.user_permissions.add( + Permission.objects.get( + content_type=ContentType.objects.get_for_model(Permission), + codename='add_permission')) + self.client.login(username='normal', password='secret') + response = self.client.get('/auth_processor_perm_in_perms/') + self.assertContains(response, "Has auth permissions") + self.assertContains(response, "Has auth.add_permission permissions") + self.assertNotContains(response, "nonexisting") def test_message_attrs(self): self.client.login(username='super', password='secret') diff --git a/django/contrib/auth/tests/templates/context_processors/auth_attrs_perm_in_perms.html b/django/contrib/auth/tests/templates/context_processors/auth_attrs_perm_in_perms.html new file mode 100644 index 0000000000..3a18cd7405 --- /dev/null +++ b/django/contrib/auth/tests/templates/context_processors/auth_attrs_perm_in_perms.html @@ -0,0 +1,4 @@ +{% if 'auth' in perms %}Has auth permissions{% endif %} +{% if 'auth.add_permission' in perms %}Has auth.add_permission permissions{% endif %} +{% if 'nonexisting' in perms %}nonexisting perm found{% endif %} +{% if 'auth.nonexisting' in perms %}auth.nonexisting perm found{% endif %} diff --git a/django/contrib/auth/tests/templates/context_processors/auth_attrs_perms.html b/django/contrib/auth/tests/templates/context_processors/auth_attrs_perms.html index a5db868e9e..6f441afc10 100644 --- a/django/contrib/auth/tests/templates/context_processors/auth_attrs_perms.html +++ b/django/contrib/auth/tests/templates/context_processors/auth_attrs_perms.html @@ -1 +1,4 @@ {% if perms.auth %}Has auth permissions{% endif %} +{% if perms.auth.add_permission %}Has auth.add_permission permissions{% endif %} +{% if perms.nonexisting %}nonexisting perm found{% endif %} +{% if perms.auth.nonexisting in perms %}auth.nonexisting perm found{% endif %} diff --git a/django/contrib/auth/tests/urls.py b/django/contrib/auth/tests/urls.py index dbbd35ee88..8f9e848aa9 100644 --- a/django/contrib/auth/tests/urls.py +++ b/django/contrib/auth/tests/urls.py @@ -37,6 +37,10 @@ def auth_processor_perms(request): return render_to_response('context_processors/auth_attrs_perms.html', RequestContext(request, {}, processors=[context_processors.auth])) +def auth_processor_perm_in_perms(request): + return render_to_response('context_processors/auth_attrs_perm_in_perms.html', + RequestContext(request, {}, processors=[context_processors.auth])) + def auth_processor_messages(request): info(request, "Message 1") return render_to_response('context_processors/auth_attrs_messages.html', @@ -58,6 +62,7 @@ urlpatterns = urlpatterns + patterns('', (r'^auth_processor_attr_access/$', auth_processor_attr_access), (r'^auth_processor_user/$', auth_processor_user), (r'^auth_processor_perms/$', auth_processor_perms), + (r'^auth_processor_perm_in_perms/$', auth_processor_perm_in_perms), (r'^auth_processor_messages/$', auth_processor_messages), url(r'^userpage/(.+)/$', userpage, name="userpage"), ) diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index b371214994..c39592122b 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -180,6 +180,10 @@ Django 1.5 also includes several smaller improvements worth noting: and inversion, expanding the types of expressions that can be passed to the database. +* When using :class:`~django.template.RequestContext`, it is now possible to + look up permissions by using ``{% if 'someapp.someperm' in perms %}`` + in templates. + Backwards incompatible changes in 1.5 ===================================== diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index 1d320df9c1..0a19f5ed5a 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -1710,6 +1710,20 @@ Thus, you can check permissions in template ``{% if %}`` statements:

    You don't have permission to do anything in the foo app.

    {% endif %} +.. versionadded:: 1.5 + Permission lookup by "if in". + +It is possible to also look permissions up by ``{% if in %}`` statements. +For example: + +.. code-block:: html+django + + {% if 'foo' in perms %} + {% if 'foo.can_vote' in perms %} +

    In lookup works, too.

    + {% endif %} + {% endif %} + Groups ====== From d0345b71146ecb60af2277585b604fbc244d267b Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sun, 30 Sep 2012 13:37:25 -0400 Subject: [PATCH 084/265] Fixed #15338 - Documented django.utils.decorators --- docs/ref/utils.txt | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index de19578cac..bd3898172a 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -170,6 +170,37 @@ The functions defined in this module share the following properties: ``tzinfo`` attribute is a :class:`~django.utils.tzinfo.FixedOffset` instance. +``django.utils.decorators`` +=========================== + +.. module:: django.utils.decorators + :synopsis: Functions that help with creating decorators for views. + +.. function:: method_decorator(decorator) + + Converts a function decorator into a method decorator. See :ref:`decorating + class based views` for example usage. + +.. function:: decorator_from_middleware(middleware_class) + + Given a middleware class, returns a view decorator. This lets you use + middleware functionality on a per-view basis. The middleware is created + with no params passed. + +.. function:: decorator_from_middleware_with_args(middleware_class) + + Like ``decorator_from_middleware``, but returns a function + that accepts the arguments to be passed to the middleware_class. + For example, the :func:`~django.views.decorators.cache.cache_page` + decorator is created from the + :class:`~django.middleware.cache.CacheMiddleware` like this:: + + cache_page = decorator_from_middleware_with_args(CacheMiddleware) + + @cache_page(3600) + def my_view(request): + pass + ``django.utils.encoding`` ========================= From 08d675a98f1ae1d27f7e1946125ed5316c576802 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 30 Sep 2012 20:44:27 +0200 Subject: [PATCH 085/265] Fixed #7936 -- Added Last-Modified header to feeds Thanks julianb for the report and the initial patch, and Roman Gladkov for working on tests. --- django/contrib/syndication/views.py | 9 +++++++++ tests/regressiontests/syndication/feeds.py | 8 ++++++++ tests/regressiontests/syndication/tests.py | 8 ++++++++ tests/regressiontests/syndication/urls.py | 1 + 4 files changed, 26 insertions(+) diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py index 4815ce5567..996b7dfb40 100644 --- a/django/contrib/syndication/views.py +++ b/django/contrib/syndication/views.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from calendar import timegm + from django.conf import settings from django.contrib.sites.models import get_current_site from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist @@ -8,6 +10,7 @@ from django.template import loader, TemplateDoesNotExist, RequestContext from django.utils import feedgenerator, tzinfo from django.utils.encoding import force_text, iri_to_uri, smart_text from django.utils.html import escape +from django.utils.http import http_date from django.utils.timezone import is_naive @@ -22,6 +25,7 @@ def add_domain(domain, url, secure=False): url = iri_to_uri('%s://%s%s' % (protocol, domain, url)) return url + class FeedDoesNotExist(ObjectDoesNotExist): pass @@ -38,6 +42,11 @@ class Feed(object): raise Http404('Feed object does not exist.') feedgen = self.get_feed(obj, request) response = HttpResponse(content_type=feedgen.mime_type) + if hasattr(self, 'item_pubdate'): + # if item_pubdate is defined for the feed, set header so as + # ConditionalGetMiddleware is able to send 304 NOT MODIFIED + response['Last-Modified'] = http_date( + timegm(feedgen.latest_post_date().utctimetuple())) feedgen.write(response, 'utf-8') return response diff --git a/tests/regressiontests/syndication/feeds.py b/tests/regressiontests/syndication/feeds.py index 748420c9d2..04a67f4bdb 100644 --- a/tests/regressiontests/syndication/feeds.py +++ b/tests/regressiontests/syndication/feeds.py @@ -46,6 +46,14 @@ class TestRss091Feed(TestRss2Feed): feed_type = feedgenerator.RssUserland091Feed +class TestNoPubdateFeed(views.Feed): + title = 'Test feed' + link = '/feed/' + + def items(self): + return Entry.objects.all() + + class TestAtomFeed(TestRss2Feed): feed_type = feedgenerator.Atom1Feed subtitle = TestRss2Feed.description diff --git a/tests/regressiontests/syndication/tests.py b/tests/regressiontests/syndication/tests.py index 1790d8cae6..10413b4ddd 100644 --- a/tests/regressiontests/syndication/tests.py +++ b/tests/regressiontests/syndication/tests.py @@ -226,6 +226,14 @@ class SyndicationFeedTest(FeedTestCase): updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText self.assertEqual(updated[-6:], '+00:42') + def test_feed_last_modified_time(self): + response = self.client.get('/syndication/naive-dates/') + self.assertEqual(response['Last-Modified'], 'Thu, 03 Jan 2008 19:30:00 GMT') + + # No last-modified when feed has no item_pubdate + response = self.client.get('/syndication/no_pubdate/') + self.assertFalse(response.has_header('Last-Modified')) + def test_feed_url(self): """ Test that the feed_url can be overridden. diff --git a/tests/regressiontests/syndication/urls.py b/tests/regressiontests/syndication/urls.py index 2199e29f06..57f9d81a73 100644 --- a/tests/regressiontests/syndication/urls.py +++ b/tests/regressiontests/syndication/urls.py @@ -9,6 +9,7 @@ urlpatterns = patterns('django.contrib.syndication.views', (r'^syndication/complex/(?P.*)/$', feeds.ComplexFeed()), (r'^syndication/rss2/$', feeds.TestRss2Feed()), (r'^syndication/rss091/$', feeds.TestRss091Feed()), + (r'^syndication/no_pubdate/$', feeds.TestNoPubdateFeed()), (r'^syndication/atom/$', feeds.TestAtomFeed()), (r'^syndication/custom/$', feeds.TestCustomFeed()), (r'^syndication/naive-dates/$', feeds.NaiveDatesFeed()), From 92b5341b19e9b35083ee671afc2afb2f252c662d Mon Sep 17 00:00:00 2001 From: Flavio Curella Date: Sat, 29 Sep 2012 14:45:56 +0200 Subject: [PATCH 086/265] Fixed #16455 -- Added support for PostGIS 2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks ckarrie for the report and the initial patches, Flavio Curella for updating the patch, and Anssi Kääriäinen for testing. See ticket for other valuable contributors. --- .../gis/db/backends/postgis/creation.py | 27 ++++- django/contrib/gis/tests/geoapp/tests.py | 4 +- docs/ref/contrib/gis/install.txt | 112 +++++++++++------- docs/releases/1.5.txt | 3 +- 4 files changed, 93 insertions(+), 53 deletions(-) diff --git a/django/contrib/gis/db/backends/postgis/creation.py b/django/contrib/gis/db/backends/postgis/creation.py index bad22bee70..06b60117f6 100644 --- a/django/contrib/gis/db/backends/postgis/creation.py +++ b/django/contrib/gis/db/backends/postgis/creation.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.core.exceptions import ImproperlyConfigured from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation class PostGISCreation(DatabaseCreation): @@ -38,12 +39,20 @@ class PostGISCreation(DatabaseCreation): style.SQL_FIELD(qn(f.column)) + style.SQL_KEYWORD(' SET NOT NULL') + ';') - if f.spatial_index: # Spatial indexes created the same way for both Geometry and - # Geography columns + # Geography columns. + # PostGIS 2.0 does not support GIST_GEOMETRY_OPS. So, on 1.5 + # we use GIST_GEOMETRY_OPS, on 2.0 we use either "nd" ops + # which are fast on multidimensional cases, or just plain + # gist index for the 2d case. if f.geography: index_opts = '' + elif self.connection.ops.spatial_version >= (2, 0): + if f.dim > 2: + index_opts = ' ' + style.SQL_KEYWORD('gist_geometry_ops_nd') + else: + index_opts = '' else: index_opts = ' ' + style.SQL_KEYWORD(self.geom_index_opts) output.append(style.SQL_KEYWORD('CREATE INDEX ') + @@ -56,5 +65,15 @@ class PostGISCreation(DatabaseCreation): return output def sql_table_creation_suffix(self): - qn = self.connection.ops.quote_name - return ' TEMPLATE %s' % qn(getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis')) + cursor = self.connection.cursor() + cursor.execute('SELECT datname FROM pg_database;') + db_names = [row[0] for row in cursor.fetchall()] + postgis_template = getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis') + + if postgis_template in db_names: + qn = self.connection.ops.quote_name + return ' TEMPLATE %s' % qn(postgis_template) + elif self.connection.ops.spatial_version < (2, 0): + raise ImproperlyConfigured("Template database '%s' does not exist." % postgis_template) + else: + return '' diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 7fc870f64b..952ac9d45b 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -576,8 +576,8 @@ class GeoQuerySetTest(TestCase): for c in City.objects.filter(point__isnull=False).num_geom(): # Oracle will return 1 for the number of geometries on non-collections, # whereas PostGIS will return None. - if postgis: - self.assertEqual(None, c.num_geom) + if postgis and connection.ops.spatial_version < (2, 0, 0): + self.assertIsNone(c.num_geom) else: self.assertEqual(1, c.num_geom) diff --git a/docs/ref/contrib/gis/install.txt b/docs/ref/contrib/gis/install.txt index d84ffc6b52..d66fd7ab77 100644 --- a/docs/ref/contrib/gis/install.txt +++ b/docs/ref/contrib/gis/install.txt @@ -63,7 +63,7 @@ supported versions, and any notes for each of the supported database backends: ================== ============================== ================== ========================================= Database Library Requirements Supported Versions Notes ================== ============================== ================== ========================================= -PostgreSQL GEOS, PROJ.4, PostGIS 8.1+ Requires PostGIS. +PostgreSQL GEOS, PROJ.4, PostGIS 8.2+ Requires PostGIS. MySQL GEOS 5.x Not OGC-compliant; limited functionality. Oracle GEOS 10.2, 11 XE not supported; not tested with 9. SQLite GEOS, GDAL, PROJ.4, SpatiaLite 3.6.+ Requires SpatiaLite 2.3+, pysqlite2 2.5+ @@ -88,7 +88,7 @@ Program Description Required `PROJ.4`_ Cartographic Projections library Yes (PostgreSQL and SQLite only) 4.8, 4.7, 4.6, 4.5, 4.4 :ref:`GDAL ` Geospatial Data Abstraction Library No (but, required for SQLite) 1.9, 1.8, 1.7, 1.6, 1.5 :ref:`GeoIP ` IP-based geolocation library No 1.4 -`PostGIS`__ Spatial extensions for PostgreSQL Yes (PostgreSQL only) 1.5, 1.4, 1.3 +`PostGIS`__ Spatial extensions for PostgreSQL Yes (PostgreSQL only) 2.0, 1.5, 1.4, 1.3 `SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 3.0, 2.4, 2.3 ======================== ==================================== ================================ ========================== @@ -226,45 +226,6 @@ Finally, configure, make and install PROJ.4:: $ sudo make install $ cd .. -.. _postgis: - -PostGIS -------- - -`PostGIS`__ adds geographic object support to PostgreSQL, turning it -into a spatial database. :ref:`geosbuild` and :ref:`proj4` should be -installed prior to building PostGIS. - -.. note:: - - The `psycopg2`_ module is required for use as the database adaptor - when using GeoDjango with PostGIS. - -.. _psycopg2: http://initd.org/psycopg/ - -First download the source archive, and extract:: - - $ wget http://postgis.refractions.net/download/postgis-1.5.5.tar.gz - $ tar xzf postgis-1.5.5.tar.gz - $ cd postgis-1.5.5 - -Next, configure, make and install PostGIS:: - - $ ./configure - -Finally, make and install:: - - $ make - $ sudo make install - $ cd .. - -.. note:: - - GeoDjango does not automatically create a spatial database. Please - consult the section on :ref:`spatialdb_template` for more information. - -__ http://postgis.refractions.net/ - .. _gdalbuild: GDAL @@ -364,6 +325,48 @@ file: SetEnv GDAL_DATA /usr/local/share +.. _postgis: + +PostGIS +------- + +`PostGIS`__ adds geographic object support to PostgreSQL, turning it +into a spatial database. :ref:`geosbuild`, :ref:`proj4` and +:ref:`gdalbuild` should be installed prior to building PostGIS. You +might also need additional libraries, see `PostGIS requirements`_. + +.. note:: + + The `psycopg2`_ module is required for use as the database adaptor + when using GeoDjango with PostGIS. + +.. _psycopg2: http://initd.org/psycopg/ +.. _PostGIS requirements: http://www.postgis.org/documentation/manual-2.0/postgis_installation.html#id2711662 + +First download the source archive, and extract:: + + $ wget http://postgis.refractions.net/download/postgis-2.0.1.tar.gz + $ tar xzf postgis-2.0.1.tar.gz + $ cd postgis-2.0.1 + +Next, configure, make and install PostGIS:: + + $ ./configure + +Finally, make and install:: + + $ make + $ sudo make install + $ cd .. + +.. note:: + + GeoDjango does not automatically create a spatial database. Please consult + the section on :ref:`spatialdb_template91` or + :ref:`spatialdb_template_earlier` for more information. + +__ http://postgis.refractions.net/ + .. _spatialite: SpatiaLite @@ -507,10 +510,27 @@ to build and install:: Post-installation ================= -.. _spatialdb_template: +.. _spatialdb_template91: -Creating a spatial database template for PostGIS ------------------------------------------------- +Creating a spatial database with PostGIS 2.0 and PostgreSQL 9.1 +--------------------------------------------------------------- + +PostGIS 2 includes an extension for Postgres 9.1 that can be used to enable +spatial functionality:: + + $ createdb + $ psql + > CREATE EXTENSION postgis; + > CREATE EXTENSION postgis_topology; + +.. _spatialdb_template_earlier: + +Creating a spatial database template for earlier versions +--------------------------------------------------------- + +If you have an earlier version of PostGIS or PostgreSQL, the CREATE +EXTENSION isn't available and you need to create the spatial database +using the following instructions. Creating a spatial database with PostGIS is different than normal because additional SQL must be loaded to enable spatial functionality. Because of @@ -540,7 +560,7 @@ user. For example, you can use the following to become the ``postgres`` user:: Once you're a database super user, then you may execute the following commands to create a PostGIS spatial database template:: - $ POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-1.5 + $ POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-2.0 # Creating the template spatial database. $ createdb -E UTF8 template_postgis $ createlang -d template_postgis plpgsql # Adding PLPGSQL language support. @@ -1083,7 +1103,7 @@ Afterwards, the ``/etc/init.d/postgresql-8.3`` script should be used to manage the starting and stopping of PostgreSQL. In addition, the SQL files for PostGIS are placed in a different location on -Debian 5.0 . Thus when :ref:`spatialdb_template` either: +Debian 5.0 . Thus when :ref:`spatialdb_template_earlier` either: * Create a symbolic link to these files: diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index c39592122b..41fb2882a7 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -119,7 +119,8 @@ GeoDjango * The wkb and hex properties of `GEOSGeometry` objects preserve the Z dimension. -* Support for GDAL < 1.5 has been dropped. +* Support for PostGIS 2.0 has been added and support for GDAL < 1.5 has been + dropped. Minor features ~~~~~~~~~~~~~~ From 864a0514b85637ee92d6f1ba596997195dfc7107 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 23 Sep 2012 22:53:40 +0200 Subject: [PATCH 087/265] Cared for PostGIS 2 renamed operations --- django/contrib/gis/db/backends/postgis/operations.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index 434d8719cc..18a59d1240 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -231,7 +231,6 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.distance_spheroid = prefix + 'distance_spheroid' self.envelope = prefix + 'Envelope' self.extent = prefix + 'Extent' - self.extent3d = prefix + 'Extent3D' self.force_rhr = prefix + 'ForceRHR' self.geohash = GEOHASH self.geojson = GEOJSON @@ -239,14 +238,12 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.intersection = prefix + 'Intersection' self.kml = prefix + 'AsKML' self.length = prefix + 'Length' - self.length3d = prefix + 'Length3D' self.length_spheroid = prefix + 'length_spheroid' self.makeline = prefix + 'MakeLine' self.mem_size = prefix + 'mem_size' self.num_geom = prefix + 'NumGeometries' self.num_points =prefix + 'npoints' self.perimeter = prefix + 'Perimeter' - self.perimeter3d = prefix + 'Perimeter3D' self.point_on_surface = prefix + 'PointOnSurface' self.polygonize = prefix + 'Polygonize' self.reverse = prefix + 'Reverse' @@ -259,6 +256,15 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.union = prefix + 'Union' self.unionagg = prefix + 'Union' + if version >= (2, 0, 0): + self.extent3d = prefix + '3DExtent' + self.length3d = prefix + '3DLength' + self.perimeter3d = prefix + '3DPerimeter' + else: + self.extent3d = prefix + 'Extent3D' + self.length3d = prefix + 'Length3D' + self.perimeter3d = prefix + 'Perimeter3D' + def check_aggregate_support(self, aggregate): """ Checks if the given aggregate name is supported (that is, if it's From 8bd7b598b6de1be1e3f72f3a1ee62803b1c02010 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 30 Sep 2012 23:16:14 +0200 Subject: [PATCH 088/265] Fixed #18807 -- Made 404.html and 500.html optional Thanks Aymeric Augustin for the report and Jannis Leidel for the review. --- django/views/defaults.py | 18 +++++++--- docs/intro/tutorial03.txt | 9 +++-- docs/ref/contrib/flatpages.txt | 4 +-- docs/releases/1.5.txt | 6 ++++ docs/topics/http/views.txt | 34 +++++++------------ tests/regressiontests/templates/tests.py | 4 +-- .../test_client_regress/tests.py | 9 ----- tests/regressiontests/views/tests/defaults.py | 22 ++++++++++-- tests/templates/404.html | 1 - tests/templates/500.html | 1 - 10 files changed, 58 insertions(+), 50 deletions(-) delete mode 100644 tests/templates/404.html delete mode 100644 tests/templates/500.html diff --git a/django/views/defaults.py b/django/views/defaults.py index 2bbc23321e..ec7a233ff7 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -1,6 +1,6 @@ from django import http from django.template import (Context, RequestContext, - loader, TemplateDoesNotExist) + loader, Template, TemplateDoesNotExist) from django.views.decorators.csrf import requires_csrf_token @@ -17,8 +17,13 @@ def page_not_found(request, template_name='404.html'): request_path The path of the requested URL (e.g., '/app/pages/bad_page/') """ - t = loader.get_template(template_name) # You need to create a 404.html template. - return http.HttpResponseNotFound(t.render(RequestContext(request, {'request_path': request.path}))) + try: + template = loader.get_template(template_name) + except TemplateDoesNotExist: + template = Template( + '

    Not Found

    ' + '

    The requested URL {{ request_path }} was not found on this server.

    ') + return http.HttpResponseNotFound(template.render(RequestContext(request, {'request_path': request.path}))) @requires_csrf_token @@ -29,8 +34,11 @@ def server_error(request, template_name='500.html'): Templates: :template:`500.html` Context: None """ - t = loader.get_template(template_name) # You need to create a 500.html template. - return http.HttpResponseServerError(t.render(Context({}))) + try: + template = loader.get_template(template_name) + except TemplateDoesNotExist: + return http.HttpResponseServerError('

    Server Error (500)

    ') + return http.HttpResponseServerError(template.render(Context({}))) # This can be called when CsrfViewMiddleware.process_view has not run, diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index d6f95008de..f3501026f8 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -366,11 +366,10 @@ special: It's just a normal view. You normally won't have to bother with writing 404 views. If you don't set ``handler404``, the built-in view :func:`django.views.defaults.page_not_found` -is used by default. In this case, you still have one obligation: create a -``404.html`` template in the root of your template directory. The default 404 -view will use that template for all 404 errors. If :setting:`DEBUG` is set to -``False`` (in your settings module) and if you didn't create a ``404.html`` -file, an ``Http500`` is raised instead. So remember to create a ``404.html``. +is used by default. Optionally, you can create a ``404.html`` template +in the root of your template directory. The default 404 view will then use that +template for all 404 errors when :setting:`DEBUG` is set to ``False`` (in your +settings module). A couple more things to note about 404 views: diff --git a/docs/ref/contrib/flatpages.txt b/docs/ref/contrib/flatpages.txt index 38cedc40fe..7ff9165642 100644 --- a/docs/ref/contrib/flatpages.txt +++ b/docs/ref/contrib/flatpages.txt @@ -158,9 +158,7 @@ For more on middleware, read the :doc:`middleware docs :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware` only steps in once another view has successfully produced a 404 response. If another view or middleware class attempts to produce a 404 but ends up - raising an exception instead (such as a ``TemplateDoesNotExist`` - exception if your site does not have an appropriate template to - use for HTTP 404 responses), the response will become an HTTP 500 + raising an exception instead, the response will become an HTTP 500 ("Internal Server Error") and the :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware` will not attempt to serve a flat page. diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 41fb2882a7..d87efda0af 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -185,6 +185,12 @@ Django 1.5 also includes several smaller improvements worth noting: look up permissions by using ``{% if 'someapp.someperm' in perms %}`` in templates. +* It's not required any more to have ``404.html`` and ``500.html`` templates in + the root templates directory. Django will output some basic error messages for + both situations when those templates are not found. Of course, it's still + recommended as good practice to provide those templates in order to present + pretty error pages to the user. + Backwards incompatible changes in 1.5 ===================================== diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index c4bd15e72e..7c4d1bbb6e 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -134,13 +134,12 @@ The 404 (page not found) view When you raise an ``Http404`` exception, Django loads a special view devoted to handling 404 errors. By default, it's the view -``django.views.defaults.page_not_found``, which loads and renders the template -``404.html``. +``django.views.defaults.page_not_found``, which either produces a very simple +"Not Found" message or loads and renders the template ``404.html`` if you +created it in your root template directory. -This means you need to define a ``404.html`` template in your root template -directory. This template will be used for all 404 errors. The default 404 view -will pass one variable to the template: ``request_path``, which is the URL -that resulted in the error. +The default 404 view will pass one variable to the template: ``request_path``, +which is the URL that resulted in the error. The ``page_not_found`` view should suffice for 99% of Web applications, but if you want to override it, you can specify ``handler404`` in your URLconf, like @@ -152,15 +151,11 @@ Behind the scenes, Django determines the 404 view by looking for ``handler404`` in your root URLconf, and falling back to ``django.views.defaults.page_not_found`` if you did not define one. -Four things to note about 404 views: +Three things to note about 404 views: * The 404 view is also called if Django doesn't find a match after checking every regular expression in the URLconf. -* If you don't define your own 404 view — and simply use the default, - which is recommended — you still have one obligation: you must create a - ``404.html`` template in the root of your template directory. - * The 404 view is passed a :class:`~django.template.RequestContext` and will have access to variables supplied by your :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting (e.g., ``MEDIA_URL``). @@ -176,13 +171,12 @@ The 500 (server error) view Similarly, Django executes special-case behavior in the case of runtime errors in view code. If a view results in an exception, Django will, by default, call -the view ``django.views.defaults.server_error``, which loads and renders the -template ``500.html``. +the view ``django.views.defaults.server_error``, which either produces a very +simple "Server Error" message or loads and renders the template ``500.html`` if +you created it in your root template directory. -This means you need to define a ``500.html`` template in your root template -directory. This template will be used for all server errors. The default 500 -view passes no variables to this template and is rendered with an empty -``Context`` to lessen the chance of additional errors. +The default 500 view passes no variables to the ``500.html`` template and is +rendered with an empty ``Context`` to lessen the chance of additional errors. This ``server_error`` view should suffice for 99% of Web applications, but if you want to override the view, you can specify ``handler500`` in your URLconf, @@ -194,11 +188,7 @@ Behind the scenes, Django determines the 500 view by looking for ``handler500`` in your root URLconf, and falling back to ``django.views.defaults.server_error`` if you did not define one. -Two things to note about 500 views: - -* If you don't define your own 500 view — and simply use the default, - which is recommended — you still have one obligation: you must create a - ``500.html`` template in the root of your template directory. +One thing to note about 500 views: * If :setting:`DEBUG` is set to ``True`` (in your settings module), then your 500 view will never be used, and the traceback will be displayed diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 41f40e7467..a150d1ce2a 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -229,11 +229,11 @@ class Templates(unittest.TestCase): loader.template_source_loaders = (filesystem.Loader(),) # We rely on the fact that runtests.py sets up TEMPLATE_DIRS to - # point to a directory containing a 404.html file. Also that + # point to a directory containing a login.html file. Also that # the file system and app directories loaders both inherit the # load_template method from the BaseLoader class, so we only need # to test one of them. - load_name = '404.html' + load_name = 'login.html' template = loader.get_template(load_name) template_name = template.nodelist[0].source[0].name self.assertTrue(template_name.endswith(load_name), diff --git a/tests/regressiontests/test_client_regress/tests.py b/tests/regressiontests/test_client_regress/tests.py index c741903c34..f424321663 100644 --- a/tests/regressiontests/test_client_regress/tests.py +++ b/tests/regressiontests/test_client_regress/tests.py @@ -628,15 +628,6 @@ class TemplateExceptionTests(TestCase): if hasattr(template_loader, 'reset'): template_loader.reset() - @override_settings(TEMPLATE_DIRS=(),) - def test_no_404_template(self): - "Missing templates are correctly reported by test client" - try: - response = self.client.get("/no_such_view/") - self.fail("Should get error about missing template") - except TemplateDoesNotExist: - pass - @override_settings( TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'bad_templates'),) ) diff --git a/tests/regressiontests/views/tests/defaults.py b/tests/regressiontests/views/tests/defaults.py index 2dd40b4a1a..3ca7f79136 100644 --- a/tests/regressiontests/views/tests/defaults.py +++ b/tests/regressiontests/views/tests/defaults.py @@ -1,7 +1,8 @@ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals -from django.test import TestCase from django.contrib.contenttypes.models import ContentType +from django.test import TestCase +from django.test.utils import setup_test_template_loader, restore_template_loaders from ..models import Author, Article, UrlArticle @@ -71,6 +72,23 @@ class DefaultsTests(TestCase): response = self.client.get('/views/server_error/') self.assertEqual(response.status_code, 500) + def test_custom_templates(self): + """ + Test that 404.html and 500.html templates are picked by their respective + handler. + """ + setup_test_template_loader( + {'404.html': 'This is a test template for a 404 error.', + '500.html': 'This is a test template for a 500 error.'} + ) + try: + for code, url in ((404, '/views/non_existing_url/'), (500, '/views/server_error/')): + response = self.client.get(url) + self.assertContains(response, "test template for a %d error" % code, + status_code=code) + finally: + restore_template_loaders() + def test_get_absolute_url_attributes(self): "A model can set attributes on the get_absolute_url method" self.assertTrue(getattr(UrlArticle.get_absolute_url, 'purge', False), diff --git a/tests/templates/404.html b/tests/templates/404.html deleted file mode 100644 index da627e2222..0000000000 --- a/tests/templates/404.html +++ /dev/null @@ -1 +0,0 @@ -Django Internal Tests: 404 Error \ No newline at end of file diff --git a/tests/templates/500.html b/tests/templates/500.html deleted file mode 100644 index ff028cbeb0..0000000000 --- a/tests/templates/500.html +++ /dev/null @@ -1 +0,0 @@ -Django Internal Tests: 500 Error \ No newline at end of file From 7cc4068c4470876c526830778cbdac2fdfd6dc26 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 12 Jul 2012 11:13:15 +0930 Subject: [PATCH 089/265] Fixed #18616 -- added user_login_fail signal to contrib.auth Thanks to Brad Pitcher for documentation --- django/contrib/auth/__init__.py | 23 ++++++++++++++++++++++- django/contrib/auth/signals.py | 1 + django/contrib/auth/tests/signals.py | 16 +++++++++++++++- docs/releases/1.5.txt | 4 ++++ docs/topics/auth.txt | 21 ++++++++++++++++++++- 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 1050d1d1bb..dd4a8484f5 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -1,6 +1,8 @@ +import re + from django.core.exceptions import ImproperlyConfigured from django.utils.importlib import import_module -from django.contrib.auth.signals import user_logged_in, user_logged_out +from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed SESSION_KEY = '_auth_user_id' BACKEND_SESSION_KEY = '_auth_user_backend' @@ -33,6 +35,21 @@ def get_backends(): return backends +def _clean_credentials(credentials): + """ + Cleans a dictionary of credentials of potentially sensitive info before + sending to less secure functions. + + Not comprehensive - intended for user_login_failed signal + """ + SENSITIVE_CREDENTIALS = re.compile('api|token|key|secret|password|signature', re.I) + CLEANSED_SUBSTITUTE = '********************' + for key in credentials: + if SENSITIVE_CREDENTIALS.search(key): + credentials[key] = CLEANSED_SUBSTITUTE + return credentials + + def authenticate(**credentials): """ If the given credentials are valid, return a User object. @@ -49,6 +66,10 @@ def authenticate(**credentials): user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) return user + # The credentials supplied are invalid to all backends, fire signal + user_login_failed.send(sender=__name__, + credentials=_clean_credentials(credentials)) + def login(request, user): """ diff --git a/django/contrib/auth/signals.py b/django/contrib/auth/signals.py index 4f0b2c235c..71ab6a11d1 100644 --- a/django/contrib/auth/signals.py +++ b/django/contrib/auth/signals.py @@ -1,4 +1,5 @@ from django.dispatch import Signal user_logged_in = Signal(providing_args=['request', 'user']) +user_login_failed = Signal(providing_args=['credentials']) user_logged_out = Signal(providing_args=['request', 'user']) diff --git a/django/contrib/auth/tests/signals.py b/django/contrib/auth/tests/signals.py index c597aa9ed0..024f44f547 100644 --- a/django/contrib/auth/tests/signals.py +++ b/django/contrib/auth/tests/signals.py @@ -18,27 +18,41 @@ class SignalTestCase(TestCase): def listener_logout(self, user, **kwargs): self.logged_out.append(user) + def listener_login_failed(self, sender, credentials, **kwargs): + self.login_failed.append(credentials) + def setUp(self): """Set up the listeners and reset the logged in/logged out counters""" self.logged_in = [] self.logged_out = [] + self.login_failed = [] signals.user_logged_in.connect(self.listener_login) signals.user_logged_out.connect(self.listener_logout) + signals.user_login_failed.connect(self.listener_login_failed) def tearDown(self): """Disconnect the listeners""" signals.user_logged_in.disconnect(self.listener_login) signals.user_logged_out.disconnect(self.listener_logout) + signals.user_login_failed.disconnect(self.listener_login_failed) def test_login(self): - # Only a successful login will trigger the signal. + # Only a successful login will trigger the success signal. self.client.login(username='testclient', password='bad') self.assertEqual(len(self.logged_in), 0) + self.assertEqual(len(self.login_failed), 1) + self.assertEqual(self.login_failed[0]['username'], 'testclient') + # verify the password is cleansed + self.assertTrue('***' in self.login_failed[0]['password']) + # Like this: self.client.login(username='testclient', password='password') self.assertEqual(len(self.logged_in), 1) self.assertEqual(self.logged_in[0].username, 'testclient') + # Ensure there were no more failures. + self.assertEqual(len(self.login_failed), 1) + def test_logout_anonymous(self): # The log_out function will still trigger the signal for anonymous # users. diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index d87efda0af..546170b2a8 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -191,6 +191,10 @@ Django 1.5 also includes several smaller improvements worth noting: recommended as good practice to provide those templates in order to present pretty error pages to the user. +* :mod:`django.contrib.auth` provides a new signal that is emitted + whenever a user fails to login successfully. See + :data:`~django.contrib.auth.signals.user_login_failed` + Backwards incompatible changes in 1.5 ===================================== diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index 0a19f5ed5a..421c371cc9 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -876,13 +876,15 @@ The auth framework uses two :doc:`signals ` that can be used for notification when a user logs in or out. .. data:: django.contrib.auth.signals.user_logged_in + :module: +.. versionadded:: 1.3 Sent when a user logs in successfully. Arguments sent with this signal: ``sender`` - As above: the class of the user that just logged in. + The class of the user that just logged in. ``request`` The current :class:`~django.http.HttpRequest` instance. @@ -891,6 +893,8 @@ Arguments sent with this signal: The user instance that just logged in. .. data:: django.contrib.auth.signals.user_logged_out + :module: +.. versionadded:: 1.3 Sent when the logout method is called. @@ -905,6 +909,21 @@ Sent when the logout method is called. The user instance that just logged out or ``None`` if the user was not authenticated. +.. data:: django.contrib.auth.signals.user_login_failed + :module: +.. versionadded:: 1.5 + +Sent when the user failed to login successfully + +``sender`` + The name of the module used for authentication. + +``credentials`` + A dictonary of keyword arguments containing the user credentials that were + passed to :func:`~django.contrib.auth.authenticate()` or your own custom + authentication backend. Credentials matching a set of 'sensitive' patterns, + (including password) will not be sent in the clear as part of the signal. + Limiting access to logged-in users ---------------------------------- From e7723683dc652613df369d5ca412e8b1217012d3 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sun, 30 Sep 2012 16:34:13 +0400 Subject: [PATCH 090/265] Fixed #9279 -- Added ignorenonexistent option to loaddata Thanks to Roman Gladkov for the initial patch and Simon Charette for review. --- django/core/management/commands/loaddata.py | 8 ++++- django/core/serializers/python.py | 12 +++++++- docs/ref/django-admin.txt | 5 ++++ docs/releases/1.5.txt | 3 ++ docs/topics/serialization.txt | 8 +++++ .../fixtures/sequence_extra.json | 13 +++++++++ .../regressiontests/fixtures_regress/tests.py | 29 +++++++++++++++++++ 7 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 30cf740cdf..a2e7f7d4c9 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -23,6 +23,7 @@ try: except ImportError: has_bz2 = False + class Command(BaseCommand): help = 'Installs the named fixture(s) in the database.' args = "fixture [fixture ...]" @@ -31,9 +32,14 @@ class Command(BaseCommand): make_option('--database', action='store', dest='database', default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load ' 'fixtures into. Defaults to the "default" database.'), + make_option('--ignorenonexistent', '-i', action='store_true', dest='ignore', + default=False, help='Ignores entries in the serialised data for fields' + ' that have been removed from the database'), ) def handle(self, *fixture_labels, **options): + + ignore = options.get('ignore') using = options.get('database') connection = connections[using] @@ -175,7 +181,7 @@ class Command(BaseCommand): self.stdout.write("Installing %s fixture '%s' from %s." % \ (format, fixture_name, humanize(fixture_dir))) - objects = serializers.deserialize(format, fixture, using=using) + objects = serializers.deserialize(format, fixture, using=using, ignorenonexistent=ignore) for obj in objects: objects_in_fixture += 1 diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index a1fff6f9bb..37fa906280 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -11,6 +11,7 @@ from django.db import models, DEFAULT_DB_ALIAS from django.utils.encoding import smart_text, is_protected_type from django.utils import six + class Serializer(base.Serializer): """ Serializes a QuerySet to basic Python objects. @@ -72,6 +73,7 @@ class Serializer(base.Serializer): def getvalue(self): return self.objects + def Deserializer(object_list, **options): """ Deserialize simple Python objects back into Django ORM instances. @@ -80,15 +82,23 @@ def Deserializer(object_list, **options): stream or a string) to the constructor """ db = options.pop('using', DEFAULT_DB_ALIAS) + ignore = options.pop('ignorenonexistent', False) + models.get_apps() for d in object_list: # Look up the model and starting build a dict of data for it. Model = _get_model(d["model"]) - data = {Model._meta.pk.attname : Model._meta.pk.to_python(d["pk"])} + data = {Model._meta.pk.attname: Model._meta.pk.to_python(d["pk"])} m2m_data = {} + model_fields = Model._meta.get_all_field_names() # Handle each field for (field_name, field_value) in six.iteritems(d["fields"]): + + if ignore and field_name not in model_fields: + # skip fields no longer on model + continue + if isinstance(field_value, str): field_value = smart_text(field_value, options.get("encoding", settings.DEFAULT_CHARSET), strings_only=True) diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 93e8fd9856..7fa7539985 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -289,6 +289,11 @@ Searches for and loads the contents of the named fixture into the database. The :djadminopt:`--database` option can be used to specify the database onto which the data will be loaded. +.. versionadded:: 1.5 + +The :djadminopt:`--ignorenonexistent` option can be used to ignore fields that +may have been removed from models since the fixture was originally generated. + What's a "fixture"? ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 546170b2a8..11b3488c11 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -195,6 +195,9 @@ Django 1.5 also includes several smaller improvements worth noting: whenever a user fails to login successfully. See :data:`~django.contrib.auth.signals.user_login_failed` +* The loaddata management command now supports an `ignorenonexistent` option to + ignore data for fields that no longer exist. + Backwards incompatible changes in 1.5 ===================================== diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt index ac1a77ed98..9b44166e42 100644 --- a/docs/topics/serialization.txt +++ b/docs/topics/serialization.txt @@ -130,6 +130,14 @@ trust your data source you could just save the object and move on. The Django object itself can be inspected as ``deserialized_object.object``. +.. versionadded:: 1.5 + +If fields in the serialized data do not exist on a model, +a ``DeserializationError`` will be raised unless the ``ignorenonexistent`` +argument is passed in as True:: + + serializers.deserialize("xml", data, ignorenonexistent=True) + .. _serialization-formats: Serialization formats diff --git a/tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json b/tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json new file mode 100644 index 0000000000..03c0f36696 --- /dev/null +++ b/tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json @@ -0,0 +1,13 @@ +[ + { + "pk": "1", + "model": "fixtures_regress.animal", + "fields": { + "name": "Lion", + "extra_name": "Super Lion", + "latin_name": "Panthera leo", + "count": 3, + "weight": 1.2 + } + } +] diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py index d675372c7a..c9b9058dff 100644 --- a/tests/regressiontests/fixtures_regress/tests.py +++ b/tests/regressiontests/fixtures_regress/tests.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, unicode_literals import os import re +from django.core.serializers.base import DeserializationError from django.core import management from django.core.management.base import CommandError from django.core.management.commands.dumpdata import sort_dependencies @@ -22,6 +23,7 @@ from .models import (Animal, Stuff, Absolute, Parent, Child, Article, Widget, class TestFixtures(TestCase): + def animal_pre_save_check(self, signal, sender, instance, **kwargs): self.pre_save_checks.append( ( @@ -54,6 +56,33 @@ class TestFixtures(TestCase): animal.save() self.assertGreater(animal.id, 1) + def test_loaddata_not_found_fields_not_ignore(self): + """ + Test for ticket #9279 -- Error is raised for entries in + the serialised data for fields that have been removed + from the database when not ignored. + """ + with self.assertRaises(DeserializationError): + management.call_command( + 'loaddata', + 'sequence_extra', + verbosity=0 + ) + + def test_loaddata_not_found_fields_ignore(self): + """ + Test for ticket #9279 -- Ignores entries in + the serialised data for fields that have been removed + from the database. + """ + management.call_command( + 'loaddata', + 'sequence_extra', + ignore=True, + verbosity=0 + ) + self.assertEqual(Animal.specimens.all()[0].name, 'Lion') + @skipIfDBFeature('interprets_empty_strings_as_nulls') def test_pretty_print_xml(self): """ From 1ce4aedcefb68086918adc4137d75a6f2c0bd1f2 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 1 Oct 2012 14:17:55 +0200 Subject: [PATCH 091/265] Prevented flatpage view from directly accessing settings.SITE_ID Refs #15089 --- django/contrib/flatpages/views.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py index 0b462ac5a4..497979e497 100644 --- a/django/contrib/flatpages/views.py +++ b/django/contrib/flatpages/views.py @@ -1,9 +1,10 @@ -from django.contrib.flatpages.models import FlatPage -from django.template import loader, RequestContext -from django.shortcuts import get_object_or_404 -from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect from django.conf import settings +from django.contrib.flatpages.models import FlatPage +from django.contrib.sites.models import get_current_site from django.core.xheaders import populate_xheaders +from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect +from django.shortcuts import get_object_or_404 +from django.template import loader, RequestContext from django.utils.safestring import mark_safe from django.views.decorators.csrf import csrf_protect @@ -30,14 +31,15 @@ def flatpage(request, url): """ if not url.startswith('/'): url = '/' + url + site_id = get_current_site(request).id try: f = get_object_or_404(FlatPage, - url__exact=url, sites__id__exact=settings.SITE_ID) + url__exact=url, sites__id__exact=site_id) except Http404: if not url.endswith('/') and settings.APPEND_SLASH: url += '/' f = get_object_or_404(FlatPage, - url__exact=url, sites__id__exact=settings.SITE_ID) + url__exact=url, sites__id__exact=site_id) return HttpResponsePermanentRedirect('%s/' % request.path) else: raise From 14681eaa53a035fa1c8fc390d5cca0b340fdcdb3 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Mon, 1 Oct 2012 09:00:41 -0700 Subject: [PATCH 092/265] Fixed #19045 -- removed 'fixed on a branch' from triage docs --- docs/internals/contributing/triaging-tickets.txt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index ab879e5caf..84f70fd731 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -171,15 +171,6 @@ concrete actionable issues. They are enhancement requests that we might consider adding someday to the framework if an excellent patch is submitted. These tickets are not a high priority. -Fixed on a branch -~~~~~~~~~~~~~~~~~ - -Used to indicate that a ticket is resolved as part of a major body of work -that will eventually be merged to trunk. Tickets in this stage generally -don't need further work. This may happen in the case of major -features/refactors in each release cycle, or as part of the annual Google -Summer of Code efforts. - Other triage attributes ----------------------- From 55fa86a97f399df117d0de5f19d97dc72185c556 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Mon, 1 Oct 2012 11:36:16 -0700 Subject: [PATCH 093/265] Fixed postgres rollback issue with fixture test --- tests/regressiontests/fixtures_regress/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py index c9b9058dff..678db4a9cc 100644 --- a/tests/regressiontests/fixtures_regress/tests.py +++ b/tests/regressiontests/fixtures_regress/tests.py @@ -79,7 +79,8 @@ class TestFixtures(TestCase): 'loaddata', 'sequence_extra', ignore=True, - verbosity=0 + verbosity=0, + commit=False ) self.assertEqual(Animal.specimens.all()[0].name, 'Lion') From 2aac3ce0c6d944093f5c32e170c9a0d90e8e1f94 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Mon, 1 Oct 2012 14:46:12 -0700 Subject: [PATCH 094/265] Cleaned up loaddata command options help text --- django/core/management/commands/loaddata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index a2e7f7d4c9..32ae8abf5a 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -33,8 +33,8 @@ class Command(BaseCommand): default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load ' 'fixtures into. Defaults to the "default" database.'), make_option('--ignorenonexistent', '-i', action='store_true', dest='ignore', - default=False, help='Ignores entries in the serialised data for fields' - ' that have been removed from the database'), + default=False, help='Ignores entries in the serialized data for fields' + ' that do not currently exist on the model.'), ) def handle(self, *fixture_labels, **options): From 030b55393ea1164080a25eb6e241e43918bae8ac Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Mon, 1 Oct 2012 14:53:21 -0700 Subject: [PATCH 095/265] Removed incorrectly reintroduced 1.3 version notes --- docs/topics/auth.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index 421c371cc9..f9c9057baa 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -877,7 +877,6 @@ for notification when a user logs in or out. .. data:: django.contrib.auth.signals.user_logged_in :module: -.. versionadded:: 1.3 Sent when a user logs in successfully. @@ -894,7 +893,6 @@ Arguments sent with this signal: .. data:: django.contrib.auth.signals.user_logged_out :module: -.. versionadded:: 1.3 Sent when the logout method is called. From 4c75344cc1d3c74ed73b7a8d6aab92a173afe8f5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 2 Oct 2012 16:04:12 +0800 Subject: [PATCH 096/265] Fixed #19056 -- Ensure admin change password template doesn't rely on username attribute. --- .../admin/templates/admin/auth/user/change_password.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index f367223bc5..b5a7715844 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -28,7 +28,7 @@

    {% endif %} -

    {% blocktrans with username=original.username %}Enter a new password for the user {{ username }}.{% endblocktrans %}

    +

    {% blocktrans with username=original %}Enter a new password for the user {{ username }}.{% endblocktrans %}

    From 5f8b97f9fb058e5e02f1f99423fc3b0020ecdeb0 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Tue, 2 Oct 2012 04:19:44 -0700 Subject: [PATCH 097/265] Fixed #19057 -- support custom user models in mod_wsgi auth handler thanks @freakboy3742 for the catch and review --- django/contrib/auth/handlers/modwsgi.py | 25 ++++++++++++++++------ django/contrib/auth/tests/handlers.py | 7 +++++- docs/howto/deployment/wsgi/apache-auth.txt | 8 +++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/django/contrib/auth/handlers/modwsgi.py b/django/contrib/auth/handlers/modwsgi.py index 0e543ef368..3229c6714b 100644 --- a/django/contrib/auth/handlers/modwsgi.py +++ b/django/contrib/auth/handlers/modwsgi.py @@ -1,4 +1,4 @@ -from django.contrib.auth.models import User +from django.contrib import auth from django import db from django.utils.encoding import force_bytes @@ -11,14 +11,21 @@ def check_password(environ, username, password): on whether the user exists and authenticates. """ + UserModel = auth.get_user_model() # db connection state is managed similarly to the wsgi handler # as mod_wsgi may call these functions outside of a request/response cycle db.reset_queries() try: try: - user = User.objects.get(username=username, is_active=True) - except User.DoesNotExist: + user = UserModel.objects.get_by_natural_key(username) + except UserModel.DoesNotExist: + return None + try: + if not user.is_active: + return None + except AttributeError as e: + # a custom user may not support is_active return None return user.check_password(password) finally: @@ -30,14 +37,20 @@ def groups_for_user(environ, username): Authorizes a user based on groups """ + UserModel = auth.get_user_model() db.reset_queries() try: try: - user = User.objects.get(username=username, is_active=True) - except User.DoesNotExist: + user = UserModel.objects.get_by_natural_key(username) + except UserModel.DoesNotExist: + return [] + try: + if not user.is_active: + return [] + except AttributeError as e: + # a custom user may not support is_active return [] - return [force_bytes(group.name) for group in user.groups.all()] finally: db.close_connection() diff --git a/django/contrib/auth/tests/handlers.py b/django/contrib/auth/tests/handlers.py index 190fcee9fa..a867aae47a 100644 --- a/django/contrib/auth/tests/handlers.py +++ b/django/contrib/auth/tests/handlers.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user from django.contrib.auth.models import User, Group +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TransactionTestCase @@ -13,7 +14,6 @@ class ModWsgiHandlerTestCase(TransactionTestCase): def setUp(self): user1 = User.objects.create_user('test', 'test@example.com', 'test') User.objects.create_user('test1', 'test1@example.com', 'test1') - group = Group.objects.create(name='test_group') user1.groups.add(group) @@ -21,6 +21,10 @@ class ModWsgiHandlerTestCase(TransactionTestCase): """ Verify that check_password returns the correct values as per http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider + + because the custom user available in the test framework does not + support the is_active attribute, we can't test this with a custom + user. """ # User not in database @@ -32,6 +36,7 @@ class ModWsgiHandlerTestCase(TransactionTestCase): # Valid user with incorrect password self.assertFalse(check_password({}, 'test', 'incorrect')) + @skipIfCustomUser def test_groups_for_user(self): """ Check that groups_for_user returns correct values as per diff --git a/docs/howto/deployment/wsgi/apache-auth.txt b/docs/howto/deployment/wsgi/apache-auth.txt index d6594d194f..5f700f1cb3 100644 --- a/docs/howto/deployment/wsgi/apache-auth.txt +++ b/docs/howto/deployment/wsgi/apache-auth.txt @@ -14,6 +14,14 @@ version >= 2.2 and mod_wsgi >= 2.0. For example, you could: * Allow certain users to connect to a WebDAV share created with mod_dav_. +.. note:: + If you have installed a :ref:`custom User model ` and + want to use this default auth handler, it must support an `is_active` + attribute. If you want to use group based authorization, your custom user + must have a relation named 'groups', referring to a related object that has + a 'name' field. You can also specify your own custom mod_wsgi + auth handler if your custom cannot conform to these requirements. + .. _Subversion: http://subversion.tigris.org/ .. _mod_dav: http://httpd.apache.org/docs/2.2/mod/mod_dav.html From 3b6f980bedbbf091fe29bececa2b262d2084ce4d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 2 Oct 2012 22:52:45 +0800 Subject: [PATCH 098/265] Fixed #19049 -- Ensure that swapped models aren't included in reverse field caches. Thanks to Ivan Virabyan for the report. --- django/core/management/validation.py | 25 ++++++++++--------- django/db/models/fields/related.py | 8 +++--- django/db/models/options.py | 24 ++++++++++-------- .../invalid_models/invalid_models/models.py | 15 +++++++++++ tests/modeltests/invalid_models/tests.py | 2 +- 5 files changed, 47 insertions(+), 27 deletions(-) diff --git a/django/core/management/validation.py b/django/core/management/validation.py index fa3edb4430..a9b0c26062 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -23,7 +23,6 @@ def get_validation_errors(outfile, app=None): validates all models of all installed apps. Writes errors, if any, to outfile. Returns number of errors. """ - from django.conf import settings from django.db import models, connection from django.db.models.loading import get_app_errors from django.db.models.fields.related import RelatedObject @@ -37,7 +36,19 @@ def get_validation_errors(outfile, app=None): for cls in models.get_models(app): opts = cls._meta - # Do field-specific validation. + # Check swappable attribute. + if opts.swapped: + try: + app_label, model_name = opts.swapped.split('.') + except ValueError: + e.add(opts, "%s is not of the form 'app_label.app_name'." % opts.swappable) + continue + if not models.get_model(app_label, model_name): + e.add(opts, "Model has been swapped out for '%s' which has not been installed or is abstract." % opts.swapped) + # No need to perform any other validation checks on a swapped model. + continue + + # Model isn't swapped; do field-specific validation. for f in opts.local_fields: if f.name == 'id' and not f.primary_key and opts.pk.name == 'id': e.add(opts, '"%s": You can\'t use "id" as a field name, because each model automatically gets an "id" field if none of the fields have primary_key=True. You need to either remove/rename your "id" field or add primary_key=True to a field.' % f.name) @@ -285,16 +296,6 @@ def get_validation_errors(outfile, app=None): if r.get_accessor_name() == rel_query_name: e.add(opts, "Reverse query name for m2m field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) - # Check swappable attribute. - if opts.swapped: - try: - app_label, model_name = opts.swapped.split('.') - except ValueError: - e.add(opts, "%s is not of the form 'app_label.app_name'." % opts.swappable) - continue - if not models.get_model(app_label, model_name): - e.add(opts, "Model has been swapped out for '%s' which has not been installed or is abstract." % opts.swapped) - # Check ordering attribute. if opts.ordering: for field_name in opts.ordering: diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index c065162aa0..2c35411d7c 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1025,8 +1025,8 @@ class ForeignKey(RelatedField, Field): def contribute_to_related_class(self, cls, related): # Internal FK's - i.e., those with a related name ending with '+' - - # don't get a related descriptor. - if not self.rel.is_hidden(): + # and swapped models don't get a related descriptor. + if not self.rel.is_hidden() and related.model._meta.swapped: setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) if self.rel.limit_choices_to: cls._meta.related_fkey_lookups.append(self.rel.limit_choices_to) @@ -1265,8 +1265,8 @@ class ManyToManyField(RelatedField, Field): def contribute_to_related_class(self, cls, related): # Internal M2Ms (i.e., those with a related name ending with '+') - # don't get a related descriptor. - if not self.rel.is_hidden(): + # and swapped models don't get a related descriptor. + if not self.rel.is_hidden() and not related.model._meta.swapped: setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related)) # Set up the accessors for the column names on the m2m table diff --git a/django/db/models/options.py b/django/db/models/options.py index d2de96ea5c..d471bba262 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -418,13 +418,14 @@ class Options(object): # Collect also objects which are in relation to some proxy child/parent of self. proxy_cache = cache.copy() for klass in get_models(include_auto_created=True, only_installed=False): - for f in klass._meta.local_fields: - if f.rel and not isinstance(f.rel.to, six.string_types): - if self == f.rel.to._meta: - cache[RelatedObject(f.rel.to, klass, f)] = None - proxy_cache[RelatedObject(f.rel.to, klass, f)] = None - elif self.concrete_model == f.rel.to._meta.concrete_model: - proxy_cache[RelatedObject(f.rel.to, klass, f)] = None + if not klass._meta.swapped: + for f in klass._meta.local_fields: + if f.rel and not isinstance(f.rel.to, six.string_types): + if self == f.rel.to._meta: + cache[RelatedObject(f.rel.to, klass, f)] = None + proxy_cache[RelatedObject(f.rel.to, klass, f)] = None + elif self.concrete_model == f.rel.to._meta.concrete_model: + proxy_cache[RelatedObject(f.rel.to, klass, f)] = None self._related_objects_cache = cache self._related_objects_proxy_cache = proxy_cache @@ -460,9 +461,12 @@ class Options(object): else: cache[obj] = model for klass in get_models(only_installed=False): - for f in klass._meta.local_many_to_many: - if f.rel and not isinstance(f.rel.to, six.string_types) and self == f.rel.to._meta: - cache[RelatedObject(f.rel.to, klass, f)] = None + if not klass._meta.swapped: + for f in klass._meta.local_many_to_many: + if (f.rel + and not isinstance(f.rel.to, six.string_types) + and self == f.rel.to._meta): + cache[RelatedObject(f.rel.to, klass, f)] = None if app_cache_ready(): self._related_many_to_many_cache = cache return cache diff --git a/tests/modeltests/invalid_models/invalid_models/models.py b/tests/modeltests/invalid_models/invalid_models/models.py index 3f95d314e3..ccb6396352 100644 --- a/tests/modeltests/invalid_models/invalid_models/models.py +++ b/tests/modeltests/invalid_models/invalid_models/models.py @@ -303,13 +303,28 @@ class SwappedModel(models.Model): References to this model *should* raise a validation error. Requires TEST_SWAPPED_MODEL to be defined in the test environment; this is guaranteed by the test runner using @override_settings. + + The foreign keys and m2m relations on this model *shouldn't* + install related accessors, so there shouldn't be clashes with + the equivalent names on the replacement. """ name = models.CharField(max_length=100) + foreign = models.ForeignKey(Target, related_name='swappable_fk_set') + m2m = models.ManyToManyField(Target, related_name='swappable_m2m_set') + class Meta: swappable = 'TEST_SWAPPED_MODEL' +class ReplacementModel(models.Model): + """A replacement model for swapping purposes.""" + name = models.CharField(max_length=100) + + foreign = models.ForeignKey(Target, related_name='swappable_fk_set') + m2m = models.ManyToManyField(Target, related_name='swappable_m2m_set') + + class BadSwappableValue(models.Model): """A model that can be swapped out; during testing, the swappable value is not of the format app.model diff --git a/tests/modeltests/invalid_models/tests.py b/tests/modeltests/invalid_models/tests.py index 6050a20880..5f6224c45d 100644 --- a/tests/modeltests/invalid_models/tests.py +++ b/tests/modeltests/invalid_models/tests.py @@ -37,7 +37,7 @@ class InvalidModelTestCase(unittest.TestCase): # easier to set this up as an override than to require every developer # to specify a value in their test settings. @override_settings( - TEST_SWAPPED_MODEL='invalid_models.Target', + TEST_SWAPPED_MODEL='invalid_models.ReplacementModel', TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model', TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target', ) From 43530384b7f2a18b44e06e7043b988427b334653 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 3 Oct 2012 09:14:55 +0800 Subject: [PATCH 099/265] Fixed #19049 -- Corrected dumb logic negation error from earlier patch. --- django/db/models/fields/related.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 2c35411d7c..157640c0e3 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1026,7 +1026,7 @@ class ForeignKey(RelatedField, Field): def contribute_to_related_class(self, cls, related): # Internal FK's - i.e., those with a related name ending with '+' - # and swapped models don't get a related descriptor. - if not self.rel.is_hidden() and related.model._meta.swapped: + if not self.rel.is_hidden() and not related.model._meta.swapped: setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) if self.rel.limit_choices_to: cls._meta.related_fkey_lookups.append(self.rel.limit_choices_to) From 934f35f1f958f48a36eff64b13e43a9d55a476e0 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 3 Oct 2012 09:16:33 +0800 Subject: [PATCH 100/265] Corrected test docstring. --- django/contrib/auth/tests/management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index 60c05a0255..18b499ef00 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -152,7 +152,7 @@ class CreatesuperuserManagementCommandTestCase(TestCase): @override_settings(AUTH_USER_MODEL='auth.CustomUser') def test_swappable_user_missing_required_field(self): - "A superuser can be created when a custom User model is in use" + "A Custom superuser won't be created when a required field isn't provided" # We can use the management command to create a superuser # We skip validation because the temporary substitution of the # swappable User model messes with validation. From c76877c1d2985e37c3c988d107c44e829fb118af Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 3 Oct 2012 12:56:15 +0200 Subject: [PATCH 101/265] Added a note about postgis_topology in install docs Thanks Paolo Corti for the suggestion. --- docs/ref/contrib/gis/install.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ref/contrib/gis/install.txt b/docs/ref/contrib/gis/install.txt index d66fd7ab77..20bec32a8d 100644 --- a/docs/ref/contrib/gis/install.txt +++ b/docs/ref/contrib/gis/install.txt @@ -523,6 +523,9 @@ spatial functionality:: > CREATE EXTENSION postgis; > CREATE EXTENSION postgis_topology; +No PostGIS topology functionalities are yet available from GeoDjango, so the +creation of the ``postgis_topology`` extension is entirely optional. + .. _spatialdb_template_earlier: Creating a spatial database template for earlier versions From d25a599dca9b7644b238f6c89a66545dfdabe393 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 3 Oct 2012 13:32:26 +0200 Subject: [PATCH 102/265] Fixed #19063 -- Fixed version parameter of gml GeoQuerySet method Thanks lmisek@go2.pl for the report. --- django/contrib/gis/db/models/query.py | 4 ++-- django/contrib/gis/tests/geoapp/tests.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index b994252002..2a8de4cde3 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -194,9 +194,9 @@ class GeoQuerySet(QuerySet): # PostGIS AsGML() aggregate function parameter order depends on the # version -- uggh. if backend.spatial_version > (1, 3, 1): - procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s' + s['procedure_fmt'] = '%(version)s,%(geo_col)s,%(precision)s' else: - procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s' + s['procedure_fmt'] = '%(geo_col)s,%(precision)s,%(version)s' s['procedure_args'] = {'precision' : precision, 'version' : version} return self._spatial_attribute('gml', s, **kwargs) diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 952ac9d45b..7d8121425a 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -529,6 +529,9 @@ class GeoQuerySetTest(TestCase): for ptown in [ptown1, ptown2]: self.assertTrue(gml_regex.match(ptown.gml)) + if postgis: + self.assertIn('', City.objects.gml(version=3).get(name='Pueblo').gml) + def test_kml(self): "Testing KML output from the database using GeoQuerySet.kml()." # Only PostGIS and Spatialite (>=2.4.0-RC4) support KML serialization From 218abcc9e550d266a9979e10f562fc21b8f34c6a Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Wed, 3 Oct 2012 19:50:12 +0300 Subject: [PATCH 103/265] Fixed #14567 -- Made ModelMultipleChoiceField return EmptyQuerySet as empty value --- django/forms/models.py | 2 +- docs/ref/forms/fields.txt | 8 +++++-- docs/releases/1.5.txt | 3 +++ tests/modeltests/model_forms/tests.py | 5 +++-- tests/regressiontests/forms/models.py | 6 ++++++ tests/regressiontests/forms/tests/models.py | 23 +++++++++++++++++++-- 6 files changed, 40 insertions(+), 7 deletions(-) diff --git a/django/forms/models.py b/django/forms/models.py index 1aa49eaaec..11fe0c09ea 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -1013,7 +1013,7 @@ class ModelMultipleChoiceField(ModelChoiceField): if self.required and not value: raise ValidationError(self.error_messages['required']) elif not self.required and not value: - return [] + return self.queryset.none() if not isinstance(value, (list, tuple)): raise ValidationError(self.error_messages['list']) key = self.to_field_name or 'pk' diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 82a3ea9ab3..27ca002312 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -997,13 +997,17 @@ objects (in the case of ``ModelMultipleChoiceField``) into the .. class:: ModelMultipleChoiceField(**kwargs) * Default widget: ``SelectMultiple`` - * Empty value: ``[]`` (an empty list) - * Normalizes to: A list of model instances. + * Empty value: An empty ``QuerySet`` (self.queryset.none()) + * Normalizes to: A ``QuerySet`` of model instances. * Validates that every id in the given list of values exists in the queryset. * Error message keys: ``required``, ``list``, ``invalid_choice``, ``invalid_pk_value`` + .. versionchanged:: 1.5 + The empty and normalized values were changed to be consistently + ``QuerySets`` instead of ``[]`` and ``QuerySet`` respectively. + Allows the selection of one or more model objects, suitable for representing a many-to-many relation. As with :class:`ModelChoiceField`, you can use ``label_from_instance`` to customize the object diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 11b3488c11..d87ec36204 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -422,6 +422,9 @@ on the form. Miscellaneous ~~~~~~~~~~~~~ +* :class:`django.forms.ModelMultipleChoiceField` now returns an empty + ``QuerySet`` as the empty value instead of an empty list. + * :func:`~django.utils.http.int_to_base36` properly raises a :exc:`TypeError` instead of :exc:`ValueError` for non-integer inputs. diff --git a/tests/modeltests/model_forms/tests.py b/tests/modeltests/model_forms/tests.py index 038ce32287..947d0cf3c3 100644 --- a/tests/modeltests/model_forms/tests.py +++ b/tests/modeltests/model_forms/tests.py @@ -8,6 +8,7 @@ from django import forms from django.core.files.uploadedfile import SimpleUploadedFile from django.core.validators import ValidationError from django.db import connection +from django.db.models.query import EmptyQuerySet from django.forms.models import model_to_dict from django.utils.unittest import skipUnless from django.test import TestCase @@ -1035,8 +1036,8 @@ class OldFormForXTests(TestCase): f.clean([c6.id]) f = forms.ModelMultipleChoiceField(Category.objects.all(), required=False) - self.assertEqual(f.clean([]), []) - self.assertEqual(f.clean(()), []) + self.assertIsInstance(f.clean([]), EmptyQuerySet) + self.assertIsInstance(f.clean(()), EmptyQuerySet) with self.assertRaises(ValidationError): f.clean(['10']) with self.assertRaises(ValidationError): diff --git a/tests/regressiontests/forms/models.py b/tests/regressiontests/forms/models.py index 2f3ee9fa31..6e9c269356 100644 --- a/tests/regressiontests/forms/models.py +++ b/tests/regressiontests/forms/models.py @@ -63,6 +63,12 @@ class ChoiceFieldModel(models.Model): multi_choice_int = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='multi_choice_int', default=lambda: [1]) +class OptionalMultiChoiceModel(models.Model): + multi_choice = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='not_relevant', + default=lambda: ChoiceOptionModel.objects.filter(name='default')) + multi_choice_optional = models.ManyToManyField(ChoiceOptionModel, blank=True, null=True, + related_name='not_relevant2') + class FileModel(models.Model): file = models.FileField(storage=temp_storage, upload_to='tests') diff --git a/tests/regressiontests/forms/tests/models.py b/tests/regressiontests/forms/tests/models.py index c351509cee..be75643b28 100644 --- a/tests/regressiontests/forms/tests/models.py +++ b/tests/regressiontests/forms/tests/models.py @@ -11,7 +11,7 @@ from django.test import TestCase from django.utils import six from ..models import (ChoiceOptionModel, ChoiceFieldModel, FileModel, Group, - BoundaryModel, Defaults) + BoundaryModel, Defaults, OptionalMultiChoiceModel) class ChoiceFieldForm(ModelForm): @@ -19,6 +19,11 @@ class ChoiceFieldForm(ModelForm): model = ChoiceFieldModel +class OptionalMultiChoiceModelForm(ModelForm): + class Meta: + model = OptionalMultiChoiceModel + + class FileForm(Form): file1 = FileField() @@ -34,6 +39,21 @@ class TestTicket12510(TestCase): field = ModelChoiceField(Group.objects.order_by('-name')) self.assertEqual('a', field.clean(self.groups[0].pk).name) + +class TestTicket14567(TestCase): + """ + Check that the return values of ModelMultipleChoiceFields are QuerySets + """ + def test_empty_queryset_return(self): + "If a model's ManyToManyField has blank=True and is saved with no data, a queryset is returned." + form = OptionalMultiChoiceModelForm({'multi_choice_optional': '', 'multi_choice': ['1']}) + self.assertTrue(form.is_valid()) + # Check that the empty value is a QuerySet + self.assertTrue(isinstance(form.cleaned_data['multi_choice_optional'], models.query.QuerySet)) + # While we're at it, test whether a QuerySet is returned if there *is* a value. + self.assertTrue(isinstance(form.cleaned_data['multi_choice'], models.query.QuerySet)) + + class ModelFormCallableModelDefault(TestCase): def test_no_empty_option(self): "If a model's ForeignKey has blank=False and a default, no empty option is created (Refs #10792)." @@ -103,7 +123,6 @@ class ModelFormCallableModelDefault(TestCase): Hold down "Control", or "Command" on a Mac, to select more than one.

    """) - class FormsModelTestCase(TestCase): def test_unicode_filename(self): # FileModel with unicode filename and data ######################### From 8c427448d53ec0d860e1669f35deed73d0240ba1 Mon Sep 17 00:00:00 2001 From: Mateusz Haligowski Date: Wed, 3 Oct 2012 22:51:11 +0300 Subject: [PATCH 104/265] Fixed #15915 -- Cleaned handling of duplicate permission codenames Previously, a duplicate model, codename for permission would lead to database integrity error. Cleaned the implementation so that this case now raises an CommandError instead. --- django/contrib/auth/management/__init__.py | 41 +++++++++++++++++++--- django/contrib/auth/tests/management.py | 41 ++++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index 2ada789cae..b5fd29a1c2 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -9,6 +9,7 @@ import unicodedata from django.contrib.auth import models as auth_app, get_user_model from django.core import exceptions +from django.core.management.base import CommandError from django.db.models import get_models, signals from django.utils import six from django.utils.six.moves import input @@ -18,13 +19,43 @@ def _get_permission_codename(action, opts): return '%s_%s' % (action, opts.object_name.lower()) -def _get_all_permissions(opts): - "Returns (codename, name) for all permissions in the given opts." +def _get_all_permissions(opts, ctype): + """ + Returns (codename, name) for all permissions in the given opts. + """ + builtin = _get_builtin_permissions(opts) + custom = list(opts.permissions) + _check_permission_clashing(custom, builtin, ctype) + return builtin + custom + +def _get_builtin_permissions(opts): + """ + Returns (codename, name) for all autogenerated permissions. + """ perms = [] for action in ('add', 'change', 'delete'): - perms.append((_get_permission_codename(action, opts), 'Can %s %s' % (action, opts.verbose_name_raw))) - return perms + list(opts.permissions) + perms.append((_get_permission_codename(action, opts), + 'Can %s %s' % (action, opts.verbose_name_raw))) + return perms +def _check_permission_clashing(custom, builtin, ctype): + """ + Check that permissions for a model do not clash. Raises CommandError if + there are duplicate permissions. + """ + pool = set() + builtin_codenames = set(p[0] for p in builtin) + for codename, _name in custom: + if codename in pool: + raise CommandError( + "The permission codename '%s' is duplicated for model '%s.%s'." % + (codename, ctype.app_label, ctype.model_class().__name__)) + elif codename in builtin_codenames: + raise CommandError( + "The permission codename '%s' clashes with a builtin permission " + "for model '%s.%s'." % + (codename, ctype.app_label, ctype.model_class().__name__)) + pool.add(codename) def create_permissions(app, created_models, verbosity, **kwargs): from django.contrib.contenttypes.models import ContentType @@ -39,7 +70,7 @@ def create_permissions(app, created_models, verbosity, **kwargs): for klass in app_models: ctype = ContentType.objects.get_for_model(klass) ctypes.add(ctype) - for perm in _get_all_permissions(klass._meta): + for perm in _get_all_permissions(klass._meta, ctype): searched_perms.append((ctype, perm)) # Find all the Permissions that have a context_type for a model we're diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index 18b499ef00..cab7b20f20 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from datetime import date from django.contrib.auth import models, management +from django.contrib.auth.management import create_permissions from django.contrib.auth.management.commands import changepassword from django.contrib.auth.models import User from django.contrib.auth.tests import CustomUser @@ -167,3 +168,43 @@ class CreatesuperuserManagementCommandTestCase(TestCase): ) self.assertEqual(CustomUser.objects.count(), 0) + + +class PermissionDuplicationTestCase(TestCase): + + def setUp(self): + self._original_user_permission = models.User._meta.permissions + + def tearUp(self): + models.User._meta.permissions = self._original_user_permissions + + def test_duplicated_permissions(self): + """ + Test that we show proper error message if we are trying to create + duplicate permissions. + """ + # check duplicated default permission + models.Permission._meta.permissions = [ + ('change_permission', 'Can edit permission (duplicate)')] + self.assertRaisesRegexp(CommandError, + "The permission codename 'change_permission' clashes with a " + "builtin permission for model 'auth.Permission'.", + create_permissions, models, [], verbosity=0) + + # check duplicated custom permissions + models.Permission._meta.permissions = [ + ('my_custom_permission', 'Some permission'), + ('other_one', 'Some other permission'), + ('my_custom_permission', 'Some permission with duplicate permission code'), + ] + self.assertRaisesRegexp(CommandError, + "The permission codename 'my_custom_permission' is duplicated for model " + "'auth.Permission'.", + create_permissions, models, [], verbosity=0) + + # should not raise anything + models.Permission._meta.permissions = [ + ('my_custom_permission', 'Some permission'), + ('other_one', 'Some other permission'), + ] + create_permissions(models, [], verbosity=0) From 1c03b23567a3098b9ab5df64b14e0dea8d1414ea Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 3 Oct 2012 06:58:16 -0400 Subject: [PATCH 105/265] Fixed #18413 - Noted that a model's files are not deleted when the model is deleted. Thanks lawgon for the report. --- docs/ref/models/fields.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 4797e8b26b..02d8453b83 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -668,6 +668,11 @@ the field. Note: This method will close the file if it happens to be open when The optional ``save`` argument controls whether or not the instance is saved after the file has been deleted. Defaults to ``True``. +Note that when a model is deleted, related files are not deleted. If you need +to cleanup orphaned files, you'll need to handle it yourself (for instance, +with a custom management command that can be run manually or scheduled to run +periodically via e.g. cron). + ``FilePathField`` ----------------- From 234ca6c61d27d1cd430a5290ff858c25afb93098 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 3 Oct 2012 14:43:36 -0400 Subject: [PATCH 106/265] Fixed #19006 - Quoted filenames in Content-Disposition header. --- docs/howto/outputting-csv.txt | 4 ++-- docs/howto/outputting-pdf.txt | 6 +++--- docs/ref/request-response.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/howto/outputting-csv.txt b/docs/howto/outputting-csv.txt index 1a606069b8..bcc6f3827b 100644 --- a/docs/howto/outputting-csv.txt +++ b/docs/howto/outputting-csv.txt @@ -21,7 +21,7 @@ Here's an example:: def some_view(request): # Create the HttpResponse object with the appropriate CSV header. response = HttpResponse(mimetype='text/csv') - response['Content-Disposition'] = 'attachment; filename=somefilename.csv' + response['Content-Disposition'] = 'attachment; filename="somefilename.csv"' writer = csv.writer(response) writer.writerow(['First row', 'Foo', 'Bar', 'Baz']) @@ -93,7 +93,7 @@ Here's an example, which generates the same CSV file as above:: def some_view(request): # Create the HttpResponse object with the appropriate CSV header. response = HttpResponse(mimetype='text/csv') - response['Content-Disposition'] = 'attachment; filename=somefilename.csv' + response['Content-Disposition'] = 'attachment; filename="somefilename.csv"' # The data is hard-coded here, but you could load it from a database or # some other source. diff --git a/docs/howto/outputting-pdf.txt b/docs/howto/outputting-pdf.txt index e7e4bdcfa5..9d87b97710 100644 --- a/docs/howto/outputting-pdf.txt +++ b/docs/howto/outputting-pdf.txt @@ -52,7 +52,7 @@ Here's a "Hello World" example:: def some_view(request): # Create the HttpResponse object with the appropriate PDF headers. response = HttpResponse(mimetype='application/pdf') - response['Content-Disposition'] = 'attachment; filename=somefilename.pdf' + response['Content-Disposition'] = 'attachment; filename="somefilename.pdf"' # Create the PDF object, using the response object as its "file." p = canvas.Canvas(response) @@ -87,7 +87,7 @@ mention: the PDF using whatever program/plugin they've been configured to use for PDFs. Here's what that code would look like:: - response['Content-Disposition'] = 'filename=somefilename.pdf' + response['Content-Disposition'] = 'filename="somefilename.pdf"' * Hooking into the ReportLab API is easy: Just pass ``response`` as the first argument to ``canvas.Canvas``. The ``Canvas`` class expects a @@ -121,7 +121,7 @@ Here's the above "Hello World" example rewritten to use :mod:`io`:: def some_view(request): # Create the HttpResponse object with the appropriate PDF headers. response = HttpResponse(mimetype='application/pdf') - response['Content-Disposition'] = 'attachment; filename=somefilename.pdf' + response['Content-Disposition'] = 'attachment; filename="somefilename.pdf"' buffer = BytesIO() diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index ff929014c0..e977e32d42 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -590,7 +590,7 @@ To tell the browser to treat the response as a file attachment, use the this is how you might return a Microsoft Excel spreadsheet:: >>> response = HttpResponse(my_data, content_type='application/vnd.ms-excel') - >>> response['Content-Disposition'] = 'attachment; filename=foo.xls' + >>> response['Content-Disposition'] = 'attachment; filename="foo.xls"' There's nothing Django-specific about the ``Content-Disposition`` header, but it's easy to forget the syntax, so we've included it here. From 89544b2bd2323cdc5c29e056838a23abcee07d6e Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 3 Oct 2012 20:03:01 +0200 Subject: [PATCH 107/265] Readded docs anchor removed in 92b5341b and still in use --- docs/ref/contrib/gis/install.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ref/contrib/gis/install.txt b/docs/ref/contrib/gis/install.txt index 20bec32a8d..0ce8253210 100644 --- a/docs/ref/contrib/gis/install.txt +++ b/docs/ref/contrib/gis/install.txt @@ -510,6 +510,7 @@ to build and install:: Post-installation ================= +.. _spatialdb_template: .. _spatialdb_template91: Creating a spatial database with PostGIS 2.0 and PostgreSQL 9.1 From 725128289398ba4bce60a4093d9d69bbcea01d92 Mon Sep 17 00:00:00 2001 From: John Paulett Date: Wed, 3 Oct 2012 20:06:05 +0200 Subject: [PATCH 108/265] Fixed #17207 -- Added a troubleshooting note about failing createdb --- docs/ref/contrib/gis/install.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/ref/contrib/gis/install.txt b/docs/ref/contrib/gis/install.txt index 0ce8253210..355fb55a47 100644 --- a/docs/ref/contrib/gis/install.txt +++ b/docs/ref/contrib/gis/install.txt @@ -757,6 +757,21 @@ Similarly, on Red Hat and CentOS systems:: $ sudo yum install binutils +PostgreSQL's createdb fails +--------------------------- + +When the PostgreSQL cluster uses a non-UTF8 encoding, the +:file:`create_template_postgis-*.sh` script will fail when executing +``createdb``:: + + createdb: database creation failed: ERROR: new encoding (UTF8) is incompatible + with the encoding of the template database (SQL_ASCII) + +The `current workaround`__ is to re-create the cluster using UTF8 (back up any +databases before dropping the cluster). + +__ http://jacobian.org/writing/pg-encoding-ubuntu/ + Platform-specific instructions ============================== From a1a5c0854f0b0d3c94a772c8b994ee2d1d2f9be1 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 4 Oct 2012 06:45:22 -0400 Subject: [PATCH 109/265] Fixed #19051 - Fixed Selenium tearDownClass method; thanks glarrain for the report. --- django/contrib/admin/tests.py | 4 ++-- docs/topics/testing.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py index eaf1c8600c..7c62c1a22f 100644 --- a/django/contrib/admin/tests.py +++ b/django/contrib/admin/tests.py @@ -21,9 +21,9 @@ class AdminSeleniumWebDriverTestCase(LiveServerTestCase): @classmethod def tearDownClass(cls): - super(AdminSeleniumWebDriverTestCase, cls).tearDownClass() if hasattr(cls, 'selenium'): cls.selenium.quit() + super(AdminSeleniumWebDriverTestCase, cls).tearDownClass() def wait_until(self, callback, timeout=10): """ @@ -98,4 +98,4 @@ class AdminSeleniumWebDriverTestCase(LiveServerTestCase): `klass`. """ return (self.selenium.find_element_by_css_selector(selector) - .get_attribute('class').find(klass) != -1) \ No newline at end of file + .get_attribute('class').find(klass) != -1) diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index 2bc8410745..3950e1c917 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -1973,8 +1973,8 @@ Then, add a ``LiveServerTestCase``-based test to your app's tests module @classmethod def tearDownClass(cls): - super(MySeleniumTests, cls).tearDownClass() cls.selenium.quit() + super(MySeleniumTests, cls).tearDownClass() def test_login(self): self.selenium.get('%s%s' % (self.live_server_url, '/login/')) From 443999a1eeea70e4deebcf31f8f845696be62c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Ehrlich?= Date: Thu, 4 Oct 2012 14:03:48 +0200 Subject: [PATCH 110/265] Fixed #18996 - Docs on overriden model methods --- docs/topics/db/models.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index f29cc28332..0b7c9d389d 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -762,7 +762,7 @@ built-in model methods, adding new arguments. If you use ``*args, **kwargs`` in your method definitions, you are guaranteed that your code will automatically support those arguments when they are added. -.. admonition:: Overriding Delete +.. admonition:: Overridden model methods are not called on bulk operations Note that the :meth:`~Model.delete()` method for an object is not necessarily called when :ref:`deleting objects in bulk using a @@ -770,6 +770,13 @@ code will automatically support those arguments when they are added. gets executed, you can use :data:`~django.db.models.signals.pre_delete` and/or :data:`~django.db.models.signals.post_delete` signals. + Unfortunately, there isn't a workaround when + :meth:`creating` or + :meth:`updating` objects in bulk, + since none of :meth:`~Model.save()`, + :data:`~django.db.models.signals.pre_save`, and + :data:`~django.db.models.signals.post_save` are called. + Executing custom SQL -------------------- From 0ad6d7e61294befd5868c5ab422669d9ef3db52f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 4 Oct 2012 22:35:59 +0200 Subject: [PATCH 111/265] Removed unused and undocumented gdal_release_date function --- django/contrib/gis/gdal/__init__.py | 2 +- django/contrib/gis/gdal/libgdal.py | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/django/contrib/gis/gdal/__init__.py b/django/contrib/gis/gdal/__init__.py index de41df90ff..c33fbcb97a 100644 --- a/django/contrib/gis/gdal/__init__.py +++ b/django/contrib/gis/gdal/__init__.py @@ -37,7 +37,7 @@ try: from django.contrib.gis.gdal.driver import Driver from django.contrib.gis.gdal.datasource import DataSource - from django.contrib.gis.gdal.libgdal import gdal_version, gdal_full_version, gdal_release_date, GDAL_VERSION + from django.contrib.gis.gdal.libgdal import gdal_version, gdal_full_version, GDAL_VERSION from django.contrib.gis.gdal.srs import SpatialReference, CoordTransform from django.contrib.gis.gdal.geometries import OGRGeometry HAS_GDAL = True diff --git a/django/contrib/gis/gdal/libgdal.py b/django/contrib/gis/gdal/libgdal.py index 0d2889d704..865c04d89e 100644 --- a/django/contrib/gis/gdal/libgdal.py +++ b/django/contrib/gis/gdal/libgdal.py @@ -73,19 +73,6 @@ def gdal_full_version(): "Returns the full GDAL version information." return _version_info('') -def gdal_release_date(date=False): - """ - Returns the release date in a string format, e.g, "2007/06/27". - If the date keyword argument is set to True, a Python datetime object - will be returned instead. - """ - from datetime import date as date_type - rel = _version_info('RELEASE_DATE') - yy, mm, dd = map(int, (rel[0:4], rel[4:6], rel[6:8])) - d = date_type(yy, mm, dd) - if date: return d - else: return d.strftime('%Y/%m/%d') - version_regex = re.compile(r'^(?P\d+)\.(?P\d+)(\.(?P\d+))?') def gdal_version_info(): ver = gdal_version().decode() From 53c8b2c0c52cf999b644184bfe51e9f59d89286e Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 4 Oct 2012 22:41:03 +0200 Subject: [PATCH 112/265] Fixed #17959 -- Silenced output during GIS tests --- django/contrib/gis/gdal/libgdal.py | 22 +++++++++++++++++++++- django/contrib/gis/gdal/tests/test_ds.py | 3 +-- django/contrib/gis/gdal/tests/test_geom.py | 11 ++--------- django/contrib/gis/geos/libgeos.py | 14 +++++++++----- django/contrib/gis/geos/tests/test_geos.py | 22 +++++----------------- 5 files changed, 38 insertions(+), 34 deletions(-) diff --git a/django/contrib/gis/gdal/libgdal.py b/django/contrib/gis/gdal/libgdal.py index 865c04d89e..7c6b128e0f 100644 --- a/django/contrib/gis/gdal/libgdal.py +++ b/django/contrib/gis/gdal/libgdal.py @@ -1,11 +1,16 @@ from __future__ import unicode_literals +import logging import os import re -from ctypes import c_char_p, CDLL +from ctypes import c_char_p, c_int, CDLL, CFUNCTYPE from ctypes.util import find_library + from django.contrib.gis.gdal.error import OGRException + +logger = logging.getLogger('django.contrib.gis') + # Custom library path set? try: from django.conf import settings @@ -86,3 +91,18 @@ GDAL_MINOR_VERSION = int(_verinfo['minor']) GDAL_SUBMINOR_VERSION = _verinfo['subminor'] and int(_verinfo['subminor']) GDAL_VERSION = (GDAL_MAJOR_VERSION, GDAL_MINOR_VERSION, GDAL_SUBMINOR_VERSION) del _verinfo + +# Set library error handling so as errors are logged +CPLErrorHandler = CFUNCTYPE(None, c_int, c_int, c_char_p) +def err_handler(error_class, error_number, message): + logger.error('GDAL_ERROR %d: %s' % (error_number, message)) +err_handler = CPLErrorHandler(err_handler) + +def function(name, args, restype): + func = std_call(name) + func.argtypes = args + func.restype = restype + return func + +set_error_handler = function('CPLSetErrorHandler', [CPLErrorHandler], CPLErrorHandler) +set_error_handler(err_handler) diff --git a/django/contrib/gis/gdal/tests/test_ds.py b/django/contrib/gis/gdal/tests/test_ds.py index 094f65b468..69e3054422 100644 --- a/django/contrib/gis/gdal/tests/test_ds.py +++ b/django/contrib/gis/gdal/tests/test_ds.py @@ -4,6 +4,7 @@ from django.contrib.gis.gdal import DataSource, Envelope, OGRGeometry, OGRExcept from django.contrib.gis.gdal.field import OFTReal, OFTInteger, OFTString from django.contrib.gis.geometry.test_data import get_ds_file, TestDS, TEST_DATA + # List of acceptable data sources. ds_list = (TestDS('test_point', nfeat=5, nfld=3, geom='POINT', gtype=1, driver='ESRI Shapefile', fields={'dbl' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, @@ -59,7 +60,6 @@ class DataSourceTest(unittest.TestCase): def test03a_layers(self): "Testing Data Source Layers." - print("\nBEGIN - expecting out of range feature id error; safe to ignore.\n") for source in ds_list: ds = DataSource(source.ds) @@ -108,7 +108,6 @@ class DataSourceTest(unittest.TestCase): # the feature values here while in this loop. for fld_name in fld_names: self.assertEqual(source.field_values[fld_name][i], feat.get(fld_name)) - print("\nEND - expecting out of range feature id error; safe to ignore.") def test03b_layer_slice(self): "Test indexing and slicing on Layers." diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py index b22bb62109..9b8ae6a26b 100644 --- a/django/contrib/gis/gdal/tests/test_geom.py +++ b/django/contrib/gis/gdal/tests/test_geom.py @@ -235,15 +235,8 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): # Both rings in this geometry are not closed. poly = OGRGeometry('POLYGON((0 0, 5 0, 5 5, 0 5), (1 1, 2 1, 2 2, 2 1))') self.assertEqual(8, poly.point_count) - print("\nBEGIN - expecting IllegalArgumentException; safe to ignore.\n") - try: - c = poly.centroid - except OGRException: - # Should raise an OGR exception, rings are not closed - pass - else: - self.fail('Should have raised an OGRException!') - print("\nEND - expecting IllegalArgumentException; safe to ignore.\n") + with self.assertRaises(OGRException): + _ = poly.centroid poly.close_rings() self.assertEqual(10, poly.point_count) # Two closing points should've been added diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index b31a7955a2..d19644ac49 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -6,13 +6,17 @@ This module also houses GEOS Pointer utilities, including get_pointer_arr(), and GEOM_PTR. """ +import logging import os import re -import sys from ctypes import c_char_p, Structure, CDLL, CFUNCTYPE, POINTER from ctypes.util import find_library + from django.contrib.gis.geos.error import GEOSException + +logger = logging.getLogger('django.contrib.gis') + # Custom library path set? try: from django.conf import settings @@ -56,23 +60,23 @@ lgeos = CDLL(lib_path) # Supposed to mimic the GEOS message handler (C below): # typedef void (*GEOSMessageHandler)(const char *fmt, ...); NOTICEFUNC = CFUNCTYPE(None, c_char_p, c_char_p) -def notice_h(fmt, lst, output_h=sys.stdout): +def notice_h(fmt, lst): fmt, lst = fmt.decode(), lst.decode() try: warn_msg = fmt % lst except: warn_msg = fmt - output_h.write('GEOS_NOTICE: %s\n' % warn_msg) + logger.warn('GEOS_NOTICE: %s\n' % warn_msg) notice_h = NOTICEFUNC(notice_h) ERRORFUNC = CFUNCTYPE(None, c_char_p, c_char_p) -def error_h(fmt, lst, output_h=sys.stderr): +def error_h(fmt, lst): fmt, lst = fmt.decode(), lst.decode() try: err_msg = fmt % lst except: err_msg = fmt - output_h.write('GEOS_ERROR: %s\n' % err_msg) + logger.error('GEOS_ERROR: %s\n' % err_msg) error_h = ERRORFUNC(error_h) #### GEOS Geometry C data structures, and utility functions. #### diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index cbe51367ae..283daa47c0 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -147,18 +147,13 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_errors(self): "Testing the Error handlers." # string-based - print("\nBEGIN - expecting GEOS_ERROR; safe to ignore.\n") for err in self.geometries.errors: - try: - g = fromstr(err.wkt) - except (GEOSException, ValueError): - pass + with self.assertRaises((GEOSException, ValueError)): + _ = fromstr(err.wkt) # Bad WKB self.assertRaises(GEOSException, GEOSGeometry, memoryview(b'0')) - print("\nEND - expecting GEOS_ERROR; safe to ignore.\n") - class NotAGeometry(object): pass @@ -458,7 +453,6 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_multipolygons(self): "Testing MultiPolygon objects." - print("\nBEGIN - expecting GEOS_NOTICE; safe to ignore.\n") prev = fromstr('POINT (0 0)') for mp in self.geometries.multipolygons: mpoly = fromstr(mp.wkt) @@ -477,8 +471,6 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(p.valid, True) self.assertEqual(mpoly.wkt, MultiPolygon(*tuple(poly.clone() for poly in mpoly)).wkt) - print("\nEND - expecting GEOS_NOTICE; safe to ignore.\n") - def test_memory_hijinks(self): "Testing Geometry __del__() on rings and polygons." #### Memory issues with rings and polygons @@ -1025,19 +1017,15 @@ class GEOSTest(unittest.TestCase, TestDataMixin): g = GEOSGeometry("POINT(0 0)") self.assertTrue(g.valid) - self.assertTrue(isinstance(g.valid_reason, six.string_types)) + self.assertIsInstance(g.valid_reason, six.string_types) self.assertEqual(g.valid_reason, "Valid Geometry") - print("\nBEGIN - expecting GEOS_NOTICE; safe to ignore.\n") - g = GEOSGeometry("LINESTRING(0 0, 0 0)") - self.assertTrue(not g.valid) - self.assertTrue(isinstance(g.valid_reason, six.string_types)) + self.assertFalse(g.valid) + self.assertIsInstance(g.valid_reason, six.string_types) self.assertTrue(g.valid_reason.startswith("Too few points in geometry component")) - print("\nEND - expecting GEOS_NOTICE; safe to ignore.\n") - @unittest.skipUnless(geos_version_info()['version'] >= '3.2.0', "geos >= 3.2.0 is required") def test_linearref(self): "Testing linear referencing" From 074e65b04a82b4105ea9649c6ab8f5a796bc6984 Mon Sep 17 00:00:00 2001 From: Michael Kelly Date: Fri, 5 Oct 2012 11:32:28 -0400 Subject: [PATCH 113/265] Fixed typo in queryset docs under update method. --- 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 8c188c67c3..53f76d6ffe 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1581,7 +1581,7 @@ does not call any ``save()`` methods on your models, nor does it emit the :attr:`~django.db.models.signals.post_save` signals (which are a consequence of calling :meth:`Model.save() <~django.db.models.Model.save()>`). If you want to update a bunch of records for a model that has a custom -:meth:`~django.db.models.Model.save()`` method, loop over them and call +:meth:`~django.db.models.Model.save()` method, loop over them and call :meth:`~django.db.models.Model.save()`, like this:: for e in Entry.objects.filter(pub_date__year=2010): From ab8c9703683856eef0346b661e36d6f05db67435 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 5 Oct 2012 23:14:56 +0200 Subject: [PATCH 114/265] Fixed #19072 -- Corrected an external file path in GeoIP docs Thanks Flavio Curella for the report and the initial patch. --- docs/ref/contrib/gis/geoip.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ref/contrib/gis/geoip.txt b/docs/ref/contrib/gis/geoip.txt index a30573d860..e37c4c60b0 100644 --- a/docs/ref/contrib/gis/geoip.txt +++ b/docs/ref/contrib/gis/geoip.txt @@ -23,10 +23,10 @@ to the GPL-licensed `Python GeoIP`__ interface provided by MaxMind. In order to perform IP-based geolocation, the :class:`GeoIP` object requires the GeoIP C libary and either the GeoIP `Country`__ or `City`__ datasets in binary format (the CSV files will not work!). These datasets may be -`downloaded from MaxMind`__. Grab the ``GeoIP.dat.gz`` and ``GeoLiteCity.dat.gz`` -and unzip them in a directory corresponding to what you set -:setting:`GEOIP_PATH` with in your settings. See the example and reference below -for more details. +`downloaded from MaxMind`__. Grab the ``GeoLiteCountry/GeoIP.dat.gz`` and +``GeoLiteCity.dat.gz`` files and unzip them in a directory corresponding to what +you set :setting:`GEOIP_PATH` with in your settings. See the example and +reference below for more details. __ http://www.maxmind.com/app/c __ http://www.maxmind.com/app/python From 950e6183c60d368c62f627eaed54961be0a207d8 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Fri, 5 Oct 2012 14:38:01 -0700 Subject: [PATCH 115/265] Need to catch `ImproperlyConfigured` to be freed from the schackles of `DJANGO_SETTINGS_MODULE`. --- django/contrib/gis/gdal/libgdal.py | 5 +++-- django/contrib/gis/geos/libgeos.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/django/contrib/gis/gdal/libgdal.py b/django/contrib/gis/gdal/libgdal.py index 7c6b128e0f..91f8d618fc 100644 --- a/django/contrib/gis/gdal/libgdal.py +++ b/django/contrib/gis/gdal/libgdal.py @@ -7,7 +7,7 @@ from ctypes import c_char_p, c_int, CDLL, CFUNCTYPE from ctypes.util import find_library from django.contrib.gis.gdal.error import OGRException - +from django.core.exceptions import ImproperlyConfigured logger = logging.getLogger('django.contrib.gis') @@ -15,7 +15,8 @@ logger = logging.getLogger('django.contrib.gis') try: from django.conf import settings lib_path = settings.GDAL_LIBRARY_PATH -except (AttributeError, EnvironmentError, ImportError): +except (AttributeError, EnvironmentError, + ImportError, ImproperlyConfigured): lib_path = None if lib_path: diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index d19644ac49..f011208ea0 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -13,7 +13,7 @@ from ctypes import c_char_p, Structure, CDLL, CFUNCTYPE, POINTER from ctypes.util import find_library from django.contrib.gis.geos.error import GEOSException - +from django.core.exceptions import ImproperlyConfigured logger = logging.getLogger('django.contrib.gis') @@ -21,7 +21,8 @@ logger = logging.getLogger('django.contrib.gis') try: from django.conf import settings lib_path = settings.GEOS_LIBRARY_PATH -except (AttributeError, EnvironmentError, ImportError): +except (AttributeError, EnvironmentError, + ImportError, ImproperlyConfigured): lib_path = None # Setting the appropriate names for the GEOS-C library. From db78086b4540f968620ffe465174d7094267f04b Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Fri, 5 Oct 2012 14:47:04 -0700 Subject: [PATCH 116/265] Added comment in `geoapp` tests about PostGIS 2.0 change in ST_NumGeometries. --- django/contrib/gis/tests/geoapp/tests.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 7d8121425a..1712eb5fe8 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -575,10 +575,13 @@ class GeoQuerySetTest(TestCase): def test_num_geom(self): "Testing the `num_geom` GeoQuerySet method." # Both 'countries' only have two geometries. - for c in Country.objects.num_geom(): self.assertEqual(2, c.num_geom) + for c in Country.objects.num_geom(): + self.assertEqual(2, c.num_geom) + for c in City.objects.filter(point__isnull=False).num_geom(): - # Oracle will return 1 for the number of geometries on non-collections, - # whereas PostGIS will return None. + # Oracle and PostGIS 2.0+ will return 1 for the number of + # geometries on non-collections, whereas PostGIS < 2.0.0 + # will return None. if postgis and connection.ops.spatial_version < (2, 0, 0): self.assertIsNone(c.num_geom) else: From c1b06c8137d82ab34204f62aa4324335b1fd9a23 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Fri, 5 Oct 2012 14:55:15 -0700 Subject: [PATCH 117/265] Lowered tolerance to fix failing distance test. --- django/contrib/gis/tests/distapp/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py index bf075add6c..5574b42738 100644 --- a/django/contrib/gis/tests/distapp/tests.py +++ b/django/contrib/gis/tests/distapp/tests.py @@ -324,7 +324,7 @@ class DistanceTest(TestCase): else: qs = Interstate.objects.length() if oracle: tol = 2 - else: tol = 5 + else: tol = 3 self.assertAlmostEqual(len_m1, qs[0].length.m, tol) # Now doing length on a projected coordinate system. From 1c010ce41d6790b94d396aabe5c585cf1cc04b70 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Fri, 5 Oct 2012 15:26:33 -0700 Subject: [PATCH 118/265] Skip `LayerMapRouterTest` if there are not multiple databases. --- django/contrib/gis/tests/layermap/tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index 4912e645c2..7c1d99520d 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import os +import unittest from copy import copy from decimal import Decimal @@ -9,6 +10,7 @@ from django.contrib.gis.tests.utils import mysql from django.contrib.gis.utils.layermapping import (LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey) from django.db import router +from django.conf import settings from django.test import TestCase from .models import ( @@ -308,6 +310,7 @@ class LayerMapRouterTest(TestCase): def tearDown(self): router.routers = self.old_routers + @unittest.skipUnless(len(settings.DATABASES) > 1, 'multiple databases required') def test_layermapping_default_db(self): lm = LayerMapping(City, city_shp, city_mapping) self.assertEqual(lm.using, 'other') From 065b52f18e7016b385feb8ad3de5dafb72ad202d Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Fri, 5 Oct 2012 15:43:04 -0700 Subject: [PATCH 119/265] Updated `GeoSQLCompiler.get_default_columns`. --- django/contrib/gis/db/models/sql/compiler.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py index 233ca5a03e..cf6a8ad047 100644 --- a/django/contrib/gis/db/models/sql/compiler.py +++ b/django/contrib/gis/db/models/sql/compiler.py @@ -119,10 +119,10 @@ class GeoSQLCompiler(compiler.SQLCompiler): result = [] if opts is None: opts = self.query.model._meta + # Skip all proxy to the root proxied model + opts = opts.concrete_model._meta aliases = set() only_load = self.deferred_to_columns() - # Skip all proxy to the root proxied model - proxied_model = opts.concrete_model if start_alias: seen = {None: start_alias} @@ -133,12 +133,9 @@ class GeoSQLCompiler(compiler.SQLCompiler): try: alias = seen[model] except KeyError: - if model is proxied_model: - alias = start_alias - else: - link_field = opts.get_ancestor_link(model) - alias = self.query.join((start_alias, model._meta.db_table, - link_field.column, model._meta.pk.column)) + link_field = opts.get_ancestor_link(model) + alias = self.query.join((start_alias, model._meta.db_table, + link_field.column, model._meta.pk.column)) seen[model] = alias else: # If we're starting from the base model of the queryset, the From 5a64bd38e61aeeaf4476a42aca06654f5ce6e429 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Fri, 5 Oct 2012 15:51:45 -0700 Subject: [PATCH 120/265] Forgot to import `unittest` from `django.utils`. --- django/contrib/gis/tests/layermap/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index 7c1d99520d..557bdf9117 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -1,7 +1,6 @@ from __future__ import absolute_import import os -import unittest from copy import copy from decimal import Decimal @@ -12,6 +11,7 @@ from django.contrib.gis.utils.layermapping import (LayerMapping, LayerMapError, from django.db import router from django.conf import settings from django.test import TestCase +from django.utils import unittest from .models import ( City, County, CountyFeat, Interstate, ICity1, ICity2, Invalid, State, From 84f9741664dadb3185c3d8822b720b340374066e Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Fri, 5 Oct 2012 16:08:16 -0700 Subject: [PATCH 121/265] Fixed GMLv3 output test failure on PostGIS versions < 1.5. --- django/contrib/gis/tests/geoapp/tests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 1712eb5fe8..f94d716b34 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -529,8 +529,10 @@ class GeoQuerySetTest(TestCase): for ptown in [ptown1, ptown2]: self.assertTrue(gml_regex.match(ptown.gml)) - if postgis: - self.assertIn('', City.objects.gml(version=3).get(name='Pueblo').gml) + # PostGIS < 1.5 doesn't include dimension im GMLv3 output. + if postgis and connection.ops.spatial_version >= (1, 5, 0): + self.assertIn('', + City.objects.gml(version=3).get(name='Pueblo').gml) def test_kml(self): "Testing KML output from the database using GeoQuerySet.kml()." From 70fac984c88d81a93502468f79f5c661dfe3b3aa Mon Sep 17 00:00:00 2001 From: Don Spaulding Date: Fri, 5 Oct 2012 19:17:00 -0500 Subject: [PATCH 122/265] Fixed format-o in docs/topics/db/queries.txt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It appears that our infamous villain, Significant Whitespace, has struck again. In this episode, little Timmy finds himself trapped in a code well.  He need not despair, however, as Indentation Man has heard his cries for help and sprung into action. With his feline helper, Octocat, at his side, Indentation Man races to the scene, flings open a web-based code editor, and with terrific aplomb, frees Timmy to be the documentation he always wanted to be. Once again Goodness has prevailed.  In the fight for readable documentation, no stray whitespace will ever be able to withstand the str.strip()ing nature of....INDENTATION MAN. --- docs/topics/db/queries.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index c724eabb8e..1f73156ab9 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -883,9 +883,9 @@ it. For example:: # This will delete the Blog and all of its Entry objects. b.delete() - This cascade behavior is customizable via the - :attr:`~django.db.models.ForeignKey.on_delete` argument to the - :class:`~django.db.models.ForeignKey`. +This cascade behavior is customizable via the +:attr:`~django.db.models.ForeignKey.on_delete` argument to the +:class:`~django.db.models.ForeignKey`. Note that :meth:`~django.db.models.query.QuerySet.delete` is the only :class:`~django.db.models.query.QuerySet` method that is not exposed on a From cd99c12f05b10cb1b3a07b7b4fcd9fb89785f7a8 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Fri, 5 Oct 2012 18:41:50 -0700 Subject: [PATCH 123/265] Fixed `F()` expression regressions in GeoDjango caused by recent datastructure changes in `SQLEvaluator`. --- django/contrib/gis/db/backends/base.py | 10 ++++++++++ django/contrib/gis/db/backends/mysql/operations.py | 2 +- django/contrib/gis/db/backends/oracle/operations.py | 2 +- django/contrib/gis/db/backends/postgis/operations.py | 2 +- .../contrib/gis/db/backends/spatialite/operations.py | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py index 2b8924d92e..0bd598997c 100644 --- a/django/contrib/gis/db/backends/base.py +++ b/django/contrib/gis/db/backends/base.py @@ -116,6 +116,16 @@ class BaseSpatialOperations(object): """ raise NotImplementedError + def get_expression_column(self, evaluator): + """ + Helper method to return the quoted column string from the evaluator + for its expression. + """ + for expr, col_tup in evaluator.cols: + if expr is evaluator.expression: + return '%s.%s' % tuple(map(self.quote_name, col_tup)) + raise Exception("Could not find the column for the expression.") + # Spatial SQL Construction def spatial_aggregate_sql(self, agg): raise NotImplementedError('Aggregate support not implemented for this spatial backend.') diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index 7152f4682d..0c1be624f1 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -44,7 +44,7 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations): modify the placeholder based on the contents of the given value. """ if hasattr(value, 'expression'): - placeholder = '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + placeholder = placeholder % self.get_expression_column(value) else: placeholder = '%s(%%s)' % self.from_text return placeholder diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index 392feb129b..35a4d9491d 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -213,7 +213,7 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations): placeholder = '%s' # No geometry value used for F expression, substitue in # the column name instead. - return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + return placeholder % self.get_expression_column(value) else: if transform_value(value, f.srid): return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (self.transform, value.srid, f.srid) diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index 18a59d1240..ff9110de01 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -381,7 +381,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): # If this is an F expression, then we don't really want # a placeholder and instead substitute in the column # of the expression. - placeholder = placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + placeholder = placeholder % self.get_expression_column(value) return placeholder diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 80f05ef076..5f76501ef1 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -208,7 +208,7 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): placeholder = '%s' # No geometry value used for F expression, substitue in # the column name instead. - return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + return placeholder % self.get_expression_column(value) else: if transform_value(value, f.srid): # Adding Transform() to the SQL placeholder. From d99639da035f3465633f18c78eee239fd18fce21 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Fri, 5 Oct 2012 18:49:59 -0700 Subject: [PATCH 124/265] Fixed type in MySQL spatial backend. --- django/contrib/gis/db/backends/mysql/operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index 0c1be624f1..fa20ca07f4 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -44,7 +44,7 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations): modify the placeholder based on the contents of the given value. """ if hasattr(value, 'expression'): - placeholder = placeholder % self.get_expression_column(value) + placeholder = self.get_expression_column(value) else: placeholder = '%s(%%s)' % self.from_text return placeholder From b9039268a17b06e7fe069721e99f6d69181c344d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 6 Oct 2012 12:43:29 +0800 Subject: [PATCH 125/265] Fixed #19060 -- Corrected assumptions about the name of the User model in the ModelBackend. Thanks to Ivan Virabyan for the report and initial patch. --- django/contrib/auth/backends.py | 4 +- django/contrib/auth/tests/auth_backends.py | 96 ++++++++++++++++++---- django/contrib/auth/tests/custom_user.py | 23 +++++- 3 files changed, 100 insertions(+), 23 deletions(-) diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index d103f32eb5..00cb67a0b5 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -30,7 +30,9 @@ class ModelBackend(object): if user_obj.is_superuser: perms = Permission.objects.all() else: - perms = Permission.objects.filter(group__user=user_obj) + user_groups_field = get_user_model()._meta.get_field('groups') + user_groups_query = 'group__%s' % user_groups_field.related_query_name() + perms = Permission.objects.filter(**{user_groups_query: user_obj}) perms = perms.values_list('content_type__app_label', 'codename').order_by() user_obj._group_perm_cache = set(["%s.%s" % (ct, name) for ct, name in perms]) return user_obj._group_perm_cache diff --git a/django/contrib/auth/tests/auth_backends.py b/django/contrib/auth/tests/auth_backends.py index a6be985412..e92f159ff9 100644 --- a/django/contrib/auth/tests/auth_backends.py +++ b/django/contrib/auth/tests/auth_backends.py @@ -1,24 +1,29 @@ from __future__ import unicode_literals +from datetime import date from django.conf import settings from django.contrib.auth.models import User, Group, Permission, AnonymousUser from django.contrib.auth.tests.utils import skipIfCustomUser +from django.contrib.auth.tests.custom_user import ExtensionUser from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.test.utils import override_settings -@skipIfCustomUser -class BackendTest(TestCase): - +class BaseModelBackendTest(object): + """ + A base class for tests that need to validate the ModelBackend + with different User models. Subclasses should define a class + level UserModel attribute, and a create_users() method to + construct two users for test purposes. + """ backend = 'django.contrib.auth.backends.ModelBackend' def setUp(self): self.curr_auth = settings.AUTHENTICATION_BACKENDS settings.AUTHENTICATION_BACKENDS = (self.backend,) - User.objects.create_user('test', 'test@example.com', 'test') - User.objects.create_superuser('test2', 'test2@example.com', 'test') + self.create_users() def tearDown(self): settings.AUTHENTICATION_BACKENDS = self.curr_auth @@ -28,7 +33,7 @@ class BackendTest(TestCase): ContentType.objects.clear_cache() def test_has_perm(self): - user = User.objects.get(username='test') + user = self.UserModel.objects.get(username='test') self.assertEqual(user.has_perm('auth.test'), False) user.is_staff = True user.save() @@ -47,14 +52,14 @@ class BackendTest(TestCase): self.assertEqual(user.has_perm('auth.test'), False) def test_custom_perms(self): - user = User.objects.get(username='test') - content_type=ContentType.objects.get_for_model(Group) + user = self.UserModel.objects.get(username='test') + content_type = ContentType.objects.get_for_model(Group) perm = Permission.objects.create(name='test', content_type=content_type, codename='test') user.user_permissions.add(perm) user.save() # reloading user to purge the _perm_cache - user = User.objects.get(username='test') + user = self.UserModel.objects.get(username='test') self.assertEqual(user.get_all_permissions() == set(['auth.test']), True) self.assertEqual(user.get_group_permissions(), set([])) self.assertEqual(user.has_module_perms('Group'), False) @@ -65,7 +70,7 @@ class BackendTest(TestCase): perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3') user.user_permissions.add(perm) user.save() - user = User.objects.get(username='test') + user = self.UserModel.objects.get(username='test') self.assertEqual(user.get_all_permissions(), set(['auth.test2', 'auth.test', 'auth.test3'])) self.assertEqual(user.has_perm('test'), False) self.assertEqual(user.has_perm('auth.test'), True) @@ -75,7 +80,7 @@ class BackendTest(TestCase): group.permissions.add(perm) group.save() user.groups.add(group) - user = User.objects.get(username='test') + user = self.UserModel.objects.get(username='test') exp = set(['auth.test2', 'auth.test', 'auth.test3', 'auth.test_group']) self.assertEqual(user.get_all_permissions(), exp) self.assertEqual(user.get_group_permissions(), set(['auth.test_group'])) @@ -87,8 +92,8 @@ class BackendTest(TestCase): def test_has_no_object_perm(self): """Regressiontest for #12462""" - user = User.objects.get(username='test') - content_type=ContentType.objects.get_for_model(Group) + user = self.UserModel.objects.get(username='test') + content_type = ContentType.objects.get_for_model(Group) perm = Permission.objects.create(name='test', content_type=content_type, codename='test') user.user_permissions.add(perm) user.save() @@ -100,9 +105,65 @@ class BackendTest(TestCase): def test_get_all_superuser_permissions(self): "A superuser has all permissions. Refs #14795" - user = User.objects.get(username='test2') + user = self.UserModel.objects.get(username='test2') self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all())) + +@skipIfCustomUser +class ModelBackendTest(BaseModelBackendTest, TestCase): + """ + Tests for the ModelBackend using the default User model. + """ + UserModel = User + + def create_users(self): + User.objects.create_user( + username='test', + email='test@example.com', + password='test', + ) + User.objects.create_superuser( + username='test2', + email='test2@example.com', + password='test', + ) + + +@override_settings(AUTH_USER_MODEL='auth.ExtensionUser') +class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase): + """ + Tests for the ModelBackend using the custom ExtensionUser model. + + This isn't a perfect test, because both the User and ExtensionUser are + synchronized to the database, which wouldn't ordinary happen in + production. As a result, it doesn't catch errors caused by the non- + existence of the User table. + + The specific problem is queries on .filter(groups__user) et al, which + makes an implicit assumption that the user model is called 'User'. In + production, the auth.User table won't exist, so the requested join + won't exist either; in testing, the auth.User *does* exist, and + so does the join. However, the join table won't contain any useful + data; for testing, we check that the data we expect actually does exist. + """ + + UserModel = ExtensionUser + + def create_users(self): + ExtensionUser.objects.create_user( + username='test', + email='test@example.com', + password='test', + date_of_birth=date(2006, 4, 25) + ) + ExtensionUser.objects.create_superuser( + username='test2', + email='test2@example.com', + password='test', + date_of_birth=date(1976, 11, 8) + ) + + class TestObj(object): pass @@ -110,7 +171,7 @@ class TestObj(object): class SimpleRowlevelBackend(object): def has_perm(self, user, perm, obj=None): if not obj: - return # We only support row level perms + return # We only support row level perms if isinstance(obj, TestObj): if user.username == 'test2': @@ -128,7 +189,7 @@ class SimpleRowlevelBackend(object): def get_all_permissions(self, user, obj=None): if not obj: - return [] # We only support row level perms + return [] # We only support row level perms if not isinstance(obj, TestObj): return ['none'] @@ -142,7 +203,7 @@ class SimpleRowlevelBackend(object): def get_group_permissions(self, user, obj=None): if not obj: - return # We only support row level perms + return # We only support row level perms if not isinstance(obj, TestObj): return ['none'] @@ -189,7 +250,6 @@ class RowlevelBackendTest(TestCase): self.assertEqual(self.user2.get_all_permissions(), set([])) def test_get_group_permissions(self): - content_type=ContentType.objects.get_for_model(Group) group = Group.objects.create(name='test_group') self.user3.groups.add(group) self.assertEqual(self.user3.get_group_permissions(TestObj()), set(['group_perm'])) diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py index 3e7fa097b5..9bd74c0ac8 100644 --- a/django/contrib/auth/tests/custom_user.py +++ b/django/contrib/auth/tests/custom_user.py @@ -1,11 +1,11 @@ +from django.db import models +from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, AbstractUser, UserManager + + # The custom User uses email as the unique identifier, and requires # that every user provide a date of birth. This lets us test # changes in username datatype, and non-text required fields. -from django.db import models -from django.contrib.auth.models import BaseUserManager, AbstractBaseUser - - class CustomUserManager(BaseUserManager): def create_user(self, email, date_of_birth, password=None): """ @@ -73,3 +73,18 @@ class CustomUser(AbstractBaseUser): @property def is_staff(self): return self.is_admin + + +# The extension user is a simple extension of the built-in user class, +# adding a required date_of_birth field. This allows us to check for +# any hard references to the name "User" in forms/handlers etc. + +class ExtensionUser(AbstractUser): + date_of_birth = models.DateField() + + objects = UserManager() + + REQUIRED_FIELDS = AbstractUser.REQUIRED_FIELDS + ['date_of_birth'] + + class Meta: + app_label = 'auth' From 12f39be508cab95a9841987c3df589ef69de706e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 6 Oct 2012 12:46:35 +0800 Subject: [PATCH 126/265] Fixed #19074 -- Corrected some minor issues with the new custom User docs. Thanks to Bradley Ayers for the review. --- docs/topics/auth.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index f9c9057baa..bbe6d6ec33 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -2074,20 +2074,20 @@ authentication app:: Creates and saves a superuser with the given email, date of birth and password. """ - u = self.create_user(username, - password=password, - date_of_birth=date_of_birth - ) - u.is_admin = True - u.save(using=self._db) - return u + user = self.create_user(username, + password=password, + date_of_birth=date_of_birth + ) + user.is_admin = True + user.save(using=self._db) + return user class MyUser(AbstractBaseUser): email = models.EmailField( - verbose_name='email address', - max_length=255 - ) + verbose_name='email address', + max_length=255 + ) date_of_birth = models.DateField() is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) From cc337a74f1808b216fff96f1695d8b066d2636f6 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 6 Oct 2012 14:21:57 +0800 Subject: [PATCH 127/265] Fixed #19069 -- Improved the error message when trying to query a swapped model. Thanks to Preston Holmes for the suggestion. --- django/db/models/manager.py | 37 +++- .../managers_regress/models.py | 13 ++ .../regressiontests/managers_regress/tests.py | 168 ++++++++++++++++-- 3 files changed, 201 insertions(+), 17 deletions(-) diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 522a8a2306..8da8af487c 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -13,7 +13,11 @@ def ensure_default_manager(sender, **kwargs): _default_manager if it's not a subclass of Manager). """ cls = sender - if cls._meta.abstract or cls._meta.swapped: + if cls._meta.abstract: + setattr(cls, 'objects', AbstractManagerDescriptor(cls)) + return + elif cls._meta.swapped: + setattr(cls, 'objects', SwappedManagerDescriptor(cls)) return if not getattr(cls, '_default_manager', None): # Create the default manager, if needed. @@ -58,7 +62,12 @@ class Manager(object): # TODO: Use weakref because of possible memory leak / circular reference. self.model = model # Only contribute the manager if the model is concrete - if not model._meta.abstract and not model._meta.swapped: + if model._meta.abstract: + setattr(model, name, AbstractManagerDescriptor(model)) + elif model._meta.swapped: + setattr(model, name, SwappedManagerDescriptor(model)) + else: + # if not model._meta.abstract and not model._meta.swapped: setattr(model, name, ManagerDescriptor(self)) if not getattr(model, '_default_manager', None) or self.creation_counter < model._default_manager.creation_counter: model._default_manager = self @@ -224,6 +233,30 @@ class ManagerDescriptor(object): return self.manager +class AbstractManagerDescriptor(object): + # This class provides a better error message when you try to access a + # manager on an abstract model. + def __init__(self, model): + self.model = model + + def __get__(self, instance, type=None): + raise AttributeError("Manager isn't available; %s is abstract" % ( + self.model._meta.object_name, + )) + + +class SwappedManagerDescriptor(object): + # This class provides a better error message when you try to access a + # manager on a swapped model. + def __init__(self, model): + self.model = model + + def __get__(self, instance, type=None): + raise AttributeError("Manager isn't available; %s has been swapped for '%s'" % ( + self.model._meta.object_name, self.model._meta.swapped + )) + + class EmptyManager(Manager): def get_query_set(self): return self.get_empty_query_set() diff --git a/tests/regressiontests/managers_regress/models.py b/tests/regressiontests/managers_regress/models.py index 892505f24a..d72970d86e 100644 --- a/tests/regressiontests/managers_regress/models.py +++ b/tests/regressiontests/managers_regress/models.py @@ -10,14 +10,17 @@ class OnlyFred(models.Manager): def get_query_set(self): return super(OnlyFred, self).get_query_set().filter(name='fred') + class OnlyBarney(models.Manager): def get_query_set(self): return super(OnlyBarney, self).get_query_set().filter(name='barney') + class Value42(models.Manager): def get_query_set(self): return super(Value42, self).get_query_set().filter(value=42) + class AbstractBase1(models.Model): name = models.CharField(max_length=50) @@ -29,6 +32,7 @@ class AbstractBase1(models.Model): manager2 = OnlyBarney() objects = models.Manager() + class AbstractBase2(models.Model): value = models.IntegerField() @@ -38,6 +42,7 @@ class AbstractBase2(models.Model): # Custom manager restricted = Value42() + # No custom manager on this class to make sure the default case doesn't break. class AbstractBase3(models.Model): comment = models.CharField(max_length=50) @@ -45,6 +50,7 @@ class AbstractBase3(models.Model): class Meta: abstract = True + @python_2_unicode_compatible class Parent(models.Model): name = models.CharField(max_length=50) @@ -54,6 +60,7 @@ class Parent(models.Model): def __str__(self): return self.name + # Managers from base classes are inherited and, if no manager is specified # *and* the parent has a manager specified, the first one (in the MRO) will # become the default. @@ -64,6 +71,7 @@ class Child1(AbstractBase1): def __str__(self): return self.data + @python_2_unicode_compatible class Child2(AbstractBase1, AbstractBase2): data = models.CharField(max_length=25) @@ -71,6 +79,7 @@ class Child2(AbstractBase1, AbstractBase2): def __str__(self): return self.data + @python_2_unicode_compatible class Child3(AbstractBase1, AbstractBase3): data = models.CharField(max_length=25) @@ -78,6 +87,7 @@ class Child3(AbstractBase1, AbstractBase3): def __str__(self): return self.data + @python_2_unicode_compatible class Child4(AbstractBase1): data = models.CharField(max_length=25) @@ -89,6 +99,7 @@ class Child4(AbstractBase1): def __str__(self): return self.data + @python_2_unicode_compatible class Child5(AbstractBase3): name = models.CharField(max_length=25) @@ -99,10 +110,12 @@ class Child5(AbstractBase3): def __str__(self): return self.name + # Will inherit managers from AbstractBase1, but not Child4. class Child6(Child4): value = models.IntegerField() + # Will not inherit default manager from parent. class Child7(Parent): pass diff --git a/tests/regressiontests/managers_regress/tests.py b/tests/regressiontests/managers_regress/tests.py index dd6cb66857..f3721a4c01 100644 --- a/tests/regressiontests/managers_regress/tests.py +++ b/tests/regressiontests/managers_regress/tests.py @@ -1,26 +1,42 @@ from __future__ import absolute_import +import copy +from django.conf import settings +from django.db import models +from django.db.models.loading import cache from django.test import TestCase +from django.test.utils import override_settings -from .models import Child1, Child2, Child3, Child4, Child5, Child6, Child7 +from .models import ( + Child1, + Child2, + Child3, + Child4, + Child5, + Child6, + Child7, + AbstractBase1, + AbstractBase2, + AbstractBase3, +) class ManagersRegressionTests(TestCase): def test_managers(self): - a1 = Child1.objects.create(name='fred', data='a1') - a2 = Child1.objects.create(name='barney', data='a2') - b1 = Child2.objects.create(name='fred', data='b1', value=1) - b2 = Child2.objects.create(name='barney', data='b2', value=42) - c1 = Child3.objects.create(name='fred', data='c1', comment='yes') - c2 = Child3.objects.create(name='barney', data='c2', comment='no') - d1 = Child4.objects.create(name='fred', data='d1') - d2 = Child4.objects.create(name='barney', data='d2') - e1 = Child5.objects.create(name='fred', comment='yes') - e2 = Child5.objects.create(name='barney', comment='no') - f1 = Child6.objects.create(name='fred', data='f1', value=42) - f2 = Child6.objects.create(name='barney', data='f2', value=42) - g1 = Child7.objects.create(name='fred') - g2 = Child7.objects.create(name='barney') + Child1.objects.create(name='fred', data='a1') + Child1.objects.create(name='barney', data='a2') + Child2.objects.create(name='fred', data='b1', value=1) + Child2.objects.create(name='barney', data='b2', value=42) + Child3.objects.create(name='fred', data='c1', comment='yes') + Child3.objects.create(name='barney', data='c2', comment='no') + Child4.objects.create(name='fred', data='d1') + Child4.objects.create(name='barney', data='d2') + Child5.objects.create(name='fred', comment='yes') + Child5.objects.create(name='barney', comment='no') + Child6.objects.create(name='fred', data='f1', value=42) + Child6.objects.create(name='barney', data='f2', value=42) + Child7.objects.create(name='fred') + Child7.objects.create(name='barney') self.assertQuerysetEqual(Child1.manager1.all(), [""]) self.assertQuerysetEqual(Child1.manager2.all(), [""]) @@ -54,3 +70,125 @@ class ManagersRegressionTests(TestCase): "" ] ) + + def test_abstract_manager(self): + # Accessing the manager on an abstract model should + # raise an attribute error with an appropriate message. + try: + AbstractBase3.objects.all() + self.fail('Should raise an AttributeError') + except AttributeError as e: + # This error message isn't ideal, but if the model is abstract and + # a lot of the class instantiation logic isn't invoked; if the + # manager is implied, then we don't get a hook to install the + # error-raising manager. + self.assertEqual(str(e), "type object 'AbstractBase3' has no attribute 'objects'") + + def test_custom_abstract_manager(self): + # Accessing the manager on an abstract model with an custom + # manager should raise an attribute error with an appropriate + # message. + try: + AbstractBase2.restricted.all() + self.fail('Should raise an AttributeError') + except AttributeError as e: + self.assertEqual(str(e), "Manager isn't available; AbstractBase2 is abstract") + + def test_explicit_abstract_manager(self): + # Accessing the manager on an abstract model with an explicit + # manager should raise an attribute error with an appropriate + # message. + try: + AbstractBase1.objects.all() + self.fail('Should raise an AttributeError') + except AttributeError as e: + self.assertEqual(str(e), "Manager isn't available; AbstractBase1 is abstract") + + def test_swappable_manager(self): + try: + # This test adds dummy models to the app cache. These + # need to be removed in order to prevent bad interactions + # with the flush operation in other tests. + old_app_models = copy.deepcopy(cache.app_models) + old_app_store = copy.deepcopy(cache.app_store) + + settings.TEST_SWAPPABLE_MODEL = 'managers_regress.Parent' + + class SwappableModel(models.Model): + class Meta: + swappable = 'TEST_SWAPPABLE_MODEL' + + # Accessing the manager on a swappable model should + # raise an attribute error with a helpful message + try: + SwappableModel.objects.all() + self.fail('Should raise an AttributeError') + except AttributeError as e: + self.assertEqual(str(e), "Manager isn't available; SwappableModel has been swapped for 'managers_regress.Parent'") + + finally: + del settings.TEST_SWAPPABLE_MODEL + cache.app_models = old_app_models + cache.app_store = old_app_store + + def test_custom_swappable_manager(self): + try: + # This test adds dummy models to the app cache. These + # need to be removed in order to prevent bad interactions + # with the flush operation in other tests. + old_app_models = copy.deepcopy(cache.app_models) + old_app_store = copy.deepcopy(cache.app_store) + + settings.TEST_SWAPPABLE_MODEL = 'managers_regress.Parent' + + class SwappableModel(models.Model): + + stuff = models.Manager() + + class Meta: + swappable = 'TEST_SWAPPABLE_MODEL' + + # Accessing the manager on a swappable model with an + # explicit manager should raise an attribute error with a + # helpful message + try: + SwappableModel.stuff.all() + self.fail('Should raise an AttributeError') + except AttributeError as e: + self.assertEqual(str(e), "Manager isn't available; SwappableModel has been swapped for 'managers_regress.Parent'") + + finally: + del settings.TEST_SWAPPABLE_MODEL + cache.app_models = old_app_models + cache.app_store = old_app_store + + def test_explicit_swappable_manager(self): + try: + # This test adds dummy models to the app cache. These + # need to be removed in order to prevent bad interactions + # with the flush operation in other tests. + old_app_models = copy.deepcopy(cache.app_models) + old_app_store = copy.deepcopy(cache.app_store) + + settings.TEST_SWAPPABLE_MODEL = 'managers_regress.Parent' + + class SwappableModel(models.Model): + + objects = models.Manager() + + class Meta: + swappable = 'TEST_SWAPPABLE_MODEL' + + # Accessing the manager on a swappable model with an + # explicit manager should raise an attribute error with a + # helpful message + try: + SwappableModel.objects.all() + self.fail('Should raise an AttributeError') + except AttributeError as e: + self.assertEqual(str(e), "Manager isn't available; SwappableModel has been swapped for 'managers_regress.Parent'") + + finally: + del settings.TEST_SWAPPABLE_MODEL + cache.app_models = old_app_models + cache.app_store = old_app_store From 6d46c740d80b0c7f75064bc6bb4d99b15b106ba4 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 6 Oct 2012 07:02:11 -0400 Subject: [PATCH 128/265] Fixed #17435 - Clarified that QuerySet.update returns the number of rows matched --- docs/ref/models/querysets.txt | 3 ++- docs/topics/db/queries.txt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index f9dbb76ea0..858371978a 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1564,7 +1564,8 @@ update .. method:: update(**kwargs) Performs an SQL update query for the specified fields, and returns -the number of rows affected. +the number of rows matched (which may not be equal to the number of rows +updated if some rows already have the new value). For example, to turn comments off for all blog entries published in 2010, you could do this:: diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index c724eabb8e..54f069248a 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -959,7 +959,8 @@ new value to be the new model instance you want to point to. For example:: >>> Entry.objects.all().update(blog=b) The ``update()`` method is applied instantly and returns the number of rows -affected by the query. The only restriction on the +matched by the query (which may not be equal to the number of rows updated if +some rows already have the new value). The only restriction on the :class:`~django.db.models.query.QuerySet` that is updated is that it can only access one database table, the model's main table. You can filter based on related fields, but you can only update columns in the model's main From 117e99511e0985701780ed1bcd3afd456e244ae3 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 6 Oct 2012 13:14:11 +0200 Subject: [PATCH 129/265] Added assertXML[Not]Equal assertions This is especially needed to compare XML when hash randomization is on, as attribute order may vary. Refs #17758, #19038. Thanks Taylor Mitchell for the initial patch, and Ian Clelland for review and cleanup. --- django/test/testcases.py | 124 ++++++---------------- django/test/utils.py | 92 ++++++++++++++++ docs/releases/1.5.txt | 5 + docs/topics/testing.txt | 19 ++++ tests/regressiontests/test_utils/tests.py | 35 ++++++ 5 files changed, 186 insertions(+), 89 deletions(-) diff --git a/django/test/testcases.py b/django/test/testcases.py index 2b1ef912b6..260b060c45 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -11,7 +11,6 @@ try: from urllib.parse import urlsplit, urlunsplit except ImportError: # Python 2 from urlparse import urlsplit, urlunsplit -from xml.dom.minidom import parseString, Node import select import socket import threading @@ -38,7 +37,7 @@ from django.test.client import Client from django.test.html import HTMLParseError, parse_html from django.test.signals import template_rendered from django.test.utils import (get_warnings_state, restore_warnings_state, - override_settings) + override_settings, compare_xml, strip_quotes) from django.test.utils import ContextList from django.utils import unittest as ut2 from django.utils.encoding import force_text @@ -134,70 +133,16 @@ class OutputChecker(doctest.OutputChecker): optionflags) def check_output_xml(self, want, got, optionsflags): - """Tries to do a 'xml-comparision' of want and got. Plain string - comparision doesn't always work because, for example, attribute - ordering should not be important. - - Based on http://codespeak.net/svn/lxml/trunk/src/lxml/doctestcompare.py - """ - _norm_whitespace_re = re.compile(r'[ \t\n][ \t\n]+') - def norm_whitespace(v): - return _norm_whitespace_re.sub(' ', v) - - def child_text(element): - return ''.join([c.data for c in element.childNodes - if c.nodeType == Node.TEXT_NODE]) - - def children(element): - return [c for c in element.childNodes - if c.nodeType == Node.ELEMENT_NODE] - - def norm_child_text(element): - return norm_whitespace(child_text(element)) - - def attrs_dict(element): - return dict(element.attributes.items()) - - def check_element(want_element, got_element): - if want_element.tagName != got_element.tagName: - return False - if norm_child_text(want_element) != norm_child_text(got_element): - return False - if attrs_dict(want_element) != attrs_dict(got_element): - return False - want_children = children(want_element) - got_children = children(got_element) - if len(want_children) != len(got_children): - return False - for want, got in zip(want_children, got_children): - if not check_element(want, got): - return False - return True - - want, got = self._strip_quotes(want, got) - want = want.replace('\\n','\n') - got = got.replace('\\n','\n') - - # If the string is not a complete xml document, we may need to add a - # root element. This allow us to compare fragments, like "" - if not want.startswith('%s' - want = wrapper % want - got = wrapper % got - - # Parse the want and got strings, and compare the parsings. try: - want_root = parseString(want).firstChild - got_root = parseString(got).firstChild + return compare_xml(want, got) except Exception: return False - return check_element(want_root, got_root) def check_output_json(self, want, got, optionsflags): """ Tries to compare want and got as if they were JSON-encoded data """ - want, got = self._strip_quotes(want, got) + want, got = strip_quotes(want, got) try: want_json = json.loads(want) got_json = json.loads(got) @@ -205,37 +150,6 @@ class OutputChecker(doctest.OutputChecker): return False return want_json == got_json - def _strip_quotes(self, want, got): - """ - Strip quotes of doctests output values: - - >>> o = OutputChecker() - >>> o._strip_quotes("'foo'") - "foo" - >>> o._strip_quotes('"foo"') - "foo" - """ - def is_quoted_string(s): - s = s.strip() - return (len(s) >= 2 - and s[0] == s[-1] - and s[0] in ('"', "'")) - - def is_quoted_unicode(s): - s = s.strip() - return (len(s) >= 3 - and s[0] == 'u' - and s[1] == s[-1] - and s[1] in ('"', "'")) - - if is_quoted_string(want) and is_quoted_string(got): - want = want.strip()[1:-1] - got = got.strip()[1:-1] - elif is_quoted_unicode(want) and is_quoted_unicode(got): - want = want.strip()[2:-1] - got = got.strip()[2:-1] - return want, got - class DocTestRunner(doctest.DocTestRunner): def __init__(self, *args, **kwargs): @@ -445,6 +359,38 @@ class SimpleTestCase(ut2.TestCase): safe_repr(dom1, True), safe_repr(dom2, True)) self.fail(self._formatMessage(msg, standardMsg)) + def assertXMLEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if not result: + standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertXMLNotEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are not semantically equivalent. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if result: + standardMsg = '%s == %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + class TransactionTestCase(SimpleTestCase): diff --git a/django/test/utils.py b/django/test/utils.py index 4fbe6f824e..71252eaac8 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -1,4 +1,7 @@ +import re import warnings +from xml.dom.minidom import parseString, Node + from django.conf import settings, UserSettingsHolder from django.core import mail from django.test.signals import template_rendered, setting_changed @@ -223,5 +226,94 @@ class override_settings(object): setting=key, value=new_value) +def compare_xml(want, got): + """Tries to do a 'xml-comparision' of want and got. Plain string + comparision doesn't always work because, for example, attribute + ordering should not be important. + + Based on http://codespeak.net/svn/lxml/trunk/src/lxml/doctestcompare.py + """ + _norm_whitespace_re = re.compile(r'[ \t\n][ \t\n]+') + def norm_whitespace(v): + return _norm_whitespace_re.sub(' ', v) + + def child_text(element): + return ''.join([c.data for c in element.childNodes + if c.nodeType == Node.TEXT_NODE]) + + def children(element): + return [c for c in element.childNodes + if c.nodeType == Node.ELEMENT_NODE] + + def norm_child_text(element): + return norm_whitespace(child_text(element)) + + def attrs_dict(element): + return dict(element.attributes.items()) + + def check_element(want_element, got_element): + if want_element.tagName != got_element.tagName: + return False + if norm_child_text(want_element) != norm_child_text(got_element): + return False + if attrs_dict(want_element) != attrs_dict(got_element): + return False + want_children = children(want_element) + got_children = children(got_element) + if len(want_children) != len(got_children): + return False + for want, got in zip(want_children, got_children): + if not check_element(want, got): + return False + return True + + want, got = strip_quotes(want, got) + want = want.replace('\\n','\n') + got = got.replace('\\n','\n') + + # If the string is not a complete xml document, we may need to add a + # root element. This allow us to compare fragments, like "" + if not want.startswith('%s' + want = wrapper % want + got = wrapper % got + + # Parse the want and got strings, and compare the parsings. + want_root = parseString(want).firstChild + got_root = parseString(got).firstChild + + return check_element(want_root, got_root) + + +def strip_quotes(want, got): + """ + Strip quotes of doctests output values: + + >>> strip_quotes("'foo'") + "foo" + >>> strip_quotes('"foo"') + "foo" + """ + def is_quoted_string(s): + s = s.strip() + return (len(s) >= 2 + and s[0] == s[-1] + and s[0] in ('"', "'")) + + def is_quoted_unicode(s): + s = s.strip() + return (len(s) >= 3 + and s[0] == 'u' + and s[1] == s[-1] + and s[1] in ('"', "'")) + + if is_quoted_string(want) and is_quoted_string(got): + want = want.strip()[1:-1] + got = got.strip()[1:-1] + elif is_quoted_unicode(want) and is_quoted_unicode(got): + want = want.strip()[2:-1] + got = got.strip()[2:-1] + return want, got + def str_prefix(s): return s % {'_': '' if six.PY3 else 'u'} diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index d87ec36204..e99b2fd578 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -198,6 +198,11 @@ Django 1.5 also includes several smaller improvements worth noting: * The loaddata management command now supports an `ignorenonexistent` option to ignore data for fields that no longer exist. +* :meth:`~django.test.SimpleTestCase.assertXMLEqual` and + :meth:`~django.test.SimpleTestCase.assertXMLNotEqual` new assertions allow + you to test equality for XML content at a semantic level, without caring for + syntax differences (spaces, attribute order, etc.). + Backwards incompatible changes in 1.5 ===================================== diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index 3950e1c917..895e721ef5 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -1783,6 +1783,25 @@ your test suite. ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be raised if one of them cannot be parsed. +.. method:: SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None) + + .. versionadded:: 1.5 + + Asserts that the strings ``xml1`` and ``xml2`` are equal. The + comparison is based on XML semantics. Similarily to + :meth:`~SimpleTestCase.assertHTMLEqual`, the comparison is + made on parsed content, hence only semantic differences are considered, not + syntax differences. When unvalid XML is passed in any parameter, an + ``AssertionError`` is always raised, even if both string are identical. + +.. method:: SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None) + + .. versionadded:: 1.5 + + Asserts that the strings ``xml1`` and ``xml2`` are *not* equal. The + comparison is based on XML semantics. See + :meth:`~SimpleTestCase.assertXMLEqual` for details. + .. _topics-testing-email: Email services diff --git a/tests/regressiontests/test_utils/tests.py b/tests/regressiontests/test_utils/tests.py index 12c639cee1..dec157eacb 100644 --- a/tests/regressiontests/test_utils/tests.py +++ b/tests/regressiontests/test_utils/tests.py @@ -450,6 +450,41 @@ class HTMLEqualTests(TestCase): self.assertContains(response, '

    Some help text for the title (with unicode ŠĐĆŽćžšđ)

    ', html=True) +class XMLEqualTests(TestCase): + def test_simple_equal(self): + xml1 = "" + xml2 = "" + self.assertXMLEqual(xml1, xml2) + + def test_simple_equal_unordered(self): + xml1 = "" + xml2 = "" + self.assertXMLEqual(xml1, xml2) + + def test_simple_equal_raise(self): + xml1 = "" + xml2 = "" + with self.assertRaises(AssertionError): + self.assertXMLEqual(xml1, xml2) + + def test_simple_not_equal(self): + xml1 = "" + xml2 = "" + self.assertXMLNotEqual(xml1, xml2) + + def test_simple_not_equal_raise(self): + xml1 = "" + xml2 = "" + with self.assertRaises(AssertionError): + self.assertXMLNotEqual(xml1, xml2) + + def test_parsing_errors(self): + xml_unvalid = "" + xml2 = "" + with self.assertRaises(AssertionError): + self.assertXMLNotEqual(xml_unvalid, xml2) + + class SkippingExtraTests(TestCase): fixtures = ['should_not_be_loaded.json'] From 8a2216648fcc1c6e504902e612b046c213de46d7 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 6 Oct 2012 14:40:00 +0200 Subject: [PATCH 130/265] Un-gzipped test geometries fixture as plain json This is easier to track changes through the VCS. --- django/contrib/gis/geometry/test_data.py | 5 +- django/contrib/gis/tests/data/geometries.json | 121 ++++++++++++++++++ .../contrib/gis/tests/data/geometries.json.gz | Bin 9100 -> 0 bytes 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 django/contrib/gis/tests/data/geometries.json delete mode 100644 django/contrib/gis/tests/data/geometries.json.gz diff --git a/django/contrib/gis/geometry/test_data.py b/django/contrib/gis/geometry/test_data.py index 833e62f224..b0f6e1ad57 100644 --- a/django/contrib/gis/geometry/test_data.py +++ b/django/contrib/gis/geometry/test_data.py @@ -2,7 +2,6 @@ This module has the mock object definitions used to hold reference geometry for the GEOS and GDAL tests. """ -import gzip import json import os @@ -100,7 +99,7 @@ class TestDataMixin(object): global GEOMETRIES if GEOMETRIES is None: # Load up the test geometry data from fixture into global. - gzf = gzip.GzipFile(os.path.join(TEST_DATA, 'geometries.json.gz')) - geometries = json.loads(gzf.read().decode()) + with open(os.path.join(TEST_DATA, 'geometries.json')) as f: + geometries = json.load(f) GEOMETRIES = TestGeomSet(**strconvert(geometries)) return GEOMETRIES diff --git a/django/contrib/gis/tests/data/geometries.json b/django/contrib/gis/tests/data/geometries.json new file mode 100644 index 0000000000..46de4d6182 --- /dev/null +++ b/django/contrib/gis/tests/data/geometries.json @@ -0,0 +1,121 @@ +{ + "polygons": [ + {"wkt": "POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0), (10 10, 10 90, 90 90, 90 10, 10 10))", "n_i": 1, "ext_ring_cs": [[0, 0], [0, 100], [100, 100], [100, 0], [0, 0]], "n_p": 10, "area": 3600.0, "centroid": [50.0, 50.0]}, + {"wkt": "POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0), (10 10, 10 20, 20 20, 20 10, 10 10), (80 80, 80 90, 90 90, 90 80, 80 80))", "n_i": 2, "ext_ring_cs": [[0, 0], [0, 100], [100, 100], [100, 0], [0, 0]], "n_p": 15, "area": 9800.0, "centroid": [50.0, 50.0]}, + {"wkt": "POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0))", "n_i": 0, "ext_ring_cs": [[0, 0], [0, 100], [100, 100], [100, 0], [0, 0]], "n_p": 5, "area": 10000.0, "centroid": [50.0, 50.0]}, + {"wkt": "POLYGON ((-95.3848703124799471 29.7056021479768511, -95.3851905195191847 29.7046588196500281, -95.3859356966379011 29.7025053545605502, -95.3860723000647539 29.7020963367038391, -95.3871517697222089 29.6989779021280995, -95.3865578518265522 29.6990856888057202, -95.3862634205175226 29.6999471753441782, -95.3861991779541967 29.6999591988978615, -95.3856773799358137 29.6998323107113578, -95.3856209915427229 29.6998005235473741, -95.3855833545501639 29.6996619391729801, -95.3855776331865002 29.6996232659570047, -95.3850162731712885 29.6997236706530536, -95.3831047357410284 29.7000847603095082, -95.3829800724914776 29.7000676365023502, -95.3828084594470909 29.6999969684031200, -95.3828131504821499 29.6999090511531065, -95.3828022942979601 29.6998152117366025, -95.3827893930918833 29.6997790953076759, -95.3825174668099862 29.6998267772748825, -95.3823521544804862 29.7000451723151606, -95.3820491918785223 29.6999682034582335, -95.3817932841505893 29.6999640407204772, -95.3815438924600443 29.7005983712500630, -95.3807812390843424 29.7007538492921590, -95.3778578936435935 29.7012966201172048, -95.3770817300034679 29.7010555145969093, -95.3772763716395957 29.7004995005932031, -95.3769891024414420 29.7005797730360186, -95.3759855007185990 29.7007754783987821, -95.3759516423090474 29.7007305400669388, -95.3765252155960042 29.6989549173240874, -95.3766842746727832 29.6985134987163164, -95.3768510987262914 29.6980530300744938, -95.3769198676258014 29.6977137204527573, -95.3769616670751930 29.6973351617272172, -95.3770309229297766 29.6969821084304186, -95.3772352596880637 29.6959751305871613, -95.3776232419333354 29.6945439060847463, -95.3776849628727064 29.6943364710766069, -95.3779699491714723 29.6926548349458947, -95.3781945479573494 29.6920088336742545, -95.3785807118394189 29.6908279316076005, -95.3787441368896651 29.6908846275832197, -95.3787903214163890 29.6907152912461640, -95.3791765069353659 29.6893335376821526, -95.3794935959513026 29.6884781789101595, -95.3796592071232112 29.6880066681407619, -95.3799788182090111 29.6873687353035081, -95.3801545516183893 29.6868782380716993, -95.3801258908302145 29.6867756621337762, -95.3801104284899566 29.6867229678809572, -95.3803803523746154 29.6863753372986459, -95.3821028558287622 29.6837392961470421, -95.3827289584682205 29.6828097375216160, -95.3827494698109035 29.6790739156259278, -95.3826022014838486 29.6776502228345507, -95.3825047356438063 29.6765773006280753, -95.3823473035336917 29.6750405250369127, -95.3824540163482055 29.6750076408228587, -95.3838984230304305 29.6745679207378679, -95.3916547074937426 29.6722459226508377, -95.3926154662749468 29.6719609085105489, -95.3967246645118081 29.6707316485589736, -95.3974588054406780 29.6705065336410989, -95.3978523748756828 29.6703795547846845, -95.3988598162279970 29.6700874981900853, -95.3995628600665952 29.6698505300412414, -95.4134721665944170 29.6656841279906232, -95.4143262068232616 29.6654291174019278, -95.4159685142480214 29.6649750989232288, -95.4180067396277565 29.6643253024318021, -95.4185886692196590 29.6641482768691063, -95.4234155309609662 29.6626925393704788, -95.4287785503196346 29.6611023620959706, -95.4310287312749352 29.6604222580752648, -95.4320295629628959 29.6603361318136720, -95.4332899683975739 29.6600560661713608, -95.4342675748811047 29.6598454934599900, -95.4343110414310871 29.6598411486215490, -95.4345576779282538 29.6598147020668499, -95.4348823041721630 29.6597875803673112, -95.4352827715209457 29.6597762346946681, -95.4355290431309982 29.6597827926562374, -95.4359197997999331 29.6598014511782715, -95.4361907884752156 29.6598444333523368, -95.4364608955807228 29.6598901433108217, -95.4367250147512323 29.6599494499910712, -95.4364898759758091 29.6601880616540186, -95.4354501111810691 29.6616378572201107, -95.4381459623171224 29.6631265631655126, -95.4367852490863129 29.6642266600024023, -95.4370040894557263 29.6643425389568769, -95.4367078350812648 29.6645492592343238, -95.4366081749871285 29.6646291473027297, -95.4358539359938192 29.6652308742342932, -95.4350327668927889 29.6658995989314462, -95.4350580905272921 29.6678812477895271, -95.4349710541447536 29.6680054925936965, -95.4349500440473548 29.6671410080890006, -95.4341492724148850 29.6678790545191688, -95.4340248868274728 29.6680353198492135, -95.4333227845797438 29.6689245624945990, -95.4331325652123326 29.6691616138940901, -95.4321314741096955 29.6704473333237253, -95.4320435792664341 29.6702578985411982, -95.4320147929883547 29.6701800936425109, -95.4319764538662980 29.6683246590817085, -95.4317490976340679 29.6684974372577166, -95.4305958185342718 29.6694049049170374, -95.4296600735653016 29.6701723430938493, -95.4284928989940937 29.6710931793380972, -95.4274630532378580 29.6719378813640091, -95.4273056811974811 29.6720684984625791, -95.4260554084574864 29.6730668861566969, -95.4253558063699643 29.6736342467365724, -95.4249278826026028 29.6739557343648919, -95.4248648873821423 29.6745400910786152, -95.4260016131471929 29.6750987014005858, -95.4258567183010911 29.6753452063069929, -95.4260238081486847 29.6754322077221353, -95.4258707374502393 29.6756647377294307, -95.4257951755816691 29.6756407098663360, -95.4257701599566985 29.6761077719536068, -95.4257726684792260 29.6761711204603955, -95.4257980187195614 29.6770219651929423, -95.4252712669032519 29.6770161558853758, -95.4249234392992065 29.6770068683962300, -95.4249574272905789 29.6779707498635759, -95.4244725881033702 29.6779825646764159, -95.4222269476429545 29.6780711474441716, -95.4223032371999267 29.6796029391538809, -95.4239133706588945 29.6795331493690355, -95.4224579084327331 29.6813706893847780, -95.4224290108823965 29.6821953228763924, -95.4230916478977349 29.6822130268724109, -95.4222928279595521 29.6832041816675343, -95.4228763710016352 29.6832087677714505, -95.4223401691637179 29.6838987872753748, -95.4211655906087088 29.6838784024852984, -95.4201984153205558 29.6851319258758082, -95.4206156387716362 29.6851623398125319, -95.4213438084897660 29.6851763011334739, -95.4212071118618752 29.6853679931624974, -95.4202651399651245 29.6865313962980508, -95.4172061157659783 29.6865816431043932, -95.4182217951255183 29.6872251197301544, -95.4178664826439160 29.6876750901471631, -95.4180678442928780 29.6877960336377207, -95.4188763472917572 29.6882826379510938, -95.4185374500596311 29.6887137897831934, -95.4182121713132290 29.6885097429738813, -95.4179857231741551 29.6888118367840086, -95.4183106010563620 29.6890048676118212, -95.4179489865331334 29.6894546700979056, -95.4175581746284820 29.6892323606815438, -95.4173439957341571 29.6894990139807007, -95.4177411199311081 29.6897435034738422, -95.4175789200209721 29.6899207529979208, -95.4170598559864800 29.6896042165807508, -95.4166733682539814 29.6900891174451367, -95.4165941362704331 29.6900347214235047, -95.4163537218065301 29.6903529467753238, -95.4126843270708775 29.6881086357212780, -95.4126604121378392 29.6880942378803496, -95.4126672298953338 29.6885951670109982, -95.4126680884821923 29.6887052446594275, -95.4158080137241882 29.6906382377959339, -95.4152061403821961 29.6910871045531586, -95.4155842583188161 29.6917382915894308, -95.4157426793520358 29.6920726941677096, -95.4154520563662203 29.6922052332446427, -95.4151389936167078 29.6923261661269571, -95.4148649784384872 29.6924343866430256, -95.4144051352401590 29.6925623927348106, -95.4146792019416665 29.6926770338507744, -95.4148824479948985 29.6928117893696388, -95.4149851734360226 29.6929823719519774, -95.4140436551925291 29.6929626643100946, -95.4140465993023241 29.6926545917254892, -95.4137269186733334 29.6927395764256090, -95.4137372859685513 29.6935432485666624, -95.4135702836218655 29.6933186678088283, -95.4133925235973237 29.6930415229852152, -95.4133017035615580 29.6928685062036166, -95.4129588921634593 29.6929391128977862, -95.4125107395559695 29.6930481664661485, -95.4102647423187307 29.6935850183258019, -95.4081931340840157 29.6940907430947760, -95.4078783596459772 29.6941703429951609, -95.4049213975000043 29.6948723732981961, -95.4045944244127071 29.6949626434239207, -95.4045865139788134 29.6954109019001358, -95.4045953345484037 29.6956972800496963, -95.4038879332535146 29.6958296089365490, -95.4040366394459340 29.6964389004769842, -95.4032774779020798 29.6965643341263892, -95.4026066501239853 29.6966646227683881, -95.4024991226393837 29.6961389766619703, -95.4011781398631911 29.6963566063186377, -95.4011524097636112 29.6962596176762190, -95.4018184046368276 29.6961399466727336, -95.4016995838361908 29.6956442609415099, -95.4007100753964608 29.6958900524002978, -95.4008032469935188 29.6962639900781404, -95.3995660267125487 29.6965636449370329, -95.3996140564775601 29.6967877962763644, -95.3996364430014410 29.6968901984825280, -95.3984003269631842 29.6968679634805746, -95.3981442026887265 29.6983660679730335, -95.3980178461957706 29.6990890276252415, -95.3977097967130163 29.7008526152273049, -95.3962347157626027 29.7009697553607630, -95.3951949050136250 29.7004740386619019, -95.3957564950617183 29.6990281830553187, -95.3965927101519924 29.6968771129030706, -95.3957496517238184 29.6970800358387095, -95.3957720559467361 29.6972264611230727, -95.3957391586571788 29.6973548894558732, -95.3956286413405365 29.6974949857280883, -95.3955111053256957 29.6975661086270186, -95.3953215342724121 29.6976022763384790, -95.3951795558443365 29.6975846977491038, -95.3950369632041060 29.6975175779330200, -95.3949401089966500 29.6974269267953304, -95.3948740281415581 29.6972903308506346, -95.3946650813866910 29.6973397326847923, -95.3947654059391112 29.6974882560192022, -95.3949627316619768 29.6980355864961858, -95.3933200807862249 29.6984590863712796, -95.3932606497523494 29.6984464798710839, -95.3932983699113350 29.6983154306484352, -95.3933058014696655 29.6982165816983610, -95.3932946347785133 29.6981089778195759, -95.3931780601756287 29.6977068906794841, -95.3929928222970602 29.6977541771878180, -95.3930873169846478 29.6980676264932946, -95.3932743746374570 29.6981249406449663, -95.3929512584706316 29.6989526513922222, -95.3919850280655197 29.7014358632108646, -95.3918950918929056 29.7014169320765724, -95.3916928317890296 29.7019232352846423, -95.3915424614970959 29.7022988712928289, -95.3901530441668939 29.7058519502930061, -95.3899656322116698 29.7059156823562418, -95.3897628748670883 29.7059900058266777, -95.3896062677805787 29.7060738276384946, -95.3893941800512266 29.7061891695242046, -95.3892150365492455 29.7062641292949436, -95.3890502563035199 29.7063339729630940, -95.3888717930715586 29.7063896908080736, -95.3886925428988945 29.7064453871994978, -95.3885376849411983 29.7064797304524149, -95.3883284158984139 29.7065153575050189, -95.3881046767627794 29.7065368368267357, -95.3878809284696132 29.7065363048447537, -95.3876046356120924 29.7065288525102424, -95.3873060894974714 29.7064822806001452, -95.3869851943158409 29.7063993367575350, -95.3865967896568065 29.7062870572919202, -95.3861785624983156 29.7061492099008184, -95.3857375009733488 29.7059887337478798, -95.3854573290902152 29.7058683664514618, -95.3848703124799471 29.7056021479768511))", "n_i": 0, "ext_ring_cs": false, "n_p": 264, "area": 0.00129917360654, "centroid": [-95.403569179437341, 29.681772571690402]} + ], + "multipolygons": [ + {"wkt": "MULTIPOLYGON (((100 20, 180 20, 180 100, 100 100, 100 20)), ((20 100, 100 100, 100 180, 20 180, 20 100)), ((100 180, 180 180, 180 260, 100 260, 100 180)), ((180 100, 260 100, 260 180, 180 180, 180 100)))","valid": true, "num_geom":4, "n_p":20}, + {"wkt": "MULTIPOLYGON (((60 300, 320 220, 260 60, 60 100, 60 300)), ((60 300, 320 220, 260 60, 60 100, 60 300)))", "valid": false}, + {"wkt": "MULTIPOLYGON (((180 60, 240 160, 300 60, 180 60)), ((80 80, 180 60, 160 140, 240 160, 360 140, 300 60, 420 100, 320 280, 120 260, 80 80)))", "valid": true, "num_geom": 2, "n_p": 14} + ], + "errors": [ + {"wkt": "GEOMETR##!@#%#............a32515", "bad": true, "hex": false}, + {"wkt": "Foo.Bar", "bad": true, "hex": false}, + {"wkt": "POINT (5, 23)", "bad": true, "hex": false}, + {"wkt": "AAABBBDDDAAD##@#1113511111-098111111111111111533333333333333", "bad": true, "hex": true}, + {"wkt": "FFFFFFFFFFFFFFFFF1355555555555555555565111", "bad": true, "hex": true}, + {"wkt": "", "bad": true, "hex": false} + ], + "wkt_out": [ + {"wkt": "POINT (110 130)", "ewkt": "POINT (110.0000000000000000 130.0000000000000000)", "kml": "110.0,130.0,0", "gml": "110,130"}, + {"wkt": "LINESTRING (40 40,50 130,130 130)", "ewkt": "LINESTRING (40.0000000000000000 40.0000000000000000, 50.0000000000000000 130.0000000000000000, 130.0000000000000000 130.0000000000000000)", "kml": "40.0,40.0,0 50.0,130.0,0 130.0,130.0,0", "gml": "40,40 50,130 130,130"}, + {"wkt": "POLYGON ((150 150,410 150,280 20,20 20,150 150),(170 120,330 120,260 50,100 50,170 120))", "ewkt": "POLYGON ((150.0000000000000000 150.0000000000000000, 410.0000000000000000 150.0000000000000000, 280.0000000000000000 20.0000000000000000, 20.0000000000000000 20.0000000000000000, 150.0000000000000000 150.0000000000000000), (170.0000000000000000 120.0000000000000000, 330.0000000000000000 120.0000000000000000, 260.0000000000000000 50.0000000000000000, 100.0000000000000000 50.0000000000000000, 170.0000000000000000 120.0000000000000000))", "kml": "150.0,150.0,0 410.0,150.0,0 280.0,20.0,0 20.0,20.0,0 150.0,150.0,0170.0,120.0,0 330.0,120.0,0 260.0,50.0,0 100.0,50.0,0 170.0,120.0,0", "gml": "150,150 410,150 280,20 20,20 150,150170,120 330,120 260,50 100,50 170,120"}, + {"wkt": "MULTIPOINT (10 80,110 170,110 120)", "ewkt": "MULTIPOINT (10.0000000000000000 80.0000000000000000, 110.0000000000000000 170.0000000000000000, 110.0000000000000000 120.0000000000000000)", "kml": "10.0,80.0,0110.0,170.0,0110.0,120.0,0", "gml": "10,80110,170110,120"}, + {"wkt": "MULTILINESTRING ((110 100,40 30,180 30),(170 30,110 90,50 30))", "ewkt": "MULTILINESTRING ((110.0000000000000000 100.0000000000000000, 40.0000000000000000 30.0000000000000000, 180.0000000000000000 30.0000000000000000), (170.0000000000000000 30.0000000000000000, 110.0000000000000000 90.0000000000000000, 50.0000000000000000 30.0000000000000000))", "kml": "110.0,100.0,0 40.0,30.0,0 180.0,30.0,0170.0,30.0,0 110.0,90.0,0 50.0,30.0,0", "gml": "110,100 40,30 180,30170,30 110,90 50,30"}, + {"wkt": "MULTIPOLYGON (((110 110,70 200,150 200,110 110),(110 110,100 180,120 180,110 110)),((110 110,150 20,70 20,110 110),(110 110,120 40,100 40,110 110)))", "ewkt": "MULTIPOLYGON (((110.0000000000000000 110.0000000000000000, 70.0000000000000000 200.0000000000000000, 150.0000000000000000 200.0000000000000000, 110.0000000000000000 110.0000000000000000), (110.0000000000000000 110.0000000000000000, 100.0000000000000000 180.0000000000000000, 120.0000000000000000 180.0000000000000000, 110.0000000000000000 110.0000000000000000)), ((110.0000000000000000 110.0000000000000000, 150.0000000000000000 20.0000000000000000, 70.0000000000000000 20.0000000000000000, 110.0000000000000000 110.0000000000000000), (110.0000000000000000 110.0000000000000000, 120.0000000000000000 40.0000000000000000, 100.0000000000000000 40.0000000000000000, 110.0000000000000000 110.0000000000000000)))", "kml": "110.0,110.0,0 70.0,200.0,0 150.0,200.0,0 110.0,110.0,0110.0,110.0,0 100.0,180.0,0 120.0,180.0,0 110.0,110.0,0110.0,110.0,0 150.0,20.0,0 70.0,20.0,0 110.0,110.0,0110.0,110.0,0 120.0,40.0,0 100.0,40.0,0 110.0,110.0,0", "gml": "110,110 70,200 150,200 110,110110,110 100,180 120,180 110,110110,110 150,20 70,20 110,110110,110 120,40 100,40 110,110"}, + {"wkt": "GEOMETRYCOLLECTION (POINT (110 260),LINESTRING (110 0,110 60))", "ewkt": "GEOMETRYCOLLECTION (POINT (110.0000000000000000 260.0000000000000000), LINESTRING (110.0000000000000000 0.0000000000000000, 110.0000000000000000 60.0000000000000000))", "kml": "110.0,260.0,0110.0,0.0,0 110.0,60.0,0", "gml": "110,260110,0 110,60"} + ], + "hex_wkt": [ + {"wkt": "POINT(0 1)", "hex": "01010000000000000000000000000000000000F03F"}, + {"wkt": "LINESTRING(0 1, 2 3, 4 5)", "hex": "0102000000030000000000000000000000000000000000F03F0000000000000040000000000000084000000000000010400000000000001440"}, + {"wkt": "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))", "hex": "010300000001000000050000000000000000000000000000000000000000000000000024400000000000000000000000000000244000000000000024400000000000000000000000000000244000000000000000000000000000000000"}, + {"wkt": "MULTIPOINT(0 0, 10 0, 10 10, 0 10, 0 0)", "hex": "010400000005000000010100000000000000000000000000000000000000010100000000000000000024400000000000000000010100000000000000000024400000000000002440010100000000000000000000000000000000002440010100000000000000000000000000000000000000"}, + {"wkt": "MULTILINESTRING((0 0, 10 0, 10 10, 0 10),(20 20, 30 20))", "hex": "01050000000200000001020000000400000000000000000000000000000000000000000000000000244000000000000000000000000000002440000000000000244000000000000000000000000000002440010200000002000000000000000000344000000000000034400000000000003E400000000000003440"}, + {"wkt": "MULTIPOLYGON(((0 0, 10 0, 10 10, 0 10, 0 0)),((20 20, 20 30, 30 30, 30 20, 20 20),(25 25, 25 26, 26 26, 26 25, 25 25)))", "hex": "010600000002000000010300000001000000050000000000000000000000000000000000000000000000000024400000000000000000000000000000244000000000000024400000000000000000000000000000244000000000000000000000000000000000010300000002000000050000000000000000003440000000000000344000000000000034400000000000003E400000000000003E400000000000003E400000000000003E40000000000000344000000000000034400000000000003440050000000000000000003940000000000000394000000000000039400000000000003A400000000000003A400000000000003A400000000000003A40000000000000394000000000000039400000000000003940"}, + {"wkt": "GEOMETRYCOLLECTION(MULTIPOLYGON(((0 0, 10 0, 10 10, 0 10, 0 0)),((20 20, 20 30, 30 30, 30 20, 20 20),(25 25, 25 26, 26 26, 26 25, 25 25))),MULTILINESTRING((0 0, 10 0, 10 10, 0 10),(20 20, 30 20)),MULTIPOINT(0 0, 10 0, 10 10, 0 10, 0 0))", "hex": "010700000003000000010600000002000000010300000001000000050000000000000000000000000000000000000000000000000024400000000000000000000000000000244000000000000024400000000000000000000000000000244000000000000000000000000000000000010300000002000000050000000000000000003440000000000000344000000000000034400000000000003E400000000000003E400000000000003E400000000000003E40000000000000344000000000000034400000000000003440050000000000000000003940000000000000394000000000000039400000000000003A400000000000003A400000000000003A400000000000003A4000000000000039400000000000003940000000000000394001050000000200000001020000000400000000000000000000000000000000000000000000000000244000000000000000000000000000002440000000000000244000000000000000000000000000002440010200000002000000000000000000344000000000000034400000000000003E400000000000003440010400000005000000010100000000000000000000000000000000000000010100000000000000000024400000000000000000010100000000000000000024400000000000002440010100000000000000000000000000000000002440010100000000000000000000000000000000000000"} + ], + "json_geoms": [ + {"wkt": "POINT(100 0)", "json": "{ \"type\": \"Point\", \"coordinates\": [ 100.000000, 0.000000 ] }"}, + {"wkt": "POLYGON((0 0, -10 0, -10 -10, 0 -10, 0 0))", "json": "{ \"type\": \"Polygon\", \"coordinates\": [ [ [ 0.000000, 0.000000 ], [ -10.000000, 0.000000 ], [ -10.000000, -10.000000 ], [ 0.000000, -10.000000 ], [ 0.000000, 0.000000 ] ] ] }"}, + {"wkt": "MULTIPOLYGON(((102 2, 103 2, 103 3, 102 3, 102 2)), ((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0), (100.2 0.2, 100.8 0.2, 100.8 0.8, 100.2 0.8, 100.2 0.2)))", "json": "{ \"type\": \"MultiPolygon\", \"coordinates\": [ [ [ [ 102.000000, 2.000000 ], [ 103.000000, 2.000000 ], [ 103.000000, 3.000000 ], [ 102.000000, 3.000000 ], [ 102.000000, 2.000000 ] ] ], [ [ [ 100.000000, 0.000000 ], [ 101.000000, 0.000000 ], [ 101.000000, 1.000000 ], [ 100.000000, 1.000000 ], [ 100.000000, 0.000000 ] ], [ [ 100.200000, 0.200000 ], [ 100.800000, 0.200000 ], [ 100.800000, 0.800000 ], [ 100.200000, 0.800000 ], [ 100.200000, 0.200000 ] ] ] ] }"}, + {"wkt": "GEOMETRYCOLLECTION(POINT(100 0),LINESTRING(101.0 0.0, 102.0 1.0))", "json": "{ \"type\": \"GeometryCollection\", \"geometries\": [ { \"type\": \"Point\", \"coordinates\": [ 100.000000, 0.000000 ] }, { \"type\": \"LineString\", \"coordinates\": [ [ 101.000000, 0.000000 ], [ 102.000000, 1.000000 ] ] } ] }"}, + {"wkt": "MULTILINESTRING((100.0 0.0, 101.0 1.0),(102.0 2.0, 103.0 3.0))", "json": "\\n\\n{ \"type\": \"MultiLineString\",\\n \"coordinates\": [\\n [ [100.0, 0.0], [101.0, 1.0] ],\\n [ [102.0, 2.0], [103.0, 3.0] ]\\n ]\\n }\\n\\n", "not_equal": true} + ], + "points": [ + {"wkt": "POINT (5 23)", "x": 5.0, "y": 23.0, "centroid": [5.0, 23.0]}, + {"wkt": "POINT (-95.338492 29.723893)", "x": -95.338492, "y": 29.723893, "centroid": [-95.338492, 29.723893]}, + {"wkt": "POINT(1.234 5.678)", "x": 1.234, "y": 5.678, "centroid": [1.234, 5.678]}, + {"wkt": "POINT(4.321 8.765)", "x": 4.321, "y": 8.765, "centroid": [4.321, 8.765]}, + {"wkt": "POINT(10 10)", "x": 10, "y": 10, "centroid": [10.0, 10.0]}, + {"wkt": "POINT (5 23 8)", "x": 5.0, "y": 23.0, "z": 8.0, "centroid": [5.0, 23.0]} + ], + "multipoints":[ + {"wkt": "MULTIPOINT(10 10, 20 20 )", "n_p": 2, "coords": [[10.0, 10.0], [20.0, 20.0]], "centroid": [15.0, 15.0]}, + {"wkt": "MULTIPOINT(10 10, 20 20, 10 20, 20 10)", "n_p": 4, "coords": [[10.0, 10.0], [20.0, 20.0], [10.0, 20.0], [20.0, 10.0]], "centroid": [15.0, 15.0]} + ], + "linestrings": [ + {"wkt": "LINESTRING (60 180, 120 100, 180 180)", "n_p": 3, "centroid": [120.0, 140.0], "coords": [[60.0, 180.0], [120.0, 100.0], [180.0, 180.0]]}, + {"wkt": "LINESTRING (0 0, 5 5, 10 5, 10 10)", "n_p": 4, "centroid": [6.1611652351681556, 4.6966991411008934], "coords": [[0.0, 0.0], [5.0, 5.0], [10.0, 5.0], [10.0, 10.0]]} + ], + "linearrings": [ + {"wkt": "LINEARRING (649899.3065171393100172 4176512.3807915160432458, 649902.7294133581453934 4176512.7834989596158266, 649906.5550170192727819 4176514.3942507002502680, 649910.5820134161040187 4176516.0050024418160319, 649914.4076170771149918 4176518.0184616246260703, 649917.2264131171396002 4176519.4278986593708396, 649920.0452871860470623 4176521.6427505780011415, 649922.0587463703704998 4176522.8507948759943247, 649924.2735982896992937 4176524.4616246484220028, 649926.2870574744883925 4176525.4683542405255139, 649927.8978092158213258 4176526.8777912775985897, 649929.3072462501004338 4176528.0858355751261115, 649930.1126611357321963 4176529.4952726080082357, 649927.4951798024121672 4176506.9444361114874482, 649899.3065171393100172 4176512.3807915160432458)", "n_p": 15} + ], + "multilinestrings": [ + {"wkt": "MULTILINESTRING ((0 0, 0 100), (100 0, 100 100))", "n_p": 4, "centroid": [50.0, 50.0], "coords": [[[0, 0], [0, 100]], [[100, 0], [100, 100]]]}, + {"wkt": "MULTILINESTRING ((20 20, 60 60), (20 -20, 60 -60), (-20 -20, -60 -60), (-20 20, -60 60), (-80 0, 0 80, 80 0, 0 -80, -80 0), (-40 20, -40 -20), (-20 40, 20 40), (40 20, 40 -20), (20 -40, -20 -40))", "n_p": 21, "centroid": [0.0, 0.0], "coords": [[[20.0, 20.0], [60.0, 60.0]], [[20.0, -20.0], [60.0, -60.0]], [[-20.0, -20.0], [-60.0, -60.0]], [[-20.0, 20.0], [-60.0, 60.0]], [[-80.0, 0.0], [0.0, 80.0], [80.0, 0.0], [0.0, -80.0], [-80.0, 0.0]], [[-40.0, 20.0], [-40.0, -20.0]], [[-20.0, 40.0], [20.0, 40.0]], [[40.0, 20.0], [40.0, -20.0]], [[20.0, -40.0], [-20.0, -40.0]]]} + ], + "buffer_geoms": [ + {"wkt": "POINT(0 0)", + "buffer_wkt": "POLYGON ((5 0,4.903926402016153 -0.97545161008064,4.619397662556435 -1.913417161825447,4.157348061512728 -2.777851165098009,3.53553390593274 -3.535533905932735,2.777851165098015 -4.157348061512724,1.913417161825454 -4.619397662556431,0.975451610080648 -4.903926402016151,0.000000000000008 -5.0,-0.975451610080632 -4.903926402016154,-1.913417161825439 -4.619397662556437,-2.777851165098002 -4.157348061512732,-3.53553390593273 -3.535533905932746,-4.157348061512719 -2.777851165098022,-4.619397662556429 -1.913417161825462,-4.903926402016149 -0.975451610080656,-5.0 -0.000000000000016,-4.903926402016156 0.975451610080624,-4.619397662556441 1.913417161825432,-4.157348061512737 2.777851165097995,-3.535533905932752 3.535533905932723,-2.777851165098029 4.157348061512714,-1.913417161825468 4.619397662556426,-0.975451610080661 4.903926402016149,-0.000000000000019 5.0,0.975451610080624 4.903926402016156,1.913417161825434 4.61939766255644,2.777851165097998 4.157348061512735,3.535533905932727 3.535533905932748,4.157348061512719 2.777851165098022,4.619397662556429 1.91341716182546,4.90392640201615 0.975451610080652,5 0))", + "width": 5.0, "quadsegs": 8 + }, + {"wkt": "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))", + "buffer_wkt": "POLYGON ((-2 0,-2 10,-1.961570560806461 10.390180644032258,-1.847759065022573 10.765366864730179,-1.662939224605091 11.111140466039204,-1.414213562373095 11.414213562373096,-1.111140466039204 11.662939224605092,-0.765366864730179 11.847759065022574,-0.390180644032256 11.961570560806461,0 12,10 12,10.390180644032256 11.961570560806461,10.765366864730179 11.847759065022574,11.111140466039204 11.66293922460509,11.414213562373096 11.414213562373096,11.66293922460509 11.111140466039204,11.847759065022574 10.765366864730179,11.961570560806461 10.390180644032256,12 10,12 0,11.961570560806461 -0.390180644032256,11.847759065022574 -0.76536686473018,11.66293922460509 -1.111140466039204,11.414213562373096 -1.414213562373095,11.111140466039204 -1.66293922460509,10.765366864730179 -1.847759065022573,10.390180644032256 -1.961570560806461,10 -2,0.0 -2.0,-0.390180644032255 -1.961570560806461,-0.765366864730177 -1.847759065022575,-1.1111404660392 -1.662939224605093,-1.41421356237309 -1.4142135623731,-1.662939224605086 -1.111140466039211,-1.84775906502257 -0.765366864730189,-1.961570560806459 -0.390180644032268,-2 0))", + "width": 2.0, "quadsegs": 8 + } + ], + "relate_geoms": [ + {"wkt_a": "MULTIPOINT(80 70, 20 20, 200 170, 140 120)", + "wkt_b": "MULTIPOINT(80 170, 140 120, 200 80, 80 70)", + "pattern": "0F0FFF0F2", "result": true + }, + {"wkt_a": "POINT(20 20)", + "wkt_b": "POINT(40 60)", + "pattern": "FF0FFF0F2", "result": true + }, + {"wkt_a": "POINT(110 110)", + "wkt_b": "LINESTRING(200 200, 110 110, 200 20, 20 20, 110 110, 20 200, 200 200)", + "pattern": "0FFFFF1F2", "result": true + }, + {"wkt_a": "MULTILINESTRING((20 20, 90 20, 170 20), (90 20, 90 80, 90 140))", + "wkt_b": "MULTILINESTRING((90 20, 170 100, 170 140), (130 140, 130 60, 90 20, 20 90, 90 20))", + "pattern": "FF10F0102", "result": true + } + ], + "topology_geoms": [ + {"wkt_a": "POLYGON ((-5.0 0.0, -5.0 10.0, 5.0 10.0, 5.0 0.0, -5.0 0.0))", + "wkt_b": "POLYGON ((0.0 -5.0, 0.0 5.0, 10.0 5.0, 10.0 -5.0, 0.0 -5.0))" + }, + {"wkt_a": "POLYGON ((2 0, 18 0, 18 15, 2 15, 2 0))", + "wkt_b": "POLYGON ((10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))" + } + ], + "diff_geoms": [ + {"wkt": "POLYGON ((-5 0,-5 10,5 10,5 5,0 5,0 0,-5 0))"}, + {"wkt": "POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0), (10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))"} + ], + "sdiff_geoms": [ + {"wkt": "MULTIPOLYGON (((-5 0,-5 10,5 10,5 5,0 5,0 0,-5 0)),((0 0,5 0,5 5,10 5,10 -5,0 -5,0 0)))"}, + {"wkt": "POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0), (10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))"} + ], + "intersect_geoms": [ + {"wkt": "POLYGON ((5 5,5 0,0 0,0 5,5 5))"}, + {"wkt": "POLYGON ((10 1, 9 3, 7 4, 5 6, 4 8, 4 10, 5 12, 7 13, 9 12, 10 10, 11 12, 13 13, 15 12, 16 10, 16 8, 15 6, 13 4, 11 3, 10 1))"} + ], + "union_geoms": [ + {"wkt": "POLYGON ((-5 0,-5 10,5 10,5 5,10 5,10 -5,0 -5,0 0,-5 0))"}, + {"wkt": "POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0))"} + ] +} \ No newline at end of file diff --git a/django/contrib/gis/tests/data/geometries.json.gz b/django/contrib/gis/tests/data/geometries.json.gz deleted file mode 100644 index 683dc83e4d7b8d3805c9836026299ff1c1113d29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9100 zcmV;7BXisziwFStD$h&+1MOXDj~vI5{qA4UJH!G*pr@JlVF-dHOB4j?Fl6lqLnMfi zG?KZdINafIZLOjIzVAg|)m7C!J-Q6TZi&;~Rgswyk#T2M_TPWA*_^$)e)0Xc*Dqh6 z{dV&OzHc^vKl|%nZt?Bx51)SYrw=~;WOIJ5Hfp<38?Eq$KjVX3Ru8wEbIq%~xXF0S z{jFZq>fyt)?dI&|v*+kqkMr~~H5;(n3+&L)?lvf8+qvx|CT@-8YLl+m~nL-HDu zS0KI43jgrGPA(Qm7ko-O2d|V#12S75atJnNrE8zYD{s9+*WN4BAcKm=f`Z_px4A;9 z9IOpME7{!pjN0oMax})MB#FPZ z>Im*7Um-CW;Ct{Ea}^pSFb6sw_+XA*nIokj0zp}oy&7C1Dwt50Av`fO$N-Fh4yYQO z$BZTCvvW~p)hGg{IV4AMq~te9ZM9b}K_YX9R1`(+fm-mBKEX$4a?C;L#!u~y*3kxt z^ni>BtON2ofw7Hth*AdSF+`s`BxJ*dK>32Sv_1{yIGX5En)GQgkasQt)dES{9ol2+ zy$-7L(3Bk&`ig0s};0;vt;&OoU-e(I14s8Wrs z3$bdkhMs$kISyc}?T`k-5Glo!$2vOZ)e~C_4m3%Q)NTkI)X+J)jzSS^5(a|Tf`m6n zkO|FzcERrC1{ouC2=)V9HhMr}A{-1(2J<@Y5OxO^g=|x&D0su^1olABG;8BAanTy5 zQgj^>5(`lXpbT1GA-%Q^LxWC?*CBCP;i?G+3Q{4VXwV`Rox|{hcGwlDjqytd~pRDAXf&3kEsnV6e%Nw<;tIQ=;>52KKT;f{!r15C_m%Z3xU3v@(-RyY6ky5d4n zf;q%LN*=6t_Z=7jE(Y_PY_m2gP=9DNG-kyPNJu95Vi_j*3JHyaY0y^0VnD)qz=S2J zc$GRpLJR}2g1H?QO!0rXK1hwOvXmfx9{s_s1UIqC^h0ookfu=mJM5f=WCzF$hU*kT z1E92+y@a5l;>lP6&K25?Ng7@zLWH0WP*7ED9Sj*v1KcyTFq`4E4IBv!AmkBL6^bI~ z7jX$h6o#mKSQuXVP1sK=Qh^Mf_Dew%Xmf|}aI|Wm6`Ef(E}}6a0k3$;j6Nap3T?Z>2H0~Xya+p=*ggSp~VcqT^TZ;P9ayTh7j8>=U5d;A$lcAZ68hf9>6d;-ir;_yuq}B}HAmn*KBBX|fX9InB~Pk-kE~gEc(Go5BV$+|(Ts{samPf55P__X(MU zZU(ADheXUkaR-@*{~Mk#RM0^-Xc>wFi(%R5R~X@hPCMd8O9Kqw&>;~|VsYC{VTz0=V2E{{P0d(?c4)+PjtLK47Q_M0 zvqFNi3`=Q&wd$iMi8GBPGp6bhgb*Bp#eqvRVc-cPg4s>HUu6m687NUuC5Uu`WLyg3 zIsK+u??4HI5I}C>dh+NK>=aK*`L2)AI>UwihQCL zf`yS!Nze587-9b35iko&|vFxqxS+6a!WC-hBrr0(55buI30383-I= zYRd?#(azL})dW+b@D?*z3Lc0D3v`DB984cr;8KlLBmF4!1yT%7cSzQd91945tl0z_ zgMJob0`r;%=`F2Su&jc(8iQFDeH`DR+MRYM3dHCR|JUdOSt^qYv01wJ$uSeW94ivl z) zqe@MdkaEEyB5|W~*wH6LE}(`MHJ)dP&e9Sr0H!o{NCZ(jf>Sy)OAAQk5x_OD7N-Ve zWFEp`K35BdWCKHkLYN8_(ge~_A|t~{)gY0DL2cmZ!P&{^@KcFBgc=256bWF+HfIJ! z5MXr5#0$3?Bv=g8A>VQE(~0RtYlOQsUIL_ILc=J^_DBGbkRqByMa`{{S>jwG&ZFthF^jA}oLnht}!=i5D~s*AU{={3uEW3d6$iQHKtR03E8K;U!J2E3$fJ zSlNRaNZm4E3Yl6d)?0Jy6D)^t(IUeAQ@AM8eKutbSPkYg z-gL;sa0@=ft3i=v(v#`@b+vVp-$d3EJad8usp;>f`~X{-8YFxKeF|c!)G1Q*Cg3si zC|@CwcSGX5glyeqHBJU3gb0ZS5@9HB1vLMNy0xT^j3KBSV3VJIC2?d0b*KhcP`3 zPPaCQ_2{*raWFZt)h-}2TrQw5v?J^BV!q(OFi@4H#5x#j?%=z1gCcF148YBsn)NWu z03Q)lS^AnzQD*!|_X(@f`b;bi5r4!jq4&w^GV7R}_Sz>x8pq0k}ajW*)Xg4Sq(Df|<)qX;OoX z5*b3|64Ymm4$c|H6_(g^lNJ&p$jre^WJyU?m4z3CDHW2L9Q2Es+<=5qGi6}bm|Cq8 zT$F4VksY|&A(1=dyFobB>{4dQIEo+jeQkmzHB7vrNHGwpkgyTRjgbFNB5HU8v2)-D zgso}^EVNi|Owh1(^c&3RDLgj!iW$8ND^W0NVFJktRibsRQ6&osPnR17u7CXi0B8N)5B&iCih($*o zPlytoJo07ec!OkE0@sn~-$$K+g`LQZ99%3Ean|XiH2vbB-81B%zy|~rt@>)XBxOmI zGkuOcBWJiCE$L5%l!`|HUm&>MGB93(zB*ZJth5`Z;Iu99AVWKlNT6G=k1qa3h#ev+@X3VAu@T11s&aILL5>Hn&?v2%zD*BaEeK zHGkGJJQaa;f`KPA5^VMobhSA`SmhOrB*kIqQD&^dVhG4>vc$j!$uGE>!q6j_eF?U? zTIBoaj2Ilm$@Q#9!y7uLz;K(0UmNXQ%tYwG)sm)1u!Jy(mRSOXsX{W*i_B%XJlXgO z>1Fz!Sz?Y&K4}4&XH)BYY@;OmWPOe+dzB)}G-kxGv>0@UWSx+USF}xa-njrGB?Oy? zHw0u72QLXxop;ZGO0iUlDNK_-VUgfgxJ264Ww;`qxP(SoZjcaa=3&f!bgkE6E+CydpyfxP*K_5F^4x3GgrxmoVjSAa zNM;Eixd&I&^q?IPX`+78d&SU4&#b^j#?GNbA_Ie=O;S2bjq{GlSmx?%uK{uuQdU#5 zl+GIGGZ*!76R}92P51-m_1q|+Q{yLD0pQ3*oVIQ#$rhN(m0Sp0=RC4fmh90>uaI*A z1lHTJNZkA;i!70aV`(rT`3-3hY_4we&dOe4+FEU*7grV$^bWRuwksY%HLb8zW zxe#GkFmPkVfr}`NewiV+m1SrJA}-5CzPt)hp!e(=nEZ(V+MhNJKA+o*7Ao z>T7&t4wI8CrHVS62uvVZ=oNkTRUt-+^!Wi!H#S+4f}GSZVJc0BXazBh$4bmA22WT&c9zl5=Cr=`$ z&QDAaA|=eR9g^(W7wioeQuJ6=r*kY7jm{+)N$y4Qj%4L7X=9i(w>r<3ziw2oYE>Udh3S}quwu>zhLrbU*RRkC?NQWars z;lFG36&ILrpo7E}i7~p8T?e$;EoMj(_G?jRttNdsCUV?t)ppS^_NH&AYWp!d#FD90 zr1&*il7a+n1IBMFwopm5 zA?rFcTU|mR1&X3aU)mr&w|B7cV!0&SjE^a zL*Bhh_V`xcZ!TZFzUupT4L60g9lUC%xUG(JuH^SgI-#z>G z>iWC0-@3+bqvm^{=N!NYvLawvk~4KoqSkOI00qO7fkhx1dLjUK;X%UK6LN)(Y)(Qy za-rNPmLn-~arx2M_+^cMtya!EW-p*zJvMNT3g;}eTjxDZ{_a8nFvqDSYi0eMHtOhfnNm$rb|$>3VABv$%x8x zR&fn@wO_j`aqGB-70^}swK^H{R7@Yv%!mXtZ@&fOb#?RZ^_!PpU*3HGA>@mqb$RpI zLKk#V0ACcq>FGC_uC1wGCZG2jt?5Y{>Cpm@=PzHLh**>{>WJ1(zX`{;6-!}i|1xcL z6te{)j~erqR<$yfUls&4y=@&YjIS?B>2!H#SYevN)|8s`P6`LZTMr#cqr(rTB53><6DdR()e;D z=sVnVoOUL|Bz1|ziTFEyXt`tfu4+QDVyB`-*uLqJPp4l zYuEU-nscvC!aC&Xj6EJ4-8&^ImRc7Z8cW_WO+5OyDc~=ahU_6z+(&U3HBKDgPR8bN z>hY7g)fhBo-9L&~GP0X8&i#y>IxV7|WNx+4zaJ0&q|8niiylQHODRTGv#)>koUS6n z;8M!|roP`3o8#GB3xXG%{Kc4CxLSDpk7lirxV?t&zXD^Y`Ne^3x-KF7>Gz+0^wImj z|NKKn5R)1;GW&6%S6|&eZ_4p)y?$b<3VKdSRo8l5yLMO3P(=Okn2R`lKFFg3 zw4aUjmOo$p?OBn-we^;Bpdd3})@sg_R(dWP%hzLNA0JuNV5i%S*;wSq8$avb)IRKK z!|Y#i`(M-kcRjn$z1)Y2{q@)^-Tvx&hSucBtbw!b1C*D)8~6FkICro6_k~}39VrkV zrGB6IHclh@BndeTxr+Enkn;Vc(NBlu>#o|;AyGjtTF;ZSWoyDd-x|M~zz@;Cd*;?D zy2t&K;0H!#31MeFEWW?LynnPlaDIo%ZO_lg@qD&$)i&nVqx;Bmzu|#bd<$G4>TmU; ze>k6rFeCDxR-Y%BH(FLOzjNa5n#lWpKV`f%JhQvs_ji8c_Y=lDAqYJG2g&sI$6BTB ziB^B6HL{{A(BrJh;*_v`DIvO90}5Mr7eIfPd13SoKu@0%xQ zx8J|IdV()c&LpRLg8NU-Cdtne*)KY(soS2|Zk}#_xFevy(EaVAShkBj19u1!DFPfo zh5xM~v&HobeafBjx!gPD&V=ZvtAx)Sk>;}swClFLS-zR}X8LZ}os`Y ze#F0WlCs(vd@<#E+W(l!N3;LYJUqtrY5nC0>nRJSv&HPO3uK;f$IkAU6YiKj_HR2T zho)ONc;_8GyQ5CHGZlnw$HMfxX4Wp9a3{^~m=o@pj>Q3iTnUz@c3@I1JKqyMQ{?TK zs4Qz7MPO<{?X1z8w2Q&_ zZsl6-R_@HYt@q{DA#5e3=y*0(>p7!!mC?hT=A>=X;c5FnLGs81&YvwQBJ#z-3Rg{V zBpKS&2V-8<2MI*t%klRU%pth4uqF%4a@74EiGmCNE=W8u>hxy^Di;e1?nD%}jj?9$ zp1upc&MNNfcrDt#qD|BFg#LU>f3&7N=rTciF7&!0;|gw5ffXef!0R86zh+g`EyOe#!9k>7+f6^>Fg&f6m%z9toJOn_)XOaKc1CW&Pfl z!ufsvUfvwx?>nDW{yH91x3hexE^0oFs(7f~#_?QcZRAY!EJw8S3=KYq8+gu++3}P# zJ}=ATCDF|F7LkWh^Vrg$<&nqQBJ4bm=$EH9c^tUTwT0vJzs57K$6pgT+iU@n&|Ycl zz&gbf31V##c9O^N1c?O1d}dr* zEyM@@EI?_IdFGy+Tf}1ocrIFLL5XqRq^J{E!sqd9X<_t^C%||<%?TE2c}`zi!1eef zkJu_69h-B-3O9Hh3m?7aaiCyttS#KmMC&u3Rh8rIY^5AGDiMw+7x8B$YKyQd&js=f zwZt=gd~M;;f(I)aIWU=z>ed!<$8(|-pBPPs2S7Hg!Y;)ad9Jb?9-nfpf364SfEmw6JQYc?^*JvIEZt4~oqXucY6Lb8uNzFh?<*Gh z!jF^RIO)H9vNTIe<9kP-eJu5MPDmoqKDh)^doKXdgU`8O&9brE&iP>T5|6lDFv2WI zvSzzr#ez95Z0GIZo9De7vz_B$@?yqGzQ-gT7Oj2Q%o<~w=7>1G znRCo6XyOq{Q<5To6lu;2Z#J_l#x58Qy65~_&RIyCIlIDQ`h(sqxV2qyB}-1SV8gsA zxH{*8o8#cNa~`BQEzp%Y3*ww9F3lB_2hJ`8W+^XoCM^ube73#W`cfh7cIU6pzrOu* zPt=g6fBpJOoOY@pKWvxw>$I&EciOfKgQoa{Hk@!EEGHe%3}7x1vGPz6e&dl~2&o9g zQ)PMTDt`q7EF)CpX%ivwv|$7+nNU2_n#ZWPpm-w6M(Z7)q~;M^JP_C@5izHEj;fq( z&tI1!)c&PFs98fo?RRe|WHTfPwU4SJ)ILFhP&2Y++vir-zo#@@|)o1!s-93n5&ar=>iv z&G}L{C?6MwZjqivEhWpZHCjk%Tq58X62GLxE_G+l6unTw6c+5&da2OMdCCjh;PYb6 zL!up7w`}I1W$O{!&DD#`+pG16?b+q5^pj*S?f8w!^5`WCB=yDq*%3>x@x2g%4NK=_h1dFT2PeqWPRAjKYrNu zoX*<7lghU#kI=~eRoNuDF>3E8JSW!+U~O@bQF6+~{V-mX%P*Hd}Z zZYFYj6F%*7(wA24LrLi5R0Ae({1C>trvE;v*&k(ZhBRbDtHbnm8Na=Lb^YS{+wYG_ z`NTE*wjd#&dW~%IY1~!218kW5cqvVlui07&HuV0dahIRa-%%NB{Tc~AQvJ(PiK&0@ zL`lo`Rjp;=)UvjKfGO~ln@y6x^4qbVj+pm(tT%w=TM2Wre2H?WfH}GD*j~Dd>@}B_ z*^Qpi)Mw@E=ihvDEK?lw!BEMw|N6)C>By}rmr3lwK&`Q zp#=#+M-VGG`U&oNN>n4U5?slz%qW~GTq+#XtHix|`Fye6H2xdjJ4KKwRPg From b6b8a3f66b5cf4f00bd7ce668ac04f21bb73e0b9 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 27 Sep 2012 19:16:49 -0300 Subject: [PATCH 131/265] Refactored URL mapping documentation. Reorganized topic document so it introduces concepts form simple to more complex. Moved reference parts to their own documents. --- docs/ref/index.txt | 4 +- docs/ref/urlresolvers.txt | 204 ++++++++++++++++ docs/ref/urls.txt | 139 +++++++++++ docs/topics/http/urls.txt | 483 +++++++------------------------------- 4 files changed, 430 insertions(+), 400 deletions(-) create mode 100644 docs/ref/urlresolvers.txt create mode 100644 docs/ref/urls.txt diff --git a/docs/ref/index.txt b/docs/ref/index.txt index 01a8ab22d1..e1959d44a6 100644 --- a/docs/ref/index.txt +++ b/docs/ref/index.txt @@ -6,7 +6,7 @@ API Reference :maxdepth: 1 authbackends - class-based-views/index + class-based-views/index clickjacking contrib/index databases @@ -22,5 +22,7 @@ API Reference signals templates/index unicode + urlresolvers + urls utils validators diff --git a/docs/ref/urlresolvers.txt b/docs/ref/urlresolvers.txt new file mode 100644 index 0000000000..965cafb29b --- /dev/null +++ b/docs/ref/urlresolvers.txt @@ -0,0 +1,204 @@ +============================================== +``django.core.urlresolvers`` utility functions +============================================== + +.. module:: django.core.urlresolvers + +reverse() +--------- + +If you need to use something similar to the :ttag:`url` template tag in +your code, Django provides the following function (in the +:mod:`django.core.urlresolvers` module): + +.. function:: reverse(viewname, [urlconf=None, args=None, kwargs=None, current_app=None]) + +``viewname`` is either the function name (either a function reference, or the +string version of the name, if you used that form in ``urlpatterns``) or the +:ref:`URL pattern name `. Normally, you won't need to +worry about the ``urlconf`` parameter and will only pass in the positional and +keyword arguments to use in the URL matching. For example:: + + from django.core.urlresolvers import reverse + + def myview(request): + return HttpResponseRedirect(reverse('arch-summary', args=[1945])) + +The ``reverse()`` function can reverse a large variety of regular expression +patterns for URLs, but not every possible one. The main restriction at the +moment is that the pattern cannot contain alternative choices using the +vertical bar (``"|"``) character. You can quite happily use such patterns for +matching against incoming URLs and sending them off to views, but you cannot +reverse such patterns. + +The ``current_app`` argument allows you to provide a hint to the resolver +indicating the application to which the currently executing view belongs. +This ``current_app`` argument is used as a hint to resolve application +namespaces into URLs on specific application instances, according to the +:ref:`namespaced URL resolution strategy `. + +You can use ``kwargs`` instead of ``args``. For example:: + + >>> reverse('admin:app_list', kwargs={'app_label': 'auth'}) + '/admin/auth/' + +``args`` and ``kwargs`` cannot be passed to ``reverse()`` at the same time. + +.. admonition:: Make sure your views are all correct. + + As part of working out which URL names map to which patterns, the + ``reverse()`` function has to import all of your URLconf files and examine + the name of each view. This involves importing each view function. If + there are *any* errors whilst importing any of your view functions, it + will cause ``reverse()`` to raise an error, even if that view function is + not the one you are trying to reverse. + + Make sure that any views you reference in your URLconf files exist and can + be imported correctly. Do not include lines that reference views you + haven't written yet, because those views will not be importable. + +.. note:: + + The string returned by :meth:`~django.core.urlresolvers.reverse` is already + :ref:`urlquoted `. For example:: + + >>> reverse('cities', args=[u'Orléans']) + '.../Orl%C3%A9ans/' + + Applying further encoding (such as :meth:`~django.utils.http.urlquote` or + ``urllib.quote``) to the output of :meth:`~django.core.urlresolvers.reverse` + may produce undesirable results. + +reverse_lazy() +-------------- + +.. versionadded:: 1.4 + +A lazily evaluated version of `reverse()`_. + +.. function:: reverse_lazy(viewname, [urlconf=None, args=None, kwargs=None, current_app=None]) + +It is useful for when you need to use a URL reversal before your project's +URLConf is loaded. Some common cases where this function is necessary are: + +* providing a reversed URL as the ``url`` attribute of a generic class-based + view. + +* providing a reversed URL to a decorator (such as the ``login_url`` argument + for the :func:`django.contrib.auth.decorators.permission_required` + decorator). + +* providing a reversed URL as a default value for a parameter in a function's + signature. + +resolve() +--------- + +The :func:`django.core.urlresolvers.resolve` function can be used for +resolving URL paths to the corresponding view functions. It has the +following signature: + +.. function:: resolve(path, urlconf=None) + +``path`` is the URL path you want to resolve. As with +:func:`~django.core.urlresolvers.reverse`, you don't need to +worry about the ``urlconf`` parameter. The function returns a +:class:`ResolverMatch` object that allows you +to access various meta-data about the resolved URL. + +If the URL does not resolve, the function raises an +:class:`~django.http.Http404` exception. + +.. class:: ResolverMatch + + .. attribute:: ResolverMatch.func + + The view function that would be used to serve the URL + + .. attribute:: ResolverMatch.args + + The arguments that would be passed to the view function, as + parsed from the URL. + + .. attribute:: ResolverMatch.kwargs + + The keyword arguments that would be passed to the view + function, as parsed from the URL. + + .. attribute:: ResolverMatch.url_name + + The name of the URL pattern that matches the URL. + + .. attribute:: ResolverMatch.app_name + + The application namespace for the URL pattern that matches the + URL. + + .. attribute:: ResolverMatch.namespace + + The instance namespace for the URL pattern that matches the + URL. + + .. attribute:: ResolverMatch.namespaces + + The list of individual namespace components in the full + instance namespace for the URL pattern that matches the URL. + i.e., if the namespace is ``foo:bar``, then namespaces will be + ``['foo', 'bar']``. + +A :class:`ResolverMatch` object can then be interrogated to provide +information about the URL pattern that matches a URL:: + + # Resolve a URL + match = resolve('/some/path/') + # Print the URL pattern that matches the URL + print(match.url_name) + +A :class:`ResolverMatch` object can also be assigned to a triple:: + + func, args, kwargs = resolve('/some/path/') + +One possible use of :func:`~django.core.urlresolvers.resolve` would be to test +whether a view would raise a ``Http404`` error before redirecting to it:: + + from urlparse import urlparse + from django.core.urlresolvers import resolve + from django.http import HttpResponseRedirect, Http404 + + def myview(request): + next = request.META.get('HTTP_REFERER', None) or '/' + response = HttpResponseRedirect(next) + + # modify the request and response as required, e.g. change locale + # and set corresponding locale cookie + + view, args, kwargs = resolve(urlparse(next)[2]) + kwargs['request'] = request + try: + view(*args, **kwargs) + except Http404: + return HttpResponseRedirect('/') + return response + + +permalink() +----------- + +The :func:`django.db.models.permalink` decorator is useful for writing short +methods that return a full URL path. For example, a model's +``get_absolute_url()`` method. See :func:`django.db.models.permalink` for more. + +get_script_prefix() +------------------- + +.. function:: get_script_prefix() + +Normally, you should always use :func:`~django.core.urlresolvers.reverse` or +:func:`~django.db.models.permalink` to define URLs within your application. +However, if your application constructs part of the URL hierarchy itself, you +may occasionally need to generate URLs. In that case, you need to be able to +find the base URL of the Django project within its Web server +(normally, :func:`~django.core.urlresolvers.reverse` takes care of this for +you). In that case, you can call ``get_script_prefix()``, which will return the +script prefix portion of the URL for your Django project. If your Django +project is at the root of its Web server, this is always ``"/"``. diff --git a/docs/ref/urls.txt b/docs/ref/urls.txt new file mode 100644 index 0000000000..3d860fc0ed --- /dev/null +++ b/docs/ref/urls.txt @@ -0,0 +1,139 @@ +====================================== +``django.conf.urls`` utility functions +====================================== + +.. module:: django.conf.urls + +.. versionchanged:: 1.4 + Starting with Django 1.4 functions ``patterns``, ``url``, ``include`` plus + the ``handler*`` symbols described below live in the ``django.conf.urls`` + module. + + Until Django 1.3 they were located in ``django.conf.urls.defaults``. You + still can import them from there but it will be removed in Django 1.6. + +patterns() +---------- + +.. function:: patterns(prefix, pattern_description, ...) + +A function that takes a prefix, and an arbitrary number of URL patterns, and +returns a list of URL patterns in the format Django needs. + +The first argument to ``patterns()`` is a string ``prefix``. See +:ref:`The view prefix `. + +The remaining arguments should be tuples in this format:: + + (regular expression, Python callback function [, optional_dictionary [, optional_name]]) + +The ``optional_dictionary`` and ``optional_name`` parameters are described in +:ref:`Passing extra options to view functions `. + +.. note:: + Because `patterns()` is a function call, it accepts a maximum of 255 + arguments (URL patterns, in this case). This is a limit for all Python + function calls. This is rarely a problem in practice, because you'll + typically structure your URL patterns modularly by using `include()` + sections. However, on the off-chance you do hit the 255-argument limit, + realize that `patterns()` returns a Python list, so you can split up the + construction of the list. + + :: + + urlpatterns = patterns('', + ... + ) + urlpatterns += patterns('', + ... + ) + + Python lists have unlimited size, so there's no limit to how many URL + patterns you can construct. The only limit is that you can only create 254 + at a time (the 255th argument is the initial prefix argument). + +url() +----- + +.. function:: url(regex, view, kwargs=None, name=None, prefix='') + +You can use the ``url()`` function, instead of a tuple, as an argument to +``patterns()``. This is convenient if you want to specify a name without the +optional extra arguments dictionary. For example:: + + urlpatterns = patterns('', + url(r'^index/$', index_view, name="main-view"), + ... + ) + +This function takes five arguments, most of which are optional:: + + url(regex, view, kwargs=None, name=None, prefix='') + +See :ref:`Naming URL patterns ` for why the ``name`` +parameter is useful. + +The ``prefix`` parameter has the same meaning as the first argument to +``patterns()`` and is only relevant when you're passing a string as the +``view`` parameter. + +include() +--------- + +.. function:: include() + +A function that takes a full Python import path to another URLconf module that +should be "included" in this place. + +:func:`include` also accepts as an argument an iterable that returns URL +patterns. + +See :ref:`Including other URLconfs `. + +handler403 +---------- + +.. data:: handler403 + +A callable, or a string representing the full Python import path to the view +that should be called if the user doesn't have the permissions required to +access a resource. + +By default, this is ``'django.views.defaults.permission_denied'``. That default +value should suffice. + +See the documentation about :ref:`the 403 (HTTP Forbidden) view +` for more information. + +.. versionadded:: 1.4 + ``handler403`` is new in Django 1.4. + +handler404 +---------- + +.. data:: handler404 + +A callable, or a string representing the full Python import path to the view +that should be called if none of the URL patterns match. + +By default, this is ``'django.views.defaults.page_not_found'``. That default +value should suffice. + +See the documentation about :ref:`the 404 (HTTP Not Found) view +` for more information. + +handler500 +---------- + +.. data:: handler500 + +A callable, or a string representing the full Python import path to the view +that should be called in case of server errors. Server errors happen when you +have runtime errors in view code. + +By default, this is ``'django.views.defaults.server_error'``. That default +value should suffice. + +See the documentation about :ref:`the 500 (HTTP Internal Server Error) view +` for more information. + diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 99afa13279..79eac88852 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -2,8 +2,6 @@ URL dispatcher ============== -.. module:: django.core.urlresolvers - A clean, elegant URL scheme is an important detail in a high-quality Web application. Django lets you design URLs however you want, with no framework limitations. @@ -160,7 +158,8 @@ vs. non-named groups in a regular expression: 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. +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 ================================= @@ -215,7 +214,6 @@ 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 ================================== @@ -223,154 +221,35 @@ Syntax of the urlpatterns variable :func:`django.conf.urls.patterns`. Always use ``patterns()`` to create the ``urlpatterns`` variable. -``django.conf.urls`` utility functions -====================================== - -.. module:: django.conf.urls - -.. deprecated:: 1.4 - Starting with Django 1.4 functions ``patterns``, ``url``, ``include`` plus - the ``handler*`` symbols described below live in the ``django.conf.urls`` - module. - - Until Django 1.3 they were located in ``django.conf.urls.defaults``. You - still can import them from there but it will be removed in Django 1.6. - -patterns --------- - -.. function:: patterns(prefix, pattern_description, ...) - -A function that takes a prefix, and an arbitrary number of URL patterns, and -returns a list of URL patterns in the format Django needs. - -The first argument to ``patterns()`` is a string ``prefix``. See -`The view prefix`_ below. - -The remaining arguments should be tuples in this format:: - - (regular expression, Python callback function [, optional_dictionary [, optional_name]]) - -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 - arguments (URL patterns, in this case). This is a limit for all Python - function calls. This is rarely a problem in practice, because you'll - typically structure your URL patterns modularly by using `include()` - sections. However, on the off-chance you do hit the 255-argument limit, - realize that `patterns()` returns a Python list, so you can split up the - construction of the list. - - :: - - urlpatterns = patterns('', - ... - ) - urlpatterns += patterns('', - ... - ) - - Python lists have unlimited size, so there's no limit to how many URL - patterns you can construct. The only limit is that you can only create 254 - at a time (the 255th argument is the initial prefix argument). - -url ---- - -.. function:: url(regex, view, kwargs=None, name=None, prefix='') - -You can use the ``url()`` function, instead of a tuple, as an argument to -``patterns()``. This is convenient if you want to specify a name without the -optional extra arguments dictionary. For example:: - - urlpatterns = patterns('', - url(r'^index/$', index_view, name="main-view"), - ... - ) - -This function takes five arguments, most of which are optional:: - - url(regex, view, kwargs=None, name=None, prefix='') - -See `Naming URL patterns`_ for why the ``name`` parameter is useful. - -The ``prefix`` parameter has the same meaning as the first argument to -``patterns()`` and is only relevant when you're passing a string as the -``view`` parameter. - -include -------- - -.. function:: include() - -A function that takes a full Python import path to another URLconf module that -should be "included" in this place. - -:func:`include` also accepts as an argument an iterable that returns URL -patterns. - -See `Including other URLconfs`_ below. - Error handling ============== When Django can't find a regex matching the requested URL, or when an -exception is raised, Django will invoke an error-handling view. The -views to use for these cases are specified by three variables which can -be set in your root URLconf. Setting these variables in any other -URLconf will have no effect. +exception is raised, Django will invoke an error-handling view. + +The views to use for these cases are specified by three variables. Their +default values should suffice for most projects, but further customization is +possible by assigning values to them. See the documentation on :ref:`customizing error views -` for more details. +` for the full details. -handler403 ----------- +Such values can be set in your root URLconf. Setting these variables in any +other URLconf will have no effect. -.. data:: handler403 +Values must be callables, or strings representing the full Python import path +to the view that should be called to handle the error condition at hand. -A callable, or a string representing the full Python import path to the view -that should be called if the user doesn't have the permissions required to -access a resource. +The variables are: -By default, this is ``'django.views.defaults.permission_denied'``. That default -value should suffice. - -See the documentation about :ref:`the 403 (HTTP Forbidden) view -` for more information. +* ``handler404`` -- See :data:`django.conf.urls.handler404`. +* ``handler500`` -- See :data:`django.conf.urls.handler500`. +* ``handler403`` -- See :data:`django.conf.urls.handler403`. .. versionadded:: 1.4 ``handler403`` is new in Django 1.4. -handler404 ----------- - -.. data:: handler404 - -A callable, or a string representing the full Python import path to the view -that should be called if none of the URL patterns match. - -By default, this is ``'django.views.defaults.page_not_found'``. That default -value should suffice. - -See the documentation about :ref:`the 404 (HTTP Not Found) view -` for more information. - -handler500 ----------- - -.. data:: handler500 - -A callable, or a string representing the full Python import path to the view -that should be called in case of server errors. Server errors happen when you -have runtime errors in view code. - -By default, this is ``'django.views.defaults.server_error'``. That default -value should suffice. - -See the documentation about :ref:`the 500 (HTTP Internal Server Error) view -` for more information. +.. _urlpatterns-view-prefix: The view prefix =============== @@ -437,6 +316,8 @@ New:: (r'^tag/(?P\w+)/$', 'tag'), ) +.. _including-other-urlconfs: + Including other URLconfs ======================== @@ -459,13 +340,14 @@ itself. It includes a number of other URLconfs:: Note that the regular expressions in this example don't have a ``$`` (end-of-string match character) but do include a trailing slash. Whenever -Django encounters ``include()``, it chops off whatever part of the URL matched -up to that point and sends the remaining string to the included URLconf for -further processing. +Django encounters ``include()`` (:func:`django.conf.urls.include()`), it chops +off whatever part of the URL matched up to that point and sends the remaining +string to the included URLconf for further processing. Another possibility is to include additional URL patterns not by specifying the -URLconf Python module defining them as the `include`_ argument but by using -directly the pattern list as returned by `patterns`_ instead. For example:: +URLconf Python module defining them as the ``include()`` argument but by using +directly the pattern list as returned by :func:`~django.conf.urls.patterns` +instead. For example:: from django.conf.urls import patterns, url, include @@ -510,57 +392,7 @@ the following example is valid:: In the above example, the captured ``"username"`` variable is passed to the included URLconf, as expected. -.. _topics-http-defining-url-namespaces: - -Defining URL namespaces ------------------------ - -When you need to deploy multiple instances of a single application, it can be -helpful to be able to differentiate between instances. This is especially -important when using :ref:`named URL patterns `, since -multiple instances of a single application will share named URLs. Namespaces -provide a way to tell these named URLs apart. - -A URL namespace comes in two parts, both of which are strings: - -* An **application namespace**. This describes the name of the application - that is being deployed. Every instance of a single application will have - the same application namespace. For example, Django's admin application - has the somewhat predictable application namespace of ``admin``. - -* An **instance namespace**. This identifies a specific instance of an - application. Instance namespaces should be unique across your entire - project. However, an instance namespace can be the same as the - application namespace. This is used to specify a default instance of an - application. For example, the default Django Admin instance has an - instance namespace of ``admin``. - -URL Namespaces can be specified in two ways. - -Firstly, you can provide the application and instance namespace as arguments -to ``include()`` when you construct your URL patterns. For example,:: - - (r'^help/', include('apps.help.urls', namespace='foo', app_name='bar')), - -This will include the URLs defined in ``apps.help.urls`` into the application -namespace ``bar``, with the instance namespace ``foo``. - -Secondly, you can include an object that contains embedded namespace data. If -you ``include()`` a ``patterns`` object, that object will be added to the -global namespace. However, you can also ``include()`` an object that contains -a 3-tuple containing:: - - (, , ) - -This will include the nominated URL patterns into the given application and -instance namespace. For example, the ``urls`` attribute of Django's -:class:`~django.contrib.admin.AdminSite` object returns a 3-tuple that contains -all the patterns in an admin site, plus the name of the admin instance, and the -application namespace ``admin``. - -Once you have defined namespaced URLs, you can reverse them. For details on -reversing namespaced urls, see the documentation on :ref:`reversing namespaced -URLs `. +.. _views-extra-options: Passing extra options to view functions ======================================= @@ -698,10 +530,10 @@ view:: ) This is completely valid, but it leads to problems when you try to do reverse -URL matching (through the ``permalink()`` decorator or the :ttag:`url` template -tag). Continuing this example, if you wanted to retrieve the URL for the -``archive`` view, Django's reverse URL matcher would get confused, because *two* -URL patterns point at that view. +URL matching (through the :func:`~django.db.models.permalink` decorator or the +:ttag:`url` template tag). Continuing this example, if you wanted to retrieve +the URL for the ``archive`` view, Django's reverse URL matcher would get +confused, because *two* URL patterns point at that view. To solve this problem, Django supports **named URL patterns**. That is, you can give a name to a URL pattern in order to distinguish it from other patterns @@ -741,10 +573,36 @@ not restricted to valid Python names. name, will decrease the chances of collision. We recommend something like ``myapp-comment`` instead of ``comment``. -.. _topics-http-reversing-url-namespaces: +.. _topics-http-defining-url-namespaces: URL namespaces --------------- +============== + +Introduction +------------ + +When you need to deploy multiple instances of a single application, it can be +helpful to be able to differentiate between instances. This is especially +important when using :ref:`named URL patterns `, since +multiple instances of a single application will share named URLs. Namespaces +provide a way to tell these named URLs apart. + +A URL namespace comes in two parts, both of which are strings: + +.. glossary:: + + application namespace + This describes the name of the application that is being deployed. Every + instance of a single application will have the same application namespace. + For example, Django's admin application has the somewhat predictable + application namespace of ``admin``. + + instance namespace + This identifies a specific instance of an application. Instance namespaces + should be unique across your entire project. However, an instance namespace + can be the same as the application namespace. This is used to specify a + default instance of an application. For example, the default Django Admin + instance has an instance namespace of ``admin``. Namespaced URLs are specified using the ``:`` operator. For example, the main index page of the admin application is referenced using ``admin:index``. This @@ -754,6 +612,11 @@ Namespaces can also be nested. The named URL ``foo:bar:whiz`` would look for a pattern named ``whiz`` in the namespace ``bar`` that is itself defined within the top-level namespace ``foo``. +.. _topics-http-reversing-url-namespaces: + +Reversing namespaced URLs +------------------------- + When given a namespaced URL (e.g. ``myapp:index``) to resolve, Django splits the fully qualified name into parts, and then tries the following lookup: @@ -787,6 +650,9 @@ If there are nested namespaces, these steps are repeated for each part of the namespace until only the view name is unresolved. The view name will then be resolved into a URL in the namespace that has been found. +Example +~~~~~~~ + To show this resolution strategy in action, consider an example of two instances of ``myapp``: one called ``foo``, and one called ``bar``. ``myapp`` has a main index page with a URL named `index`. Using this setup, the following lookups are @@ -818,209 +684,28 @@ following would happen: * ``foo:index`` will again resolve to the index page of the instance ``foo``. +URL namespaces and included URLconfs +------------------------------------ -``django.core.urlresolvers`` utility functions -============================================== +URL namespaces of included URLconfs can be specified in two ways. -.. currentmodule:: django.core.urlresolvers +Firstly, you can provide the application and instance namespace as arguments +to ``include()`` when you construct your URL patterns. For example,:: -reverse() ---------- + (r'^help/', include('apps.help.urls', namespace='foo', app_name='bar')), -If you need to use something similar to the :ttag:`url` template tag in -your code, Django provides the following function (in the -:mod:`django.core.urlresolvers` module): +This will include the URLs defined in ``apps.help.urls`` into the application +namespace ``bar``, with the instance namespace ``foo``. -.. function:: reverse(viewname, [urlconf=None, args=None, kwargs=None, current_app=None]) +Secondly, you can include an object that contains embedded namespace data. If +you ``include()`` a ``patterns`` object, that object will be added to the +global namespace. However, you can also ``include()`` an object that contains +a 3-tuple containing:: -``viewname`` is either the function name (either a function reference, or the -string version of the name, if you used that form in ``urlpatterns``) or the -`URL pattern name`_. Normally, you won't need to worry about the -``urlconf`` parameter and will only pass in the positional and keyword -arguments to use in the URL matching. For example:: + (, , ) - from django.core.urlresolvers import reverse - - def myview(request): - return HttpResponseRedirect(reverse('arch-summary', args=[1945])) - -.. _URL pattern name: `Naming URL patterns`_ - -The ``reverse()`` function can reverse a large variety of regular expression -patterns for URLs, but not every possible one. The main restriction at the -moment is that the pattern cannot contain alternative choices using the -vertical bar (``"|"``) character. You can quite happily use such patterns for -matching against incoming URLs and sending them off to views, but you cannot -reverse such patterns. - -The ``current_app`` argument allows you to provide a hint to the resolver -indicating the application to which the currently executing view belongs. -This ``current_app`` argument is used as a hint to resolve application -namespaces into URLs on specific application instances, according to the -:ref:`namespaced URL resolution strategy `. - -You can use ``kwargs`` instead of ``args``. For example:: - - >>> reverse('admin:app_list', kwargs={'app_label': 'auth'}) - '/admin/auth/' - -``args`` and ``kwargs`` cannot be passed to ``reverse()`` at the same time. - -.. admonition:: Make sure your views are all correct. - - As part of working out which URL names map to which patterns, the - ``reverse()`` function has to import all of your URLconf files and examine - the name of each view. This involves importing each view function. If - there are *any* errors whilst importing any of your view functions, it - will cause ``reverse()`` to raise an error, even if that view function is - not the one you are trying to reverse. - - Make sure that any views you reference in your URLconf files exist and can - be imported correctly. Do not include lines that reference views you - haven't written yet, because those views will not be importable. - -.. note:: - - The string returned by :meth:`~django.core.urlresolvers.reverse` is already - :ref:`urlquoted `. For example:: - - >>> reverse('cities', args=[u'Orléans']) - '.../Orl%C3%A9ans/' - - Applying further encoding (such as :meth:`~django.utils.http.urlquote` or - ``urllib.quote``) to the output of :meth:`~django.core.urlresolvers.reverse` - may produce undesirable results. - -reverse_lazy() --------------- - -.. versionadded:: 1.4 - -A lazily evaluated version of `reverse()`_. - -.. function:: reverse_lazy(viewname, [urlconf=None, args=None, kwargs=None, current_app=None]) - -It is useful for when you need to use a URL reversal before your project's -URLConf is loaded. Some common cases where this function is necessary are: - -* providing a reversed URL as the ``url`` attribute of a generic class-based - view. - -* providing a reversed URL to a decorator (such as the ``login_url`` argument - for the :func:`django.contrib.auth.decorators.permission_required` - decorator). - -* providing a reversed URL as a default value for a parameter in a function's - signature. - -resolve() ---------- - -The :func:`django.core.urlresolvers.resolve` function can be used for -resolving URL paths to the corresponding view functions. It has the -following signature: - -.. function:: resolve(path, urlconf=None) - -``path`` is the URL path you want to resolve. As with -:func:`~django.core.urlresolvers.reverse`, you don't need to -worry about the ``urlconf`` parameter. The function returns a -:class:`ResolverMatch` object that allows you -to access various meta-data about the resolved URL. - -If the URL does not resolve, the function raises an -:class:`~django.http.Http404` exception. - -.. class:: ResolverMatch - - .. attribute:: ResolverMatch.func - - The view function that would be used to serve the URL - - .. attribute:: ResolverMatch.args - - The arguments that would be passed to the view function, as - parsed from the URL. - - .. attribute:: ResolverMatch.kwargs - - The keyword arguments that would be passed to the view - function, as parsed from the URL. - - .. attribute:: ResolverMatch.url_name - - The name of the URL pattern that matches the URL. - - .. attribute:: ResolverMatch.app_name - - The application namespace for the URL pattern that matches the - URL. - - .. attribute:: ResolverMatch.namespace - - The instance namespace for the URL pattern that matches the - URL. - - .. attribute:: ResolverMatch.namespaces - - The list of individual namespace components in the full - instance namespace for the URL pattern that matches the URL. - i.e., if the namespace is ``foo:bar``, then namespaces will be - ``['foo', 'bar']``. - -A :class:`ResolverMatch` object can then be interrogated to provide -information about the URL pattern that matches a URL:: - - # Resolve a URL - match = resolve('/some/path/') - # Print the URL pattern that matches the URL - print(match.url_name) - -A :class:`ResolverMatch` object can also be assigned to a triple:: - - func, args, kwargs = resolve('/some/path/') - -One possible use of :func:`~django.core.urlresolvers.resolve` would be to test -whether a view would raise a ``Http404`` error before redirecting to it:: - - from urlparse import urlparse - from django.core.urlresolvers import resolve - from django.http import HttpResponseRedirect, Http404 - - def myview(request): - next = request.META.get('HTTP_REFERER', None) or '/' - response = HttpResponseRedirect(next) - - # modify the request and response as required, e.g. change locale - # and set corresponding locale cookie - - view, args, kwargs = resolve(urlparse(next)[2]) - kwargs['request'] = request - try: - view(*args, **kwargs) - except Http404: - return HttpResponseRedirect('/') - return response - - -permalink() ------------ - -The :func:`django.db.models.permalink` decorator is useful for writing short -methods that return a full URL path. For example, a model's -``get_absolute_url()`` method. See :func:`django.db.models.permalink` for more. - -get_script_prefix() -------------------- - -.. function:: get_script_prefix() - -Normally, you should always use :func:`~django.core.urlresolvers.reverse` or -:func:`~django.db.models.permalink` to define URLs within your application. -However, if your application constructs part of the URL hierarchy itself, you -may occasionally need to generate URLs. In that case, you need to be able to -find the base URL of the Django project within its Web server -(normally, :func:`~django.core.urlresolvers.reverse` takes care of this for -you). In that case, you can call ``get_script_prefix()``, which will return the -script prefix portion of the URL for your Django project. If your Django -project is at the root of its Web server, this is always ``"/"``. +This will include the nominated URL patterns into the given application and +instance namespace. For example, the ``urls`` attribute of Django's +:class:`~django.contrib.admin.AdminSite` object returns a 3-tuple that contains +all the patterns in an admin site, plus the name of the admin instance, and the +application namespace ``admin``. From 91ef2a5253a50a602523bdda943c32c4cfe719fe Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Sat, 6 Oct 2012 09:57:24 -0700 Subject: [PATCH 132/265] Use native geometry types on PostGIS 2.0+ instead of `AddGeometryColumn` and don't query database in `PostGISCreation.sql_table_creation_suffix`. --- django/contrib/gis/db/backends/base.py | 3 +- .../gis/db/backends/postgis/creation.py | 32 +++++++------------ .../gis/db/backends/postgis/operations.py | 23 ++++++++++--- django/contrib/gis/db/models/fields.py | 2 +- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py index 0bd598997c..171a304439 100644 --- a/django/contrib/gis/db/backends/base.py +++ b/django/contrib/gis/db/backends/base.py @@ -32,8 +32,9 @@ class BaseSpatialOperations(object): # How the geometry column should be selected. select = None - # Does the spatial database have a geography type? + # Does the spatial database have a geometry or geography type? geography = False + geometry = False area = False centroid = False diff --git a/django/contrib/gis/db/backends/postgis/creation.py b/django/contrib/gis/db/backends/postgis/creation.py index 06b60117f6..406dc4e487 100644 --- a/django/contrib/gis/db/backends/postgis/creation.py +++ b/django/contrib/gis/db/backends/postgis/creation.py @@ -4,7 +4,8 @@ from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation class PostGISCreation(DatabaseCreation): geom_index_type = 'GIST' - geom_index_opts = 'GIST_GEOMETRY_OPS' + geom_index_ops = 'GIST_GEOMETRY_OPS' + geom_index_ops_nd = 'GIST_GEOMETRY_OPS_ND' def sql_indexes_for_field(self, model, f, style): "Return any spatial index creation SQL for the field." @@ -17,8 +18,9 @@ class PostGISCreation(DatabaseCreation): qn = self.connection.ops.quote_name db_table = model._meta.db_table - if f.geography: - # Geogrophy columns are created normally. + if f.geography or self.connection.ops.geometry: + # Geography and Geometry (PostGIS 2.0+) columns are + # created normally. pass else: # Geometry columns are created by `AddGeometryColumn` @@ -47,33 +49,23 @@ class PostGISCreation(DatabaseCreation): # which are fast on multidimensional cases, or just plain # gist index for the 2d case. if f.geography: - index_opts = '' - elif self.connection.ops.spatial_version >= (2, 0): + index_ops = '' + elif self.connection.ops.geometry: if f.dim > 2: - index_opts = ' ' + style.SQL_KEYWORD('gist_geometry_ops_nd') + index_ops = ' ' + style.SQL_KEYWORD(self.geom_index_ops_nd) else: - index_opts = '' + index_ops = '' else: - index_opts = ' ' + style.SQL_KEYWORD(self.geom_index_opts) + index_ops = ' ' + style.SQL_KEYWORD(self.geom_index_ops) output.append(style.SQL_KEYWORD('CREATE INDEX ') + style.SQL_TABLE(qn('%s_%s_id' % (db_table, f.column))) + style.SQL_KEYWORD(' ON ') + style.SQL_TABLE(qn(db_table)) + style.SQL_KEYWORD(' USING ') + style.SQL_COLTYPE(self.geom_index_type) + ' ( ' + - style.SQL_FIELD(qn(f.column)) + index_opts + ' );') + style.SQL_FIELD(qn(f.column)) + index_ops + ' );') return output def sql_table_creation_suffix(self): - cursor = self.connection.cursor() - cursor.execute('SELECT datname FROM pg_database;') - db_names = [row[0] for row in cursor.fetchall()] postgis_template = getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis') - - if postgis_template in db_names: - qn = self.connection.ops.quote_name - return ' TEMPLATE %s' % qn(postgis_template) - elif self.connection.ops.spatial_version < (2, 0): - raise ImproperlyConfigured("Template database '%s' does not exist." % postgis_template) - else: - return '' + return ' TEMPLATE %s' % self.connection.ops.quote_name(postgis_template) diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index ff9110de01..aa23b974db 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -103,11 +103,12 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.geom_func_prefix = prefix self.spatial_version = version except DatabaseError: - raise ImproperlyConfigured('Cannot determine PostGIS version for database "%s". ' - 'GeoDjango requires at least PostGIS version 1.3. ' - 'Was the database created from a spatial database ' - 'template?' % self.connection.settings_dict['NAME'] - ) + raise ImproperlyConfigured( + 'Cannot determine PostGIS version for database "%s". ' + 'GeoDjango requires at least PostGIS version 1.3. ' + 'Was the database created from a spatial database ' + 'template?' % self.connection.settings_dict['NAME'] + ) # TODO: Raise helpful exceptions as they become known. # PostGIS-specific operators. The commented descriptions of these @@ -215,6 +216,10 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): 'bboverlaps' : PostGISOperator('&&'), } + # Native geometry type support added in PostGIS 2.0. + if version >= (2, 0, 0): + self.geometry = True + # Creating a dictionary lookup of all GIS terms for PostGIS. gis_terms = ['isnull'] gis_terms += list(self.geometry_operators) @@ -320,6 +325,14 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): 'only with an SRID of 4326.') return 'geography(%s,%d)'% (f.geom_type, f.srid) + elif self.geometry: + # Postgis 2.0 supports type-based geometries. + # TODO: Support 'M' extension. + if f.dim == 3: + geom_type = f.geom_type + 'Z' + else: + geom_type = f.geom_type + return 'geometry(%s,%d)' % (geom_type, f.srid) else: return None diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py index c8b8901d59..d90ce309d4 100644 --- a/django/contrib/gis/db/models/fields.py +++ b/django/contrib/gis/db/models/fields.py @@ -95,7 +95,7 @@ class GeometryField(Field): # Is this a geography rather than a geometry column? self.geography = geography - # Oracle-specific private attributes for creating the entrie in + # Oracle-specific private attributes for creating the entry in # `USER_SDO_GEOM_METADATA` self._extent = kwargs.pop('extent', (-180.0, -90.0, 180.0, 90.0)) self._tolerance = kwargs.pop('tolerance', 0.05) From 69035b0b1c6e3bd4569070bf0f0c774def397f0d Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 6 Oct 2012 16:19:51 -0300 Subject: [PATCH 133/265] More URL mapping documentation fixes. --- docs/ref/urls.txt | 29 +++++++--- docs/topics/http/urls.txt | 110 ++++++++++++++++++++------------------ 2 files changed, 81 insertions(+), 58 deletions(-) diff --git a/docs/ref/urls.txt b/docs/ref/urls.txt index 3d860fc0ed..b9a0199984 100644 --- a/docs/ref/urls.txt +++ b/docs/ref/urls.txt @@ -80,15 +80,32 @@ The ``prefix`` parameter has the same meaning as the first argument to include() --------- -.. function:: include() +.. function:: include(module[, namespace=None, app_name=None]) + include(pattern_list) + include((pattern_list, app_namespace, instance_namespace)) -A function that takes a full Python import path to another URLconf module that -should be "included" in this place. + A function that takes a full Python import path to another URLconf module + that should be "included" in this place. Optionally, the :term:`application + namespace` and :term:`instance namespace` where the entries will be included + into can also be specified. -:func:`include` also accepts as an argument an iterable that returns URL -patterns. + ``include()`` also accepts as an argument either an iterable that returns + URL patterns or a 3-tuple containing such iterable plus the names of the + application and instance namespaces. -See :ref:`Including other URLconfs `. + :arg module: URLconf module (or module name) + :type module: Module or string + :arg namespace: Instance namespace for the URL entries being included + :type namespace: string + :arg app_name: Application namespace for the URL entries being included + :type app_name: string + :arg pattern_list: Iterable of URL entries as returned by :func:`patterns` + :arg app_namespace: Application namespace for the URL entries being included + :type app_namespace: string + :arg instance_namespace: Instance namespace for the URL entries being included + :type instance_namespace: string + +See :ref:`including-other-urlconfs` and :ref:`namespaces-and-include`. handler403 ---------- diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 79eac88852..d7b3b03d84 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -327,7 +327,7 @@ essentially "roots" a set of URLs below other ones. For example, here's an excerpt of the URLconf for the `Django Web site`_ itself. It includes a number of other URLconfs:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns, include urlpatterns = patterns('', # ... snip ... @@ -347,28 +347,23 @@ string to the included URLconf for further processing. Another possibility is to include additional URL patterns not by specifying the URLconf Python module defining them as the ``include()`` argument but by using directly the pattern list as returned by :func:`~django.conf.urls.patterns` -instead. For example:: +instead. For example, consider this URLconf:: from django.conf.urls import patterns, url, include extra_patterns = patterns('', - url(r'^reports/(?P\d+)/$', 'credit.views.report', name='credit-reports'), - url(r'^charge/$', 'credit.views.charge', name='credit-charge'), + url(r'^reports/(?P\d+)/$', 'credit.views.report'), + url(r'^charge/$', 'credit.views.charge'), ) urlpatterns = patterns('', - url(r'^$', 'apps.main.views.homepage', name='site-homepage'), + url(r'^$', 'apps.main.views.homepage'), (r'^help/', include('apps.help.urls')), (r'^credit/', include(extra_patterns)), ) -This approach can be seen in use when you deploy an instance of the Django -Admin application. The Django Admin is deployed as instances of a -:class:`~django.contrib.admin.AdminSite`; each -:class:`~django.contrib.admin.AdminSite` instance has an attribute ``urls`` -that returns the url patterns available to that instance. It is this attribute -that you ``include()`` into your projects ``urlpatterns`` when you deploy the -admin instance. +In this example, the ``/credit/reports/`` URL will be handled by the +``credit.views.report()`` Django view. .. _`Django Web site`: https://www.djangoproject.com/ @@ -595,33 +590,33 @@ A URL namespace comes in two parts, both of which are strings: This describes the name of the application that is being deployed. Every instance of a single application will have the same application namespace. For example, Django's admin application has the somewhat predictable - application namespace of ``admin``. + application namespace of ``'admin'``. instance namespace This identifies a specific instance of an application. Instance namespaces should be unique across your entire project. However, an instance namespace can be the same as the application namespace. This is used to specify a default instance of an application. For example, the default Django Admin - instance has an instance namespace of ``admin``. + instance has an instance namespace of ``'admin'``. -Namespaced URLs are specified using the ``:`` operator. For example, the main -index page of the admin application is referenced using ``admin:index``. This -indicates a namespace of ``admin``, and a named URL of ``index``. +Namespaced URLs are specified using the ``':'`` operator. For example, the main +index page of the admin application is referenced using ``'admin:index'``. This +indicates a namespace of ``'admin'``, and a named URL of ``'index'``. -Namespaces can also be nested. The named URL ``foo:bar:whiz`` would look for -a pattern named ``whiz`` in the namespace ``bar`` that is itself defined within -the top-level namespace ``foo``. +Namespaces can also be nested. The named URL ``'foo:bar:whiz'`` would look for +a pattern named ``'whiz'`` in the namespace ``'bar'`` that is itself defined +within the top-level namespace ``'foo'``. .. _topics-http-reversing-url-namespaces: Reversing namespaced URLs ------------------------- -When given a namespaced URL (e.g. ``myapp:index``) to resolve, Django splits +When given a namespaced URL (e.g. ``'myapp:index'``) to resolve, Django splits the fully qualified name into parts, and then tries the following lookup: -1. First, Django looks for a matching application namespace (in this - example, ``myapp``). This will yield a list of instances of that +1. First, Django looks for a matching :term:`application namespace` (in this + example, ``'myapp'``). This will yield a list of instances of that application. 2. If there is a *current* application defined, Django finds and returns @@ -632,19 +627,20 @@ the fully qualified name into parts, and then tries the following lookup: render a template. The current application can also be specified manually as an argument - to the :func:`reverse()` function. + to the :func:`django.core.urlresolvers.reverse()` function. 3. If there is no current application. Django looks for a default application instance. The default application instance is the instance - that has an instance namespace matching the application namespace (in - this example, an instance of the ``myapp`` called ``myapp``). + that has an :term:`instance namespace` matching the :term:`application + namespace` (in this example, an instance of the ``myapp`` called + ``'myapp'``). 4. If there is no default application instance, Django will pick the last deployed instance of the application, whatever its instance name may be. -5. If the provided namespace doesn't match an application namespace in +5. If the provided namespace doesn't match an :term:`application namespace` in step 1, Django will attempt a direct lookup of the namespace as an - instance namespace. + :term:`instance namespace`. If there are nested namespaces, these steps are repeated for each part of the namespace until only the view name is unresolved. The view name will then be @@ -654,58 +650,68 @@ Example ~~~~~~~ To show this resolution strategy in action, consider an example of two instances -of ``myapp``: one called ``foo``, and one called ``bar``. ``myapp`` has a main -index page with a URL named `index`. Using this setup, the following lookups are -possible: +of ``myapp``: one called ``'foo'``, and one called ``'bar'``. ``myapp`` has a +main index page with a URL named ``'index'``. Using this setup, the following +lookups are possible: * If one of the instances is current - say, if we were rendering a utility page - in the instance ``bar`` - ``myapp:index`` will resolve to the index page of - the instance ``bar``. + in the instance ``'bar'`` - ``'myapp:index'`` will resolve to the index page + of the instance ``'bar'``. * If there is no current instance - say, if we were rendering a page - somewhere else on the site - ``myapp:index`` will resolve to the last + somewhere else on the site - ``'myapp:index'`` will resolve to the last registered instance of ``myapp``. Since there is no default instance, the last instance of ``myapp`` that is registered will be used. This could - be ``foo`` or ``bar``, depending on the order they are introduced into the + be ``'foo'`` or ``'bar'``, depending on the order they are introduced into the urlpatterns of the project. -* ``foo:index`` will always resolve to the index page of the instance ``foo``. +* ``'foo:index'`` will always resolve to the index page of the instance + ``'foo'``. -If there was also a default instance - i.e., an instance named `myapp` - the +If there was also a default instance - i.e., an instance named ``'myapp'`` - the following would happen: * If one of the instances is current - say, if we were rendering a utility page - in the instance ``bar`` - ``myapp:index`` will resolve to the index page of - the instance ``bar``. + in the instance ``'bar'`` - ``'myapp:index'`` will resolve to the index page + of the instance ``'bar'``. * If there is no current instance - say, if we were rendering a page somewhere - else on the site - ``myapp:index`` will resolve to the index page of the + else on the site - ``'myapp:index'`` will resolve to the index page of the default instance. -* ``foo:index`` will again resolve to the index page of the instance ``foo``. +* ``'foo:index'`` will again resolve to the index page of the instance + ``'foo'``. + +.. _namespaces-and-include: URL namespaces and included URLconfs ------------------------------------ URL namespaces of included URLconfs can be specified in two ways. -Firstly, you can provide the application and instance namespace as arguments -to ``include()`` when you construct your URL patterns. For example,:: +Firstly, you can provide the application and :term:`instance namespace` as +arguments to :func:`django.conf.urls.include()` when you construct your URL +patterns. For example,:: (r'^help/', include('apps.help.urls', namespace='foo', app_name='bar')), -This will include the URLs defined in ``apps.help.urls`` into the application -namespace ``bar``, with the instance namespace ``foo``. +This will include the URLs defined in ``apps.help.urls`` into the +:term:`application namespace` ``'bar'``, with the :term:`instance namespace` +``'foo'``. Secondly, you can include an object that contains embedded namespace data. If -you ``include()`` a ``patterns`` object, that object will be added to the -global namespace. However, you can also ``include()`` an object that contains -a 3-tuple containing:: +you ``include()`` an object as returned by :func:`~django.conf.urls.patterns`, +the URLs contained in that object will be added to the global namespace. +However, you can also ``include()`` a 3-tuple containing:: (, , ) This will include the nominated URL patterns into the given application and -instance namespace. For example, the ``urls`` attribute of Django's -:class:`~django.contrib.admin.AdminSite` object returns a 3-tuple that contains -all the patterns in an admin site, plus the name of the admin instance, and the -application namespace ``admin``. +instance namespace. + +For example, the Django Admin is deployed as instances of +:class:`~django.contrib.admin.AdminSite`. ``AdminSite`` objects have a ``urls`` +attribute: A 3-tuple that contains all the patterns in the corresponding admin +site, plus the application namespace ``'admin'``, and the name of the admin +instance. It is this ``urls`` attribute that you ``include()`` into your +projects ``urlpatterns`` when you deploy an Admin instance. From 2100da9dcdba7a8bc18f6180fd7e61640319b48a Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 6 Oct 2012 18:40:58 -0300 Subject: [PATCH 134/265] Ensure we ignore __pycache__ PEP 3174 dirs in a few more places. --- MANIFEST.in | 2 ++ django/core/management/templates.py | 2 +- django/db/utils.py | 2 +- django/template/defaultfilters.py | 2 +- setup.py | 5 +++-- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 185e57646a..fbda541d22 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -32,3 +32,5 @@ recursive-include django/contrib/gis/tests/geogapp/fixtures * recursive-include django/contrib/gis/tests/relatedapp/fixtures * recursive-include django/contrib/sitemaps/templates * recursive-include django/contrib/sitemaps/tests/templates * +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/django/core/management/templates.py b/django/core/management/templates.py index aa65593e9c..d34a0deb7e 100644 --- a/django/core/management/templates.py +++ b/django/core/management/templates.py @@ -138,7 +138,7 @@ class TemplateCommand(BaseCommand): os.mkdir(target_dir) for dirname in dirs[:]: - if dirname.startswith('.'): + if dirname.startswith('.') or dirname == '__pycache__': dirs.remove(dirname) for filename in files: diff --git a/django/db/utils.py b/django/db/utils.py index 0ce09bab70..b889cdf9ad 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -30,7 +30,7 @@ def load_backend(backend_name): try: available_backends = [f for f in os.listdir(backend_dir) if os.path.isdir(os.path.join(backend_dir, f)) - and not f.startswith('.')] + and not (f.startswith('.') or f == '__pycache__')] except EnvironmentError: available_backends = [] full_notation = backend_name.startswith('django.db.backends.') diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 1bfb627023..e15440f90e 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -96,7 +96,7 @@ def fix_ampersands_filter(value): # Values for testing floatformat input against infinity and NaN representations, # which differ across platforms and Python versions. Some (i.e. old Windows # ones) are not recognized by Decimal but we want to return them unchanged vs. -# returning an empty string as we do for completley invalid input. Note these +# returning an empty string as we do for completely invalid input. Note these # need to be built up from values that are not inf/nan, since inf/nan values do # not reload properly from .pyc files on Windows prior to some level of Python 2.5 # (see Python Issue757815 and Issue1080440). diff --git a/setup.py b/setup.py index 165c5e9f73..333d57ac70 100644 --- a/setup.py +++ b/setup.py @@ -67,9 +67,10 @@ if root_dir != '': django_dir = 'django' for dirpath, dirnames, filenames in os.walk(django_dir): - # Ignore dirnames that start with '.' + # Ignore PEP 3147 cache dirs and those whose names start with '.' for i, dirname in enumerate(dirnames): - if dirname.startswith('.'): del dirnames[i] + if dirname.startswith('.') or dirname == '__pycache__': + del dirnames[i] if '__init__.py' in filenames: packages.append('.'.join(fullsplit(dirpath))) elif filenames: From ab232937355e8f7ed959ebafd16f8ac7b2a2f85d Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 6 Oct 2012 18:50:30 -0300 Subject: [PATCH 135/265] Updated .gitignore with files created when running setup.py sdist. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a34f512130..17e39abd38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.egg-info *.pot *.py[co] +MANIFEST +dist/ docs/_build/ From 35e8dc5169013437d71ae60e05c7f3356d86236e Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 6 Oct 2012 19:56:28 -0300 Subject: [PATCH 136/265] Removed ad-hoc support for usage of short names of built-in DB backends. This non-standard naming was deprecated in Django 1.2. --- django/db/utils.py | 23 +++++++---------------- tests/regressiontests/backends/tests.py | 7 ------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/django/db/utils.py b/django/db/utils.py index b889cdf9ad..146de7b9f4 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -28,28 +28,19 @@ def load_backend(backend_name): # listing all possible (built-in) database backends. backend_dir = os.path.join(os.path.dirname(__file__), 'backends') try: - available_backends = [f for f in os.listdir(backend_dir) + builtin_backends = [f for f in os.listdir(backend_dir) if os.path.isdir(os.path.join(backend_dir, f)) - and not (f.startswith('.') or f == '__pycache__')] + and not (f.startswith('.') or f in ('__pycache__', 'dummy'))] except EnvironmentError: - available_backends = [] - full_notation = backend_name.startswith('django.db.backends.') - if full_notation: - backend_name = backend_name[19:] # See #15621. - if backend_name not in available_backends: - backend_reprs = map(repr, sorted(available_backends)) + builtin_backends = [] + if backend_name not in ['django.db.backends.%s' % b for b in + builtin_backends]: + backend_reprs = map(repr, sorted(builtin_backends)) error_msg = ("%r isn't an available database backend.\n" - "Try using django.db.backends.XXX, where XXX " + "Try using 'django.db.backends.XXX', where XXX " "is one of:\n %s\nError was: %s" % (backend_name, ", ".join(backend_reprs), e_user)) raise ImproperlyConfigured(error_msg) - elif not full_notation: - # user tried to use the old notation for the database backend - error_msg = ("%r isn't an available database backend.\n" - "Try using django.db.backends.%s instead.\n" - "Error was: %s" % - (backend_name, backend_name, e_user)) - raise ImproperlyConfigured(error_msg) else: # If there's some other error, this must be an error in Django raise diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index cfa298253c..d284cfaac8 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -666,13 +666,6 @@ class ThreadTests(TestCase): self.assertEqual(len(exceptions), 0) -class BackendLoadingTests(TestCase): - def test_old_style_backends_raise_useful_exception(self): - six.assertRaisesRegex(self, ImproperlyConfigured, - "Try using django.db.backends.sqlite3 instead", - load_backend, 'sqlite3') - - class MySQLPKZeroTests(TestCase): """ Zero as id for AutoField should raise exception in MySQL, because MySQL From cb9f71dd99f0e524c871830b248cf28fcacd5753 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 7 Oct 2012 16:20:29 +0200 Subject: [PATCH 137/265] Fixed #18640 -- Allowed access to GDAL Feature without Datasource Thanks Justin Bronn for improving my initial patch. --- django/contrib/gis/gdal/feature.py | 24 +++++++++++++++--------- django/contrib/gis/gdal/field.py | 20 ++++++++++++-------- django/contrib/gis/gdal/layer.py | 4 ++-- django/contrib/gis/gdal/tests/test_ds.py | 8 +++++++- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/django/contrib/gis/gdal/feature.py b/django/contrib/gis/gdal/feature.py index b8737cd1a0..cf154d74b8 100644 --- a/django/contrib/gis/gdal/feature.py +++ b/django/contrib/gis/gdal/feature.py @@ -16,15 +16,21 @@ from django.utils.six.moves import xrange # # The OGR_F_* routines are relevant here. class Feature(GDALBase): - "A class that wraps an OGR Feature, needs to be instantiated from a Layer object." + """ + This class that wraps an OGR Feature, needs to be instantiated + from a Layer object. + """ #### Python 'magic' routines #### - def __init__(self, feat, fdefn): - "Initializes on the pointers for the feature and the layer definition." - if not feat or not fdefn: + def __init__(self, feat, layer): + """ + Initializes on the feature pointers for the feature and the layer + definition, as well as the Layer. + """ + if not feat: raise OGRException('Cannot create OGR Feature, invalid pointer given.') self.ptr = feat - self._fdefn = fdefn + self._layer = layer def __del__(self): "Releases a reference to this object." @@ -43,7 +49,7 @@ class Feature(GDALBase): if index < 0 or index > self.num_fields: raise OGRIndexError('index out of range') i = index - return Field(self.ptr, i) + return Field(self, i) def __iter__(self): "Iterates over each field in the Feature." @@ -71,7 +77,7 @@ class Feature(GDALBase): @property def layer_name(self): "Returns the name of the layer for the feature." - return capi.get_feat_name(self._fdefn) + return capi.get_feat_name(self._layer._ldefn) @property def num_fields(self): @@ -81,7 +87,7 @@ class Feature(GDALBase): @property def fields(self): "Returns a list of fields in the Feature." - return [capi.get_field_name(capi.get_field_defn(self._fdefn, i)) + return [capi.get_field_name(capi.get_field_defn(self._layer._ldefn, i)) for i in xrange(self.num_fields)] @property @@ -94,7 +100,7 @@ class Feature(GDALBase): @property def geom_type(self): "Returns the OGR Geometry Type for this Feture." - return OGRGeomType(capi.get_fd_geom_type(self._fdefn)) + return OGRGeomType(capi.get_fd_geom_type(self._layer._ldefn)) #### Feature Methods #### def get(self, field): diff --git a/django/contrib/gis/gdal/field.py b/django/contrib/gis/gdal/field.py index 12dc8b921f..16230afdc8 100644 --- a/django/contrib/gis/gdal/field.py +++ b/django/contrib/gis/gdal/field.py @@ -9,12 +9,15 @@ from django.contrib.gis.gdal.prototypes import ds as capi # # The OGR_Fld_* routines are relevant here. class Field(GDALBase): - "A class that wraps an OGR Field, needs to be instantiated from a Feature object." + """ + This class wraps an OGR Field, and needs to be instantiated + from a Feature object. + """ #### Python 'magic' routines #### def __init__(self, feat, index): """ - Initializes on the feature pointer and the integer index of + Initializes on the feature object and the integer index of the field within the feature. """ # Setting the feature pointer and index. @@ -22,7 +25,7 @@ class Field(GDALBase): self._index = index # Getting the pointer for this field. - fld_ptr = capi.get_feat_field_defn(feat, index) + fld_ptr = capi.get_feat_field_defn(feat.ptr, index) if not fld_ptr: raise OGRException('Cannot create OGR Field, invalid pointer given.') self.ptr = fld_ptr @@ -42,21 +45,22 @@ class Field(GDALBase): #### Field Methods #### def as_double(self): "Retrieves the Field's value as a double (float)." - return capi.get_field_as_double(self._feat, self._index) + return capi.get_field_as_double(self._feat.ptr, self._index) def as_int(self): "Retrieves the Field's value as an integer." - return capi.get_field_as_integer(self._feat, self._index) + return capi.get_field_as_integer(self._feat.ptr, self._index) def as_string(self): "Retrieves the Field's value as a string." - return capi.get_field_as_string(self._feat, self._index) + return capi.get_field_as_string(self._feat.ptr, self._index) def as_datetime(self): "Retrieves the Field's value as a tuple of date & time components." yy, mm, dd, hh, mn, ss, tz = [c_int() for i in range(7)] - status = capi.get_field_as_datetime(self._feat, self._index, byref(yy), byref(mm), byref(dd), - byref(hh), byref(mn), byref(ss), byref(tz)) + status = capi.get_field_as_datetime( + self._feat.ptr, self._index, byref(yy), byref(mm), byref(dd), + byref(hh), byref(mn), byref(ss), byref(tz)) if status: return (yy, mm, dd, hh, mn, ss, tz) else: diff --git a/django/contrib/gis/gdal/layer.py b/django/contrib/gis/gdal/layer.py index d7bf6969ca..da0f566969 100644 --- a/django/contrib/gis/gdal/layer.py +++ b/django/contrib/gis/gdal/layer.py @@ -61,7 +61,7 @@ class Layer(GDALBase): # ResetReading() must be called before iteration is to begin. capi.reset_reading(self._ptr) for i in xrange(self.num_feat): - yield Feature(capi.get_next_feature(self._ptr), self._ldefn) + yield Feature(capi.get_next_feature(self._ptr), self) def __len__(self): "The length is the number of features." @@ -81,7 +81,7 @@ class Layer(GDALBase): if self._random_read: # If the Layer supports random reading, return. try: - return Feature(capi.get_feature(self.ptr, feat_id), self._ldefn) + return Feature(capi.get_feature(self.ptr, feat_id), self) except OGRException: pass else: diff --git a/django/contrib/gis/gdal/tests/test_ds.py b/django/contrib/gis/gdal/tests/test_ds.py index 69e3054422..9ac1bc8d70 100644 --- a/django/contrib/gis/gdal/tests/test_ds.py +++ b/django/contrib/gis/gdal/tests/test_ds.py @@ -125,7 +125,10 @@ class DataSourceTest(unittest.TestCase): self.assertEqual(control_vals, test_vals) def test03c_layer_references(self): - "Test to make sure Layer access is still available without the DataSource." + """ + Test to make sure Layer/Feature access is still available without + the DataSource/Feature. + """ source = ds_list[0] # See ticket #9448. @@ -141,6 +144,9 @@ class DataSourceTest(unittest.TestCase): self.assertEqual(source.nfeat, len(lyr)) self.assertEqual(source.gtype, lyr.geom_type.num) + # Same issue for Feature/Field objects, see #18640 + self.assertEqual(str(lyr[0]['str']), "1") + def test04_features(self): "Testing Data Source Features." for source in ds_list: From 34a736b7521def321b2104d541e634134c5d1c62 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 7 Oct 2012 21:16:01 +0200 Subject: [PATCH 138/265] Used pkgutil to get list of backend modules Refs #18827. --- django/db/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/django/db/utils.py b/django/db/utils.py index 146de7b9f4..5fa78fe350 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -1,4 +1,5 @@ import os +import pkgutil from threading import local from django.conf import settings @@ -28,9 +29,9 @@ def load_backend(backend_name): # listing all possible (built-in) database backends. backend_dir = os.path.join(os.path.dirname(__file__), 'backends') try: - builtin_backends = [f for f in os.listdir(backend_dir) - if os.path.isdir(os.path.join(backend_dir, f)) - and not (f.startswith('.') or f in ('__pycache__', 'dummy'))] + builtin_backends = [ + name for _, name, ispkg in pkgutil.iter_modules([backend_dir]) + if ispkg and name != 'dummy'] except EnvironmentError: builtin_backends = [] if backend_name not in ['django.db.backends.%s' % b for b in From ec1aad1671bfbba7ef58e7477dd14d7add065838 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sun, 7 Oct 2012 20:11:12 -0300 Subject: [PATCH 139/265] Added section about URL reversion to URL mapper document. --- docs/ref/contrib/formtools/form-wizard.txt | 2 +- docs/ref/models/instances.txt | 12 ++- docs/ref/templates/builtins.txt | 2 +- docs/ref/urlresolvers.txt | 16 ++- docs/topics/http/urls.txt | 119 +++++++++++++++++++-- docs/topics/testing.txt | 2 +- 6 files changed, 132 insertions(+), 21 deletions(-) diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index d5231de3e5..0ced1bf155 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -226,7 +226,7 @@ Hooking the wizard into a URLconf --------------------------------- Finally, we need to specify which forms to use in the wizard, and then -deploy the new :class:`WizardView` object at an URL in the ``urls.py``. The +deploy the new :class:`WizardView` object at a URL in the ``urls.py``. The wizard's :meth:`as_view` method takes a list of your :class:`~django.forms.Form` classes as an argument during instantiation:: diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 92fc4ef31a..1ba41148b0 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -494,12 +494,16 @@ defined. If it makes sense for your model's instances to each have a unique URL, you should define ``get_absolute_url()``. It's good practice to use ``get_absolute_url()`` in templates, instead of -hard-coding your objects' URLs. For example, this template code is bad:: +hard-coding your objects' URLs. For example, this template code is bad: + +.. code-block:: html+django {{ object.name }} -This template code is much better:: +This template code is much better: + +.. code-block:: html+django {{ object.name }} @@ -535,7 +539,9 @@ pattern name) and a list of position or keyword arguments and uses the URLconf patterns to construct the correct, full URL. It returns a string for the correct URL, with all parameters substituted in the correct positions. -The ``permalink`` decorator is a Python-level equivalent to the :ttag:`url` template tag and a high-level wrapper for the :func:`django.core.urlresolvers.reverse()` function. +The ``permalink`` decorator is a Python-level equivalent to the :ttag:`url` +template tag and a high-level wrapper for the +:func:`django.core.urlresolvers.reverse()` function. An example should make it clear how to use ``permalink()``. Suppose your URLconf contains a line such as:: diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 07ac284905..3b8d058fb4 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -997,7 +997,7 @@ refer to the name of the pattern in the ``url`` tag instead of using the path to the view. Note that if the URL you're reversing doesn't exist, you'll get an -:exc:`^django.core.urlresolvers.NoReverseMatch` exception raised, which will +:exc:`~django.core.urlresolvers.NoReverseMatch` exception raised, which will cause your site to display an error page. If you'd like to retrieve a URL without displaying it, you can use a slightly diff --git a/docs/ref/urlresolvers.txt b/docs/ref/urlresolvers.txt index 965cafb29b..1bb33c7ca1 100644 --- a/docs/ref/urlresolvers.txt +++ b/docs/ref/urlresolvers.txt @@ -8,8 +8,7 @@ reverse() --------- If you need to use something similar to the :ttag:`url` template tag in -your code, Django provides the following function (in the -:mod:`django.core.urlresolvers` module): +your code, Django provides the following function: .. function:: reverse(viewname, [urlconf=None, args=None, kwargs=None, current_app=None]) @@ -59,15 +58,15 @@ You can use ``kwargs`` instead of ``args``. For example:: .. note:: - The string returned by :meth:`~django.core.urlresolvers.reverse` is already + The string returned by ``reverse()`` is already :ref:`urlquoted `. For example:: >>> reverse('cities', args=[u'Orléans']) '.../Orl%C3%A9ans/' Applying further encoding (such as :meth:`~django.utils.http.urlquote` or - ``urllib.quote``) to the output of :meth:`~django.core.urlresolvers.reverse` - may produce undesirable results. + ``urllib.quote``) to the output of ``reverse()`` may produce undesirable + results. reverse_lazy() -------------- @@ -94,9 +93,8 @@ URLConf is loaded. Some common cases where this function is necessary are: resolve() --------- -The :func:`django.core.urlresolvers.resolve` function can be used for -resolving URL paths to the corresponding view functions. It has the -following signature: +The ``resolve()`` function can be used for resolving URL paths to the +corresponding view functions. It has the following signature: .. function:: resolve(path, urlconf=None) @@ -184,7 +182,7 @@ whether a view would raise a ``Http404`` error before redirecting to it:: permalink() ----------- -The :func:`django.db.models.permalink` decorator is useful for writing short +The :func:`~django.db.models.permalink` decorator is useful for writing short methods that return a full URL path. For example, a model's ``get_absolute_url()`` method. See :func:`django.db.models.permalink` for more. diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index d7b3b03d84..c51ce2d2a4 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -421,9 +421,9 @@ options to views. Passing extra options to ``include()`` -------------------------------------- -Similarly, you can pass extra options to ``include()``. When you pass extra -options to ``include()``, *each* line in the included URLconf will be passed -the extra options. +Similarly, you can pass extra options to :func:`~django.conf.urls.include`. +When you pass extra options to ``include()``, *each* line in the included +URLconf will be passed the extra options. For example, these two URLconf sets are functionally identical: @@ -510,6 +510,103 @@ imported:: (r'^myview/$', ClassBasedView.as_view()), ) +Reverse resolution of URLs +========================== + +A common need when working on a Django project is the possibility to obtain URLs +in their final forms either for embedding in generated content (views and assets +URLs, URLs shown to the user, etc.) or for handling of the navigation flow on +the server side (redirections, etc.) + +It is strongly desirable not having to hard-code these URLs (a laborious, +non-scalable and error-prone strategy) or having to devise ad-hoc mechanisms for +generating URLs that are parallel to the design described by the URLconf and as +such in danger of producing stale URLs at some point. + +In other words, what's needed is a DRY mechanism. Among other advantages it +would allow evolution of the URL design without having to go all over the +project source code to search and replace outdated URLs. + +The piece of information we have available as a starting point to get a URL is +an identification (e.g. the name) of the view in charge of handling it, other +pieces of information that necessarily must participate in the lookup of the +right URL are the types (positional, keyword) and values of the view arguments. + +Django provides a solution such that the URL mapper is the only repository of +the URL design. You feed it with your URLconf and then it can be used in both +directions: + +* Starting with a URL requested by the user/browser, it calls the right Django + view providing any arguments it might need with their values as extracted from + the URL. + +* Starting with the identification of the corresponding Django view plus the + values of arguments that would be passed to it, obtain the associated URL. + +The first one is the usage we've been discussing in the previous sections. The +second one is what is known as *reverse resolution of URLs*, *reverse URL +matching*, *reverse URL lookup*, or simply *URL reversing*. + +Django provides tools for performing URL reversing that match the different +layers where URLs are needed: + +* In templates: Using the :ttag:`url` template tag. + +* In Python code: Using the :func:`django.core.urlresolvers.reverse()` + function. + +* In higher level code related to handling of URLs of Django model instances: + The :meth:`django.db.models.Model.get_absolute_url()` method and the + :func:`django.db.models.permalink` decorator. + +Examples +-------- + +Consider again this URLconf entry:: + + from django.conf.urls import patterns, url + + urlpatterns = patterns('', + #... + url(r'^articles/(\d{4})/$', 'news.views.year_archive'), + #... + ) + +According to this design, the URL for the archive corresponding to year *nnnn* +is ``/articles/nnnn/``. + +You can obtain these in template code by using: + +.. code-block:: html+django + + 2012 Archive + {# Or with the year in a template context variable: #} + + +Or in Python code:: + + from django.core.urlresolvers import reverse + from django.http import HttpResponseRedirect + + def redirect_to_year(request): + # ... + year = 2006 + # ... + return HttpResponseRedirect(reverse('new.views.year_archive', args=(year,))) + +If, for some reason, it was decided that the URL where content for yearly +article archives are published at should be changed then you would only need to +change the entry in the URLconf. + +In some scenarios where views are of a generic nature, a many-to-one +relationship might exist between URLs and views. For these cases the view name +isn't a good enough identificator for it when it comes the time of reversing +URLs. Read the next section to know about the solution Django provides for this. + .. _naming-url-patterns: Naming URL patterns @@ -689,9 +786,10 @@ URL namespaces and included URLconfs URL namespaces of included URLconfs can be specified in two ways. -Firstly, you can provide the application and :term:`instance namespace` as -arguments to :func:`django.conf.urls.include()` when you construct your URL -patterns. For example,:: +Firstly, you can provide the :term:`application ` and +:term:`instance ` namespaces as arguments to +:func:`django.conf.urls.include()` when you construct your URL patterns. For +example,:: (r'^help/', include('apps.help.urls', namespace='foo', app_name='bar')), @@ -706,6 +804,15 @@ However, you can also ``include()`` a 3-tuple containing:: (, , ) +For example:: + + help_patterns = patterns('', + url(r'^basic/$', 'apps.help.views.views.basic'), + url(r'^advanced/$', 'apps.help.views.views.advanced'), + ) + + (r'^help/', include(help_patterns, 'bar', 'foo')), + This will include the nominated URL patterns into the given application and instance namespace. diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index 895e721ef5..e2d424aec5 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -769,7 +769,7 @@ Use the ``django.test.client.Client`` class to make requests. and a ``redirect_chain`` attribute will be set in the response object containing tuples of the intermediate urls and status codes. - If you had an url ``/redirect_me/`` that redirected to ``/next/``, that + If you had a URL ``/redirect_me/`` that redirected to ``/next/``, that redirected to ``/final/``, this is what you'd see:: >>> response = c.get('/redirect_me/', follow=True) From 08eb54ae711ec087457333a259a7861eb5b39063 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Sun, 7 Oct 2012 17:28:19 -0700 Subject: [PATCH 140/265] GDAL docstring tweaks. --- django/contrib/gis/gdal/feature.py | 3 +-- django/contrib/gis/gdal/tests/test_ds.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/django/contrib/gis/gdal/feature.py b/django/contrib/gis/gdal/feature.py index cf154d74b8..6f338ad269 100644 --- a/django/contrib/gis/gdal/feature.py +++ b/django/contrib/gis/gdal/feature.py @@ -24,8 +24,7 @@ class Feature(GDALBase): #### Python 'magic' routines #### def __init__(self, feat, layer): """ - Initializes on the feature pointers for the feature and the layer - definition, as well as the Layer. + Initializes Feature from a pointer and its Layer object. """ if not feat: raise OGRException('Cannot create OGR Feature, invalid pointer given.') diff --git a/django/contrib/gis/gdal/tests/test_ds.py b/django/contrib/gis/gdal/tests/test_ds.py index 9ac1bc8d70..634f204b86 100644 --- a/django/contrib/gis/gdal/tests/test_ds.py +++ b/django/contrib/gis/gdal/tests/test_ds.py @@ -126,8 +126,7 @@ class DataSourceTest(unittest.TestCase): def test03c_layer_references(self): """ - Test to make sure Layer/Feature access is still available without - the DataSource/Feature. + Ensure OGR objects keep references to the objects they belong to. """ source = ds_list[0] From 75301d99d314a992a202fad64b2667528f9d51c8 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Sun, 7 Oct 2012 20:08:31 -0700 Subject: [PATCH 141/265] Fixed `inspectapp` tests to work with improved PG driver in GDAL 1.9+. --- django/contrib/gis/tests/inspectapp/tests.py | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/django/contrib/gis/tests/inspectapp/tests.py b/django/contrib/gis/tests/inspectapp/tests.py index a3d19784c2..8fc39db58d 100644 --- a/django/contrib/gis/tests/inspectapp/tests.py +++ b/django/contrib/gis/tests/inspectapp/tests.py @@ -4,7 +4,7 @@ import os from django.db import connections from django.test import TestCase -from django.contrib.gis.gdal import Driver +from django.contrib.gis.gdal import Driver, GDAL_VERSION from django.contrib.gis.geometry.test_data import TEST_DATA from django.contrib.gis.utils.ogrinspect import ogrinspect @@ -74,15 +74,33 @@ class OGRInspectTest(TestCase): '', 'class Measurement(models.Model):', ' f_decimal = models.DecimalField(max_digits=0, decimal_places=0)', + ] + + if GDAL_VERSION < (1, 9, 0): + # Prior to GDAL 1.9, the order of the model fields was not + # the same as the columns in the database. + expected.extend([ ' f_int = models.IntegerField()', ' f_datetime = models.DateTimeField()', ' f_time = models.TimeField()', ' f_float = models.FloatField()', ' f_char = models.CharField(max_length=10)', ' f_date = models.DateField()', + ]) + else: + expected.extend([ + ' f_float = models.FloatField()', + ' f_int = models.IntegerField()', + ' f_char = models.CharField(max_length=10)', + ' f_date = models.DateField()', + ' f_datetime = models.DateTimeField()', + ' f_time = models.TimeField()', + ]) + + expected.extend([ ' geom = models.PolygonField()', ' objects = models.GeoManager()', - ] + ]) self.assertEqual(model_def, '\n'.join(expected)) From 88cc002e16635c9ad9f76b737366c5206f421c6e Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Sun, 7 Oct 2012 21:05:10 -0700 Subject: [PATCH 142/265] Moved Travis Pinney and Dane Springmeyer into the AUTHORS file where they belong. --- AUTHORS | 2 ++ django/contrib/gis/templates/gis/admin/openlayers.js | 2 +- django/contrib/gis/utils/ogrinspect.py | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 2904bd0d99..5799b941ff 100644 --- a/AUTHORS +++ b/AUTHORS @@ -425,6 +425,7 @@ answer newbie questions, and generally made Django that much better: phil@produxion.net phil.h.smith@gmail.com Gustavo Picon + Travis Pinney Michael Placentra II plisk Daniel Poelzleithner @@ -500,6 +501,7 @@ answer newbie questions, and generally made Django that much better: Wiliam Alves de Souza Don Spaulding Calvin Spealman + Dane Springmeyer Bjørn Stabell Georgi Stanojevski starrynight diff --git a/django/contrib/gis/templates/gis/admin/openlayers.js b/django/contrib/gis/templates/gis/admin/openlayers.js index a67980da40..eb40edae8f 100644 --- a/django/contrib/gis/templates/gis/admin/openlayers.js +++ b/django/contrib/gis/templates/gis/admin/openlayers.js @@ -1,4 +1,4 @@ -{% load l10n %}{# Author: Justin Bronn, Travis Pinney & Dane Springmeyer #} +{% load l10n %} OpenLayers.Projection.addTransform("EPSG:4326", "EPSG:3857", OpenLayers.Layer.SphericalMercator.projectForward); {% block vars %}var {{ module }} = {}; {{ module }}.map = null; {{ module }}.controls = null; {{ module }}.panel = null; {{ module }}.re = new RegExp("^SRID=\\d+;(.+)", "i"); {{ module }}.layers = {}; diff --git a/django/contrib/gis/utils/ogrinspect.py b/django/contrib/gis/utils/ogrinspect.py index 4266ee4b4c..1c870eaa30 100644 --- a/django/contrib/gis/utils/ogrinspect.py +++ b/django/contrib/gis/utils/ogrinspect.py @@ -2,8 +2,6 @@ This module is for inspecting OGR data sources and generating either models for GeoDjango and/or mapping dictionaries for use with the `LayerMapping` utility. - -Author: Travis Pinney, Dane Springmeyer, & Justin Bronn """ from django.utils.six.moves import zip # Requires GDAL to use. From 4797ad80da57f3f8a7c029008f3e937240cf23e6 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 8 Oct 2012 10:04:29 +0200 Subject: [PATCH 143/265] [py3] Decoded the parsed source file encoding in debug view --- django/views/debug.py | 2 +- tests/regressiontests/views/tests/debug.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/django/views/debug.py b/django/views/debug.py index ed99d8dfe6..c59fe31fc8 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -360,7 +360,7 @@ class ExceptionReporter(object): # (http://www.python.org/dev/peps/pep-0263/) match = re.search(br'coding[:=]\s*([-\w.]+)', line) if match: - encoding = match.group(1) + encoding = match.group(1).decode('ascii') break source = [six.text_type(sline, encoding, 'replace') for sline in source] diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py index 8592b07efe..e616d184b8 100644 --- a/tests/regressiontests/views/tests/debug.py +++ b/tests/regressiontests/views/tests/debug.py @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- +# This coding header is significant for tests, as the debug view is parsing +# files to search for such a header to decode the source file content from __future__ import absolute_import, unicode_literals import inspect From a62d53c03252bdf82b21b64874efe053160cbdb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Mon, 8 Oct 2012 18:36:51 +0300 Subject: [PATCH 144/265] Fixed #19087 -- Ensured query's base table is never LOUTER joined This fixes a regression created by join promotion logic refactoring: 01b9c3d5193fe61b82ae8b26242a13fdec22f211 Thanks to Ivan Virabyan for the report. --- django/db/models/sql/query.py | 8 ++++++++ tests/regressiontests/aggregation_regress/tests.py | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 77f24fcf24..ad82b167a6 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -702,6 +702,11 @@ class Query(object): aliases = list(aliases) while aliases: alias = aliases.pop(0) + if self.alias_map[alias].rhs_join_col is None: + # This is the base table (first FROM entry) - this table + # isn't really joined at all in the query, so we should not + # alter its join type. + continue parent_alias = self.alias_map[alias].lhs_alias parent_louter = (parent_alias and self.alias_map[parent_alias].join_type == self.LOUTER) @@ -1188,6 +1193,9 @@ class Query(object): for alias in join_list: if self.alias_map[alias].join_type == self.LOUTER: j_col = self.alias_map[alias].rhs_join_col + # The join promotion logic should never produce + # a LOUTER join for the base join - assert that. + assert j_col is not None entry = self.where_class() entry.add( (Constraint(alias, j_col, None), 'isnull', True), diff --git a/tests/regressiontests/aggregation_regress/tests.py b/tests/regressiontests/aggregation_regress/tests.py index b9f3ab27eb..af0f421502 100644 --- a/tests/regressiontests/aggregation_regress/tests.py +++ b/tests/regressiontests/aggregation_regress/tests.py @@ -878,3 +878,14 @@ class AggregationTests(TestCase): connection.ops.convert_values(testData, testField), testData ) + + def test_annotate_joins(self): + """ + Test that the base table's join isn't promoted to LOUTER. This could + cause the query generation to fail if there is an exclude() for fk-field + in the query, too. Refs #19087. + """ + qs = Book.objects.annotate(n=Count('pk')) + self.assertIs(qs.query.alias_map['aggregation_regress_book'].join_type, None) + # Check that the query executes without problems. + self.assertEqual(len(qs.exclude(publisher=-1)), 6) From 9a2bceed1aab52f65820c378f5ae1f608322b55c Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 6 Oct 2012 22:56:47 +0200 Subject: [PATCH 145/265] Use smarter string decoding in GeoDjango The first try to solve the Python 3 GIS encoding/decoding issue was too naive. Using decode() on all read strings is bound to fail as soon as a non-ascii string is concerned. This patch is a little more clever, leaving ascii decoding when plain ascii strings are expected, and allowing to specify a custom encoding in DataSource hierarchy. --- django/contrib/gis/gdal/datasource.py | 9 ++++++--- django/contrib/gis/gdal/feature.py | 9 +++++++-- django/contrib/gis/gdal/field.py | 8 ++++++-- django/contrib/gis/gdal/layer.py | 10 ++++++---- django/contrib/gis/gdal/prototypes/ds.py | 2 +- django/contrib/gis/gdal/prototypes/errcheck.py | 9 ++++----- django/contrib/gis/gdal/prototypes/generation.py | 14 ++++++++++---- django/contrib/gis/gdal/prototypes/geom.py | 10 +++++----- django/contrib/gis/gdal/prototypes/srs.py | 14 +++++++------- django/contrib/gis/gdal/srs.py | 5 ++--- django/contrib/gis/gdal/tests/test_ds.py | 3 ++- django/contrib/gis/tests/data/ch-city/ch-city.dbf | Bin 0 -> 285 bytes django/contrib/gis/tests/data/ch-city/ch-city.prj | 1 + django/contrib/gis/tests/data/ch-city/ch-city.shp | Bin 0 -> 128 bytes django/contrib/gis/tests/data/ch-city/ch-city.shx | Bin 0 -> 108 bytes django/contrib/gis/tests/layermap/tests.py | 10 +++++++++- django/contrib/gis/utils/layermapping.py | 8 +++++--- 17 files changed, 71 insertions(+), 41 deletions(-) create mode 100644 django/contrib/gis/tests/data/ch-city/ch-city.dbf create mode 100644 django/contrib/gis/tests/data/ch-city/ch-city.prj create mode 100644 django/contrib/gis/tests/data/ch-city/ch-city.shp create mode 100644 django/contrib/gis/tests/data/ch-city/ch-city.shx diff --git a/django/contrib/gis/gdal/datasource.py b/django/contrib/gis/gdal/datasource.py index fa3d86afba..c92b2e170b 100644 --- a/django/contrib/gis/gdal/datasource.py +++ b/django/contrib/gis/gdal/datasource.py @@ -45,7 +45,7 @@ from django.contrib.gis.gdal.layer import Layer # Getting the ctypes prototypes for the DataSource. from django.contrib.gis.gdal.prototypes import ds as capi -from django.utils.encoding import force_bytes +from django.utils.encoding import force_bytes, force_text from django.utils import six from django.utils.six.moves import xrange @@ -57,12 +57,14 @@ class DataSource(GDALBase): "Wraps an OGR Data Source object." #### Python 'magic' routines #### - def __init__(self, ds_input, ds_driver=False, write=False): + def __init__(self, ds_input, ds_driver=False, write=False, encoding='utf-8'): # The write flag. if write: self._write = 1 else: self._write = 0 + # See also http://trac.osgeo.org/gdal/wiki/rfc23_ogr_unicode + self.encoding = encoding # Registering all the drivers, this needs to be done # _before_ we try to open up a data source. @@ -129,4 +131,5 @@ class DataSource(GDALBase): @property def name(self): "Returns the name of the data source." - return capi.get_ds_name(self._ptr) + name = capi.get_ds_name(self._ptr) + return force_text(name, self.encoding, strings_only=True) diff --git a/django/contrib/gis/gdal/feature.py b/django/contrib/gis/gdal/feature.py index 6f338ad269..a11a6873c5 100644 --- a/django/contrib/gis/gdal/feature.py +++ b/django/contrib/gis/gdal/feature.py @@ -7,7 +7,7 @@ from django.contrib.gis.gdal.geometries import OGRGeometry, OGRGeomType # ctypes function prototypes from django.contrib.gis.gdal.prototypes import ds as capi, geom as geom_api -from django.utils.encoding import force_bytes +from django.utils.encoding import force_bytes, force_text from django.utils import six from django.utils.six.moves import xrange @@ -68,6 +68,10 @@ class Feature(GDALBase): return bool(capi.feature_equal(self.ptr, other._ptr)) #### Feature Properties #### + @property + def encoding(self): + return self._layer._ds.encoding + @property def fid(self): "Returns the feature identifier." @@ -76,7 +80,8 @@ class Feature(GDALBase): @property def layer_name(self): "Returns the name of the layer for the feature." - return capi.get_feat_name(self._layer._ldefn) + name = capi.get_feat_name(self._layer._ldefn) + return force_text(name, self.encoding, strings_only=True) @property def num_fields(self): diff --git a/django/contrib/gis/gdal/field.py b/django/contrib/gis/gdal/field.py index 16230afdc8..2415f32b26 100644 --- a/django/contrib/gis/gdal/field.py +++ b/django/contrib/gis/gdal/field.py @@ -3,6 +3,8 @@ from datetime import date, datetime, time from django.contrib.gis.gdal.base import GDALBase from django.contrib.gis.gdal.error import OGRException from django.contrib.gis.gdal.prototypes import ds as capi +from django.utils.encoding import force_text + # For more information, see the OGR C API source code: # http://www.gdal.org/ogr/ogr__api_8h.html @@ -53,7 +55,8 @@ class Field(GDALBase): def as_string(self): "Retrieves the Field's value as a string." - return capi.get_field_as_string(self._feat.ptr, self._index) + string = capi.get_field_as_string(self._feat.ptr, self._index) + return force_text(string, encoding=self._feat.encoding, strings_only=True) def as_datetime(self): "Retrieves the Field's value as a tuple of date & time components." @@ -70,7 +73,8 @@ class Field(GDALBase): @property def name(self): "Returns the name of this Field." - return capi.get_field_name(self.ptr) + name = capi.get_field_name(self.ptr) + return force_text(name, encoding=self._feat.encoding, strings_only=True) @property def precision(self): diff --git a/django/contrib/gis/gdal/layer.py b/django/contrib/gis/gdal/layer.py index da0f566969..7f935cd114 100644 --- a/django/contrib/gis/gdal/layer.py +++ b/django/contrib/gis/gdal/layer.py @@ -14,7 +14,7 @@ from django.contrib.gis.gdal.srs import SpatialReference # GDAL ctypes function prototypes. from django.contrib.gis.gdal.prototypes import ds as capi, geom as geom_api, srs as srs_api -from django.utils.encoding import force_bytes +from django.utils.encoding import force_bytes, force_text from django.utils import six from django.utils.six.moves import xrange @@ -103,7 +103,8 @@ class Layer(GDALBase): @property def name(self): "Returns the name of this layer in the Data Source." - return capi.get_fd_name(self._ldefn) + name = capi.get_fd_name(self._ldefn) + return force_text(name, self._ds.encoding, strings_only=True) @property def num_feat(self, force=1): @@ -135,8 +136,9 @@ class Layer(GDALBase): Returns a list of string names corresponding to each of the Fields available in this Layer. """ - return [capi.get_field_name(capi.get_field_defn(self._ldefn, i)) - for i in xrange(self.num_fields) ] + return [force_text(capi.get_field_name(capi.get_field_defn(self._ldefn, i)), + self._ds.encoding, strings_only=True) + for i in xrange(self.num_fields)] @property def field_types(self): diff --git a/django/contrib/gis/gdal/prototypes/ds.py b/django/contrib/gis/gdal/prototypes/ds.py index d8537bcaa4..f798069fd0 100644 --- a/django/contrib/gis/gdal/prototypes/ds.py +++ b/django/contrib/gis/gdal/prototypes/ds.py @@ -17,7 +17,7 @@ cleanup_all = void_output(lgdal.OGRCleanupAll, [], errcheck=False) get_driver = voidptr_output(lgdal.OGRGetDriver, [c_int]) get_driver_by_name = voidptr_output(lgdal.OGRGetDriverByName, [c_char_p]) get_driver_count = int_output(lgdal.OGRGetDriverCount, []) -get_driver_name = const_string_output(lgdal.OGR_Dr_GetName, [c_void_p]) +get_driver_name = const_string_output(lgdal.OGR_Dr_GetName, [c_void_p], decoding='ascii') ### DataSource ### open_ds = voidptr_output(lgdal.OGROpen, [c_char_p, c_int, POINTER(c_void_p)]) diff --git a/django/contrib/gis/gdal/prototypes/errcheck.py b/django/contrib/gis/gdal/prototypes/errcheck.py index 9103022896..2d2791124c 100644 --- a/django/contrib/gis/gdal/prototypes/errcheck.py +++ b/django/contrib/gis/gdal/prototypes/errcheck.py @@ -30,10 +30,9 @@ def check_const_string(result, func, cargs, offset=None): if offset: check_err(result) ptr = ptr_byref(cargs, offset) - return ptr.value.decode() + return ptr.value else: - if result is not None: - return result.decode() + return result def check_string(result, func, cargs, offset=-1, str_result=False): """ @@ -48,13 +47,13 @@ def check_string(result, func, cargs, offset=-1, str_result=False): # For routines that return a string. ptr = result if not ptr: s = None - else: s = string_at(result).decode() + else: s = string_at(result) else: # Error-code return specified. check_err(result) ptr = ptr_byref(cargs, offset) # Getting the string value - s = ptr.value.decode() + s = ptr.value # Correctly freeing the allocated memory beind GDAL pointer # w/the VSIFree routine. if ptr: lgdal.VSIFree(ptr) diff --git a/django/contrib/gis/gdal/prototypes/generation.py b/django/contrib/gis/gdal/prototypes/generation.py index 45cffd645a..577d29bbaa 100644 --- a/django/contrib/gis/gdal/prototypes/generation.py +++ b/django/contrib/gis/gdal/prototypes/generation.py @@ -57,7 +57,7 @@ def srs_output(func, argtypes): func.errcheck = check_srs return func -def const_string_output(func, argtypes, offset=None): +def const_string_output(func, argtypes, offset=None, decoding=None): func.argtypes = argtypes if offset: func.restype = c_int @@ -65,12 +65,15 @@ def const_string_output(func, argtypes, offset=None): func.restype = c_char_p def _check_const(result, func, cargs): - return check_const_string(result, func, cargs, offset=offset) + res = check_const_string(result, func, cargs, offset=offset) + if res and decoding: + res = res.decode(decoding) + return res func.errcheck = _check_const return func -def string_output(func, argtypes, offset=-1, str_result=False): +def string_output(func, argtypes, offset=-1, str_result=False, decoding=None): """ Generates a ctypes prototype for the given function with the given argument types that returns a string from a GDAL pointer. @@ -90,8 +93,11 @@ def string_output(func, argtypes, offset=-1, str_result=False): # Dynamically defining our error-checking function with the # given offset. def _check_str(result, func, cargs): - return check_string(result, func, cargs, + res = check_string(result, func, cargs, offset=offset, str_result=str_result) + if res and decoding: + res = res.decode(decoding) + return res func.errcheck = _check_str return func diff --git a/django/contrib/gis/gdal/prototypes/geom.py b/django/contrib/gis/gdal/prototypes/geom.py index f2c833d576..fa0b503c65 100644 --- a/django/contrib/gis/gdal/prototypes/geom.py +++ b/django/contrib/gis/gdal/prototypes/geom.py @@ -27,8 +27,8 @@ def topology_func(f): # GeoJSON routines. from_json = geom_output(lgdal.OGR_G_CreateGeometryFromJson, [c_char_p]) -to_json = string_output(lgdal.OGR_G_ExportToJson, [c_void_p], str_result=True) -to_kml = string_output(lgdal.OGR_G_ExportToKML, [c_void_p, c_char_p], str_result=True) +to_json = string_output(lgdal.OGR_G_ExportToJson, [c_void_p], str_result=True, decoding='ascii') +to_kml = string_output(lgdal.OGR_G_ExportToKML, [c_void_p, c_char_p], str_result=True, decoding='ascii') # GetX, GetY, GetZ all return doubles. getx = pnt_func(lgdal.OGR_G_GetX) @@ -57,8 +57,8 @@ destroy_geom = void_output(lgdal.OGR_G_DestroyGeometry, [c_void_p], errcheck=Fal # Geometry export routines. to_wkb = void_output(lgdal.OGR_G_ExportToWkb, None, errcheck=True) # special handling for WKB. -to_wkt = string_output(lgdal.OGR_G_ExportToWkt, [c_void_p, POINTER(c_char_p)]) -to_gml = string_output(lgdal.OGR_G_ExportToGML, [c_void_p], str_result=True) +to_wkt = string_output(lgdal.OGR_G_ExportToWkt, [c_void_p, POINTER(c_char_p)], decoding='ascii') +to_gml = string_output(lgdal.OGR_G_ExportToGML, [c_void_p], str_result=True, decoding='ascii') get_wkbsize = int_output(lgdal.OGR_G_WkbSize, [c_void_p]) # Geometry spatial-reference related routines. @@ -73,7 +73,7 @@ get_coord_dim = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p]) set_coord_dim = void_output(lgdal.OGR_G_SetCoordinateDimension, [c_void_p, c_int], errcheck=False) get_geom_count = int_output(lgdal.OGR_G_GetGeometryCount, [c_void_p]) -get_geom_name = const_string_output(lgdal.OGR_G_GetGeometryName, [c_void_p]) +get_geom_name = const_string_output(lgdal.OGR_G_GetGeometryName, [c_void_p], decoding='ascii') get_geom_type = int_output(lgdal.OGR_G_GetGeometryType, [c_void_p]) get_point_count = int_output(lgdal.OGR_G_GetPointCount, [c_void_p]) get_point = void_output(lgdal.OGR_G_GetPoint, [c_void_p, c_int, POINTER(c_double), POINTER(c_double), POINTER(c_double)], errcheck=False) diff --git a/django/contrib/gis/gdal/prototypes/srs.py b/django/contrib/gis/gdal/prototypes/srs.py index 66cf84c34f..58ceb75456 100644 --- a/django/contrib/gis/gdal/prototypes/srs.py +++ b/django/contrib/gis/gdal/prototypes/srs.py @@ -49,17 +49,17 @@ linear_units = units_func(lgdal.OSRGetLinearUnits) angular_units = units_func(lgdal.OSRGetAngularUnits) # For exporting to WKT, PROJ.4, "Pretty" WKT, and XML. -to_wkt = string_output(std_call('OSRExportToWkt'), [c_void_p, POINTER(c_char_p)]) -to_proj = string_output(std_call('OSRExportToProj4'), [c_void_p, POINTER(c_char_p)]) -to_pretty_wkt = string_output(std_call('OSRExportToPrettyWkt'), [c_void_p, POINTER(c_char_p), c_int], offset=-2) +to_wkt = string_output(std_call('OSRExportToWkt'), [c_void_p, POINTER(c_char_p)], decoding='ascii') +to_proj = string_output(std_call('OSRExportToProj4'), [c_void_p, POINTER(c_char_p)], decoding='ascii') +to_pretty_wkt = string_output(std_call('OSRExportToPrettyWkt'), [c_void_p, POINTER(c_char_p), c_int], offset=-2, decoding='ascii') # Memory leak fixed in GDAL 1.5; still exists in 1.4. -to_xml = string_output(lgdal.OSRExportToXML, [c_void_p, POINTER(c_char_p), c_char_p], offset=-2) +to_xml = string_output(lgdal.OSRExportToXML, [c_void_p, POINTER(c_char_p), c_char_p], offset=-2, decoding='ascii') # String attribute retrival routines. -get_attr_value = const_string_output(std_call('OSRGetAttrValue'), [c_void_p, c_char_p, c_int]) -get_auth_name = const_string_output(lgdal.OSRGetAuthorityName, [c_void_p, c_char_p]) -get_auth_code = const_string_output(lgdal.OSRGetAuthorityCode, [c_void_p, c_char_p]) +get_attr_value = const_string_output(std_call('OSRGetAttrValue'), [c_void_p, c_char_p, c_int], decoding='ascii') +get_auth_name = const_string_output(lgdal.OSRGetAuthorityName, [c_void_p, c_char_p], decoding='ascii') +get_auth_code = const_string_output(lgdal.OSRGetAuthorityCode, [c_void_p, c_char_p], decoding='ascii') # SRS Properties isgeographic = int_output(lgdal.OSRIsGeographic, [c_void_p]) diff --git a/django/contrib/gis/gdal/srs.py b/django/contrib/gis/gdal/srs.py index 1a110b0114..66a8d4ec93 100644 --- a/django/contrib/gis/gdal/srs.py +++ b/django/contrib/gis/gdal/srs.py @@ -34,7 +34,7 @@ from django.contrib.gis.gdal.error import SRSException from django.contrib.gis.gdal.prototypes import srs as capi from django.utils import six -from django.utils.encoding import force_bytes, force_text +from django.utils.encoding import force_bytes #### Spatial Reference class. #### @@ -139,8 +139,7 @@ class SpatialReference(GDALBase): """ if not isinstance(target, six.string_types) or not isinstance(index, int): raise TypeError - value = capi.get_attr_value(self.ptr, force_bytes(target), index) - return force_text(value, 'ascii', strings_only=True) + return capi.get_attr_value(self.ptr, force_bytes(target), index) def auth_name(self, target): "Returns the authority name for the given string target node." diff --git a/django/contrib/gis/gdal/tests/test_ds.py b/django/contrib/gis/gdal/tests/test_ds.py index 634f204b86..a87a1c6c35 100644 --- a/django/contrib/gis/gdal/tests/test_ds.py +++ b/django/contrib/gis/gdal/tests/test_ds.py @@ -167,7 +167,8 @@ class DataSourceTest(unittest.TestCase): self.assertEqual(True, isinstance(feat[k], v)) # Testing Feature.__iter__ - for fld in feat: self.assertEqual(True, fld.name in source.fields.keys()) + for fld in feat: + self.assertEqual(True, fld.name in source.fields.keys()) def test05_geometries(self): "Testing Geometries from Data Source Features." diff --git a/django/contrib/gis/tests/data/ch-city/ch-city.dbf b/django/contrib/gis/tests/data/ch-city/ch-city.dbf new file mode 100644 index 0000000000000000000000000000000000000000..6ba9d698f71d22173e926533d44b0bbcf43b3122 GIT binary patch literal 285 zcmZRMXP07RU|?9tPy-|}fnQ>7Dpb@NL;W1sTJI%`5<|Ga3B= literal 0 HcmV?d00001 diff --git a/django/contrib/gis/tests/data/ch-city/ch-city.shx b/django/contrib/gis/tests/data/ch-city/ch-city.shx new file mode 100644 index 0000000000000000000000000000000000000000..a1487ad82911e9af570d3a04af7163aa8976d89b GIT binary patch literal 108 wcmZQzQ0HR64$NLKGcYg$<+!b%mR*{(-huB@%#;WH3IUu00pxV@&Et; literal 0 HcmV?d00001 diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index 557bdf9117..a976954d25 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -1,4 +1,5 @@ -from __future__ import absolute_import +# coding: utf-8 +from __future__ import absolute_import, unicode_literals import os from copy import copy @@ -286,6 +287,13 @@ class LayerMapTest(TestCase): self.assertEqual(City.objects.count(), 3) self.assertEqual(City.objects.all().order_by('name_txt')[0].name_txt, "Houston") + def test_encoded_name(self): + """ Test a layer containing utf-8-encoded name """ + city_shp = os.path.join(shp_path, 'ch-city', 'ch-city.shp') + lm = LayerMapping(City, city_shp, city_mapping) + lm.save(silent=True, strict=True) + self.assertEqual(City.objects.count(), 1) + self.assertEqual(City.objects.all()[0].name, "Zürich") class OtherRouter(object): def db_for_read(self, model, **hints): diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index 9511815426..8a793b96c3 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -18,6 +18,8 @@ from django.contrib.gis.gdal.field import ( from django.db import models, transaction from django.contrib.localflavor.us.models import USStateField from django.utils import six +from django.utils.encoding import force_text + # LayerMapping exceptions. class LayerMapError(Exception): pass @@ -65,7 +67,7 @@ class LayerMapping(object): } def __init__(self, model, data, mapping, layer=0, - source_srs=None, encoding=None, + source_srs=None, encoding='utf-8', transaction_mode='commit_on_success', transform=True, unique=None, using=None): """ @@ -76,7 +78,7 @@ class LayerMapping(object): """ # Getting the DataSource and the associated Layer. if isinstance(data, six.string_types): - self.ds = DataSource(data) + self.ds = DataSource(data, encoding=encoding) else: self.ds = data self.layer = self.ds[layer] @@ -330,7 +332,7 @@ class LayerMapping(object): if self.encoding: # The encoding for OGR data sources may be specified here # (e.g., 'cp437' for Census Bureau boundary files). - val = six.text_type(ogr_field.value, self.encoding) + val = force_text(ogr_field.value, self.encoding) else: val = ogr_field.value if model_field.max_length and len(val) > model_field.max_length: From a8f888feb464afd7cfbe024242fa2a405a2d5f90 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 9 Oct 2012 13:34:03 +0200 Subject: [PATCH 146/265] Improved assertion error messages in validators tests --- tests/modeltests/validators/tests.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/modeltests/validators/tests.py b/tests/modeltests/validators/tests.py index 018be6ae59..32bbf62c94 100644 --- a/tests/modeltests/validators/tests.py +++ b/tests/modeltests/validators/tests.py @@ -162,11 +162,23 @@ def create_simple_test_method(validator, expected, value, num): if expected is not None and issubclass(expected, Exception): test_mask = 'test_%s_raises_error_%d' def test_func(self): - self.assertRaises(expected, validator, value) + # assertRaises not used, so as to be able to produce an error message + # containing the tested value + try: + validator(value) + except expected: + pass + else: + self.fail("%s not raised when validating '%s'" % ( + expected.__name__, value)) else: test_mask = 'test_%s_%d' def test_func(self): - self.assertEqual(expected, validator(value)) + try: + self.assertEqual(expected, validator(value)) + except ValidationError as e: + self.fail("Validation of '%s' failed. Error message was: %s" % ( + value, str(e))) if isinstance(validator, types.FunctionType): val_name = validator.__name__ else: From 8cb9968a91cf533cb82e1e13948551e91b510878 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 9 Oct 2012 14:56:19 +0200 Subject: [PATCH 147/265] Moved some email validation tests to validators tests --- tests/modeltests/validators/tests.py | 9 +++++++++ tests/regressiontests/forms/tests/fields.py | 19 +++++-------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/modeltests/validators/tests.py b/tests/modeltests/validators/tests.py index 32bbf62c94..5f93b1631b 100644 --- a/tests/modeltests/validators/tests.py +++ b/tests/modeltests/validators/tests.py @@ -26,13 +26,22 @@ TEST_DATA = ( (validate_email, 'email@here.com', None), (validate_email, 'weirder-email@here.and.there.com', None), (validate_email, 'email@[127.0.0.1]', None), + (validate_email, 'example@valid-----hyphens.com', None), + (validate_email, 'example@valid-with-hyphens.com', None), (validate_email, None, ValidationError), (validate_email, '', ValidationError), (validate_email, 'abc', ValidationError), + (validate_email, 'abc@', ValidationError), + (validate_email, 'abc@bar', ValidationError), (validate_email, 'a @x.cz', ValidationError), + (validate_email, 'abc@.com', ValidationError), (validate_email, 'something@@somewhere.com', ValidationError), (validate_email, 'email@127.0.0.1', ValidationError), + (validate_email, 'example@invalid-.com', ValidationError), + (validate_email, 'example@-invalid.com', ValidationError), + (validate_email, 'example@inv-.alid-.com', ValidationError), + (validate_email, 'example@inv-.-alid.com', ValidationError), # Quoted-string format (CR not allowed) (validate_email, '"\\\011"@here.com', None), (validate_email, '"\\\012"@here.com', ValidationError), diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index 8695256d64..12661d01f5 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -501,6 +501,7 @@ class FieldsTests(SimpleTestCase): self.assertRaisesMessage(ValidationError, "'Enter a valid value.'", f.clean, 'abcd') # EmailField ################################################################## + # See also modeltests/validators tests for validate_email specific tests def test_emailfield_1(self): f = EmailField() @@ -508,16 +509,8 @@ class FieldsTests(SimpleTestCase): self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) self.assertEqual('person@example.com', f.clean('person@example.com')) self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo') - self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@') - self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@bar') - self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@invalid-.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@-invalid.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@inv-.alid-.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@inv-.-alid.com') - self.assertEqual('example@valid-----hyphens.com', f.clean('example@valid-----hyphens.com')) - self.assertEqual('example@valid-with-hyphens.com', f.clean('example@valid-with-hyphens.com')) - self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@.com') - self.assertEqual('local@domain.with.idn.xyz\xe4\xf6\xfc\xdfabc.part.com', f.clean('local@domain.with.idn.xyzäöüßabc.part.com')) + self.assertEqual('local@domain.with.idn.xyz\xe4\xf6\xfc\xdfabc.part.com', + f.clean('local@domain.with.idn.xyzäöüßabc.part.com')) def test_email_regexp_for_performance(self): f = EmailField() @@ -530,17 +523,15 @@ class FieldsTests(SimpleTestCase): 'viewx3dtextx26qx3d@yahoo.comx26latlngx3d15854521645943074058' ) - def test_emailfield_2(self): + def test_emailfield_not_required(self): f = EmailField(required=False) self.assertEqual('', f.clean('')) self.assertEqual('', f.clean(None)) self.assertEqual('person@example.com', f.clean('person@example.com')) self.assertEqual('example@example.com', f.clean(' example@example.com \t \t ')) self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo') - self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@') - self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@bar') - def test_emailfield_3(self): + def test_emailfield_min_max_length(self): f = EmailField(min_length=10, max_length=15) self.assertRaisesMessage(ValidationError, "'Ensure this value has at least 10 characters (it has 9).'", f.clean, 'a@foo.com') self.assertEqual('alf@foo.com', f.clean('alf@foo.com')) From 273b96ef9d3acb25d69e206555412774abab6022 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 9 Oct 2012 15:01:38 +0200 Subject: [PATCH 148/265] Fixed #17867 -- Made email validation pass with IDN domains Thanks Pierre Matri for the report and the initial patch. --- django/core/validators.py | 2 +- tests/modeltests/validators/tests.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/django/core/validators.py b/django/core/validators.py index c7bda682ac..251b5d8856 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -99,7 +99,7 @@ email_re = re.compile( r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom # quoted-string, see also http://tools.ietf.org/html/rfc2822#section-3.2.5 r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' - r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$)' # domain + r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)$)' # domain r'|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) # literal form, ipv4 address (SMTP 4.1.3) validate_email = EmailValidator(email_re, _('Enter a valid email address.'), 'invalid') diff --git a/tests/modeltests/validators/tests.py b/tests/modeltests/validators/tests.py index 5f93b1631b..0174a606df 100644 --- a/tests/modeltests/validators/tests.py +++ b/tests/modeltests/validators/tests.py @@ -28,6 +28,7 @@ TEST_DATA = ( (validate_email, 'email@[127.0.0.1]', None), (validate_email, 'example@valid-----hyphens.com', None), (validate_email, 'example@valid-with-hyphens.com', None), + (validate_email, 'test@domain.with.idn.tld.उदाहरण.परीक्षा', None), (validate_email, None, ValidationError), (validate_email, '', ValidationError), From c9b4e9ac3a6c56f2b006b723972e77722d3ac937 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 9 Oct 2012 21:23:02 +0200 Subject: [PATCH 149/265] Fixed #19095 -- invalid code in multidb tests. --- tests/regressiontests/multiple_database/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/regressiontests/multiple_database/tests.py b/tests/regressiontests/multiple_database/tests.py index 782fe2bfc6..e6d224e302 100644 --- a/tests/regressiontests/multiple_database/tests.py +++ b/tests/regressiontests/multiple_database/tests.py @@ -652,7 +652,8 @@ class QueryTestCase(TestCase): new_bob_profile = UserProfile(flavor="spring surprise") - charlie = User(username='charlie',email='charlie@example.com') + # assigning a profile requires a explicit pk as the object isn't saved + charlie = User(pk=51, username='charlie', email='charlie@example.com') charlie.set_unusable_password() # initially, no db assigned From 3190abcd75b1fcd660353da4001885ef82cbc596 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 9 Oct 2012 21:36:35 +0200 Subject: [PATCH 150/265] Fixed #18153 -- Reverse OneToOne lookups on unsaved instances. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks David Hatch and Anssi Kääriäinen for their inputs. --- django/db/models/fields/related.py | 21 +++++++--- .../one_to_one_regress/tests.py | 40 +++++++++++++++++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 157640c0e3..a7af237714 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -261,13 +261,17 @@ class SingleRelatedObjectDescriptor(object): try: rel_obj = getattr(instance, self.cache_name) except AttributeError: - params = {'%s__pk' % self.related.field.name: instance._get_pk_val()} - try: - rel_obj = self.get_query_set(instance=instance).get(**params) - except self.related.model.DoesNotExist: + related_pk = instance._get_pk_val() + if related_pk is None: rel_obj = None else: - setattr(rel_obj, self.related.field.get_cache_name(), instance) + params = {'%s__pk' % self.related.field.name: related_pk} + try: + rel_obj = self.get_query_set(instance=instance).get(**params) + except self.related.model.DoesNotExist: + rel_obj = None + else: + setattr(rel_obj, self.related.field.get_cache_name(), instance) setattr(instance, self.cache_name, rel_obj) if rel_obj is None: raise self.related.model.DoesNotExist @@ -301,8 +305,13 @@ class SingleRelatedObjectDescriptor(object): raise ValueError('Cannot assign "%r": instance is on database "%s", value is on database "%s"' % (value, instance._state.db, value._state.db)) + related_pk = getattr(instance, self.related.field.rel.get_related_field().attname) + if related_pk is None: + raise ValueError('Cannot assign "%r": "%s" instance isn\'t saved in the database.' % + (value, self.related.opts.object_name)) + # Set the value of the related field to the value of the related object's related field - setattr(value, self.related.field.attname, getattr(instance, self.related.field.rel.get_related_field().attname)) + setattr(value, self.related.field.attname, related_pk) # Since we already know what the related object is, seed the related # object caches now, too. This avoids another db hit if you get the diff --git a/tests/regressiontests/one_to_one_regress/tests.py b/tests/regressiontests/one_to_one_regress/tests.py index eced88598b..615536ba38 100644 --- a/tests/regressiontests/one_to_one_regress/tests.py +++ b/tests/regressiontests/one_to_one_regress/tests.py @@ -202,3 +202,43 @@ class OneToOneRegressionTests(TestCase): with self.assertNumQueries(0): with self.assertRaises(UndergroundBar.DoesNotExist): self.p1.undergroundbar + + def test_get_reverse_on_unsaved_object(self): + """ + Regression for #18153 and #19089. + + Accessing the reverse relation on an unsaved object + always raises an exception. + """ + p = Place() + + # When there's no instance of the origin of the one-to-one + with self.assertNumQueries(0): + with self.assertRaises(UndergroundBar.DoesNotExist): + p.undergroundbar + + UndergroundBar.objects.create() + + # When there's one instance of the origin + # (p.undergroundbar used to return that instance) + with self.assertNumQueries(0): + with self.assertRaises(UndergroundBar.DoesNotExist): + p.undergroundbar + + UndergroundBar.objects.create() + + # When there are several instances of the origin + with self.assertNumQueries(0): + with self.assertRaises(UndergroundBar.DoesNotExist): + p.undergroundbar + + def test_set_reverse_on_unsaved_object(self): + """ + Writing to the reverse relation on an unsaved object + is impossible too. + """ + p = Place() + b = UndergroundBar.objects.create() + with self.assertNumQueries(0): + with self.assertRaises(ValueError): + p.undergroundbar = b From 0a0fe8f71d54e8479e3050ef3bb9d545fd734a65 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 9 Oct 2012 22:06:55 +0200 Subject: [PATCH 151/265] Fix exception message from 3190abcd. Refs #18153. Thanks Preston Holmes. --- django/db/models/fields/related.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index a7af237714..dd9fef34d5 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -308,7 +308,7 @@ class SingleRelatedObjectDescriptor(object): related_pk = getattr(instance, self.related.field.rel.get_related_field().attname) if related_pk is None: raise ValueError('Cannot assign "%r": "%s" instance isn\'t saved in the database.' % - (value, self.related.opts.object_name)) + (value, instance._meta.object_name)) # Set the value of the related field to the value of the related object's related field setattr(value, self.related.field.attname, related_pk) From 252cd271e88e1c60fc49c06fac9d45e4c7f8750e Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 9 Oct 2012 22:40:44 +0200 Subject: [PATCH 152/265] Fixed test failure after IDN domain validation fix Refs #17867. The address in test_email_regexp_for_performance used to take forever (security issue), then was supposed to fail after the fix (commit 9f8287a3f). Now we are less strict with domain validation, due to new IDN domains, hence the validation of this address pass now. --- tests/regressiontests/forms/tests/fields.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index 12661d01f5..1027afceb1 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -516,12 +516,8 @@ class FieldsTests(SimpleTestCase): f = EmailField() # Check for runaway regex security problem. This will take for-freeking-ever # if the security fix isn't in place. - self.assertRaisesMessage( - ValidationError, - "'Enter a valid email address.'", - f.clean, - 'viewx3dtextx26qx3d@yahoo.comx26latlngx3d15854521645943074058' - ) + addr = 'viewx3dtextx26qx3d@yahoo.comx26latlngx3d15854521645943074058' + self.assertEqual(addr, f.clean(addr)) def test_emailfield_not_required(self): f = EmailField(required=False) From c2150d4d2c5342488e474825c67dd3210fafc0e7 Mon Sep 17 00:00:00 2001 From: Michael Manfre Date: Tue, 9 Oct 2012 17:06:37 -0400 Subject: [PATCH 153/265] Fixed #19096 -- Made can_return_id_from_insert more extendable RETURNING is an extension of the SQL standard, which is not implemented the same by all databases. Allow DatabaseOperations.return_insert_id to return a None to allow for other 3rd party backends with a different implementation. --- django/db/models/sql/compiler.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index f6b6bba1d9..a68f6e0290 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -897,8 +897,11 @@ class SQLInsertCompiler(SQLCompiler): col = "%s.%s" % (qn(opts.db_table), qn(opts.pk.column)) result.append("VALUES (%s)" % ", ".join(placeholders[0])) r_fmt, r_params = self.connection.ops.return_insert_id() - result.append(r_fmt % col) - params += r_params + # Skip empty r_fmt to allow subclasses to customize behaviour for + # 3rd party backends. Refs #19096. + if r_fmt: + result.append(r_fmt % col) + params += r_params return [(" ".join(result), tuple(params))] if can_bulk: result.append(self.connection.ops.bulk_insert_sql(fields, len(values))) From 7f4dbdc036a4439ed3e4954625fe91596b314a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Wed, 3 Oct 2012 18:09:02 +0300 Subject: [PATCH 154/265] Revert "Splitted expressions tests into smaller methods" This reverts commit c2532825dbe2a422bbce67285637febb0ef9c9f1. --- tests/modeltests/expressions/tests.py | 43 ++++++++++----------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/tests/modeltests/expressions/tests.py b/tests/modeltests/expressions/tests.py index 5898eac7bb..14419ec55b 100644 --- a/tests/modeltests/expressions/tests.py +++ b/tests/modeltests/expressions/tests.py @@ -9,7 +9,7 @@ from .models import Company, Employee class ExpressionsTests(TestCase): - def setUp(self): + def test_filter(self): Company.objects.create( name="Example Inc.", num_employees=2300, num_chairs=5, is_large=False, ceo=Employee.objects.create(firstname="Joe", lastname="Smith") @@ -23,8 +23,6 @@ class ExpressionsTests(TestCase): ceo=Employee.objects.create(firstname="Max", lastname="Mustermann") ) - - def test_filter(self): company_query = Company.objects.values( "name", "num_employees", "num_chairs", "is_large" ).order_by( @@ -160,13 +158,6 @@ class ExpressionsTests(TestCase): ], lambda o: o, ) - - def test_comparisons(self): - company_query = Company.objects.values( - "name", "num_employees", "num_chairs", "is_large" - ).order_by( - "name", "num_employees", "num_chairs", "is_large" - ) # The comparison operators and the bitwise unary not can be used # to assign to boolean fields for expression in ( @@ -189,19 +180,19 @@ class ExpressionsTests(TestCase): self.assertQuerysetEqual( company_query, [ { - 'num_chairs': 5, + 'num_chairs': 5294600, 'name': 'Example Inc.', 'num_employees': 2300, 'is_large': True }, { - 'num_chairs': 4, + 'num_chairs': 15, 'name': 'Foobar Ltd.', 'num_employees': 3, 'is_large': False }, { - 'num_chairs': 1, + 'num_chairs': 1088, 'name': 'Test GmbH', 'num_employees': 32, 'is_large': False @@ -239,31 +230,28 @@ class ExpressionsTests(TestCase): lambda c: six.text_type(c.point_of_contact), ) - def test_joins(self): c = Company.objects.all()[0] - c.point_of_contact = Employee.objects.create( - firstname="Guido", lastname="van Rossum") - old_ceo = c.ceo - c.ceo = c.point_of_contact + c.point_of_contact = Employee.objects.create(firstname="Guido", lastname="van Rossum") c.save() # F Expressions can also span joins self.assertQuerysetEqual( - Company.objects.filter( - ceo__firstname=F("point_of_contact__firstname")), - [ - "Example Inc.", + Company.objects.filter(ceo__firstname=F("point_of_contact__firstname")), [ + "Foobar Ltd.", + "Test GmbH", ], lambda c: c.name ) - c.ceo = old_ceo - c.save() - # Guido is point of contanct but not CEO. For the null cases we do - # not generate a match. + Company.objects.exclude( ceo__firstname=F("point_of_contact__firstname") ).update(name="foo") - self.assertEqual(Company.objects.filter(name="foo").count(), 1) + self.assertEqual( + Company.objects.exclude( + ceo__firstname=F('point_of_contact__firstname') + ).get().name, + "foo", + ) self.assertRaises(FieldError, lambda: Company.objects.exclude( @@ -271,7 +259,6 @@ class ExpressionsTests(TestCase): ).update(name=F('point_of_contact__lastname')) ) - def test_save(self): # F expressions can be used to update attributes on single objects test_gmbh = Company.objects.get(name="Test GmbH") self.assertEqual(test_gmbh.num_employees, 32) From a8b1861fc4d0a48b4879af803bba094eef145017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Wed, 3 Oct 2012 18:21:39 +0300 Subject: [PATCH 155/265] Revert "Fixed #16211 -- Added comparison and negation ops to F() expressions" This reverts commit 28abf5f0ebc9d380f25dd278d7ef4642c4504545. Conflicts: docs/releases/1.5.txt --- django/db/backends/__init__.py | 3 - django/db/models/expressions.py | 37 --------- django/utils/tree.py | 8 +- docs/releases/1.5.txt | 4 - docs/topics/db/queries.txt | 9 -- tests/modeltests/expressions/models.py | 2 - tests/modeltests/expressions/tests.py | 109 +++++-------------------- 7 files changed, 22 insertions(+), 150 deletions(-) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 4edde04f42..02d2a16a46 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -913,9 +913,6 @@ class BaseDatabaseOperations(object): can vary between backends (e.g., Oracle with %% and &) and between subexpression types (e.g., date expressions) """ - if connector == 'NOT': - assert len(sub_expressions) == 1 - return 'NOT (%s)' % sub_expressions[0] conn = ' %s ' % connector return conn.join(sub_expressions) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 972440b858..639ef6ee10 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -18,17 +18,6 @@ class ExpressionNode(tree.Node): AND = '&' OR = '|' - # Unary operator (needs special attention in combine_expression) - NOT = 'NOT' - - # Comparison operators - EQ = '=' - GE = '>=' - GT = '>' - LE = '<=' - LT = '<' - NE = '<>' - def __init__(self, children=None, connector=None, negated=False): if children is not None and len(children) > 1 and connector is None: raise TypeError('You have to specify a connector.') @@ -104,32 +93,6 @@ class ExpressionNode(tree.Node): def __ror__(self, other): return self._combine(other, self.OR, True) - def __invert__(self): - obj = ExpressionNode([self], connector=self.NOT, negated=True) - return obj - - def __eq__(self, other): - return self._combine(other, self.EQ, False) - - def __ge__(self, other): - return self._combine(other, self.GE, False) - - def __gt__(self, other): - return self._combine(other, self.GT, False) - - def __le__(self, other): - return self._combine(other, self.LE, False) - - def __lt__(self, other): - return self._combine(other, self.LT, False) - - def __ne__(self, other): - return self._combine(other, self.NE, False) - - def __bool__(self): - raise TypeError('Boolean operators should be avoided. Use bitwise operators.') - __nonzero__ = __bool__ - def prepare_database_save(self, unused): return self diff --git a/django/utils/tree.py b/django/utils/tree.py index 6229493544..717181d2b9 100644 --- a/django/utils/tree.py +++ b/django/utils/tree.py @@ -88,12 +88,8 @@ class Node(object): Otherwise, the whole tree is pushed down one level and a new root connector is created, connecting the existing tree and the new node. """ - # Using for loop with 'is' instead of 'if node in children' so node - # __eq__ method doesn't get called. The __eq__ method can be overriden - # by subtypes, for example the F-expression. - for child in self.children: - if node is child and conn_type == self.connector: - return + if node in self.children and conn_type == self.connector: + return if len(self.children) < 2: self.connector = conn_type if self.connector == conn_type: diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index e99b2fd578..78ba77308b 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -177,10 +177,6 @@ Django 1.5 also includes several smaller improvements worth noting: :setting:`DEBUG` is `True` are sent to the console (unless you redefine the logger in your :setting:`LOGGING` setting). -* :ref:`F() expressions ` now support comparison operations - and inversion, expanding the types of expressions that can be passed to the - database. - * When using :class:`~django.template.RequestContext`, it is now possible to look up permissions by using ``{% if 'someapp.someperm' in perms %}`` in templates. diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 54f069248a..fa98c91739 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -640,15 +640,6 @@ that were modified more than 3 days after they were published:: >>> from datetime import timedelta >>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3)) -.. versionadded:: 1.5 - Comparisons and negation operators for ``F()`` expressions - -Django also supports the comparison operators ``==``, ``!=``, ``<=``, ``<``, -``>``, ``>=`` and the bitwise negation operator ``~`` (boolean ``not`` operator -will raise ``TypeError``):: - - >>> Entry.objects.filter(is_heavily_quoted=~(F('n_pingbacks') < 100)) - The pk lookup shortcut ---------------------- diff --git a/tests/modeltests/expressions/models.py b/tests/modeltests/expressions/models.py index 15f0d24541..f592a0eb13 100644 --- a/tests/modeltests/expressions/models.py +++ b/tests/modeltests/expressions/models.py @@ -27,8 +27,6 @@ class Company(models.Model): Employee, related_name='company_point_of_contact_set', null=True) - is_large = models.BooleanField( - blank=True) def __str__(self): return self.name diff --git a/tests/modeltests/expressions/tests.py b/tests/modeltests/expressions/tests.py index 14419ec55b..99eb07e370 100644 --- a/tests/modeltests/expressions/tests.py +++ b/tests/modeltests/expressions/tests.py @@ -11,22 +11,22 @@ from .models import Company, Employee class ExpressionsTests(TestCase): def test_filter(self): Company.objects.create( - name="Example Inc.", num_employees=2300, num_chairs=5, is_large=False, + name="Example Inc.", num_employees=2300, num_chairs=5, ceo=Employee.objects.create(firstname="Joe", lastname="Smith") ) Company.objects.create( - name="Foobar Ltd.", num_employees=3, num_chairs=4, is_large=False, + name="Foobar Ltd.", num_employees=3, num_chairs=4, ceo=Employee.objects.create(firstname="Frank", lastname="Meyer") ) Company.objects.create( - name="Test GmbH", num_employees=32, num_chairs=1, is_large=False, + name="Test GmbH", num_employees=32, num_chairs=1, ceo=Employee.objects.create(firstname="Max", lastname="Mustermann") ) company_query = Company.objects.values( - "name", "num_employees", "num_chairs", "is_large" + "name", "num_employees", "num_chairs" ).order_by( - "name", "num_employees", "num_chairs", "is_large" + "name", "num_employees", "num_chairs" ) # We can filter for companies where the number of employees is greater @@ -37,13 +37,11 @@ class ExpressionsTests(TestCase): "num_chairs": 5, "name": "Example Inc.", "num_employees": 2300, - "is_large": False }, { "num_chairs": 1, "name": "Test GmbH", - "num_employees": 32, - "is_large": False + "num_employees": 32 }, ], lambda o: o @@ -57,20 +55,17 @@ class ExpressionsTests(TestCase): { "num_chairs": 2300, "name": "Example Inc.", - "num_employees": 2300, - "is_large": False + "num_employees": 2300 }, { "num_chairs": 3, "name": "Foobar Ltd.", - "num_employees": 3, - "is_large": False + "num_employees": 3 }, { "num_chairs": 32, "name": "Test GmbH", - "num_employees": 32, - "is_large": False + "num_employees": 32 } ], lambda o: o @@ -84,20 +79,17 @@ class ExpressionsTests(TestCase): { 'num_chairs': 2302, 'name': 'Example Inc.', - 'num_employees': 2300, - 'is_large': False + 'num_employees': 2300 }, { 'num_chairs': 5, 'name': 'Foobar Ltd.', - 'num_employees': 3, - 'is_large': False + 'num_employees': 3 }, { 'num_chairs': 34, 'name': 'Test GmbH', - 'num_employees': 32, - 'is_large': False + 'num_employees': 32 } ], lambda o: o, @@ -112,20 +104,17 @@ class ExpressionsTests(TestCase): { 'num_chairs': 6900, 'name': 'Example Inc.', - 'num_employees': 2300, - 'is_large': False + 'num_employees': 2300 }, { 'num_chairs': 9, 'name': 'Foobar Ltd.', - 'num_employees': 3, - 'is_large': False + 'num_employees': 3 }, { 'num_chairs': 96, 'name': 'Test GmbH', - 'num_employees': 32, - 'is_large': False + 'num_employees': 32 } ], lambda o: o, @@ -140,80 +129,21 @@ class ExpressionsTests(TestCase): { 'num_chairs': 5294600, 'name': 'Example Inc.', - 'num_employees': 2300, - 'is_large': False + 'num_employees': 2300 }, { 'num_chairs': 15, 'name': 'Foobar Ltd.', - 'num_employees': 3, - 'is_large': False + 'num_employees': 3 }, { 'num_chairs': 1088, 'name': 'Test GmbH', - 'num_employees': 32, - 'is_large': False + 'num_employees': 32 } ], lambda o: o, ) - # The comparison operators and the bitwise unary not can be used - # to assign to boolean fields - for expression in ( - # Check boundaries - ~(F('num_employees') < 33), - ~(F('num_employees') <= 32), - (F('num_employees') > 2299), - (F('num_employees') >= 2300), - (F('num_employees') == 2300), - ((F('num_employees') + 1 != 4) & (32 != F('num_employees'))), - # Inverted argument order works too - (2299 < F('num_employees')), - (2300 <= F('num_employees')) - ): - # Test update by F-expression - company_query.update( - is_large=expression - ) - # Compare results - self.assertQuerysetEqual( - company_query, [ - { - 'num_chairs': 5294600, - 'name': 'Example Inc.', - 'num_employees': 2300, - 'is_large': True - }, - { - 'num_chairs': 15, - 'name': 'Foobar Ltd.', - 'num_employees': 3, - 'is_large': False - }, - { - 'num_chairs': 1088, - 'name': 'Test GmbH', - 'num_employees': 32, - 'is_large': False - } - ], - lambda o: o, - ) - # Reset values - company_query.update( - is_large=False - ) - - # The python boolean operators should be avoided as they yield - # unexpected results - test_gmbh = Company.objects.get(name="Test GmbH") - with self.assertRaises(TypeError): - test_gmbh.is_large = not F('is_large') - with self.assertRaises(TypeError): - test_gmbh.is_large = F('is_large') and F('is_large') - with self.assertRaises(TypeError): - test_gmbh.is_large = F('is_large') or F('is_large') # The relation of a foreign key can become copied over to an other # foreign key. @@ -272,8 +202,9 @@ class ExpressionsTests(TestCase): test_gmbh.point_of_contact = None test_gmbh.save() self.assertTrue(test_gmbh.point_of_contact is None) - with self.assertRaises(ValueError): + def test(): test_gmbh.point_of_contact = F("ceo") + self.assertRaises(ValueError, test) test_gmbh.point_of_contact = test_gmbh.ceo test_gmbh.save() From b625e8272bd41714c838cfda3fb54e1f5177f009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Wed, 3 Oct 2012 18:53:40 +0300 Subject: [PATCH 156/265] Moved F() '&' and '|' to .bitand() and .bitor() Done for consistency with Q() expressions and QuerySet combining. This will allow usage of '&' and '|' as boolean logical operators in the future. Refs #16211. --- django/db/models/expressions.py | 30 ++++++++++++++----- docs/releases/1.5.txt | 6 ++++ docs/topics/db/queries.txt | 12 ++++++++ .../expressions_regress/tests.py | 18 ++--------- 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 639ef6ee10..30c44bacde 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -14,9 +14,11 @@ class ExpressionNode(tree.Node): # because it can be used in strings that also # have parameter substitution. - # Bitwise operators - AND = '&' - OR = '|' + # Bitwise operators - note that these are generated by .bitand() + # and .bitor(), the '&' and '|' are reserved for boolean operator + # usage. + BITAND = '&' + BITOR = '|' def __init__(self, children=None, connector=None, negated=False): if children is not None and len(children) > 1 and connector is None: @@ -66,10 +68,20 @@ class ExpressionNode(tree.Node): return self._combine(other, self.MOD, False) def __and__(self, other): - return self._combine(other, self.AND, False) + raise NotImplementedError( + "Use .bitand() and .bitor() for bitwise logical operations." + ) + + def bitand(self, other): + return self._combine(other, self.BITAND, False) def __or__(self, other): - return self._combine(other, self.OR, False) + raise NotImplementedError( + "Use .bitand() and .bitor() for bitwise logical operations." + ) + + def bitor(self, other): + return self._combine(other, self.BITOR, False) def __radd__(self, other): return self._combine(other, self.ADD, True) @@ -88,10 +100,14 @@ class ExpressionNode(tree.Node): return self._combine(other, self.MOD, True) def __rand__(self, other): - return self._combine(other, self.AND, True) + raise NotImplementedError( + "Use .bitand() and .bitor() for bitwise logical operations." + ) def __ror__(self, other): - return self._combine(other, self.OR, True) + raise NotImplementedError( + "Use .bitand() and .bitor() for bitwise logical operations." + ) def prepare_database_save(self, unused): return self diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 78ba77308b..263392fdc7 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -438,6 +438,12 @@ Miscellaneous needs. The new default value is `0666` (octal) and the current umask value is first masked out. +* The :ref:`F() expressions ` supported bitwise operators by + ``&`` and ``|``. These operators are now available using ``.bitand()`` and + ``.bitor()`` instead. The removal of ``&`` and ``|`` was done to be consistent with + :ref:`Q() expressions ` and ``QuerySet`` combining where + the operators are used as boolean AND and OR operators. + Features deprecated in 1.5 ========================== diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index fa98c91739..543edf6280 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -640,6 +640,18 @@ that were modified more than 3 days after they were published:: >>> from datetime import timedelta >>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3)) +.. versionadded:: 1.5 + ``.bitand()`` and ``.bitor()`` + +The ``F()`` objects now support bitwise operations by ``.bitand()`` and +``.bitor()``, for example:: + + >>> F('somefield').bitand(16) + +.. versionchanged:: 1.5 + The previously undocumented operators ``&`` and ``|`` no longer produce + bitwise operations, use ``.bitand()`` and ``.bitor()`` instead. + The pk lookup shortcut ---------------------- diff --git a/tests/regressiontests/expressions_regress/tests.py b/tests/regressiontests/expressions_regress/tests.py index 80ddfadbe7..06d97d2b32 100644 --- a/tests/regressiontests/expressions_regress/tests.py +++ b/tests/regressiontests/expressions_regress/tests.py @@ -128,7 +128,7 @@ class ExpressionOperatorTests(TestCase): def test_lefthand_bitwise_and(self): # LH Bitwise ands on integers - Number.objects.filter(pk=self.n.pk).update(integer=F('integer') & 56) + Number.objects.filter(pk=self.n.pk).update(integer=F('integer').bitand(56)) self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 40) self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) @@ -136,7 +136,7 @@ class ExpressionOperatorTests(TestCase): @skipUnlessDBFeature('supports_bitwise_or') def test_lefthand_bitwise_or(self): # LH Bitwise or on integers - Number.objects.filter(pk=self.n.pk).update(integer=F('integer') | 48) + Number.objects.filter(pk=self.n.pk).update(integer=F('integer').bitor(48)) self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 58) self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) @@ -181,20 +181,6 @@ class ExpressionOperatorTests(TestCase): self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 27) self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) - def test_right_hand_bitwise_and(self): - # RH Bitwise ands on integers - Number.objects.filter(pk=self.n.pk).update(integer=15 & F('integer')) - - self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 10) - self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) - - @skipUnlessDBFeature('supports_bitwise_or') - def test_right_hand_bitwise_or(self): - # RH Bitwise or on integers - Number.objects.filter(pk=self.n.pk).update(integer=15 | F('integer')) - - self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 47) - self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) class FTimeDeltaTests(TestCase): From 32ac067a6ddca465cd94fc7ee35bfe54a675979b Mon Sep 17 00:00:00 2001 From: Michael Manfre Date: Fri, 28 Sep 2012 13:25:09 -0400 Subject: [PATCH 157/265] Fixed #18927 -- Fixed bulk_create tests when no has_bulk_insert --- tests/regressiontests/bulk_create/tests.py | 46 ++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/tests/regressiontests/bulk_create/tests.py b/tests/regressiontests/bulk_create/tests.py index 33108ea9b0..5d61242b9b 100644 --- a/tests/regressiontests/bulk_create/tests.py +++ b/tests/regressiontests/bulk_create/tests.py @@ -3,7 +3,7 @@ from __future__ import absolute_import from operator import attrgetter from django.db import connection -from django.test import TestCase, skipIfDBFeature +from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature from django.test.utils import override_settings from .models import Country, Restaurant, Pizzeria, State, TwoFields @@ -29,6 +29,7 @@ class BulkCreateTests(TestCase): self.assertEqual(created, []) self.assertEqual(Country.objects.count(), 4) + @skipUnlessDBFeature('has_bulk_insert') def test_efficiency(self): with self.assertNumQueries(1): Country.objects.bulk_create(self.data) @@ -50,6 +51,16 @@ class BulkCreateTests(TestCase): ], attrgetter("name")) def test_non_auto_increment_pk(self): + State.objects.bulk_create([ + State(two_letter_code=s) + for s in ["IL", "NY", "CA", "ME"] + ]) + self.assertQuerysetEqual(State.objects.order_by("two_letter_code"), [ + "CA", "IL", "ME", "NY", + ], attrgetter("two_letter_code")) + + @skipUnlessDBFeature('has_bulk_insert') + def test_non_auto_increment_pk_efficiency(self): with self.assertNumQueries(1): State.objects.bulk_create([ State(two_letter_code=s) @@ -77,13 +88,21 @@ class BulkCreateTests(TestCase): TwoFields.objects.bulk_create([ TwoFields(f1=i, f2=i+1) for i in range(0, 1001) ]) - self.assertTrue(len(connection.queries) < 10) self.assertEqual(TwoFields.objects.count(), 1001) self.assertEqual( TwoFields.objects.filter(f1__gte=450, f1__lte=550).count(), 101) self.assertEqual(TwoFields.objects.filter(f2__gte=901).count(), 101) + @skipUnlessDBFeature('has_bulk_insert') + def test_large_batch_efficiency(self): + with override_settings(DEBUG=True): + connection.queries = [] + TwoFields.objects.bulk_create([ + TwoFields(f1=i, f2=i+1) for i in range(0, 1001) + ]) + self.assertTrue(len(connection.queries) < 10) + def test_large_batch_mixed(self): """ Test inserting a large batch with objects having primary key set @@ -94,7 +113,6 @@ class BulkCreateTests(TestCase): TwoFields.objects.bulk_create([ TwoFields(id=i if i % 2 == 0 else None, f1=i, f2=i+1) for i in range(100000, 101000)]) - self.assertTrue(len(connection.queries) < 10) self.assertEqual(TwoFields.objects.count(), 1000) # We can't assume much about the ID's created, except that the above # created IDs must exist. @@ -102,7 +120,29 @@ class BulkCreateTests(TestCase): self.assertEqual(TwoFields.objects.filter(id__in=id_range).count(), 500) self.assertEqual(TwoFields.objects.exclude(id__in=id_range).count(), 500) + @skipUnlessDBFeature('has_bulk_insert') + def test_large_batch_mixed_efficiency(self): + """ + Test inserting a large batch with objects having primary key set + mixed together with objects without PK set. + """ + with override_settings(DEBUG=True): + connection.queries = [] + TwoFields.objects.bulk_create([ + TwoFields(id=i if i % 2 == 0 else None, f1=i, f2=i+1) + for i in range(100000, 101000)]) + self.assertTrue(len(connection.queries) < 10) + def test_explicit_batch_size(self): + objs = [TwoFields(f1=i, f2=i) for i in range(0, 4)] + TwoFields.objects.bulk_create(objs, 2) + self.assertEqual(TwoFields.objects.count(), len(objs)) + TwoFields.objects.all().delete() + TwoFields.objects.bulk_create(objs, len(objs)) + self.assertEqual(TwoFields.objects.count(), len(objs)) + + @skipUnlessDBFeature('has_bulk_insert') + def test_explicit_batch_size_efficiency(self): objs = [TwoFields(f1=i, f2=i) for i in range(0, 100)] with self.assertNumQueries(2): TwoFields.objects.bulk_create(objs, 50) From f578ee32fa41b7d2b3308d98e9d528ab4853a720 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Tue, 9 Oct 2012 17:20:51 -0700 Subject: [PATCH 158/265] Mark the test for left/right lookup types as a known failure on PostGIS 2.0. --- django/contrib/gis/tests/geoapp/tests.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index f94d716b34..3ae8876471 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -11,7 +11,7 @@ from django.contrib.gis.tests.utils import ( no_mysql, no_oracle, no_spatialite, mysql, oracle, postgis, spatialite) from django.test import TestCase -from django.utils import six +from django.utils import six, unittest from .models import Country, City, PennsylvaniaCity, State, Track @@ -295,6 +295,13 @@ class GeoLookupTest(TestCase): self.assertEqual(2, len(qs)) for c in qs: self.assertEqual(True, c.name in cities) + # The left/right lookup tests are known failures on PostGIS 2.0+ + # until the following bug is fixed: + # http://trac.osgeo.org/postgis/ticket/2035 + # TODO: Ensure fixed in 2.0.2, else modify upper bound for version here. + if (2, 0, 0) <= connection.ops.spatial_version <= (2, 0, 1): + test_left_right_lookups = unittest.expectedFailure(test_left_right_lookups) + def test_equals_lookups(self): "Testing the 'same_as' and 'equals' lookup types." pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326) From 041ef9ed68722fa5f8c38c9e39fad67714f35014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Wed, 10 Oct 2012 03:28:48 +0300 Subject: [PATCH 159/265] Removed some uses of F() expression & and | Refs #16211 --- tests/regressiontests/expressions_regress/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/regressiontests/expressions_regress/tests.py b/tests/regressiontests/expressions_regress/tests.py index 06d97d2b32..508a497151 100644 --- a/tests/regressiontests/expressions_regress/tests.py +++ b/tests/regressiontests/expressions_regress/tests.py @@ -371,7 +371,7 @@ class FTimeDeltaTests(TestCase): def test_delta_invalid_op_and(self): raised = False try: - r = repr(Experiment.objects.filter(end__lt=F('start')&self.deltas[0])) + r = repr(Experiment.objects.filter(end__lt=F('start').bitand(self.deltas[0]))) except TypeError: raised = True self.assertTrue(raised, "TypeError not raised on attempt to binary and a datetime with a timedelta.") @@ -379,7 +379,7 @@ class FTimeDeltaTests(TestCase): def test_delta_invalid_op_or(self): raised = False try: - r = repr(Experiment.objects.filter(end__lt=F('start')|self.deltas[0])) + r = repr(Experiment.objects.filter(end__lt=F('start').bitor(self.deltas[0]))) except TypeError: raised = True self.assertTrue(raised, "TypeError not raised on attempt to binary or a datetime with a timedelta.") From c99ad64df7f8b7bdf504ef1c329610fce3c7f1b0 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Tue, 9 Oct 2012 20:30:28 -0700 Subject: [PATCH 160/265] Fixed #19097 -- documented module of origin for HttpRes/req objects --- docs/ref/request-response.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index e977e32d42..90872a6feb 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -16,7 +16,8 @@ passing the :class:`HttpRequest` as the first argument to the view function. Each view is responsible for returning an :class:`HttpResponse` object. This document explains the APIs for :class:`HttpRequest` and -:class:`HttpResponse` objects. +:class:`HttpResponse` objects, which are defined in the :mod:`django.http` +module. HttpRequest objects =================== From dcdaf9a07901c80c0d93663b348595dd2ba7b7a6 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 10 Oct 2012 17:54:27 +0200 Subject: [PATCH 161/265] Fixed error output from runserver This has been missed in commit 822d6d6dab (Refs #18325). --- django/core/management/commands/runserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index 9c24701d2e..391e0b440a 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -123,7 +123,7 @@ class Command(BaseCommand): error_text = ERRORS[e.args[0].args[0]] except (AttributeError, KeyError): error_text = str(e) - sys.stderr.write("Error: %s" % error_text) + self.stderr.write("Error: %s" % error_text) # Need to use an OS exit because sys.exit doesn't work in a thread os._exit(1) except KeyboardInterrupt: From f7b69665fd0ce21adae7cfaed7fade63f8aae1fd Mon Sep 17 00:00:00 2001 From: Ian Clelland Date: Wed, 10 Oct 2012 19:57:16 +0200 Subject: [PATCH 162/265] Use renamed threading event API in Python 3.3 Refs #19038. --- django/test/testcases.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/django/test/testcases.py b/django/test/testcases.py index 260b060c45..1d52fed69f 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -867,7 +867,9 @@ class QuietWSGIRequestHandler(WSGIRequestHandler): pass -if sys.version_info >= (2, 7, 0): +if sys.version_info >= (3, 3, 0): + _ImprovedEvent = threading.Event +elif sys.version_info >= (2, 7, 0): _ImprovedEvent = threading._Event else: class _ImprovedEvent(threading._Event): From f315006d50a1391393507cef1c52147cb766e45d Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Wed, 10 Oct 2012 20:38:48 -0300 Subject: [PATCH 163/265] Remove heteronormativity from coding style doc --- docs/internals/contributing/writing-code/coding-style.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/internals/contributing/writing-code/coding-style.txt b/docs/internals/contributing/writing-code/coding-style.txt index 2fa0233e3d..a699e39bd8 100644 --- a/docs/internals/contributing/writing-code/coding-style.txt +++ b/docs/internals/contributing/writing-code/coding-style.txt @@ -140,9 +140,9 @@ Model style a tuple of tuples, with an all-uppercase name, either near the top of the model module or just above the model class. Example:: - GENDER_CHOICES = ( - ('M', 'Male'), - ('F', 'Female'), + DIRECTION_CHOICES = ( + ('U', 'Up'), + ('D', 'Down'), ) Use of ``django.conf.settings`` From 7ef2781ca0ce48872e21dce2f322c9e4106d1cfd Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 10 Oct 2012 20:03:27 -0400 Subject: [PATCH 164/265] Fixed #4501 - Documented how to use coverage.py with Django tests. Thanks krzysiumed for the draft patch. --- docs/topics/testing.txt | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index e2d424aec5..f907c72a5e 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -589,6 +589,34 @@ to a faster hashing algorithm:: Don't forget to also include in :setting:`PASSWORD_HASHERS` any hashing algorithm used in fixtures, if any. +Integration with coverage.py +---------------------------- + +Code coverage describes how much source code has been tested. It shows which +parts of your code are being exercised by tests and which are not. It's an +important part of testing applications, so it's strongly recommended to check +the coverage of your tests. + +Django can be easily integrated with `coverage.py`_, a tool for measuring code +coverage of Python programs. First, `install coverage.py`_. Next, run the +following from your project folder containing ``manage.py``:: + + coverage run --source='.' manage.py test myapp + +This runs your tests and collects coverage data of the executed files in your +project. You can see a report of this data by typing following command:: + + coverage report + +Note that some Django code was executed while running tests, but it is not +listed here because of the ``source`` flag passed to the previous command. + +For more options like annotated HTML listings detailing missed lines, see the +`coverage.py`_ docs. + +.. _coverage.py: http://nedbatchelder.com/code/coverage/ +.. _install coverage.py: http://pypi.python.org/pypi/coverage + Testing tools ============= From b498ce820384c6967fbec3a32be3b9cd5b01e63d Mon Sep 17 00:00:00 2001 From: Dmitry Medvinsky Date: Thu, 11 Oct 2012 12:38:14 +0400 Subject: [PATCH 165/265] Fix typo in URLs reversing docs --- docs/topics/http/urls.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index c51ce2d2a4..7b5d3ded63 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -596,7 +596,7 @@ Or in Python code:: # ... year = 2006 # ... - return HttpResponseRedirect(reverse('new.views.year_archive', args=(year,))) + return HttpResponseRedirect(reverse('news.views.year_archive', args=(year,))) If, for some reason, it was decided that the URL where content for yearly article archives are published at should be changed then you would only need to From 06f5da3d7813e9a23b1e98ecf8b75fc6073800e9 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 11 Oct 2012 06:11:52 -0400 Subject: [PATCH 166/265] Fixed #16817 - Added a guide of code coverage to contributing docs. Thanks Pedro Lima for the draft patch. --- .gitignore | 2 ++ .hgignore | 2 ++ .../contributing/writing-code/unit-tests.txt | 20 +++++++++++++++++++ docs/topics/testing.txt | 2 ++ tests/.coveragerc | 5 +++++ 5 files changed, 31 insertions(+) create mode 100644 tests/.coveragerc diff --git a/.gitignore b/.gitignore index 17e39abd38..2d028c7287 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ MANIFEST dist/ docs/_build/ +tests/coverage_html/ +tests/.coverage \ No newline at end of file diff --git a/.hgignore b/.hgignore index 765a29d091..3dc253a3c1 100644 --- a/.hgignore +++ b/.hgignore @@ -4,3 +4,5 @@ syntax:glob *.pot *.py[co] docs/_build/ +tests/coverage_html/ +tests/.coverage \ No newline at end of file diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 4de506a654..a828b06b36 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -163,6 +163,26 @@ associated tests will be skipped. .. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html .. _selenium: http://pypi.python.org/pypi/selenium +Code coverage +~~~~~~~~~~~~~ + +Contributors are encouraged to run coverage on the test suite to identify areas +that need additional tests. The coverage tool installation and use is described +in :ref:`testing code coverage`. + +To run coverage on the Django test suite using the standard test settings:: + + coverage run ./runtests.py --settings=test_sqlite + +After running coverage, generate the html report by running:: + + coverage html + +When running coverage for the Django tests, the included ``.coveragerc`` +settings file defines ``coverage_html`` as the output directory for the report +and also excludes several directories not relevant to the results +(test code or external code included in Django). + .. _contrib-apps: Contrib apps diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index f907c72a5e..d0b2e7cdf9 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -589,6 +589,8 @@ to a faster hashing algorithm:: Don't forget to also include in :setting:`PASSWORD_HASHERS` any hashing algorithm used in fixtures, if any. +.. _topics-testing-code-coverage: + Integration with coverage.py ---------------------------- diff --git a/tests/.coveragerc b/tests/.coveragerc new file mode 100644 index 0000000000..b979e94c58 --- /dev/null +++ b/tests/.coveragerc @@ -0,0 +1,5 @@ +[run] +omit = runtests,test_sqlite,regressiontests*,modeltests*,*/django/contrib/*/tests*,*/django/utils/unittest*,*/django/utils/simplejson*,*/django/utils/importlib.py,*/django/test/_doctest.py,*/django/core/servers/fastcgi.py,*/django/utils/autoreload.py,*/django/utils/dictconfig.py + +[html] +directory = coverage_html From 0614e99fbdb9d14a57035da320a4fc7aca232469 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 11 Oct 2012 15:40:38 -0300 Subject: [PATCH 167/265] More URL reversion docs typo fixes. --- docs/topics/http/urls.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 7b5d3ded63..e178df2af2 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -567,9 +567,9 @@ Consider again this URLconf entry:: from django.conf.urls import patterns, url urlpatterns = patterns('', - #... + #... url(r'^articles/(\d{4})/$', 'news.views.year_archive'), - #... + #... ) According to this design, the URL for the archive corresponding to year *nnnn* @@ -598,7 +598,7 @@ Or in Python code:: # ... return HttpResponseRedirect(reverse('news.views.year_archive', args=(year,))) -If, for some reason, it was decided that the URL where content for yearly +If, for some reason, it was decided that the URLs where content for yearly article archives are published at should be changed then you would only need to change the entry in the URLconf. From 501d793398c3ecc3c8a54475d07609cf34643553 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 11 Oct 2012 21:09:12 +0200 Subject: [PATCH 168/265] Fixed #19107 -- Workarounded message-encoding bug on Python < 2.6.6 Thanks Bernardo Pires for the report. --- django/core/mail/message.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/django/core/mail/message.py b/django/core/mail/message.py index db9023a0bb..98ab3c9075 100644 --- a/django/core/mail/message.py +++ b/django/core/mail/message.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import mimetypes import os import random +import sys import time from email import charset as Charset, encoders as Encoders from email.generator import Generator @@ -138,6 +139,9 @@ class SafeMIMEText(MIMEText): """ fp = six.StringIO() g = Generator(fp, mangle_from_ = False) + if sys.version_info < (2, 6, 6) and isinstance(self._payload, six.text_type): + # Workaround for http://bugs.python.org/issue1368247 + self._payload = self._payload.encode(self._charset.output_charset) g.flatten(self, unixfrom=unixfrom) return fp.getvalue() From b5f224e8e23ca380a93304abe583f1de59c2404e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Thu, 11 Oct 2012 23:58:50 +0300 Subject: [PATCH 169/265] Fixed tests introduced for #15915 The tests didn't clean up properly. The commit that introduced the errors was 8c427448d53ec0d860e1669f35deed73d0240ba1. Thanks to Trac alias rizumu for spotting this. --- django/contrib/auth/tests/management.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index cab7b20f20..7074e04799 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -173,10 +173,10 @@ class CreatesuperuserManagementCommandTestCase(TestCase): class PermissionDuplicationTestCase(TestCase): def setUp(self): - self._original_user_permission = models.User._meta.permissions + self._original_permissions = models.Permission._meta.permissions[:] - def tearUp(self): - models.User._meta.permissions = self._original_user_permissions + def tearDown(self): + models.Permission._meta.permissions = self._original_permissions def test_duplicated_permissions(self): """ From 2d1214d92ae67acaf2246c3dc2ea37cdf7e1c2a5 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 11 Oct 2012 06:47:29 -0400 Subject: [PATCH 170/265] Fixed #14165 - Documented that TransactionMiddleware only applies to the default database. --- docs/ref/middleware.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index a6ea9a6c41..0ce4177e00 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -203,9 +203,9 @@ Transaction middleware .. class:: TransactionMiddleware -Binds commit and rollback to the request/response phase. If a view function -runs successfully, a commit is done. If it fails with an exception, a rollback -is done. +Binds commit and rollback of the default database to the request/response +phase. If a view function runs successfully, a commit is done. If it fails with +an exception, a rollback is done. The order of this middleware in the stack is important: middleware modules running outside of it run with commit-on-save - the default Django behavior. From dd0cbc6bdccfc51329427b8a6023f6e866d48cba Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 11 Oct 2012 18:04:25 -0400 Subject: [PATCH 171/265] Fixed #16588 - Warned about field names that conflict with the model API --- docs/topics/db/models.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index f29cc28332..beb62f049c 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -84,7 +84,9 @@ Fields The most important part of a model -- and the only required part of a model -- is the list of database fields it defines. Fields are specified by class -attributes. +attributes. Be careful not to choose field names that conflict with the +:doc:`models API ` like ``clean``, ``save``, or +``delete``. Example:: From 470deb5cbb765e2e731c5b0b184247c7f87482aa Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 11 Oct 2012 19:54:52 -0400 Subject: [PATCH 172/265] Fixed #10936 - Noted that using SQLite for development is a good idea --- docs/topics/install.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/topics/install.txt b/docs/topics/install.txt index 39b9a93c04..0ee4113c04 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -80,7 +80,12 @@ Get your database running If you plan to use Django's database API functionality, you'll need to make sure a database server is running. Django supports many different database servers and is officially supported with PostgreSQL_, MySQL_, Oracle_ and -SQLite_ (although SQLite doesn't require a separate server to be running). +SQLite_. + +It is common practice to use SQLite in a desktop development environment. +Unless you need database feature parity between your desktop development +environment and your deployment environment, using SQLite for development is +generally the simplest option as it doesn't require running a separate server. In addition to the officially supported databases, there are backends provided by 3rd parties that allow you to use other databases with Django: From 95f7ea3af1854f575a47218a08d1a8d5357f8d9b Mon Sep 17 00:00:00 2001 From: Brian Galey Date: Fri, 12 Oct 2012 17:22:20 +0200 Subject: [PATCH 173/265] Fixed #19028 -- Support GeoJSON output with SpatiaLite 3.0+ --- .../gis/db/backends/spatialite/operations.py | 2 ++ django/contrib/gis/db/models/query.py | 5 +++-- django/contrib/gis/tests/geoapp/tests.py | 16 ++++++++-------- docs/ref/contrib/gis/db-api.txt | 2 +- docs/ref/contrib/gis/geoquerysets.txt | 2 +- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 5f76501ef1..5eaa77843c 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -146,6 +146,8 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): except DatabaseError: # we are using < 2.4.0-RC4 pass + if version >= (3, 0, 0): + self.geojson = 'AsGeoJSON' def check_aggregate_support(self, aggregate): """ diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index 2a8de4cde3..2ffbd2021b 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -146,13 +146,14 @@ class GeoQuerySet(QuerySet): """ backend = connections[self.db].ops if not backend.geojson: - raise NotImplementedError('Only PostGIS 1.3.4+ supports GeoJSON serialization.') + raise NotImplementedError('Only PostGIS 1.3.4+ and SpatiaLite 3.0+ ' + 'support GeoJSON serialization.') if not isinstance(precision, six.integer_types): raise TypeError('Precision keyword must be set with an integer.') # Setting the options flag -- which depends on which version of - # PostGIS we're using. + # PostGIS we're using. SpatiaLite only uses the first group of options. if backend.spatial_version >= (1, 4, 0): options = 0 if crs and bbox: options = 3 diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 3ae8876471..8f2c22e841 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -474,21 +474,21 @@ class GeoQuerySetTest(TestCase): def test_geojson(self): "Testing GeoJSON output from the database using GeoQuerySet.geojson()." - # Only PostGIS 1.3.4+ supports GeoJSON. + # Only PostGIS 1.3.4+ and SpatiaLite 3.0+ support GeoJSON. if not connection.ops.geojson: self.assertRaises(NotImplementedError, Country.objects.all().geojson, field_name='mpoly') return - if connection.ops.spatial_version >= (1, 4, 0): - pueblo_json = '{"type":"Point","coordinates":[-104.609252,38.255001]}' - houston_json = '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"coordinates":[-95.363151,29.763374]}' - victoria_json = '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],"coordinates":[-123.305196,48.462611]}' - chicago_json = '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}' - else: + pueblo_json = '{"type":"Point","coordinates":[-104.609252,38.255001]}' + houston_json = '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"coordinates":[-95.363151,29.763374]}' + victoria_json = '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],"coordinates":[-123.305196,48.462611]}' + chicago_json = '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}' + if postgis and connection.ops.spatial_version < (1, 4, 0): pueblo_json = '{"type":"Point","coordinates":[-104.60925200,38.25500100]}' houston_json = '{"type":"Point","crs":{"type":"EPSG","properties":{"EPSG":4326}},"coordinates":[-95.36315100,29.76337400]}' victoria_json = '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],"coordinates":[-123.30519600,48.46261100]}' - chicago_json = '{"type":"Point","crs":{"type":"EPSG","properties":{"EPSG":4326}},"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}' + elif spatialite: + victoria_json = '{"type":"Point","bbox":[-123.305196,48.462611,-123.305196,48.462611],"coordinates":[-123.305196,48.462611]}' # Precision argument should only be an integer self.assertRaises(TypeError, City.objects.geojson, precision='foo') diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 318110ef04..519f79f0d4 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -282,7 +282,7 @@ Method PostGIS Oracle SpatiaLite :meth:`GeoQuerySet.extent3d` X :meth:`GeoQuerySet.force_rhr` X :meth:`GeoQuerySet.geohash` X -:meth:`GeoQuerySet.geojson` X +:meth:`GeoQuerySet.geojson` X X :meth:`GeoQuerySet.gml` X X X :meth:`GeoQuerySet.intersection` X X X :meth:`GeoQuerySet.kml` X X diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index eeec2e2133..69280dc028 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -947,7 +947,7 @@ __ http://geohash.org/ .. method:: GeoQuerySet.geojson(**kwargs) -*Availability*: PostGIS +*Availability*: PostGIS, SpatiaLite Attaches a ``geojson`` attribute to every model in the queryset that contains the `GeoJSON`__ representation of the geometry. From 24c7d828b00c5cfb203addca06c82fc30d2e989a Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 12 Oct 2012 23:16:55 +0200 Subject: [PATCH 174/265] Moved de_CH/formats.py in its correct location Refs #16188. --- django/conf/locale/de_CH/__init__.py | 0 django/{contrib/localflavor => conf/locale}/de_CH/formats.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 django/conf/locale/de_CH/__init__.py rename django/{contrib/localflavor => conf/locale}/de_CH/formats.py (99%) diff --git a/django/conf/locale/de_CH/__init__.py b/django/conf/locale/de_CH/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/localflavor/de_CH/formats.py b/django/conf/locale/de_CH/formats.py similarity index 99% rename from django/contrib/localflavor/de_CH/formats.py rename to django/conf/locale/de_CH/formats.py index 9d56f9f298..7cbf76e7db 100644 --- a/django/contrib/localflavor/de_CH/formats.py +++ b/django/conf/locale/de_CH/formats.py @@ -35,7 +35,7 @@ DATETIME_INPUT_FORMATS = ( '%Y-%m-%d', # '2006-10-25' ) -# these are the separators for non-monetary numbers. For monetary numbers, +# these are the separators for non-monetary numbers. For monetary numbers, # the DECIMAL_SEPARATOR is a . (decimal point) and the THOUSAND_SEPARATOR is a # ' (single quote). # For details, please refer to http://www.bk.admin.ch/dokumentation/sprachen/04915/05016/index.html?lang=de From ffbc599f77120674cd16e6dcac2f22998c84e407 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 12 Oct 2012 16:21:40 -0500 Subject: [PATCH 175/265] Removed country-specific localflavor tests. They now live in separate django-contrib-XX packages. --- .../localflavor/ar/__init__.py | 0 tests/regressiontests/localflavor/ar/tests.py | 103 ---- .../localflavor/at/__init__.py | 0 tests/regressiontests/localflavor/at/tests.py | 49 -- .../localflavor/au/__init__.py | 0 tests/regressiontests/localflavor/au/forms.py | 11 - .../regressiontests/localflavor/au/models.py | 14 - tests/regressiontests/localflavor/au/tests.py | 108 ---- .../localflavor/be/__init__.py | 0 tests/regressiontests/localflavor/be/tests.py | 81 --- .../localflavor/br/__init__.py | 0 tests/regressiontests/localflavor/br/tests.py | 146 ----- .../localflavor/ca/__init__.py | 0 tests/regressiontests/localflavor/ca/tests.py | 115 ---- .../localflavor/ch/__init__.py | 0 tests/regressiontests/localflavor/ch/tests.py | 76 --- .../localflavor/cl/__init__.py | 0 tests/regressiontests/localflavor/cl/tests.py | 58 -- .../localflavor/cn/__init__.py | 0 tests/regressiontests/localflavor/cn/tests.py | 113 ---- .../localflavor/co/__init__.py | 0 tests/regressiontests/localflavor/co/tests.py | 45 -- .../localflavor/cz/__init__.py | 0 tests/regressiontests/localflavor/cz/tests.py | 80 --- .../localflavor/de/__init__.py | 0 tests/regressiontests/localflavor/de/tests.py | 51 -- .../localflavor/ec/__init__.py | 0 tests/regressiontests/localflavor/ec/tests.py | 36 -- .../localflavor/es/__init__.py | 0 tests/regressiontests/localflavor/es/tests.py | 173 ------ .../localflavor/fi/__init__.py | 0 tests/regressiontests/localflavor/fi/tests.py | 383 ------------ .../localflavor/fr/__init__.py | 0 tests/regressiontests/localflavor/fr/tests.py | 152 ----- .../localflavor/gb/__init__.py | 0 tests/regressiontests/localflavor/gb/tests.py | 32 - .../localflavor/hk/__init__.py | 0 tests/regressiontests/localflavor/hk/forms.py | 11 - tests/regressiontests/localflavor/hk/tests.py | 35 -- .../localflavor/hr/__init__.py | 0 tests/regressiontests/localflavor/hr/tests.py | 192 ------ .../localflavor/id/__init__.py | 0 tests/regressiontests/localflavor/id/tests.py | 195 ------ .../localflavor/ie/__init__.py | 0 tests/regressiontests/localflavor/ie/tests.py | 45 -- .../localflavor/il/__init__.py | 0 tests/regressiontests/localflavor/il/tests.py | 41 -- .../localflavor/in_/__init__.py | 0 .../regressiontests/localflavor/in_/tests.py | 178 ------ .../localflavor/is_/__init__.py | 0 .../regressiontests/localflavor/is_/tests.py | 200 ------- .../localflavor/it/__init__.py | 0 tests/regressiontests/localflavor/it/tests.py | 71 --- .../localflavor/jp/__init__.py | 0 tests/regressiontests/localflavor/jp/tests.py | 75 --- .../localflavor/kw/__init__.py | 0 tests/regressiontests/localflavor/kw/tests.py | 17 - .../localflavor/mk/__init__.py | 0 tests/regressiontests/localflavor/mk/forms.py | 11 - .../regressiontests/localflavor/mk/models.py | 15 - tests/regressiontests/localflavor/mk/tests.py | 308 ---------- .../localflavor/mx/__init__.py | 0 tests/regressiontests/localflavor/mx/forms.py | 11 - .../regressiontests/localflavor/mx/models.py | 13 - tests/regressiontests/localflavor/mx/mx.py | 0 tests/regressiontests/localflavor/mx/tests.py | 198 ------- .../localflavor/nl/__init__.py | 0 tests/regressiontests/localflavor/nl/tests.py | 64 -- .../localflavor/pl/__init__.py | 0 tests/regressiontests/localflavor/pl/tests.py | 480 --------------- .../localflavor/pt/__init__.py | 0 tests/regressiontests/localflavor/pt/tests.py | 33 -- .../localflavor/py/__init__.py | 0 tests/regressiontests/localflavor/py/tests.py | 55 -- .../localflavor/ro/__init__.py | 0 tests/regressiontests/localflavor/ro/tests.py | 144 ----- .../localflavor/ru/__init__.py | 0 tests/regressiontests/localflavor/ru/tests.py | 150 ----- .../localflavor/se/__init__.py | 0 tests/regressiontests/localflavor/se/tests.py | 166 ------ .../localflavor/si/__init__.py | 0 tests/regressiontests/localflavor/si/tests.py | 557 ------------------ .../localflavor/sk/__init__.py | 0 tests/regressiontests/localflavor/sk/tests.py | 118 ---- tests/regressiontests/localflavor/tests.py | 42 -- .../localflavor/tr/__init__.py | 0 tests/regressiontests/localflavor/tr/tests.py | 75 --- .../localflavor/us/__init__.py | 0 tests/regressiontests/localflavor/us/forms.py | 11 - .../regressiontests/localflavor/us/models.py | 15 - tests/regressiontests/localflavor/us/tests.py | 286 --------- .../localflavor/uy/__init__.py | 0 tests/regressiontests/localflavor/uy/tests.py | 53 -- .../localflavor/za/__init__.py | 0 tests/regressiontests/localflavor/za/tests.py | 31 - 95 files changed, 5722 deletions(-) delete mode 100644 tests/regressiontests/localflavor/ar/__init__.py delete mode 100644 tests/regressiontests/localflavor/ar/tests.py delete mode 100644 tests/regressiontests/localflavor/at/__init__.py delete mode 100644 tests/regressiontests/localflavor/at/tests.py delete mode 100644 tests/regressiontests/localflavor/au/__init__.py delete mode 100644 tests/regressiontests/localflavor/au/forms.py delete mode 100644 tests/regressiontests/localflavor/au/models.py delete mode 100644 tests/regressiontests/localflavor/au/tests.py delete mode 100644 tests/regressiontests/localflavor/be/__init__.py delete mode 100644 tests/regressiontests/localflavor/be/tests.py delete mode 100644 tests/regressiontests/localflavor/br/__init__.py delete mode 100644 tests/regressiontests/localflavor/br/tests.py delete mode 100644 tests/regressiontests/localflavor/ca/__init__.py delete mode 100644 tests/regressiontests/localflavor/ca/tests.py delete mode 100644 tests/regressiontests/localflavor/ch/__init__.py delete mode 100644 tests/regressiontests/localflavor/ch/tests.py delete mode 100644 tests/regressiontests/localflavor/cl/__init__.py delete mode 100644 tests/regressiontests/localflavor/cl/tests.py delete mode 100644 tests/regressiontests/localflavor/cn/__init__.py delete mode 100644 tests/regressiontests/localflavor/cn/tests.py delete mode 100644 tests/regressiontests/localflavor/co/__init__.py delete mode 100644 tests/regressiontests/localflavor/co/tests.py delete mode 100644 tests/regressiontests/localflavor/cz/__init__.py delete mode 100644 tests/regressiontests/localflavor/cz/tests.py delete mode 100644 tests/regressiontests/localflavor/de/__init__.py delete mode 100644 tests/regressiontests/localflavor/de/tests.py delete mode 100644 tests/regressiontests/localflavor/ec/__init__.py delete mode 100644 tests/regressiontests/localflavor/ec/tests.py delete mode 100644 tests/regressiontests/localflavor/es/__init__.py delete mode 100644 tests/regressiontests/localflavor/es/tests.py delete mode 100644 tests/regressiontests/localflavor/fi/__init__.py delete mode 100644 tests/regressiontests/localflavor/fi/tests.py delete mode 100644 tests/regressiontests/localflavor/fr/__init__.py delete mode 100644 tests/regressiontests/localflavor/fr/tests.py delete mode 100644 tests/regressiontests/localflavor/gb/__init__.py delete mode 100644 tests/regressiontests/localflavor/gb/tests.py delete mode 100644 tests/regressiontests/localflavor/hk/__init__.py delete mode 100644 tests/regressiontests/localflavor/hk/forms.py delete mode 100644 tests/regressiontests/localflavor/hk/tests.py delete mode 100644 tests/regressiontests/localflavor/hr/__init__.py delete mode 100644 tests/regressiontests/localflavor/hr/tests.py delete mode 100644 tests/regressiontests/localflavor/id/__init__.py delete mode 100644 tests/regressiontests/localflavor/id/tests.py delete mode 100644 tests/regressiontests/localflavor/ie/__init__.py delete mode 100644 tests/regressiontests/localflavor/ie/tests.py delete mode 100644 tests/regressiontests/localflavor/il/__init__.py delete mode 100644 tests/regressiontests/localflavor/il/tests.py delete mode 100644 tests/regressiontests/localflavor/in_/__init__.py delete mode 100644 tests/regressiontests/localflavor/in_/tests.py delete mode 100644 tests/regressiontests/localflavor/is_/__init__.py delete mode 100644 tests/regressiontests/localflavor/is_/tests.py delete mode 100644 tests/regressiontests/localflavor/it/__init__.py delete mode 100644 tests/regressiontests/localflavor/it/tests.py delete mode 100644 tests/regressiontests/localflavor/jp/__init__.py delete mode 100644 tests/regressiontests/localflavor/jp/tests.py delete mode 100644 tests/regressiontests/localflavor/kw/__init__.py delete mode 100644 tests/regressiontests/localflavor/kw/tests.py delete mode 100644 tests/regressiontests/localflavor/mk/__init__.py delete mode 100644 tests/regressiontests/localflavor/mk/forms.py delete mode 100644 tests/regressiontests/localflavor/mk/models.py delete mode 100644 tests/regressiontests/localflavor/mk/tests.py delete mode 100644 tests/regressiontests/localflavor/mx/__init__.py delete mode 100644 tests/regressiontests/localflavor/mx/forms.py delete mode 100644 tests/regressiontests/localflavor/mx/models.py delete mode 100644 tests/regressiontests/localflavor/mx/mx.py delete mode 100644 tests/regressiontests/localflavor/mx/tests.py delete mode 100644 tests/regressiontests/localflavor/nl/__init__.py delete mode 100644 tests/regressiontests/localflavor/nl/tests.py delete mode 100644 tests/regressiontests/localflavor/pl/__init__.py delete mode 100644 tests/regressiontests/localflavor/pl/tests.py delete mode 100644 tests/regressiontests/localflavor/pt/__init__.py delete mode 100644 tests/regressiontests/localflavor/pt/tests.py delete mode 100644 tests/regressiontests/localflavor/py/__init__.py delete mode 100644 tests/regressiontests/localflavor/py/tests.py delete mode 100644 tests/regressiontests/localflavor/ro/__init__.py delete mode 100644 tests/regressiontests/localflavor/ro/tests.py delete mode 100644 tests/regressiontests/localflavor/ru/__init__.py delete mode 100644 tests/regressiontests/localflavor/ru/tests.py delete mode 100644 tests/regressiontests/localflavor/se/__init__.py delete mode 100644 tests/regressiontests/localflavor/se/tests.py delete mode 100644 tests/regressiontests/localflavor/si/__init__.py delete mode 100644 tests/regressiontests/localflavor/si/tests.py delete mode 100644 tests/regressiontests/localflavor/sk/__init__.py delete mode 100644 tests/regressiontests/localflavor/sk/tests.py delete mode 100644 tests/regressiontests/localflavor/tr/__init__.py delete mode 100644 tests/regressiontests/localflavor/tr/tests.py delete mode 100644 tests/regressiontests/localflavor/us/__init__.py delete mode 100644 tests/regressiontests/localflavor/us/forms.py delete mode 100644 tests/regressiontests/localflavor/us/models.py delete mode 100644 tests/regressiontests/localflavor/us/tests.py delete mode 100644 tests/regressiontests/localflavor/uy/__init__.py delete mode 100644 tests/regressiontests/localflavor/uy/tests.py delete mode 100644 tests/regressiontests/localflavor/za/__init__.py delete mode 100644 tests/regressiontests/localflavor/za/tests.py diff --git a/tests/regressiontests/localflavor/ar/__init__.py b/tests/regressiontests/localflavor/ar/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/regressiontests/localflavor/ar/tests.py b/tests/regressiontests/localflavor/ar/tests.py deleted file mode 100644 index 0bc228eae9..0000000000 --- a/tests/regressiontests/localflavor/ar/tests.py +++ /dev/null @@ -1,103 +0,0 @@ -from __future__ import unicode_literals - -from django.contrib.localflavor.ar.forms import (ARProvinceSelect, - ARPostalCodeField, ARDNIField, ARCUITField) - -from django.test import SimpleTestCase - - -class ARLocalFlavorTests(SimpleTestCase): - def test_ARProvinceSelect(self): - f = ARProvinceSelect() - out = '''''' - self.assertHTMLEqual(f.render('provincias', 'A'), out) - - def test_ARPostalCodeField(self): - error_format = ['Enter a postal code in the format NNNN or ANNNNAAA.'] - error_atmost = ['Ensure this value has at most 8 characters (it has 9).'] - error_atleast = ['Ensure this value has at least 4 characters (it has 3).'] - valid = { - '5000': '5000', - 'C1064AAB': 'C1064AAB', - 'c1064AAB': 'C1064AAB', - 'C1064aab': 'C1064AAB', - '4400': '4400', - 'C1064AAB': 'C1064AAB', - } - invalid = { - 'C1064AABB': error_atmost + error_format, - 'C1064AA': error_format, - 'C1064AB': error_format, - '106AAB': error_format, - '500': error_atleast + error_format, - '5PPP': error_format, - } - self.assertFieldOutput(ARPostalCodeField, valid, invalid) - - def test_ARDNIField(self): - error_length = ['This field requires 7 or 8 digits.'] - error_digitsonly = ['This field requires only numbers.'] - valid = { - '20123456': '20123456', - '20.123.456': '20123456', - '20123456': '20123456', - '20.123.456': '20123456', - '20.123456': '20123456', - '9123456': '9123456', - '9.123.456': '9123456', - } - invalid = { - '101234566': error_length, - 'W0123456': error_digitsonly, - '10,123,456': error_digitsonly, - } - self.assertFieldOutput(ARDNIField, valid, invalid) - - def test_ARCUITField(self): - error_format = ['Enter a valid CUIT in XX-XXXXXXXX-X or XXXXXXXXXXXX format.'] - error_invalid = ['Invalid CUIT.'] - error_legal_type = ['Invalid legal type. Type must be 27, 20, 23 or 30.'] - valid = { - '20-10123456-9': '20-10123456-9', - '20-10123456-9': '20-10123456-9', - '27-10345678-4': '27-10345678-4', - '20101234569': '20-10123456-9', - '27103456784': '27-10345678-4', - '30011111110': '30-01111111-0', - } - invalid = { - '2-10123456-9': error_format, - '210123456-9': error_format, - '20-10123456': error_format, - '20-10123456-': error_format, - '20-10123456-5': error_invalid, - '27-10345678-1': error_invalid, - '27-10345678-1': error_invalid, - '11211111110': error_legal_type, - } - self.assertFieldOutput(ARCUITField, valid, invalid) diff --git a/tests/regressiontests/localflavor/at/__init__.py b/tests/regressiontests/localflavor/at/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/regressiontests/localflavor/at/tests.py b/tests/regressiontests/localflavor/at/tests.py deleted file mode 100644 index 9123ba4e88..0000000000 --- a/tests/regressiontests/localflavor/at/tests.py +++ /dev/null @@ -1,49 +0,0 @@ -from __future__ import unicode_literals - -from django.contrib.localflavor.at.forms import (ATZipCodeField, ATStateSelect, - ATSocialSecurityNumberField) - -from django.test import SimpleTestCase - - -class ATLocalFlavorTests(SimpleTestCase): - def test_ATStateSelect(self): - f = ATStateSelect() - out = '''''' - self.assertHTMLEqual(f.render('bundesland', 'WI'), out) - - def test_ATZipCodeField(self): - error_format = ['Enter a zip code in the format XXXX.'] - valid = { - '1150': '1150', - '4020': '4020', - '8020': '8020', - } - invalid = { - '0000' : error_format, - '0123' : error_format, - '111222': error_format, - 'eeffee': error_format, - } - self.assertFieldOutput(ATZipCodeField, valid, invalid) - - def test_ATSocialSecurityNumberField(self): - error_format = ['Enter a valid Austrian Social Security Number in XXXX XXXXXX format.'] - valid = { - '1237 010180': '1237 010180', - } - invalid = { - '1237 010181': error_format, - '12370 010180': error_format, - } - self.assertFieldOutput(ATSocialSecurityNumberField, valid, invalid) diff --git a/tests/regressiontests/localflavor/au/__init__.py b/tests/regressiontests/localflavor/au/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/regressiontests/localflavor/au/forms.py b/tests/regressiontests/localflavor/au/forms.py deleted file mode 100644 index aec00694fe..0000000000 --- a/tests/regressiontests/localflavor/au/forms.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import absolute_import - -from django.forms import ModelForm - -from .models import AustralianPlace - - -class AustralianPlaceForm(ModelForm): - """ Form for storing an Australian place. """ - class Meta: - model = AustralianPlace diff --git a/tests/regressiontests/localflavor/au/models.py b/tests/regressiontests/localflavor/au/models.py deleted file mode 100644 index 39061c5bb3..0000000000 --- a/tests/regressiontests/localflavor/au/models.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.contrib.localflavor.au.models import AUStateField, AUPostCodeField -from django.db import models - -class AustralianPlace(models.Model): - state = AUStateField(blank=True) - state_required = AUStateField() - state_default = AUStateField(default="NSW", blank=True) - postcode = AUPostCodeField(blank=True) - postcode_required = AUPostCodeField() - postcode_default = AUPostCodeField(default="2500", blank=True) - name = models.CharField(max_length=20) - - class Meta: - app_label = 'localflavor' diff --git a/tests/regressiontests/localflavor/au/tests.py b/tests/regressiontests/localflavor/au/tests.py deleted file mode 100644 index 69472f0935..0000000000 --- a/tests/regressiontests/localflavor/au/tests.py +++ /dev/null @@ -1,108 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import re - -from django.test import SimpleTestCase -from django.contrib.localflavor.au.forms import (AUPostCodeField, - AUPhoneNumberField, AUStateSelect) - -from .forms import AustralianPlaceForm - - -SELECTED_OPTION_PATTERN = r'