From b16f8b5fbe4d912daa48b0209dea871b854d8376 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Mon, 9 Jul 2012 08:58:24 +0100 Subject: 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 --- docs/topics/class-based-views/generic-editing.txt | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'docs') 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 -- cgit v1.3 From e8c6aff3bf31b775dd70581b1cf6f86d5abd4001 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Wed, 5 Sep 2012 18:05:28 +0300 Subject: 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(-) (limited to 'docs') 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): -- cgit v1.3 From b1b32b2074a1aa906ba02fe221bfabef25617026 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Mon, 17 Sep 2012 22:02:16 -0700 Subject: Added myself as a committer. --- AUTHORS | 1 + docs/internals/committers.txt | 12 ++++++++++++ 2 files changed, 13 insertions(+) (limited to 'docs') 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 ----------- -- cgit v1.3 From b771bcc7b4ce37368d28db307030cf7c4c773ea2 Mon Sep 17 00:00:00 2001 From: Collin Anderson Date: Tue, 18 Sep 2012 10:56:39 -0400 Subject: document changes for YearArchiveView. --- docs/ref/class-based-views/generic-date-based.txt | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'docs') 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``. -- cgit v1.3 From 901af865505310a70dd02ea5b3becbf45819b652 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 19 Sep 2012 10:06:53 -0600 Subject: 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(-) (limited to 'docs') 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) -- cgit v1.3 From 4e9a74b81df1c7aaea2f90a3a4911920e134b275 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 19 Sep 2012 11:15:12 -0600 Subject: 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(-) (limited to 'docs') 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) -- cgit v1.3 From 1360bd4186239d7e4c4481b7d6a1a650fe69d12f Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 19 Sep 2012 07:13:10 -0400 Subject: 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(-) (limited to 'docs') 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 -- cgit v1.3 From acd74ffa358a64861fae8fd7bf020fc3a50341b2 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 19 Sep 2012 16:36:34 -0400 Subject: 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(-) (limited to 'docs') 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 -- cgit v1.3 From 0fdfcee257155ad29ff161725d94f41f0e77691f Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 19 Sep 2012 16:09:46 -0400 Subject: 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(+) (limited to 'docs') 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 ~~~~~~~~~~~~~~~~~~~~~~~ -- cgit v1.3 From 3ae397a98c427a56717a20933d76ade97bfb0886 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 20 Sep 2012 09:36:48 +0200 Subject: 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(+) (limited to 'docs') 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 ~~~~~~~~~~ -- cgit v1.3 From 89136b2725db3cb774ae4b39849684ae8f3847aa Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 20 Sep 2012 10:31:37 +0200 Subject: 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(-) (limited to 'docs') 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 -- cgit v1.3 From 2315f1a2ee4e83f7514f20302cdac4782b63751a Mon Sep 17 00:00:00 2001 From: Ian Clelland Date: Thu, 20 Sep 2012 12:07:34 -0700 Subject: Add documentation for get_caches function --- docs/topics/cache.txt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'docs') 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. -- cgit v1.3 From e06b54391dd06a0448b7676ec38f3734a4f86300 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 20 Sep 2012 13:49:26 -0700 Subject: Removed an excess colon. Thanks to jMyles for the patch. --- docs/internals/deprecation.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') 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 -- cgit v1.3 From 837425b425c2d58596f3ed04a7ed79541279ee7e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 19 Sep 2012 16:39:14 -0400 Subject: 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 ------------------ docs/topics/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(-) (limited to 'docs') 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 -- cgit v1.3 From 2b1ae4dbd2b0389e1e412d68262cd2a9b8209a70 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Fri, 21 Sep 2012 16:22:50 -0700 Subject: Fixed #19008 typo in signals docs --- docs/ref/signals.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') 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: -- cgit v1.3 From 69ff1b7390e140f332a5aa55c44a091c838923fb Mon Sep 17 00:00:00 2001 From: Dan Loewenherz Date: Fri, 7 Sep 2012 12:42:06 -0400 Subject: 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(-) (limited to 'docs') 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 -- cgit v1.3 From baa33cd8faa16737524b1ac355802a10dd63571c Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 22 Sep 2012 11:45:51 +0200 Subject: 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 +-- docs/ref/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(-) (limited to 'docs') 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'] -- cgit v1.3 From 822cfce3df53301d9f9f4c14bd8a0cb2a1956e2e Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 22 Sep 2012 12:02:21 +0200 Subject: 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(-) (limited to 'docs') 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) -- cgit v1.3 From 2aaa467a2ab57d5616d384a70e2b6f8217ece63e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 22 Sep 2012 07:08:40 -0400 Subject: 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(+) (limited to 'docs') 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 ----------------------------- -- cgit v1.3 From 98b6ce60f4f4456fb00259ec118e1fed2a4dfaa4 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 23 Sep 2012 20:17:36 +0200 Subject: Made a version condition less confusing. Fixed #18762 (again). --- docs/intro/tutorial03.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') 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 -- cgit v1.3 From fc69fff9ab5bba8a6cff3eaf51f02a3204b1c015 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 24 Sep 2012 22:11:42 +0200 Subject: 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 ------------------------ tests/regressiontests/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 (limited to 'docs') 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") -- cgit v1.3 From 29cd3d6c01d7afbcf5141430b2dd93daede22ade Mon Sep 17 00:00:00 2001 From: Andrew Badr Date: Mon, 24 Sep 2012 17:14:11 -0700 Subject: 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(-) (limited to 'docs') 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 -- cgit v1.3 From 5a1bf7eccb100ea4f4f61ceba9381a11e3e0afc5 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 25 Sep 2012 17:08:07 +0200 Subject: Fix little typo in cache documentation --- docs/topics/cache.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') 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. -- cgit v1.3 From 70a0de37d132e5f1514fb939875f69649f103124 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 26 Sep 2012 18:48:09 +0800 Subject: 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 +- django/contrib/admin/templates/admin/base.html | 2 +- django/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 +- .../auth/management/commands/changepassword.py | 16 +- .../auth/management/commands/createsuperuser.py | 122 +++---- django/contrib/auth/models.py | 173 ++++++---- django/contrib/auth/tests/__init__.py | 37 +-- django/contrib/auth/tests/auth_backends.py | 5 + django/contrib/auth/tests/basic.py | 45 ++- django/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, 1412 insertions(+), 343 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 (limited to 'docs') 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,7 +152,25 @@ class UserManager(models.Manager): email = '@'.join([email_name, domain_part.lower()]) return email - def create_user(self, username, email=None, password=None): + def make_random_password(self, length=10, + allowed_chars='abcdefghjkmnpqrstuvwxyz' + 'ABCDEFGHJKLMNPQRSTUVWXYZ' + '23456789'): + """ + Generates a random password with the given length and given + allowed_chars. Note that the default value of allowed_chars does not + have "I" or "O" or letters and digits that look similar -- just to + avoid confusion. + """ + return get_random_string(length, allowed_chars) + + def get_by_natural_key(self, 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. """ @@ -158,35 +180,20 @@ class UserManager(models.Manager): 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) + 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): - u = self.create_user(username, email, password) + 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 - def make_random_password(self, length=10, - allowed_chars='abcdefghjkmnpqrstuvwxyz' - 'ABCDEFGHJKLMNPQRSTUVWXYZ' - '23456789'): - """ - Generates a random password with the given length and given - allowed_chars. Note that the default value of allowed_chars does not - have "I" or "O" or letters and digits that look similar -- just to - avoid confusion. - """ - return get_random_string(length, allowed_chars) - - def get_by_natural_key(self, username): - return self.get(username=username) - # A few helper functions for common logic between User and AnonymousUser. def _user_get_all_permissions(user, obj): @@ -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,21 +227,73 @@ def _user_has_module_perms(user, app_label): return False +class AbstractBaseUser(models.Model): + password = models.CharField(_('password'), max_length=128) + last_login = models.DateTimeField(_('last login'), default=timezone.now) + + REQUIRED_FIELDS = [] + + class Meta: + abstract = True + + def is_anonymous(self): + """ + Always returns False. This is a way of comparing User objects to + anonymous users. + """ + return False + + def is_authenticated(self): + """ + Always return True. This is a way to tell if the user has been + authenticated in templates. + """ + return True + + def set_password(self, raw_password): + self.password = make_password(raw_password) + + def check_password(self, raw_password): + """ + Returns a boolean of whether the raw_password was correct. Handles + hashing formats behind the scenes. + """ + def setter(raw_password): + self.set_password(raw_password) + self.save(update_fields=["password"]) + return check_password(raw_password, self.password, setter) + + def set_unusable_password(self): + # Sets a value that will never be a valid hash + self.password = make_password(None) + + 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 User(models.Model): +class AbstractUser(AbstractBaseUser): """ - Users within the Django authentication system are represented by this - model. + An abstract base class implementing a fully featured User model with + admin-compliant permissions. - Username and password are required. Other fields are optional. + 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')) + '@/./+/-/_ 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(_('e-mail address'), blank=True) - password = models.CharField(_('password'), max_length=128) + 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.')) @@ -248,7 +303,6 @@ class User(models.Model): 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 ' @@ -257,11 +311,15 @@ class User(models.Model): 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 @@ -272,20 +330,6 @@ class User(models.Model): def get_absolute_url(self): return "/users/%s/" % urlquote(self.username) - def is_anonymous(self): - """ - Always returns False. This is a way of comparing User objects to - anonymous users. - """ - return False - - def is_authenticated(self): - """ - Always return True. This is a way to tell if the user has been - authenticated in templates. - """ - return True - def get_full_name(self): """ Returns the first_name plus the last_name, with a space in between. @@ -293,25 +337,9 @@ class User(models.Model): 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) - - def check_password(self, raw_password): - """ - Returns a boolean of whether the raw_password was correct. Handles - hashing formats behind the scenes. - """ - def setter(raw_password): - self.set_password(raw_password) - self.save(update_fields=["password"]) - return check_password(raw_password, self.password, setter) - - def set_unusable_password(self): - # Sets a value that will never be a valid hash - self.password = make_password(None) - - def has_usable_password(self): - return is_password_usable(self.password) + def get_short_name(self): + "Returns the short name for the user." + return self.first_name def get_group_permissions(self, obj=None): """ @@ -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) - - except TypeError as e: + call_command("createsuperuser", + interactive=True, + username="nolocale@somewhere.org", + email="nolocale@somewhere.org", + stdout=new_io + ) + + 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): """ -- cgit v1.3 From 2c8267bf3db608b99c04ae903c424b60cafaaf93 Mon Sep 17 00:00:00 2001 From: Adrien Lemaire Date: Wed, 26 Sep 2012 14:14:51 +0200 Subject: 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 +++++++++++----------- tests/regressiontests/test_client_regress/tests.py | 4 ++-- tests/regressiontests/test_utils/tests.py | 2 +- 24 files changed, 51 insertions(+), 51 deletions(-) (limited to 'docs') 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}) -- cgit v1.3 From b946db5241b924c72c1079ce30d9b368e2b82f07 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Thu, 27 Sep 2012 15:06:58 +0200 Subject: 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 +++ tests/regressiontests/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(-) (limited to 'docs') 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): -- cgit v1.3 From 373932fa6b9137a7e760d81dc66d49fc10ff2942 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sun, 23 Sep 2012 22:48:13 -0700 Subject: 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 (limited to 'docs') 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 ===================================== -- cgit v1.3 From 1df58968a4b2247aff91db40f1325f079ba3cdce Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Thu, 27 Sep 2012 13:19:04 -0700 Subject: Added a note regarding interaction between GitHub and Trac Plugin --- docs/internals/contributing/committing-code.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'docs') 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" -- cgit v1.3 From 84fa9099c6a760104d69a87d3cc2cba192f1ebf2 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 27 Sep 2012 17:33:52 -0400 Subject: Fixed two broken links introduced in recent commits. --- docs/index.txt | 1 - docs/topics/auth.txt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) (limited to 'docs') 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. -- cgit v1.3 From d08096317ab598b7a350d61b9b8396a3be7b8c79 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 27 Sep 2012 17:17:21 -0400 Subject: 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(-) (limited to 'docs') 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 ====================== -- cgit v1.3 From e44bedd13f974321c0c5deece9f5ac3da02e64c0 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 27 Sep 2012 20:25:31 -0400 Subject: Fixed a typo in runserver docs --- docs/ref/django-admin.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') 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 -- cgit v1.3 From 751a7d0c32746dc6774f1b561db523b25365148a Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 27 Sep 2012 20:16:38 -0600 Subject: 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(+) (limited to 'docs') 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 ========================== -- cgit v1.3 From 1cd6e04cd4f768bcd4385b75de433d497d938f82 Mon Sep 17 00:00:00 2001 From: Anssi Kääriäinen Date: Thu, 20 Sep 2012 18:51:30 +0300 Subject: 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(-) (limited to 'docs') 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): -- cgit v1.3 From 6c2faaceb0482267cec19da0ff432984028f9d0c Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 28 Sep 2012 20:10:22 +0200 Subject: Made more extensive use of get_current_site Refs #15089 --- django/contrib/comments/moderation.py | 4 +- django/contrib/contenttypes/tests.py | 5 +- django/contrib/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(-) (limited to 'docs') 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 -- cgit v1.3 From fea0ca4334b8c35100c0ca1048f81b9b3573bc26 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 28 Sep 2012 09:50:02 -0400 Subject: 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(+) (limited to 'docs') 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 -- cgit v1.3 From 2f6e00a840176f95c836f25a41cc1a7d31941ba5 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 29 Sep 2012 11:01:08 +0200 Subject: 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 ++++++++++++++++++++++++++ django/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(-) (limited to 'docs') 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. -- cgit v1.3 From ffdd6595ea2220f8e8a6fb3aacd3213b751d982f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 22 Sep 2012 11:55:37 +0200 Subject: 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 | 43 +++++++++++++----------------- 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, 62 insertions(+), 51 deletions(-) (limited to 'docs') 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. - """ - 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) + extension of the WKB specification that includes SRID value that are + a part of this geometry. + """ + 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 -- cgit v1.3 From 8867c276135887458c21536f53c8b4045baefefc Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 29 Sep 2012 20:04:08 +0200 Subject: Added link to PostGIS matrix on OSGeo Wiki --- docs/ref/contrib/gis/install.txt | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'docs') 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 -- cgit v1.3 From 15202baace1453e7576806f13d137ae930de6dcb Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 29 Sep 2012 16:41:55 -0400 Subject: Fixed #17058 - Clarified where extras/csrf_migration_helper.py is located --- docs/ref/contrib/csrf.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'docs') 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: -- cgit v1.3 From a014ddfef2f606471f25c756d97b3b50fcbd9e91 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 24 Sep 2012 22:30:38 +0200 Subject: 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(-) (limited to 'docs') 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``). -- cgit v1.3 From f0f327bbfe1caae6d11fbe20a3b5b96eed1704cf Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 26 Sep 2012 19:56:21 +0200 Subject: 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(-) (limited to 'docs') 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) -- cgit v1.3 From dad7eec6e1c1770f5d81d5c5ed2de296c1eca969 Mon Sep 17 00:00:00 2001 From: Anssi Kääriäinen Date: Sun, 30 Sep 2012 02:43:47 +0300 Subject: Corrected links to only()/defer() in Model documentation Refs #18306 --- docs/ref/models/instances.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'docs') 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 ================ -- cgit v1.3 From 3abf6105b6c953c6feb28708b9903f583cb28438 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sat, 29 Sep 2012 21:46:32 -0700 Subject: 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(-) (limited to 'docs') 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``). -- cgit v1.3 From ab2a1773fdef2ff240b124268f5ae1118d8e27b5 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sat, 29 Sep 2012 21:53:13 -0700 Subject: Added a missing comma --- docs/howto/deployment/wsgi/modwsgi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') 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 -- cgit v1.3 From 28abf5f0ebc9d380f25dd278d7ef4642c4504545 Mon Sep 17 00:00:00 2001 From: Anssi Kääriäinen Date: Sun, 30 Sep 2012 17:51:06 +0300 Subject: 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(-) (limited to 'docs') 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() -- cgit v1.3 From d5a4f209c3889a76a23a19f3212ab2d7b5c62e1c Mon Sep 17 00:00:00 2001 From: Anssi Kääriäinen Date: Sun, 30 Sep 2012 18:13:23 +0300 Subject: 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 ++++++ django/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 (limited to 'docs') 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 ====== -- cgit v1.3 From d0345b71146ecb60af2277585b604fbc244d267b Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sun, 30 Sep 2012 13:37:25 -0400 Subject: Fixed #15338 - Documented django.utils.decorators --- docs/ref/utils.txt | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'docs') 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`` ========================= -- cgit v1.3 From 92b5341b19e9b35083ee671afc2afb2f252c662d Mon Sep 17 00:00:00 2001 From: Flavio Curella Date: Sat, 29 Sep 2012 14:45:56 +0200 Subject: 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. --- django/contrib/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(-) (limited to 'docs') 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 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 +--------------------------------------------------------- -Creating a spatial database template for PostGIS ------------------------------------------------- +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 ~~~~~~~~~~~~~~ -- cgit v1.3 From 8bd7b598b6de1be1e3f72f3a1ee62803b1c02010 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 30 Sep 2012 23:16:14 +0200 Subject: 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 +-- tests/regressiontests/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 (limited to 'docs') 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 -- cgit v1.3 From 7cc4068c4470876c526830778cbdac2fdfd6dc26 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 12 Jul 2012 11:13:15 +0930 Subject: 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(-) (limited to 'docs') 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 ---------------------------------- -- cgit v1.3 From e7723683dc652613df369d5ca412e8b1217012d3 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sun, 30 Sep 2012 16:34:13 +0400 Subject: 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_regress/fixtures/sequence_extra.json | 13 ++++++++++ tests/regressiontests/fixtures_regress/tests.py | 29 ++++++++++++++++++++++ 7 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json (limited to 'docs') 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): """ -- cgit v1.3 From 14681eaa53a035fa1c8fc390d5cca0b340fdcdb3 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Mon, 1 Oct 2012 09:00:41 -0700 Subject: Fixed #19045 -- removed 'fixed on a branch' from triage docs --- docs/internals/contributing/triaging-tickets.txt | 9 --------- 1 file changed, 9 deletions(-) (limited to 'docs') 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 ----------------------- -- cgit v1.3 From 030b55393ea1164080a25eb6e241e43918bae8ac Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Mon, 1 Oct 2012 14:53:21 -0700 Subject: Removed incorrectly reintroduced 1.3 version notes --- docs/topics/auth.txt | 2 -- 1 file changed, 2 deletions(-) (limited to 'docs') 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. -- cgit v1.3 From 5f8b97f9fb058e5e02f1f99423fc3b0020ecdeb0 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Tue, 2 Oct 2012 04:19:44 -0700 Subject: 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(-) (limited to 'docs') 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 -- cgit v1.3 From c76877c1d2985e37c3c988d107c44e829fb118af Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 3 Oct 2012 12:56:15 +0200 Subject: 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(+) (limited to 'docs') 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 -- cgit v1.3 From 218abcc9e550d266a9979e10f562fc21b8f34c6a Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Wed, 3 Oct 2012 19:50:12 +0300 Subject: 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(-) (limited to 'docs') 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 ######################### -- cgit v1.3 From 1c03b23567a3098b9ab5df64b14e0dea8d1414ea Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 3 Oct 2012 06:58:16 -0400 Subject: 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(+) (limited to 'docs') 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`` ----------------- -- cgit v1.3 From 234ca6c61d27d1cd430a5290ff858c25afb93098 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 3 Oct 2012 14:43:36 -0400 Subject: 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(-) (limited to 'docs') 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. -- cgit v1.3 From 89544b2bd2323cdc5c29e056838a23abcee07d6e Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 3 Oct 2012 20:03:01 +0200 Subject: Readded docs anchor removed in 92b5341b and still in use --- docs/ref/contrib/gis/install.txt | 1 + 1 file changed, 1 insertion(+) (limited to 'docs') 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 -- cgit v1.3 From 725128289398ba4bce60a4093d9d69bbcea01d92 Mon Sep 17 00:00:00 2001 From: John Paulett Date: Wed, 3 Oct 2012 20:06:05 +0200 Subject: Fixed #17207 -- Added a troubleshooting note about failing createdb --- docs/ref/contrib/gis/install.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'docs') 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 ============================== -- cgit v1.3 From a1a5c0854f0b0d3c94a772c8b994ee2d1d2f9be1 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 4 Oct 2012 06:45:22 -0400 Subject: 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(-) (limited to 'docs') 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/')) -- cgit v1.3 From 443999a1eeea70e4deebcf31f8f845696be62c3d Mon Sep 17 00:00:00 2001 From: Tomáš Ehrlich Date: Thu, 4 Oct 2012 14:03:48 +0200 Subject: Fixed #18996 - Docs on overriden model methods --- docs/topics/db/models.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'docs') 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 -------------------- -- cgit v1.3 From 074e65b04a82b4105ea9649c6ab8f5a796bc6984 Mon Sep 17 00:00:00 2001 From: Michael Kelly Date: Fri, 5 Oct 2012 11:32:28 -0400 Subject: Fixed typo in queryset docs under update method. --- docs/ref/models/querysets.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') 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): -- cgit v1.3 From ab8c9703683856eef0346b661e36d6f05db67435 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 5 Oct 2012 23:14:56 +0200 Subject: 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(-) (limited to 'docs') 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 -- cgit v1.3 From 70fac984c88d81a93502468f79f5c661dfe3b3aa Mon Sep 17 00:00:00 2001 From: Don Spaulding Date: Fri, 5 Oct 2012 19:17:00 -0500 Subject: 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(-) (limited to 'docs') 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 -- cgit v1.3 From 12f39be508cab95a9841987c3df589ef69de706e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 6 Oct 2012 12:46:35 +0800 Subject: 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(-) (limited to 'docs') 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) -- cgit v1.3 From 6d46c740d80b0c7f75064bc6bb4d99b15b106ba4 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 6 Oct 2012 07:02:11 -0400 Subject: 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(-) (limited to 'docs') 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 -- cgit v1.3 From 117e99511e0985701780ed1bcd3afd456e244ae3 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 6 Oct 2012 13:14:11 +0200 Subject: 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(-) (limited to 'docs') 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'] -- cgit v1.3 From b6b8a3f66b5cf4f00bd7ce668ac04f21bb73e0b9 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 27 Sep 2012 19:16:49 -0300 Subject: 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 (limited to 'docs') 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. -See the documentation on :ref:`customizing error views -` for more details. +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. -handler403 ----------- +See the documentation on :ref:`customizing error views +` for the full details. -.. data:: handler403 +Such values can be set in your root URLconf. Setting these variables in any +other URLconf will have no effect. -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. +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. -By default, this is ``'django.views.defaults.permission_denied'``. That default -value should suffice. +The variables are: -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 -============================================== - -.. currentmodule:: 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 -`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() ---------- +URL namespaces of included URLconfs can be specified in two ways. -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 +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')), -permalink() ------------ +This will include the URLs defined in ``apps.help.urls`` into the application +namespace ``bar``, with the instance namespace ``foo``. -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. +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:: -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``. -- cgit v1.3 From 69035b0b1c6e3bd4569070bf0f0c774def397f0d Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 6 Oct 2012 16:19:51 -0300 Subject: More URL mapping documentation fixes. --- docs/ref/urls.txt | 35 +++++++++++---- docs/topics/http/urls.txt | 110 ++++++++++++++++++++++++---------------------- 2 files changed, 84 insertions(+), 61 deletions(-) (limited to 'docs') 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() - -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 `. +.. 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. Optionally, the :term:`application + namespace` and :term:`instance namespace` where the entries will be included + into can also be specified. + + ``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. + + :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. -- cgit v1.3 From ec1aad1671bfbba7ef58e7477dd14d7add065838 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sun, 7 Oct 2012 20:11:12 -0300 Subject: 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(-) (limited to 'docs') 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) -- cgit v1.3 From a8b1861fc4d0a48b4879af803bba094eef145017 Mon Sep 17 00:00:00 2001 From: Anssi Kääriäinen Date: Wed, 3 Oct 2012 18:21:39 +0300 Subject: 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(-) (limited to 'docs') 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() -- cgit v1.3 From b625e8272bd41714c838cfda3fb54e1f5177f009 Mon Sep 17 00:00:00 2001 From: Anssi Kääriäinen Date: Wed, 3 Oct 2012 18:53:40 +0300 Subject: 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 +++++++++ tests/regressiontests/expressions_regress/tests.py | 18 ++----------- 4 files changed, 43 insertions(+), 23 deletions(-) (limited to 'docs') 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): -- cgit v1.3 From c99ad64df7f8b7bdf504ef1c329610fce3c7f1b0 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Tue, 9 Oct 2012 20:30:28 -0700 Subject: Fixed #19097 -- documented module of origin for HttpRes/req objects --- docs/ref/request-response.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'docs') 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 =================== -- cgit v1.3 From f315006d50a1391393507cef1c52147cb766e45d Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Wed, 10 Oct 2012 20:38:48 -0300 Subject: Remove heteronormativity from coding style doc --- docs/internals/contributing/writing-code/coding-style.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'docs') 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`` -- cgit v1.3 From 7ef2781ca0ce48872e21dce2f322c9e4106d1cfd Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 10 Oct 2012 20:03:27 -0400 Subject: 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(+) (limited to 'docs') 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 ============= -- cgit v1.3 From b498ce820384c6967fbec3a32be3b9cd5b01e63d Mon Sep 17 00:00:00 2001 From: Dmitry Medvinsky Date: Thu, 11 Oct 2012 12:38:14 +0400 Subject: Fix typo in URLs reversing docs --- docs/topics/http/urls.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') 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 -- cgit v1.3 From 06f5da3d7813e9a23b1e98ecf8b75fc6073800e9 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 11 Oct 2012 06:11:52 -0400 Subject: 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 (limited to 'docs') 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 -- cgit v1.3 From 0614e99fbdb9d14a57035da320a4fc7aca232469 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 11 Oct 2012 15:40:38 -0300 Subject: More URL reversion docs typo fixes. --- docs/topics/http/urls.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'docs') 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. -- cgit v1.3 From 2d1214d92ae67acaf2246c3dc2ea37cdf7e1c2a5 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 11 Oct 2012 06:47:29 -0400 Subject: Fixed #14165 - Documented that TransactionMiddleware only applies to the default database. --- docs/ref/middleware.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'docs') 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. -- cgit v1.3 From dd0cbc6bdccfc51329427b8a6023f6e866d48cba Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 11 Oct 2012 18:04:25 -0400 Subject: 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(-) (limited to 'docs') 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:: -- cgit v1.3 From 470deb5cbb765e2e731c5b0b184247c7f87482aa Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 11 Oct 2012 19:54:52 -0400 Subject: Fixed #10936 - Noted that using SQLite for development is a good idea --- docs/topics/install.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'docs') 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: -- cgit v1.3 From 95f7ea3af1854f575a47218a08d1a8d5357f8d9b Mon Sep 17 00:00:00 2001 From: Brian Galey Date: Fri, 12 Oct 2012 17:22:20 +0200 Subject: Fixed #19028 -- Support GeoJSON output with SpatiaLite 3.0+ --- django/contrib/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(-) (limited to 'docs') 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. -- cgit v1.3 From f8c3acc8074988eed88667e58255abab1b093119 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 12 Oct 2012 17:00:35 -0500 Subject: Updated localflavor docs to note the new packages --- docs/ref/contrib/localflavor.txt | 1468 +++----------------------------------- 1 file changed, 107 insertions(+), 1361 deletions(-) (limited to 'docs') diff --git a/docs/ref/contrib/localflavor.txt b/docs/ref/contrib/localflavor.txt index 0d1319ec61..dfcb5028b3 100644 --- a/docs/ref/contrib/localflavor.txt +++ b/docs/ref/contrib/localflavor.txt @@ -6,1396 +6,142 @@ The "local flavor" add-ons :synopsis: A collection of various Django snippets that are useful only for a particular country or culture. -Following its "batteries included" philosophy, Django comes with assorted -pieces of code that are useful for particular countries or cultures. These are -called the "local flavor" add-ons and live in the -:mod:`django.contrib.localflavor` package. - -Inside that package, country- or culture-specific code is organized into -subpackages, named using `ISO 3166 country codes`_. - -Most of the ``localflavor`` add-ons are localized form components deriving -from the :doc:`forms ` framework -- for example, a -:class:`~django.contrib.localflavor.us.forms.USStateField` that knows how to -validate U.S. state abbreviations, and a -:class:`~django.contrib.localflavor.fi.forms.FISocialSecurityNumber` that -knows how to validate Finnish social security numbers. +Historically, Django has shipped with ``django.contrib.localflavor`` -- +assorted pieces of code that are useful for particular countries or cultures. +Starting with Django 1.5, we've started the process of moving the code to +outside packages (i.e., packages distributed separately from Django), for +easier maintenance and to trim the size of Django's codebase. + +The localflavor packages are named ``django-localflavor-*``, where the asterisk +is an `ISO 3166 country code`_. For example: ``django-localflavor-us`` is the +localflavor package for the U.S.A. + +Most of these ``localflavor`` add-ons are country-specific fields for the +:doc:`forms ` framework -- for example, a +``USStateField`` that knows how to validate U.S. state abbreviations and a +``FISocialSecurityNumber`` that knows how to validate Finnish social security +numbers. To use one of these localized components, just import the relevant subpackage. For example, here's how you can create a form with a field representing a French telephone number:: from django import forms - from django.contrib.localflavor.fr.forms import FRPhoneNumberField + from django_localflavor_fr.forms import FRPhoneNumberField class MyForm(forms.Form): my_french_phone_no = FRPhoneNumberField() -Supported countries -=================== - -Countries currently supported by :mod:`~django.contrib.localflavor` are: - -* Argentina_ -* Australia_ -* Austria_ -* Belgium_ -* Brazil_ -* Canada_ -* Chile_ -* China_ -* Colombia_ -* Croatia_ -* Czech_ -* Ecuador_ -* Finland_ -* France_ -* Germany_ -* `Hong Kong`_ -* Iceland_ -* India_ -* Indonesia_ -* Ireland_ -* Israel_ -* Italy_ -* Japan_ -* Kuwait_ -* Macedonia_ -* Mexico_ -* `The Netherlands`_ -* Norway_ -* Peru_ -* Poland_ -* Portugal_ -* Paraguay_ -* Romania_ -* Russia_ -* Slovakia_ -* Slovenia_ -* `South Africa`_ -* Spain_ -* Sweden_ -* Switzerland_ -* Turkey_ -* `United Kingdom`_ -* `United States of America`_ -* Uruguay_ - -The ``django.contrib.localflavor`` package also includes a ``generic`` subpackage, -containing useful code that is not specific to one particular country or culture. -Currently, it defines date, datetime and split datetime input fields based on -those from :doc:`forms `, but with non-US default formats. -Here's an example of how to use them:: - - from django import forms - from django.contrib.localflavor import generic - - class MyForm(forms.Form): - my_date_field = generic.forms.DateField() - -.. _ISO 3166 country codes: http://www.iso.org/iso/country_codes.htm -.. _Argentina: `Argentina (ar)`_ -.. _Australia: `Australia (au)`_ -.. _Austria: `Austria (at)`_ -.. _Belgium: `Belgium (be)`_ -.. _Brazil: `Brazil (br)`_ -.. _Canada: `Canada (ca)`_ -.. _Chile: `Chile (cl)`_ -.. _China: `China (cn)`_ -.. _Colombia: `Colombia (co)`_ -.. _Croatia: `Croatia (hr)`_ -.. _Czech: `Czech (cz)`_ -.. _Ecuador: `Ecuador (ec)`_ -.. _Finland: `Finland (fi)`_ -.. _France: `France (fr)`_ -.. _Germany: `Germany (de)`_ -.. _Hong Kong: `Hong Kong (hk)`_ -.. _The Netherlands: `The Netherlands (nl)`_ -.. _Iceland: `Iceland (is\_)`_ -.. _India: `India (in\_)`_ -.. _Indonesia: `Indonesia (id)`_ -.. _Ireland: `Ireland (ie)`_ -.. _Israel: `Israel (il)`_ -.. _Italy: `Italy (it)`_ -.. _Japan: `Japan (jp)`_ -.. _Kuwait: `Kuwait (kw)`_ -.. _Macedonia: `Macedonia (mk)`_ -.. _Mexico: `Mexico (mx)`_ -.. _Norway: `Norway (no)`_ -.. _Paraguay: `Paraguay (py)`_ -.. _Peru: `Peru (pe)`_ -.. _Poland: `Poland (pl)`_ -.. _Portugal: `Portugal (pt)`_ -.. _Romania: `Romania (ro)`_ -.. _Russia: `Russia (ru)`_ -.. _Slovakia: `Slovakia (sk)`_ -.. _Slovenia: `Slovenia (si)`_ -.. _South Africa: `South Africa (za)`_ -.. _Spain: `Spain (es)`_ -.. _Sweden: `Sweden (se)`_ -.. _Switzerland: `Switzerland (ch)`_ -.. _Turkey: `Turkey (tr)`_ -.. _United Kingdom: `United Kingdom (gb)`_ -.. _United States of America: `United States of America (us)`_ -.. _Uruguay: `Uruguay (uy)`_ - -Internationalization of localflavor -=================================== +For documentation on a given country's localflavor helpers, see its README +file. -Localflavor has its own catalog of translations, in the directory -``django/contrib/localflavor/locale``, and it's not loaded automatically like -Django's general catalog in ``django/conf/locale``. If you want localflavor's -texts to be translated, like form fields error messages, you must include -:mod:`django.contrib.localflavor` in the :setting:`INSTALLED_APPS` setting, so -the internationalization system can find the catalog, as explained in -:ref:`how-django-discovers-translations`. +.. _ISO 3166 country code: http://www.iso.org/iso/country_codes.htm -Adding flavors +How to migrate ============== -We'd love to add more of these to Django, so please `create a ticket`_ with -any code you'd like to contribute. One thing we ask is that you please use -Unicode objects (``u'mystring'``) for strings, rather than setting the encoding -in the file. See any of the existing flavors for examples. - -.. _create a ticket: https://code.djangoproject.com/newticket - -Localflavor and backwards compatibility -======================================= - -As documented in our :ref:`API stability -` policy, Django will always attempt -to make :mod:`django.contrib.localflavor` reflect the officially -gazetted policies of the appropriate local government authority. For -example, if a government body makes a change to add, alter, or remove -a province (or state, or county), that change will be reflected in -Django's localflavor in the next stable Django release. - -When a backwards-incompatible change is made (for example, the removal -or renaming of a province) the localflavor in question will raise a -warning when that localflavor is imported. This provides a runtime -indication that something may require attention. - -However, once you have addressed the backwards compatibility (for -example, auditing your code to see if any data migration is required), -the warning serves no purpose. The warning can then be supressed. -For example, to suppress the warnings raised by the Indonesian -localflavor you would use the following code:: - - import warnings - warnings.filterwarnings('ignore', - category=RuntimeWarning, - module='django.contrib.localflavor.id') - from django.contrib.localflavor.id import forms as id_forms - - -Argentina (``ar``) -============================================= - -.. class:: ar.forms.ARPostalCodeField - - A form field that validates input as either a classic four-digit Argentinian - postal code or a CPA_. - -.. _CPA: http://www.correoargentino.com.ar/consulta_cpa/home.php - -.. class:: ar.forms.ARDNIField - - A form field that validates input as a Documento Nacional de Identidad (DNI) - number. - -.. class:: ar.forms.ARCUITField - - A form field that validates input as a Codigo Unico de Identificacion - Tributaria (CUIT) number. - -.. class:: ar.forms.ARProvinceSelect - - A ``Select`` widget that uses a list of Argentina's provinces and autonomous - cities as its choices. - -Australia (``au``) -============================================= - -.. versionadded:: 1.4 - -.. class:: au.forms.AUPostCodeField - - A form field that validates input as an Australian postcode. - -.. class:: au.forms.AUPhoneNumberField - - A form field that validates input as an Australian phone number. Valid numbers - have ten digits. - -.. class:: au.forms.AUStateSelect - - A ``Select`` widget that uses a list of Australian states/territories as its - choices. - -.. class:: au.models.AUPhoneNumberField - - A model field that checks that the value is a valid Australian phone - number (ten digits). - -.. class:: au.models.AUStateField - - A model field that forms represent as a ``forms.AUStateField`` field and - stores the three-letter Australian state abbreviation in the database. - -.. class:: au.models.AUPostCodeField - - A model field that forms represent as a ``forms.AUPostCodeField`` field - and stores the four-digit Australian postcode in the database. - -Austria (``at``) -================ - -.. class:: at.forms.ATZipCodeField - - A form field that validates its input as an Austrian zip code, with the - format XXXX (first digit must be greater than 0). - -.. class:: at.forms.ATStateSelect - - A ``Select`` widget that uses a list of Austrian states as its choices. - -.. class:: at.forms.ATSocialSecurityNumberField - - A form field that validates its input as an Austrian social security number. - -Belgium (``be``) -================ - -.. class:: be.forms.BEPhoneNumberField - - A form field that validates input as a Belgium phone number, with one of - the formats 0x xxx xx xx, 0xx xx xx xx, 04xx xx xx xx, 0x/xxx.xx.xx, - 0xx/xx.xx.xx, 04xx/xx.xx.xx, 0x.xxx.xx.xx, 0xx.xx.xx.xx, 04xx.xx.xx.xx, - 0xxxxxxxx or 04xxxxxxxx. - -.. class:: be.forms.BEPostalCodeField - - A form field that validates input as a Belgium postal code, in the range - and format 1XXX-9XXX. - -.. class:: be.forms.BEProvinceSelect - - A ``Select`` widget that uses a list of Belgium provinces as its - choices. - -.. class:: be.forms.BERegionSelect - - A ``Select`` widget that uses a list of Belgium regions as its - choices. - -Brazil (``br``) -=============== - -.. class:: br.forms.BRPhoneNumberField - - A form field that validates input as a Brazilian phone number, with the format - XX-XXXX-XXXX. - -.. class:: br.forms.BRZipCodeField - - A form field that validates input as a Brazilian zip code, with the format - XXXXX-XXX. - -.. class:: br.forms.BRStateSelect - - A ``Select`` widget that uses a list of Brazilian states/territories as its - choices. - -.. class:: br.forms.BRCPFField - - A form field that validates input as `Brazilian CPF`_. - - Input can either be of the format XXX.XXX.XXX-VD or be a group of 11 digits. - -.. _Brazilian CPF: http://en.wikipedia.org/wiki/Cadastro_de_Pessoas_F%C3%ADsicas - -.. class:: br.forms.BRCNPJField - - A form field that validates input as `Brazilian CNPJ`_. - - Input can either be of the format XX.XXX.XXX/XXXX-XX or be a group of 14 - digits. - -.. _Brazilian CNPJ: http://en.wikipedia.org/wiki/National_identification_number#Brazil - -Canada (``ca``) -=============== - -.. class:: ca.forms.CAPhoneNumberField - - A form field that validates input as a Canadian phone number, with the format - XXX-XXX-XXXX. - -.. class:: ca.forms.CAPostalCodeField - - A form field that validates input as a Canadian postal code, with the format - XXX XXX. - -.. class:: ca.forms.CAProvinceField - - A form field that validates input as a Canadian province name or abbreviation. - -.. class:: ca.forms.CASocialInsuranceNumberField - - A form field that validates input as a Canadian Social Insurance Number (SIN). - A valid number must have the format XXX-XXX-XXX and pass a `Luhn mod-10 - checksum`_. - -.. _Luhn mod-10 checksum: http://en.wikipedia.org/wiki/Luhn_algorithm - -.. class:: ca.forms.CAProvinceSelect - - A ``Select`` widget that uses a list of Canadian provinces and territories as - its choices. - -Chile (``cl``) -============== - -.. class:: cl.forms.CLRutField - - A form field that validates input as a Chilean national identification number - ('Rol Unico Tributario' or RUT). The valid format is XX.XXX.XXX-X. - -.. class:: cl.forms.CLRegionSelect - - A ``Select`` widget that uses a list of Chilean regions (Regiones) as its - choices. - -China (``cn``) -============== - -.. versionadded:: 1.4 - -.. class:: cn.forms.CNProvinceSelect - - A ``Select`` widget that uses a list of Chinese regions as its choices. - -.. class:: cn.forms.CNPostCodeField - - A form field that validates input as a Chinese post code. - Valid formats are XXXXXX where X is digit. - -.. class:: cn.forms.CNIDCardField - - A form field that validates input as a Chinese Identification Card Number. - Both 1st and 2nd generation ID Card Number are validated. - -.. class:: cn.forms.CNPhoneNumberField - - A form field that validates input as a Chinese phone number. - Valid formats are 0XX-XXXXXXXX, composed of 3 or 4 digits of region code - and 7 or 8 digits of phone number. - -.. class:: cn.forms.CNCellNumberField - - A form field that validates input as a Chinese mobile phone number. - Valid formats are like 1XXXXXXXXXX, where X is digit. - The second digit could only be 3, 5 and 8. - -Colombia (``co``) -================= - -.. versionadded:: 1.4 - -.. class:: co.forms.CoDepartmentSelect - - A ``Select`` widget that uses a list of Colombian departments - as its choices. - -Croatia (``hr``) -================ - -.. versionadded:: 1.4 - -.. class:: hr.forms.HRCountySelect - - A ``Select`` widget that uses a list of counties of Croatia as its choices. - -.. class:: hr.forms.HRPhoneNumberPrefixSelect - - A ``Select`` widget that uses a list of phone number prefixes of Croatia as - its choices. +If you've used the old ``django.contrib.localflavor`` package, follow these two +easy steps to update your code: -.. class:: hr.forms.HRLicensePlatePrefixSelect +1. Install the appropriate third-party ``django-localflavor-*`` package(s). + Go to https://github.com/django/ and find the package for your country. - A ``Select`` widget that uses a list of vehicle license plate prefixes of - Croatia as its choices. +2. Change your app's import statements to reference the new packages. -.. class:: hr.forms.HRPhoneNumberField + For example, change this:: - A form field that validates input as a phone number of Croatia. - A valid format is a country code or a leading zero, area code prefix, 6 or 7 - digit number; e.g. +385XXXXXXXX or 0XXXXXXXX - Validates fixed, mobile and FGSM numbers. Normalizes to a full number with - country code (+385 prefix). + from django.contrib.localflavor.fr.forms import FRPhoneNumberField -.. class:: hr.forms.HRLicensePlateField + ...to this:: - A form field that validates input as a vehicle license plate of Croatia. - Normalizes to the specific format XX YYYY-XX where X is a letter and Y a - digit. There can be three or four digits. - Suffix is constructed from the shared letters of the Croatian and English - alphabets. - It is used for standardized license plates only. Special cases like license - plates for oldtimers, temporary license plates, government institution - license plates and customized license plates are not covered by this field. + from django_localflavor_fr.forms import FRPhoneNumberField -.. class:: hr.forms.HRPostalCodeField +The code in the new packages is the same (it was copied directly from Django), +so you don't have to worry about backwards compatibility in terms of +functionality. Only the imports have changed. - A form field that validates input as a postal code of Croatia. - It consists of exactly five digits ranging from 10000 to 59999 inclusive. - -.. class:: hr.forms.HROIBField - - A form field that validates input as a Personal Identification Number (OIB) - of Croatia. - It consists of exactly eleven digits. - -.. class:: hr.forms.HRJMBGField - - A form field that validates input as a Unique Master Citizen Number (JMBG). - The number is still in use in Croatia, but it is being replaced by OIB. - This field works for other ex-Yugoslavia countries as well where the JMBG is - still in use. - The area segment of the JMBG is not validated because the citizens might - have emigrated to another ex-Yugoslavia country. - The number consists of exactly thirteen digits. - -.. class:: hr.forms.HRJMBAGField - - A form field that validates input as a Unique Master Academic Citizen Number - (JMBAG) of Croatia. - This number is used by college students and professors in Croatia. - The number consists of exactly nineteen digits. - -Czech (``cz``) -============== - -.. class:: cz.forms.CZPostalCodeField - - A form field that validates input as a Czech postal code. Valid formats - are XXXXX or XXX XX, where X is a digit. - -.. class:: cz.forms.CZBirthNumberField - - A form field that validates input as a Czech Birth Number. - A valid number must be in format XXXXXX/XXXX (slash is optional). - -.. class:: cz.forms.CZICNumberField - - A form field that validates input as a Czech IC number field. - -.. class:: cz.forms.CZRegionSelect - - A ``Select`` widget that uses a list of Czech regions as its choices. - -Ecuador (``ec``) -================ - -.. versionadded:: 1.4 - -.. class:: ec.forms.EcProvinceSelect - - A ``Select`` widget that uses a list of Ecuatorian provinces as - its choices. - -Finland (``fi``) -================ - -.. class:: fi.forms.FISocialSecurityNumber - - A form field that validates input as a Finnish social security number. - -.. class:: fi.forms.FIZipCodeField - - A form field that validates input as a Finnish zip code. Valid codes - consist of five digits. - -.. class:: fi.forms.FIMunicipalitySelect - - A ``Select`` widget that uses a list of Finnish municipalities as its - choices. - -France (``fr``) -=============== - -.. class:: fr.forms.FRPhoneNumberField - - A form field that validates input as a French local phone number. The - correct format is 0X XX XX XX XX. 0X.XX.XX.XX.XX and 0XXXXXXXXX validate - but are corrected to 0X XX XX XX XX. - -.. class:: fr.forms.FRZipCodeField - - A form field that validates input as a French zip code. Valid codes - consist of five digits. - -.. class:: fr.forms.FRDepartmentSelect - - A ``Select`` widget that uses a list of French departments as its choices. - -Germany (``de``) -================ - -.. class:: de.forms.DEIdentityCardNumberField - - A form field that validates input as a German identity card number - (Personalausweis_). Valid numbers have the format - XXXXXXXXXXX-XXXXXXX-XXXXXXX-X, with no group consisting entirely of zeroes. - -.. _Personalausweis: http://de.wikipedia.org/wiki/Personalausweis - -.. class:: de.forms.DEZipCodeField - - A form field that validates input as a German zip code. Valid codes - consist of five digits. - -.. class:: de.forms.DEStateSelect - - A ``Select`` widget that uses a list of German states as its choices. - -Hong Kong (``hk``) -================== - -.. class:: hk.forms.HKPhoneNumberField - - A form field that validates input as a Hong Kong phone number. - - -The Netherlands (``nl``) -======================== - -.. class:: nl.forms.NLPhoneNumberField - - A form field that validates input as a Dutch telephone number. - -.. class:: nl.forms.NLSofiNumberField - - A form field that validates input as a Dutch social security number - (SoFI/BSN). - -.. class:: nl.forms.NLZipCodeField - - A form field that validates input as a Dutch zip code. - -.. class:: nl.forms.NLProvinceSelect - - A ``Select`` widget that uses a list of Dutch provinces as its list of - choices. - -Iceland (``is_``) -================= - -.. class:: is_.forms.ISIdNumberField - - A form field that validates input as an Icelandic identification number - (kennitala). The format is XXXXXX-XXXX. - -.. class:: is_.forms.ISPhoneNumberField - - A form field that validates input as an Icelandtic phone number (seven - digits with an optional hyphen or space after the first three digits). - -.. class:: is_.forms.ISPostalCodeSelect - - A ``Select`` widget that uses a list of Icelandic postal codes as its - choices. - -India (``in_``) -=============== - -.. class:: in_.forms.INStateField - - A form field that validates input as an Indian state/territory name or - abbreviation. Input is normalized to the standard two-letter vehicle - registration abbreviation for the given state or territory. - -.. class:: in_.forms.INZipCodeField - - A form field that validates input as an Indian zip code, with the - format XXXXXXX. - -.. class:: in_.forms.INStateSelect - - A ``Select`` widget that uses a list of Indian states/territories as its - choices. - -.. versionadded:: 1.4 - -.. class:: in_.forms.INPhoneNumberField - - A form field that validates that the data is a valid Indian phone number, - including the STD code. It's normalised to 0XXX-XXXXXXX or 0XXX XXXXXXX - format. The first string is the STD code which is a '0' followed by 2-4 - digits. The second string is 8 digits if the STD code is 3 digits, 7 - digits if the STD code is 4 digits and 6 digits if the STD code is 5 - digits. The second string will start with numbers between 1 and 6. The - separator is either a space or a hyphen. - -Ireland (``ie``) -================ - -.. class:: ie.forms.IECountySelect - - A ``Select`` widget that uses a list of Irish Counties as its choices. - -Indonesia (``id``) +Deprecation policy ================== -.. class:: id.forms.IDPostCodeField - - A form field that validates input as an Indonesian post code field. - -.. class:: id.forms.IDProvinceSelect - - A ``Select`` widget that uses a list of Indonesian provinces as its choices. - -.. class:: id.forms.IDPhoneNumberField - - A form field that validates input as an Indonesian telephone number. - -.. class:: id.forms.IDLicensePlatePrefixSelect - - A ``Select`` widget that uses a list of Indonesian license plate - prefix code as its choices. - -.. class:: id.forms.IDLicensePlateField - - A form field that validates input as an Indonesian vehicle license plate. - -.. class:: id.forms.IDNationalIdentityNumberField - - A form field that validates input as an Indonesian national identity - number (`NIK`_/KTP). The output will be in the format of - 'XX.XXXX.DDMMYY.XXXX'. Dots or spaces can be used in the input to break - down the numbers. - -.. _NIK: http://en.wikipedia.org/wiki/Indonesian_identity_card - -Israel (``il``) -=============== - -.. class:: il.forms.ILPostalCodeField - - A form field that validates its input as an Israeli five-digit postal code. - -.. class:: il.forms.ILIDNumberField - - A form field that validates its input as an `Israeli identification number`_. - The output will be in the format of a 2-9 digit number, consisting of a - 1-8 digit ID number followed by a single checksum digit, calculated using - the `Luhn algorithm`_. - - Input may contain an optional hyphen separating the ID number from the checksum - digit. +In Django 1.5, importing from ``django.contrib.localflavor`` will result in a +``DeprecationWarning``. This means your code will still work, but you should +change it as soon as possible. -.. _Israeli identification number: http://he.wikipedia.org/wiki/%D7%9E%D7%A1%D7%A4%D7%A8_%D7%96%D7%94%D7%95%D7%AA_(%D7%99%D7%A9%D7%A8%D7%90%D7%9C) -.. _Luhn algorithm: http://en.wikipedia.org/wiki/Luhn_algorithm +In Django 1.6, importing from ``django.contrib.localflavor`` will no longer +work. -Italy (``it``) -============== - -.. class:: it.forms.ITSocialSecurityNumberField - - A form field that validates input as an Italian social security number - (`codice fiscale`_). - -.. _codice fiscale: http://www.agenziaentrate.gov.it/wps/content/Nsilib/Nsi/Home/CosaDeviFare/Richiedere/Codice+fiscale+e+tessera+sanitaria/Richiesta+TS_CF/SchedaI/Informazioni+codificazione+pf/ - -.. class:: it.forms.ITVatNumberField - - A form field that validates Italian VAT numbers (partita IVA). - -.. class:: it.forms.ITZipCodeField - - A form field that validates input as an Italian zip code. Valid codes - must have five digits. - -.. class:: it.forms.ITProvinceSelect - - A ``Select`` widget that uses a list of Italian provinces as its choices. - -.. class:: it.forms.ITRegionSelect - - A ``Select`` widget that uses a list of Italian regions as its choices. - -Japan (``jp``) -============== - -.. class:: jp.forms.JPPostalCodeField - - A form field that validates input as a Japanese postcode. It accepts seven - digits, with or without a hyphen. - -.. class:: jp.forms.JPPrefectureSelect - - A ``Select`` widget that uses a list of Japanese prefectures as its choices. - -Kuwait (``kw``) -=============== - -.. class:: kw.forms.KWCivilIDNumberField - - A form field that validates input as a Kuwaiti Civil ID number. A valid - Civil ID number must obey the following rules: - - * The number consist of 12 digits. - * The birthdate of the person is a valid date. - * The calculated checksum equals to the last digit of the Civil ID. - -Macedonia (``mk``) +Supported countries =================== -.. versionadded:: 1.4 - -.. class:: mk.forms.MKIdentityCardNumberField - - A form field that validates input as a Macedonian identity card number. - Both old and new identity card numbers are supported. - - -.. class:: mk.forms.MKMunicipalitySelect - - A form ``Select`` widget that uses a list of Macedonian municipalities as - choices. - - -.. class:: mk.forms.UMCNField +The following countries have django-localflavor- packages. + +* Argentina: https://github.com/django/django-localflavor-ar +* Australia: https://github.com/django/django-localflavor-au +* Austria: https://github.com/django/django-localflavor-at +* Belgium: https://github.com/django/django-localflavor-be +* Brazil: https://github.com/django/django-localflavor-br +* Canada: https://github.com/django/django-localflavor-ca +* Chile: https://github.com/django/django-localflavor-cl +* China: https://github.com/django/django-localflavor-cn +* Colombia: https://github.com/django/django-localflavor-co +* Croatia: https://github.com/django/django-localflavor-cr +* Czech Republic: https://github.com/django/django-localflavor-cz +* Ecuador: https://github.com/django/django-localflavor-ec +* Finland: https://github.com/django/django-localflavor-fi +* France: https://github.com/django/django-localflavor-fr +* Germany: https://github.com/django/django-localflavor-de +* Hong Kong: https://github.com/django/django-localflavor-hk +* Iceland: https://github.com/django/django-localflavor-is +* India: https://github.com/django/django-localflavor-in +* Indonesia: https://github.com/django/django-localflavor-id +* Ireland: https://github.com/django/django-localflavor-ie +* Israel: https://github.com/django/django-localflavor-il +* Italy: https://github.com/django/django-localflavor-it +* Japan: https://github.com/django/django-localflavor-jp +* Kuwait: https://github.com/django/django-localflavor-kw +* Macedonia: https://github.com/django/django-localflavor-mk +* Mexico: https://github.com/django/django-localflavor-mx +* The Netherlands: https://github.com/django/django-localflavor-nl +* Norway: https://github.com/django/django-localflavor-no +* Peru: https://github.com/django/django-localflavor-pe +* Poland: https://github.com/django/django-localflavor-pl +* Portugal: https://github.com/django/django-localflavor-pt +* Paraguay: https://github.com/django/django-localflavor-py +* Romania: https://github.com/django/django-localflavor-ro +* Russia: https://github.com/django/django-localflavor-ru +* Slovakia: https://github.com/django/django-localflavor-sk +* Slovenia: https://github.com/django/django-localflavor-si +* South Africa: https://github.com/django/django-localflavor-za +* Spain: https://github.com/django/django-localflavor-es +* Sweden: https://github.com/django/django-localflavor-se +* Switzerland: https://github.com/django/django-localflavor-ch +* Turkey: https://github.com/django/django-localflavor-tr +* United Kingdom: https://github.com/django/django-localflavor-gb +* United States of America: https://github.com/django/django-localflavor-us +* Uruguay: https://github.com/django/django-localflavor-uy + +django.contrib.localflavor.generic +================================== + +The ``django.contrib.localflavor.generic`` package, which hasn't been removed from +Django yet, contains useful code that is not specific to one particular country +or culture. Currently, it defines date, datetime and split datetime input +fields based on those from :doc:`forms `, but with non-US +default formats. Here's an example of how to use them:: - A form field that validates input as a unique master citizen - number. - - The format of the unique master citizen number is not unique - to Macedonia. For more information see: - https://secure.wikimedia.org/wikipedia/en/wiki/Unique_Master_Citizen_Number - - A value will pass validation if it complies to the following rules: - - * Consists of exactly 13 digits - * The first 7 digits represent a valid past date in the format DDMMYYY - * The last digit of the UMCN passes a checksum test - - -.. class:: mk.models.MKIdentityCardNumberField - - A model field that forms represent as a - ``forms.MKIdentityCardNumberField`` field. - - -.. class:: mk.models.MKMunicipalityField - - A model field that forms represent as a - ``forms.MKMunicipalitySelect`` and stores the 2 character code of the - municipality in the database. - - -.. class:: mk.models.UMCNField - - A model field that forms represent as a ``forms.UMCNField`` field. - - -Mexico (``mx``) -=============== - -.. class:: mx.forms.MXZipCodeField - - .. versionadded:: 1.4 - - A form field that accepts a Mexican Zip Code. - - More info about this: List of postal codes in Mexico (zipcodes_) - -.. _zipcodes: http://en.wikipedia.org/wiki/List_of_postal_codes_in_Mexico - -.. class:: mx.forms.MXRFCField - - .. versionadded:: 1.4 - - A form field that validates a Mexican *Registro Federal de Contribuyentes* for - either **Persona física** or **Persona moral**. This field accepts RFC strings - whether or not it contains a *homoclave*. - - More info about this: Registro Federal de Contribuyentes (rfc_) - -.. _rfc: http://es.wikipedia.org/wiki/Registro_Federal_de_Contribuyentes_(M%C3%A9xico) - -.. class:: mx.forms.MXCURPField - - .. versionadded:: 1.4 - - A field that validates a Mexican *Clave Única de Registro de Población*. - - More info about this: Clave Unica de Registro de Poblacion (curp_) - -.. _curp: http://www.condusef.gob.mx/index.php/clave-unica-de-registro-de-poblacion-curp - -.. class:: mx.forms.MXStateSelect - - A ``Select`` widget that uses a list of Mexican states as its choices. - -.. class:: mx.models.MXStateField - - .. versionadded:: 1.4 - - A model field that stores the three-letter Mexican state abbreviation in the - database. - -.. class:: mx.models.MXZipCodeField - - .. versionadded:: 1.4 - - A model field that forms represent as a ``forms.MXZipCodeField`` field and - stores the five-digit Mexican zip code. - -.. class:: mx.models.MXRFCField - - .. versionadded:: 1.4 - - A model field that forms represent as a ``forms.MXRFCField`` field and - stores the value of a valid Mexican RFC. - -.. class:: mx.models.MXCURPField - - .. versionadded:: 1.4 - - A model field that forms represent as a ``forms.MXCURPField`` field and - stores the value of a valid Mexican CURP. - -Additionally, a choice tuple is provided in ``django.contrib.localflavor.mx.mx_states``, -allowing customized model and form fields, and form presentations, for subsets of -Mexican states abbreviations: - -.. data:: mx.mx_states.STATE_CHOICES - - A tuple of choices of the states abbreviations for all 31 Mexican states, - plus the `Distrito Federal`. - -Norway (``no``) -=============== - -.. class:: no.forms.NOSocialSecurityNumber - - A form field that validates input as a Norwegian social security number - (personnummer_). - -.. _personnummer: http://no.wikipedia.org/wiki/Personnummer - -.. class:: no.forms.NOZipCodeField - - A form field that validates input as a Norwegian zip code. Valid codes - have four digits. - -.. class:: no.forms.NOMunicipalitySelect - - A ``Select`` widget that uses a list of Norwegian municipalities (fylker) as - its choices. - -Paraguay (``py``) -================= - -.. versionadded:: 1.4 - -.. class:: py.forms.PyDepartmentSelect - - A ``Select`` widget with a list of Paraguayan departments as choices. - -.. class:: py.forms.PyNumberedDepartmentSelect - - A ``Select`` widget with a roman numbered list of Paraguayan departments as choices. - -Peru (``pe``) -============= - -.. class:: pe.forms.PEDNIField - - A form field that validates input as a DNI (Peruvian national identity) - number. - -.. class:: pe.forms.PERUCField - - A form field that validates input as an RUC (Registro Unico de - Contribuyentes) number. Valid RUC numbers have 11 digits. - -.. class:: pe.forms.PEDepartmentSelect - - A ``Select`` widget that uses a list of Peruvian Departments as its choices. - -Poland (``pl``) -=============== - -.. class:: pl.forms.PLPESELField - - A form field that validates input as a Polish national identification number - (PESEL_). - -.. _PESEL: http://en.wikipedia.org/wiki/PESEL - -.. versionadded:: 1.4 - -.. class:: pl.forms.PLNationalIDCardNumberField - - A form field that validates input as a Polish National ID Card number. The - valid format is AAAXXXXXX, where A is letter (A-Z), X is digit and left-most - digit is checksum digit. More information about checksum calculation algorithm - see `Polish identity card`_. - -.. _`Polish identity card`: http://en.wikipedia.org/wiki/Polish_identity_card - -.. class:: pl.forms.PLREGONField - - A form field that validates input as a Polish National Official Business - Register Number (REGON_), having either seven or nine digits. The checksum - algorithm used for REGONs is documented at - http://wipos.p.lodz.pl/zylla/ut/nip-rego.html. - -.. _REGON: http://www.stat.gov.pl/bip/regon_ENG_HTML.htm - -.. class:: pl.forms.PLPostalCodeField - - A form field that validates input as a Polish postal code. The valid format - is XX-XXX, where X is a digit. - -.. class:: pl.forms.PLNIPField - - A form field that validates input as a Polish Tax Number (NIP). Valid formats - are XXX-XXX-XX-XX, XXX-XX-XX-XXX or XXXXXXXXXX. The checksum algorithm used - for NIPs is documented at http://wipos.p.lodz.pl/zylla/ut/nip-rego.html. - -.. class:: pl.forms.PLCountySelect - - A ``Select`` widget that uses a list of Polish administrative units as its - choices. - -.. class:: pl.forms.PLProvinceSelect - - A ``Select`` widget that uses a list of Polish voivodeships (administrative - provinces) as its choices. - -Portugal (``pt``) -================= - -.. class:: pt.forms.PTZipCodeField - - A form field that validates input as a Portuguese zip code. - -.. class:: pt.forms.PTPhoneNumberField - - A form field that validates input as a Portuguese phone number. - Valid numbers have 9 digits (may include spaces) or start by 00 - or + (international). - -Romania (``ro``) -================ - -.. class:: ro.forms.ROCIFField - - A form field that validates Romanian fiscal identification codes (CIF). The - return value strips the leading RO, if given. - -.. class:: ro.forms.ROCNPField - - A form field that validates Romanian personal numeric codes (CNP). - -.. class:: ro.forms.ROCountyField - - A form field that validates its input as a Romanian county (judet) name or - abbreviation. It normalizes the input to the standard vehicle registration - abbreviation for the given county. This field will only accept names written - with diacritics; consider using ROCountySelect as an alternative. - -.. class:: ro.forms.ROCountySelect - - A ``Select`` widget that uses a list of Romanian counties (judete) as its - choices. - -.. class:: ro.forms.ROIBANField - - A form field that validates its input as a Romanian International Bank - Account Number (IBAN). The valid format is ROXX-XXXX-XXXX-XXXX-XXXX-XXXX, - with or without hyphens. - -.. class:: ro.forms.ROPhoneNumberField - - A form field that validates Romanian phone numbers, short special numbers - excluded. - -.. class:: ro.forms.ROPostalCodeField - - A form field that validates Romanian postal codes. - -Russia (``ru``) -=============== - -.. versionadded:: 1.4 - -.. class:: ru.forms.RUPostalCodeField - - Russian Postal code field. The valid format is XXXXXX, where X is any - digit and the first digit is not zero. - -.. class:: ru.forms.RUCountySelect - - A ``Select`` widget that uses a list of Russian Counties as its choices. - -.. class:: ru.forms.RURegionSelect - - A ``Select`` widget that uses a list of Russian Regions as its choices. - -.. class:: ru.forms.RUPassportNumberField - - Russian internal passport number. The valid format is XXXX XXXXXX, where X - is any digit. - -.. class:: ru.forms.RUAlienPassportNumberField - - Russian alien's passport number. The valid format is XX XXXXXXX, where X - is any digit. - -Slovakia (``sk``) -================= - -.. class:: sk.forms.SKPostalCodeField - - A form field that validates input as a Slovak postal code. Valid formats - are XXXXX or XXX XX, where X is a digit. - -.. class:: sk.forms.SKDistrictSelect - - A ``Select`` widget that uses a list of Slovak districts as its choices. - -.. class:: sk.forms.SKRegionSelect - - A ``Select`` widget that uses a list of Slovak regions as its choices. - -Slovenia (``si``) -================= - -.. class:: si.forms.SIEMSOField - - A form field that validates input as Slovenian personal identification - number and stores gender and birthday to self.info dictionary. - -.. class:: si.forms.SITaxNumberField - - A form field that validates input as a Slovenian tax number. Valid input - is SIXXXXXXXX or XXXXXXXX. - -.. class:: si.forms.SIPhoneNumberField - - A form field that validates input as a Slovenian phone number. Phone - number must contain at least local area code with optional country code. - -.. class:: si.forms.SIPostalCodeField - - A form field that provides a choice field of major Slovenian postal - codes. - -.. class:: si.forms.SIPostalCodeSelect - - A ``Select`` widget that uses a list of major Slovenian postal codes as - its choices. - - -South Africa (``za``) -===================== - -.. class:: za.forms.ZAIDField - - A form field that validates input as a South African ID number. Validation - uses the Luhn checksum and a simplistic (i.e., not entirely accurate) check - for birth date. - -.. class:: za.forms.ZAPostCodeField - - A form field that validates input as a South African postcode. Valid - postcodes must have four digits. - -Spain (``es``) -============== - -.. class:: es.forms.ESIdentityCardNumberField - - A form field that validates input as a Spanish NIF/NIE/CIF (Fiscal - Identification Number) code. - -.. class:: es.forms.ESCCCField - - A form field that validates input as a Spanish bank account number (Codigo - Cuenta Cliente or CCC). A valid CCC number has the format - EEEE-OOOO-CC-AAAAAAAAAA, where the E, O, C and A digits denote the entity, - office, checksum and account, respectively. The first checksum digit - validates the entity and office. The second checksum digit validates the - account. It is also valid to use a space as a delimiter, or to use no - delimiter. - -.. class:: es.forms.ESPhoneNumberField - - A form field that validates input as a Spanish phone number. Valid numbers - have nine digits, the first of which is 6, 8 or 9. - -.. class:: es.forms.ESPostalCodeField - - A form field that validates input as a Spanish postal code. Valid codes - have five digits, the first two being in the range 01 to 52, representing - the province. - -.. class:: es.forms.ESProvinceSelect - - A ``Select`` widget that uses a list of Spanish provinces as its choices. - -.. class:: es.forms.ESRegionSelect - - A ``Select`` widget that uses a list of Spanish regions as its choices. - -Sweden (``se``) -=============== - -.. class:: se.forms.SECountySelect - - A Select form widget that uses a list of the Swedish counties (län) as its - choices. - - The cleaned value is the official county code -- see - http://en.wikipedia.org/wiki/Counties_of_Sweden for a list. - -.. class:: se.forms.SEOrganisationNumber - - A form field that validates input as a Swedish organisation number - (organisationsnummer). - - It accepts the same input as SEPersonalIdentityField (for sole - proprietorships (enskild firma). However, co-ordination numbers are not - accepted. - - It also accepts ordinary Swedish organisation numbers with the format - NNNNNNNNNN. - - The return value will be YYYYMMDDXXXX for sole proprietors, and NNNNNNNNNN - for other organisations. - -.. class:: se.forms.SEPersonalIdentityNumber - - A form field that validates input as a Swedish personal identity number - (personnummer). - - The correct formats are YYYYMMDD-XXXX, YYYYMMDDXXXX, YYMMDD-XXXX, - YYMMDDXXXX and YYMMDD+XXXX. - - A \+ indicates that the person is older than 100 years, which will be taken - into consideration when the date is validated. - - The checksum will be calculated and checked. The birth date is checked - to be a valid date. - - By default, co-ordination numbers (samordningsnummer) will be accepted. To - only allow real personal identity numbers, pass the keyword argument - coordination_number=False to the constructor. - - The cleaned value will always have the format YYYYMMDDXXXX. - -.. class:: se.forms.SEPostalCodeField - - A form field that validates input as a Swedish postal code (postnummer). - Valid codes consist of five digits (XXXXX). The number can optionally be - formatted with a space after the third digit (XXX XX). - - The cleaned value will never contain the space. - -Switzerland (``ch``) -==================== - -.. class:: ch.forms.CHIdentityCardNumberField - - A form field that validates input as a Swiss identity card number. - A valid number must confirm to the X1234567<0 or 1234567890 format and - have the correct checksums. - -.. class:: ch.forms.CHPhoneNumberField - - A form field that validates input as a Swiss phone number. The correct - format is 0XX XXX XX XX. 0XX.XXX.XX.XX and 0XXXXXXXXX validate but are - corrected to 0XX XXX XX XX. - -.. class:: ch.forms.CHZipCodeField - - A form field that validates input as a Swiss zip code. Valid codes - consist of four digits. - -.. class:: ch.forms.CHStateSelect - - A ``Select`` widget that uses a list of Swiss states as its choices. - -Turkey (``tr``) -=============== - -.. class:: tr.forms.TRZipCodeField - - A form field that validates input as a Turkish zip code. Valid codes - consist of five digits. - -.. class:: tr.forms.TRPhoneNumberField - - A form field that validates input as a Turkish phone number. The correct - format is 0xxx xxx xxxx. +90xxx xxx xxxx and inputs without spaces also - validates. The result is normalized to xxx xxx xxxx format. - -.. class:: tr.forms.TRIdentificationNumberField - - A form field that validates input as a TR identification number. A valid - number must satisfy the following: - - * The number consist of 11 digits. - * The first digit cannot be 0. - * (sum(1st, 3rd, 5th, 7th, 9th)*7 - sum(2nd,4th,6th,8th)) % 10) must be - equal to the 10th digit. - * (sum(1st to 10th) % 10) must be equal to the 11th digit. - -.. class:: tr.forms.TRProvinceSelect - - A ``select`` widget that uses a list of Turkish provinces as its choices. - -United Kingdom (``gb``) -======================= - -.. class:: gb.forms.GBPostcodeField - - A form field that validates input as a UK postcode. The regular - expression used is sourced from the schema for British Standard BS7666 - address types at http://www.cabinetoffice.gov.uk/media/291293/bs7666-v2-0.xml. - -.. class:: gb.forms.GBCountySelect - - A ``Select`` widget that uses a list of UK counties/regions as its choices. - -.. class:: gb.forms.GBNationSelect - - A ``Select`` widget that uses a list of UK nations as its choices. - -United States of America (``us``) -================================= - -.. class:: us.forms.USPhoneNumberField - - A form field that validates input as a U.S. phone number. - -.. class:: us.forms.USSocialSecurityNumberField - - A form field that validates input as a U.S. Social Security Number (SSN). - A valid SSN must obey the following rules: - - * Format of XXX-XX-XXXX - * No group of digits consisting entirely of zeroes - * Leading group of digits cannot be 666 - * Number not in promotional block 987-65-4320 through 987-65-4329 - * Number not one known to be invalid due to widespread promotional - use or distribution (e.g., the Woolworth's number or the 1962 - promotional number) - -.. class:: us.forms.USStateField - - A form field that validates input as a U.S. state name or abbreviation. It - normalizes the input to the standard two-letter postal service abbreviation - for the given state. - -.. class:: us.forms.USZipCodeField - - A form field that validates input as a U.S. ZIP code. Valid formats are - XXXXX or XXXXX-XXXX. - -.. class:: us.forms.USStateSelect - - A form ``Select`` widget that uses a list of U.S. states/territories as its - choices. - -.. class:: us.forms.USPSSelect - - A form ``Select`` widget that uses a list of U.S Postal Service - state, territory and country abbreviations as its choices. - -.. class:: us.models.PhoneNumberField - - A :class:`CharField` that checks that the value is a valid U.S.A.-style phone - number (in the format ``XXX-XXX-XXXX``). - -.. class:: us.models.USStateField - - A model field that forms represent as a ``forms.USStateField`` field and - stores the two-letter U.S. state abbreviation in the database. - -.. class:: us.models.USPostalCodeField - - A model field that forms represent as a ``forms.USPSSelect`` field - and stores the two-letter U.S Postal Service abbreviation in the - database. - -Additionally, a variety of choice tuples are provided in -``django.contrib.localflavor.us.us_states``, allowing customized model -and form fields, and form presentations, for subsets of U.S states, -territories and U.S Postal Service abbreviations: - -.. data:: us.us_states.CONTIGUOUS_STATES - - A tuple of choices of the postal abbreviations for the - contiguous or "lower 48" states (i.e., all except Alaska and - Hawaii), plus the District of Columbia. - -.. data:: us.us_states.US_STATES - - A tuple of choices of the postal abbreviations for all - 50 U.S. states, plus the District of Columbia. - -.. data:: us.us_states.US_TERRITORIES - - A tuple of choices of the postal abbreviations for U.S - territories: American Samoa, Guam, the Northern Mariana Islands, - Puerto Rico and the U.S. Virgin Islands. - -.. data:: us.us_states.ARMED_FORCES_STATES - - A tuple of choices of the postal abbreviations of the three U.S - military postal "states": Armed Forces Americas, Armed Forces - Europe and Armed Forces Pacific. - -.. data:: us.us_states.COFA_STATES - - A tuple of choices of the postal abbreviations of the three - independent nations which, under the Compact of Free Association, - are served by the U.S. Postal Service: the Federated States of - Micronesia, the Marshall Islands and Palau. - -.. data:: us.us_states.OBSOLETE_STATES - - A tuple of choices of obsolete U.S Postal Service state - abbreviations: the former abbreviation for the Northern Mariana - Islands, plus the Panama Canal Zone, the Philippines and the - former Pacific trust territories. - -.. data:: us.us_states.STATE_CHOICES - - A tuple of choices of all postal abbreviations corresponding to U.S states or - territories, and the District of Columbia.. - -.. data:: us.us_states.USPS_CHOICES - - A tuple of choices of all postal abbreviations recognized by the - U.S Postal Service (including all states and territories, the - District of Columbia, armed forces "states" and independent - nations serviced by USPS). - -Uruguay (``uy``) -================ - -.. class:: uy.forms.UYCIField + from django import forms + from django.contrib.localflavor import generic - A field that validates Uruguayan 'Cedula de identidad' (CI) numbers. + class MyForm(forms.Form): + my_date_field = generic.forms.DateField() -.. class:: uy.forms.UYDepartamentSelect +Internationalization of localflavor +=================================== - A ``Select`` widget that uses a list of Uruguayan departments as its - choices. +Localflavor has its own catalog of translations, in the directory +``django/contrib/localflavor/locale``, and it's not loaded automatically like +Django's general catalog in ``django/conf/locale``. If you want localflavor's +texts to be translated, like form fields error messages, you must include +:mod:`django.contrib.localflavor` in the :setting:`INSTALLED_APPS` setting, so +the internationalization system can find the catalog, as explained in +:ref:`how-django-discovers-translations`. -- cgit v1.3 From c870cb48cd0ec4b5dfdc5df95e6f0b5f5f8a738b Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 12 Oct 2012 06:37:35 -0400 Subject: Fixed #18256 - Added a potential pitfall when upgrading to MySQL 5.5.5 --- docs/ref/databases.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'docs') diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 3e256e9d9e..3a52f838e7 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -158,6 +158,16 @@ Since MySQL 5.5.5, the default storage engine is InnoDB_. This engine is fully transactional and supports foreign key references. It's probably the best choice at this point. +If you upgrade an existing project to MySQL 5.5.5 and subsequently add some +tables, ensure that your tables are using the same storage engine (i.e. MyISAM +vs. InnoDB). Specifically, if tables that have a ``ForeignKey`` between them +use different storage engines, you may see an error like the following when +running ``syncdb``:: + + _mysql_exceptions.OperationalError: ( + 1005, "Can't create table '\\db_name\\.#sql-4a8_ab' (errno: 150)" + ) + .. versionchanged:: 1.4 In previous versions of Django, fixtures with forward references (i.e. -- cgit v1.3 From c433fcb3fb34fccd69782979f0e7cd5f2d4a4893 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 13 Oct 2012 11:44:50 +0800 Subject: Fixed #19077, #19079 -- Made USERNAME_FIELD a required field, and modified UserAdmin to match. --- django/contrib/admin/templates/admin/base.html | 2 +- .../admin/templates/admin/object_history.html | 2 +- .../registration/password_reset_email.html | 2 +- django/contrib/auth/admin.py | 7 +- django/contrib/auth/backends.py | 2 +- django/contrib/auth/forms.py | 8 +- .../auth/management/commands/changepassword.py | 2 +- .../auth/management/commands/createsuperuser.py | 6 +- django/contrib/auth/middleware.py | 4 +- django/contrib/auth/models.py | 21 ++- django/contrib/comments/admin.py | 13 +- django/contrib/comments/models.py | 36 ++-- django/contrib/comments/views/comments.py | 25 ++- docs/topics/auth.txt | 197 ++++++++++++++++++++- 14 files changed, 260 insertions(+), 67 deletions(-) (limited to 'docs') diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index 3d2a07eba2..7bbd73a464 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.get_short_name user.username %}{% endfilter %}. + {% filter force_escape %}{% firstof user.get_short_name user.get_username %}{% endfilter %}. {% block userlinks %} {% url 'django-admindocs-docroot' as docsroot %} {% if docsroot %} diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html index 55dd4a3b4c..870c4648a6 100644 --- a/django/contrib/admin/templates/admin/object_history.html +++ b/django/contrib/admin/templates/admin/object_history.html @@ -29,7 +29,7 @@ {% for action in action_list %} {{ action.action_time|date:"DATETIME_FORMAT" }} - {{ action.user.username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %} + {{ action.user.get_username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %} {{ action.change_message }} {% endfor %} diff --git a/django/contrib/admin/templates/registration/password_reset_email.html b/django/contrib/admin/templates/registration/password_reset_email.html index 0eef4a7f9d..a220f12033 100644 --- a/django/contrib/admin/templates/registration/password_reset_email.html +++ b/django/contrib/admin/templates/registration/password_reset_email.html @@ -5,7 +5,7 @@ {% block reset_link %} {{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} {% endblock %} -{% trans "Your username, in case you've forgotten:" %} {{ user.username }} +{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} {% trans "Thanks for using our site!" %} diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py index 5c08b0615f..7fc723f475 100644 --- a/django/contrib/auth/admin.py +++ b/django/contrib/auth/admin.py @@ -11,14 +11,13 @@ from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse from django.utils.html import escape from django.utils.decorators import method_decorator -from django.utils.safestring import mark_safe -from django.utils import six from django.utils.translation import ugettext, ugettext_lazy as _ from django.views.decorators.csrf import csrf_protect from django.views.decorators.debug import sensitive_post_parameters csrf_protect_m = method_decorator(csrf_protect) + class GroupAdmin(admin.ModelAdmin): search_fields = ('name',) ordering = ('name',) @@ -106,9 +105,10 @@ class UserAdmin(admin.ModelAdmin): raise PermissionDenied if extra_context is None: extra_context = {} + username_field = self.model._meta.get_field(self.model.USERNAME_FIELD) defaults = { 'auto_populated_fields': (), - 'username_help_text': self.model._meta.get_field('username').help_text, + 'username_help_text': username_field.help_text, } extra_context.update(defaults) return super(UserAdmin, self).add_view(request, form_url, @@ -171,4 +171,3 @@ class UserAdmin(admin.ModelAdmin): admin.site.register(Group, GroupAdmin) admin.site.register(User, UserAdmin) - diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index 00cb67a0b5..db99c94838 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -105,7 +105,7 @@ class RemoteUserBackend(ModelBackend): # built-in safeguards for multiple threads. if self.create_unknown_user: user, created = UserModel.objects.get_or_create(**{ - getattr(UserModel, 'USERNAME_FIELD', 'username'): username + UserModel.USERNAME_FIELD: username }) if created: user = self.configure_user(user) diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index c114c18afe..fbd8d0482e 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -52,6 +52,9 @@ class ReadOnlyPasswordHashField(forms.Field): kwargs.setdefault("required", False) super(ReadOnlyPasswordHashField, self).__init__(*args, **kwargs) + def clean_password(self): + return self.initial + class UserCreationForm(forms.ModelForm): """ @@ -118,9 +121,6 @@ class UserChangeForm(forms.ModelForm): "this user's password, but you can change the password " "using this form.")) - def clean_password(self): - return self.initial["password"] - class Meta: model = User @@ -160,7 +160,7 @@ class AuthenticationForm(forms.Form): # Set the label for the "username" field. UserModel = get_user_model() - username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username')) + username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD) self.fields['username'].label = capfirst(username_field.verbose_name) def clean(self): diff --git a/django/contrib/auth/management/commands/changepassword.py b/django/contrib/auth/management/commands/changepassword.py index 1a2387442c..ff38836a95 100644 --- a/django/contrib/auth/management/commands/changepassword.py +++ b/django/contrib/auth/management/commands/changepassword.py @@ -34,7 +34,7 @@ class Command(BaseCommand): try: u = UserModel.objects.using(options.get('database')).get(**{ - getattr(UserModel, 'USERNAME_FIELD', 'username'): username + UserModel.USERNAME_FIELD: username }) except UserModel.DoesNotExist: raise CommandError("user '%s' does not exist" % username) diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index 8130b326c5..cb5d906342 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -42,7 +42,7 @@ class Command(BaseCommand): UserModel = get_user_model() - username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username')) + username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD) other_fields = UserModel.REQUIRED_FIELDS # If not provided, create the user with an unusable password @@ -74,7 +74,7 @@ class Command(BaseCommand): # Get a username while username is None: - username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username')) + username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD) if not username: input_msg = capfirst(username_field.verbose_name) if default_username: @@ -91,7 +91,7 @@ class Command(BaseCommand): continue try: UserModel.objects.using(database).get(**{ - getattr(UserModel, 'USERNAME_FIELD', 'username'): username + UserModel.USERNAME_FIELD: username }) except UserModel.DoesNotExist: pass diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py index df616a9243..0398cfaf1e 100644 --- a/django/contrib/auth/middleware.py +++ b/django/contrib/auth/middleware.py @@ -55,7 +55,7 @@ class RemoteUserMiddleware(object): # getting passed in the headers, then the correct user is already # persisted in the session and we don't need to continue. if request.user.is_authenticated(): - if request.user.username == self.clean_username(username, request): + if request.user.get_username() == self.clean_username(username, request): return # We are seeing this user for the first time in this session, attempt # to authenticate the user. @@ -75,6 +75,6 @@ class RemoteUserMiddleware(object): backend = auth.load_backend(backend_str) try: username = backend.clean_username(username) - except AttributeError: # Backend has no clean_username method. + except AttributeError: # Backend has no clean_username method. pass return username diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index abcc7ceafc..bd7bf4a162 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -165,7 +165,7 @@ class BaseUserManager(models.Manager): return get_random_string(length, allowed_chars) def get_by_natural_key(self, username): - return self.get(**{getattr(self.model, 'USERNAME_FIELD', 'username'): username}) + return self.get(**{self.model.USERNAME_FIELD: username}) class UserManager(BaseUserManager): @@ -227,6 +227,7 @@ def _user_has_module_perms(user, app_label): return False +@python_2_unicode_compatible class AbstractBaseUser(models.Model): password = models.CharField(_('password'), max_length=128) last_login = models.DateTimeField(_('last login'), default=timezone.now) @@ -236,6 +237,16 @@ class AbstractBaseUser(models.Model): class Meta: abstract = True + def get_username(self): + "Return the identifying username for this User" + return getattr(self, self.USERNAME_FIELD) + + def __str__(self): + return self.get_username() + + def natural_key(self): + return (self.get_username(),) + def is_anonymous(self): """ Always returns False. This is a way of comparing User objects to @@ -277,7 +288,6 @@ class AbstractBaseUser(models.Model): raise NotImplementedError() -@python_2_unicode_compatible class AbstractUser(AbstractBaseUser): """ An abstract base class implementing a fully featured User model with @@ -314,6 +324,7 @@ class AbstractUser(AbstractBaseUser): objects = UserManager() + USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] class Meta: @@ -321,12 +332,6 @@ class AbstractUser(AbstractBaseUser): 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) diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py index 0024a1d1b5..a651baaadf 100644 --- a/django/contrib/comments/admin.py +++ b/django/contrib/comments/admin.py @@ -1,11 +1,22 @@ from __future__ import unicode_literals from django.contrib import admin +from django.contrib.auth import get_user_model from django.contrib.comments.models import Comment from django.utils.translation import ugettext_lazy as _, ungettext from django.contrib.comments import get_model from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete + +class UsernameSearch(object): + """The User object may not be auth.User, so we need to provide + a mechanism for issuing the equivalent of a .filter(user__username=...) + search in CommentAdmin. + """ + def __str__(self): + return 'user__%s' % get_user_model().USERNAME_FIELD + + class CommentsAdmin(admin.ModelAdmin): fieldsets = ( (None, @@ -24,7 +35,7 @@ class CommentsAdmin(admin.ModelAdmin): date_hierarchy = 'submit_date' ordering = ('-submit_date',) raw_id_fields = ('user',) - search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address') + search_fields = ('comment', UsernameSearch(), 'user_name', 'user_email', 'user_url', 'ip_address') actions = ["flag_comments", "approve_comments", "remove_comments"] def get_actions(self, request): diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py index a39c2622dd..c263ea7d10 100644 --- a/django/contrib/comments/models.py +++ b/django/contrib/comments/models.py @@ -19,14 +19,14 @@ class BaseCommentAbstractModel(models.Model): """ # Content-object field - content_type = models.ForeignKey(ContentType, + content_type = models.ForeignKey(ContentType, verbose_name=_('content type'), related_name="content_type_set_for_%(class)s") - object_pk = models.TextField(_('object ID')) + object_pk = models.TextField(_('object ID')) content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk") # Metadata about the comment - site = models.ForeignKey(Site) + site = models.ForeignKey(Site) class Meta: abstract = True @@ -50,21 +50,21 @@ 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(settings.AUTH_USER_MODEL, 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) - user_url = models.URLField(_("user's URL"), blank=True) + user_name = models.CharField(_("user's name"), max_length=50, blank=True) + user_email = models.EmailField(_("user's email address"), blank=True) + user_url = models.URLField(_("user's URL"), blank=True) comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH) # Metadata about the comment submit_date = models.DateTimeField(_('date/time submitted'), default=None) - ip_address = models.IPAddressField(_('IP address'), blank=True, null=True) - is_public = models.BooleanField(_('is public'), default=True, + ip_address = models.IPAddressField(_('IP address'), blank=True, null=True) + is_public = models.BooleanField(_('is public'), default=True, help_text=_('Uncheck this box to make the comment effectively ' \ 'disappear from the site.')) - is_removed = models.BooleanField(_('is removed'), default=False, + is_removed = models.BooleanField(_('is removed'), default=False, help_text=_('Check this box if the comment is inappropriate. ' \ 'A "This comment has been removed" message will ' \ 'be displayed instead.')) @@ -96,9 +96,9 @@ class Comment(BaseCommentAbstractModel): """ if not hasattr(self, "_userinfo"): userinfo = { - "name" : self.user_name, - "email" : self.user_email, - "url" : self.user_url + "name": self.user_name, + "email": self.user_email, + "url": self.user_url } if self.user_id: u = self.user @@ -111,7 +111,7 @@ class Comment(BaseCommentAbstractModel): if u.get_full_name(): userinfo["name"] = self.user.get_full_name() elif not self.user_name: - userinfo["name"] = u.username + userinfo["name"] = u.get_username() self._userinfo = userinfo return self._userinfo userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__) @@ -174,9 +174,9 @@ 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(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) + 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) # Constants for flag types @@ -192,7 +192,7 @@ class CommentFlag(models.Model): def __str__(self): return "%s flag of comment ID %s by %s" % \ - (self.flag, self.comment_id, self.user.username) + (self.flag, self.comment_id, self.user.get_username()) def save(self, *args, **kwargs): if self.flag_date is None: diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index c9a11606b3..27d5a48ac6 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -15,7 +15,6 @@ from django.views.decorators.csrf import csrf_protect from django.views.decorators.http import require_POST - class CommentPostBadRequest(http.HttpResponseBadRequest): """ Response returned when a comment post is invalid. If ``DEBUG`` is on a @@ -27,6 +26,7 @@ class CommentPostBadRequest(http.HttpResponseBadRequest): if settings.DEBUG: self.content = render_to_string("comments/400-debug.html", {"why": why}) + @csrf_protect @require_POST def post_comment(request, next=None, using=None): @@ -40,7 +40,7 @@ def post_comment(request, next=None, using=None): data = request.POST.copy() if request.user.is_authenticated(): if not data.get('name', ''): - data["name"] = request.user.get_full_name() or request.user.username + data["name"] = request.user.get_full_name() or request.user.get_username() if not data.get('email', ''): data["email"] = request.user.email @@ -98,8 +98,8 @@ def post_comment(request, next=None, using=None): ] return render_to_response( template_list, { - "comment" : form.data.get("comment", ""), - "form" : form, + "comment": form.data.get("comment", ""), + "form": form, "next": next, }, RequestContext(request, {}) @@ -113,9 +113,9 @@ def post_comment(request, next=None, using=None): # Signal that the comment is about to be saved responses = signals.comment_will_be_posted.send( - sender = comment.__class__, - comment = comment, - request = request + sender=comment.__class__, + comment=comment, + request=request ) for (receiver, response) in responses: @@ -126,15 +126,14 @@ def post_comment(request, next=None, using=None): # Save the comment and signal that it was saved comment.save() signals.comment_was_posted.send( - sender = comment.__class__, - comment = comment, - request = request + sender=comment.__class__, + comment=comment, + request=request ) return next_redirect(data, next, comment_done, c=comment._get_pk_val()) comment_done = confirmation_view( - template = "comments/posted.html", - doc = """Display a "comment was posted" success page.""" + template="comments/posted.html", + doc="""Display a "comment was posted" success page.""" ) - diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index bbe6d6ec33..fd2e56ebeb 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -149,6 +149,12 @@ Methods :class:`~django.contrib.auth.models.User` objects have the following custom methods: + .. method:: models.User.get_username() + + Returns the username for the user. Since the User model can be swapped + out, you should use this method instead of referencing the username + attribute directly. + .. method:: models.User.is_anonymous() Always returns ``False``. This is a way of differentiating @@ -1826,11 +1832,12 @@ 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 +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:: +When you define a foreign key or many-to-many 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 @@ -1910,6 +1917,60 @@ password resets. You must then provide some key implementation details: identifies the user in an informal way. It may also return the same value as :meth:`django.contrib.auth.User.get_full_name()`. +The following methods are available on any subclass of +:class:`~django.contrib.auth.models.AbstractBaseUser`:: + +.. class:: models.AbstractBaseUser + + .. method:: models.AbstractBaseUser.get_username() + + Returns the value of the field nominated by ``USERNAME_FIELD``. + + .. method:: models.AbstractBaseUser.is_anonymous() + + Always returns ``False``. This is a way of differentiating + from :class:`~django.contrib.auth.models.AnonymousUser` objects. + Generally, you should prefer using + :meth:`~django.contrib.auth.models.AbstractBaseUser.is_authenticated()` to this + method. + + .. method:: models.AbstractBaseUser.is_authenticated() + + Always returns ``True``. This is a way to tell if the user has been + authenticated. This does not imply any permissions, and doesn't check + if the user is active - it only indicates that the user has provided a + valid username and password. + + .. method:: models.AbstractBaseUser.set_password(raw_password) + + Sets the user's password to the given raw string, taking care of the + password hashing. Doesn't save the + :class:`~django.contrib.auth.models.AbstractBaseUser` object. + + .. method:: models.AbstractBaseUser.check_password(raw_password) + + Returns ``True`` if the given raw string is the correct password for + the user. (This takes care of the password hashing in making the + comparison.) + + .. method:: models.AbstractBaseUser.set_unusable_password() + + Marks the user as having no password set. This isn't the same as + having a blank string for a password. + :meth:`~django.contrib.auth.models.AbstractBaseUser.check_password()` for this user + will never return ``True``. Doesn't save the + :class:`~django.contrib.auth.models.AbstractBaseUser` object. + + You may need this if authentication for your application takes place + against an existing external source such as an LDAP directory. + + .. method:: models.AbstractBaseUser.has_usable_password() + + Returns ``False`` if + :meth:`~django.contrib.auth.models.AbstractBaseUser.set_unusable_password()` has + been called for this user. + + 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 @@ -1941,6 +2002,31 @@ additional methods: Unlike `create_user()`, `create_superuser()` *must* require the caller to provider a password. +:class:`~django.contrib.auth.models.BaseUserManager` provides the following +utility methods: + +.. class:: models.BaseUserManager + .. method:: models.BaseUserManager.normalize_email(email) + + A classmethod that normalizes email addresses by lowercasing + the domain portion of the email address. + + .. method:: models.BaseUserManager.get_by_natural_key(username) + + Retrieves a user instance using the contents of the field + nominated by ``USERNAME_FIELD``. + + .. method:: models.BaseUserManager.make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789') + + Returns a random password with the given length and given string of + allowed characters. (Note that the default value of ``allowed_chars`` + doesn't contain letters that can cause user confusion, including: + + * ``i``, ``l``, ``I``, and ``1`` (lowercase letter i, lowercase + letter L, uppercase letter i, and the number one) + * ``o``, ``O``, and ``0`` (uppercase letter o, lowercase letter o, + and zero) + Extending Django's default User ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2020,6 +2106,16 @@ control access of the User to admin content: Returns True if the user has permission to access models in the given app. +You will also need to register your custom User model with the admin. If +your custom User model extends :class:`~django.contrib.auth.models.AbstractUser`, +you can use Django's existing :class:`~django.contrib.auth.admin.UserAdmin` +class. However, if your User model extends +:class:`~django.contrib.auth.models.AbstractBaseUser`, you'll need to define +a custom ModelAdmin class. It may be possible to subclass the default +:class:`~django.contrib.auth.admin.UserAdmin`; however, you'll need to +override any of the definitions that refer to fields on +:class:`~django.contrib.auth.models.AbstractUser` that aren't on your +custom User class. Custom users and Proxy models ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2036,11 +2132,11 @@ 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. +Here is an example of 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:: @@ -2086,7 +2182,9 @@ authentication app:: class MyUser(AbstractBaseUser): email = models.EmailField( verbose_name='email address', - max_length=255 + max_length=255, + unique=True, + db_index=True, ) date_of_birth = models.DateField() is_active = models.BooleanField(default=True) @@ -2124,6 +2222,87 @@ authentication app:: # Simplest possible answer: All admins are staff return self.is_admin +Then, to register this custom User model with Django's admin, the following +code would be required in ``admin.py``:: + + from django import forms + from django.contrib import admin + from django.contrib.auth.models import Group + from django.contrib.auth.admin import UserAdmin + from django.contrib.auth.forms import ReadOnlyPasswordHashField + + from customauth.models import MyUser + + + class UserCreationForm(forms.ModelForm): + """A form for creating new users. Includes all the required + fields, plus a repeated password.""" + password1 = forms.CharField(label='Password', widget=forms.PasswordInput) + password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput) + + class Meta: + model = MyUser + fields = ('email', 'date_of_birth') + + def clean_password2(self): + # Check that the two password entries match + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError('Passwords don't match') + return password2 + + def save(self, commit=True): + # Save the provided password in hashed format + user = super(UserCreationForm, self).save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.save() + return user + + + class UserChangeForm(forms.ModelForm): + """A form for updateing users. Includes all the fields on + the user, but replaces the password field with admin's + pasword hash display field. + """ + password = ReadOnlyPasswordHashField() + + class Meta: + model = MyUser + + + class MyUserAdmin(UserAdmin): + # The forms to add and change user instances + form = UserChangeForm + add_form = UserCreationForm + + # The fields to be used in displaying the User model. + # These override the definitions on the base UserAdmin + # that reference specific fields on auth.User. + list_display = ('email', 'date_of_birth', 'is_admin') + list_filter = ('is_admin',) + fieldsets = ( + (None, {'fields': ('email', 'password')}), + ('Personal info', {'fields': ('date_of_birth',)}), + ('Permissions', {'fields': ('is_admin',)}), + ('Important dates', {'fields': ('last_login',)}), + ) + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('email', 'date_of_birth', 'password1', 'password2')} + ), + ) + search_fields = ('email',) + ordering = ('email',) + filter_horizontal = () + + # Now register the new UserAdmin... + admin.site.register(MyUser, MyUserAdmin) + # ... and, since we're not using Django's builtin permissions, + # unregister the Group model from admin. + admin.site.unregister(Group) .. _authentication-backends: -- cgit v1.3 From b3b3db3d954a5226f870a0b4403343c78efae8dc Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 13 Oct 2012 13:36:07 +0800 Subject: Fixed #19067 -- Clarified handling of username in createsuperuser. Thanks to clelland for the report, and Preston Holmes for the draft patch. --- .../auth/management/commands/createsuperuser.py | 90 +++++++++-------- django/contrib/auth/tests/custom_user.py | 4 +- django/contrib/auth/tests/management.py | 2 +- docs/topics/auth.txt | 110 +++++++++++---------- 4 files changed, 111 insertions(+), 95 deletions(-) (limited to 'docs') diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index cb5d906342..216d56d730 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -16,50 +16,56 @@ 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('--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 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 - ) + def __init__(self, *args, **kwargs): + # Options are defined in an __init__ method to support swapping out + # custom user models in tests. + super(Command, self).__init__(*args, **kwargs) + self.UserModel = get_user_model() + self.username_field = self.UserModel._meta.get_field(self.UserModel.USERNAME_FIELD) + + self.option_list = BaseCommand.option_list + ( + make_option('--%s' % self.UserModel.USERNAME_FIELD, dest=self.UserModel.USERNAME_FIELD, default=None, + help='Specifies the login 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 --%s 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.' % + self.UserModel.USERNAME_FIELD)), + 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 self.UserModel.REQUIRED_FIELDS + ) + + option_list = BaseCommand.option_list help = 'Used to create a superuser.' def handle(self, *args, **options): - username = options.get('username', None) + username = options.get(self.UserModel.USERNAME_FIELD, None) interactive = options.get('interactive') verbosity = int(options.get('verbosity', 1)) database = options.get('database') - UserModel = get_user_model() - - username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD) - other_fields = UserModel.REQUIRED_FIELDS - # If not provided, create the user with an unusable password password = None - other_data = {} + user_data = {} # 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) + raise CommandError("You must use --%s with --noinput." % + self.UserModel.USERNAME_FIELD) + username = self.username_field.clean(username, None) - for field_name in other_fields: + for field_name in self.UserModel.REQUIRED_FIELDS: if options.get(field_name): - field = UserModel._meta.get_field(field_name) - other_data[field_name] = field.clean(options[field_name], None) + field = self.UserModel._meta.get_field(field_name) + user_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: @@ -74,9 +80,8 @@ class Command(BaseCommand): # Get a username while username is None: - username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD) if not username: - input_msg = capfirst(username_field.verbose_name) + input_msg = capfirst(self.username_field.verbose_name) if default_username: input_msg += " (leave blank to use '%s')" % default_username raw_value = input(input_msg + ': ') @@ -84,31 +89,30 @@ class Command(BaseCommand): if default_username and raw_value == '': raw_value = default_username try: - username = username_field.clean(raw_value, None) + username = self.username_field.clean(raw_value, None) except exceptions.ValidationError as e: self.stderr.write("Error: %s" % '; '.join(e.messages)) username = None continue try: - UserModel.objects.using(database).get(**{ - UserModel.USERNAME_FIELD: username - }) - except UserModel.DoesNotExist: + self.UserModel.objects.db_manager(database).get_by_natural_key(username) + except self.UserModel.DoesNotExist: pass else: - self.stderr.write("Error: That username is already taken.") + self.stderr.write("Error: That %s is already taken." % + self.username_field.verbose_name) username = None - 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: + for field_name in self.UserModel.REQUIRED_FIELDS: + field = self.UserModel._meta.get_field(field_name) + user_data[field_name] = options.get(field_name) + while user_data[field_name] is None: raw_value = input(capfirst(field.verbose_name + ': ')) try: - other_data[field_name] = field.clean(raw_value, None) + user_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 + user_data[field_name] = None # Get a password while password is None: @@ -128,6 +132,8 @@ class Command(BaseCommand): self.stderr.write("\nOperation cancelled.") sys.exit(1) - UserModel.objects.db_manager(database).create_superuser(username=username, password=password, **other_data) + user_data[self.UserModel.USERNAME_FIELD] = username + user_data['password'] = password + self.UserModel.objects.db_manager(database).create_superuser(**user_data) if verbosity >= 1: self.stdout.write("Superuser created successfully.") diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py index 9bd74c0ac8..a29ed6a104 100644 --- a/django/contrib/auth/tests/custom_user.py +++ b/django/contrib/auth/tests/custom_user.py @@ -23,8 +23,8 @@ class CustomUserManager(BaseUserManager): 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) + def create_superuser(self, email, password, date_of_birth): + u = self.create_user(email, password=password, date_of_birth=date_of_birth) u.is_admin = True u.save(using=self._db) return u diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index 7074e04799..976c0c4972 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -138,7 +138,7 @@ class CreatesuperuserManagementCommandTestCase(TestCase): new_io = StringIO() call_command("createsuperuser", interactive=False, - username="joe@somewhere.org", + email="joe@somewhere.org", date_of_birth="1976-04-01", stdout=new_io, skip_validation=True diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index fd2e56ebeb..41159984f6 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -1878,47 +1878,54 @@ The easiest way to construct a compliant custom User model is to inherit from implementation of a `User` model, including hashed passwords and tokenized password resets. You must then provide some key implementation details: -.. attribute:: User.USERNAME_FIELD +.. class:: models.CustomUser - 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:: + .. attribute:: User.USERNAME_FIELD - class MyUser(AbstractBaseUser): - identfier = models.CharField(max_length=40, unique=True, db_index=True) - ... - USERNAME_FIELD = 'identifier' + 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:: -.. attribute:: User.REQUIRED_FIELDS + class MyUser(AbstractBaseUser): + identfier = models.CharField(max_length=40, unique=True, db_index=True) + ... + USERNAME_FIELD = 'identifier' - 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:: + .. attribute:: User.REQUIRED_FIELDS - class MyUser(AbstractBaseUser): - ... - date_of_birth = models.DateField() - height = models.FloatField() - ... - REQUIRED_FIELDS = ['date_of_birth', 'height'] + 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(): + .. note:: - 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. + ``REQUIRED_FIELDS`` must contain all required fields on your User + model, but should *not* contain the ``USERNAME_FIELD``. -.. method:: User.get_short_name(): + .. method:: User.get_full_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()`. + 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()`. The following methods are available on any subclass of -:class:`~django.contrib.auth.models.AbstractBaseUser`:: +:class:`~django.contrib.auth.models.AbstractBaseUser`: .. class:: models.AbstractBaseUser @@ -1979,33 +1986,36 @@ 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) +.. class:: models.CustomUserManager - 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:: + .. method:: models.CustomUserManager.create_user(*username_field*, password=None, **other_fields) - def create_user(self, username, date_of_birth, password=None): - # create user here + The prototype of `create_user()` should accept the username field, + plus all required fields as arguments. For example, if your user model + uses `email` as the username field, and has `date_of_birth` as a required + fields, then create_user should be defined as:: -.. method:: UserManager.create_superuser(username, password, **other_fields) + def create_user(self, email, date_of_birth, password=None): + # create user here - 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:: + .. method:: models.CustomUserManager.create_superuser(*username_field*, password, **other_fields) - def create_superuser(self, username, date_of_birth, password): - # create superuser here + The prototype of `create_user()` should accept the username field, + plus all required fields as arguments. For example, if your user model + uses `email` as the username field, and has `date_of_birth` as a required + fields, then create_superuser should be defined as:: - Unlike `create_user()`, `create_superuser()` *must* require the caller - to provider a password. + def create_superuser(self, email, date_of_birth, password): + # create superuser here + + Unlike `create_user()`, `create_superuser()` *must* require the caller + to provider a password. :class:`~django.contrib.auth.models.BaseUserManager` provides the following utility methods: .. class:: models.BaseUserManager + .. method:: models.BaseUserManager.normalize_email(email) A classmethod that normalizes email addresses by lowercasing @@ -2165,12 +2175,12 @@ authentication app:: user.save(using=self._db) return user - def create_superuser(self, username, date_of_birth, password): + def create_superuser(self, email, date_of_birth, password): """ Creates and saves a superuser with the given email, date of birth and password. """ - user = self.create_user(username, + user = self.create_user(email, password=password, date_of_birth=date_of_birth ) @@ -2223,7 +2233,7 @@ authentication app:: return self.is_admin Then, to register this custom User model with Django's admin, the following -code would be required in ``admin.py``:: +code would be required in the app's ``admin.py`` file:: from django import forms from django.contrib import admin @@ -2249,7 +2259,7 @@ code would be required in ``admin.py``:: password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: - raise forms.ValidationError('Passwords don't match') + raise forms.ValidationError("Passwords don't match") return password2 def save(self, commit=True): -- cgit v1.3 From 10dc4797eadc1868c794c746953036c87ed0ea73 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 13 Oct 2012 11:02:18 +0200 Subject: Fixed #19119 -- Corrected default date input formats in docs Thanks henrik@aisti.fi for the report. --- docs/ref/forms/fields.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 27ca002312..1b209ace22 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -405,7 +405,7 @@ For each field, we describe the default widget used if you don't specify Additionally, if you specify :setting:`USE_L10N=False` in your settings, the following will also be included in the default input formats:: - '%b %m %d', # 'Oct 25 2006' + '%b %d %Y', # 'Oct 25 2006' '%b %d, %Y', # 'Oct 25, 2006' '%d %b %Y', # '25 Oct 2006' '%d %b, %Y', # '25 Oct, 2006' -- cgit v1.3 From 22742e4ac40274e8c91bdad9b8be251da50a3753 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 13 Oct 2012 11:17:25 +0200 Subject: Added ref to format localization in Date[Time]Field docs Thanks henrik@aisti.fi for the suggestion in #19119. --- docs/ref/forms/fields.txt | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'docs') diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 1b209ace22..7c8d509031 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -414,6 +414,8 @@ For each field, we describe the default widget used if you don't specify '%d %B %Y', # '25 October 2006' '%d %B, %Y', # '25 October, 2006' + See also :ref:`format localization `. + ``DateTimeField`` ~~~~~~~~~~~~~~~~~ @@ -445,6 +447,8 @@ For each field, we describe the default widget used if you don't specify '%m/%d/%y %H:%M', # '10/25/06 14:30' '%m/%d/%y', # '10/25/06' + See also :ref:`format localization `. + ``DecimalField`` ~~~~~~~~~~~~~~~~ -- cgit v1.3 From e6f45aa623d9a67a2d6389665ca1bea0556dc832 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 13 Oct 2012 20:59:58 +0200 Subject: Added release note about removed div around csrf token Refs #18484. Thanks Simon Charette for the suggestion. --- docs/releases/1.5.txt | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'docs') diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 263392fdc7..d49bae801d 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -444,6 +444,10 @@ Miscellaneous :ref:`Q() expressions ` and ``QuerySet`` combining where the operators are used as boolean AND and OR operators. +* The :ttag:`csrf_token` template tag is no longer enclosed in a div. If you need + HTML validation against pre-HTML5 Strict DTDs, you should add a div around it + in your pages. + Features deprecated in 1.5 ========================== -- cgit v1.3 From a451d2b4a21af062bd1295f71ae62ef770963d4f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 14 Oct 2012 23:03:01 +0200 Subject: Replaced mentions of Subversion by Git in docs --- docs/internals/contributing/localizing.txt | 2 +- docs/misc/distributions.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/internals/contributing/localizing.txt b/docs/internals/contributing/localizing.txt index 263087b5fa..0cde77882c 100644 --- a/docs/internals/contributing/localizing.txt +++ b/docs/internals/contributing/localizing.txt @@ -55,7 +55,7 @@ The format files aren't managed by the use of Transifex. To change them, you must :doc:`create a patch` against the Django source tree, as for any code change: -* Create a diff against the current Subversion trunk. +* Create a diff against the current Git master branch. * Open a ticket in Django's ticket system, set its ``Component`` field to ``Translations``, and attach the patch to it. diff --git a/docs/misc/distributions.txt b/docs/misc/distributions.txt index 729ce0717b..1b324234d1 100644 --- a/docs/misc/distributions.txt +++ b/docs/misc/distributions.txt @@ -11,7 +11,7 @@ requires. Typically, these packages are based on the latest stable release of Django, so if you want to use the development version of Django you'll need to follow the instructions for :ref:`installing the development version -` from our Subversion repository. +` from our Git repository. If you're using Linux or a Unix installation, such as OpenSolaris, check with your distributor to see if they already package Django. If -- cgit v1.3 From 1636b0338273290e762f83baffe4bbe375163319 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 15 Oct 2012 14:18:16 -0500 Subject: Added docs link to new third-party Lithuanian localflavor --- docs/ref/contrib/localflavor.txt | 1 + 1 file changed, 1 insertion(+) (limited to 'docs') diff --git a/docs/ref/contrib/localflavor.txt b/docs/ref/contrib/localflavor.txt index dfcb5028b3..9bb27e6e74 100644 --- a/docs/ref/contrib/localflavor.txt +++ b/docs/ref/contrib/localflavor.txt @@ -99,6 +99,7 @@ The following countries have django-localflavor- packages. * Italy: https://github.com/django/django-localflavor-it * Japan: https://github.com/django/django-localflavor-jp * Kuwait: https://github.com/django/django-localflavor-kw +* Lithuania: https://github.com/simukis/django-localflavor-lt * Macedonia: https://github.com/django/django-localflavor-mk * Mexico: https://github.com/django/django-localflavor-mx * The Netherlands: https://github.com/django/django-localflavor-nl -- cgit v1.3 From 07abb7a6b7af2c45be553acf08d85cd2d72057ad Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 13 Oct 2012 14:37:39 -0400 Subject: Fixed #18715 - Refactored tutorial 3. Thank-you Daniel Greenfeld! --- docs/intro/tutorial02.txt | 16 +- docs/intro/tutorial03.txt | 522 +++++++++++++++++++++++----------------------- docs/intro/tutorial04.txt | 108 ++-------- 3 files changed, 292 insertions(+), 354 deletions(-) (limited to 'docs') diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index fd13230c8b..b87b280d7c 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -440,20 +440,30 @@ Open your settings file (``mysite/settings.py``, remember) and look at the filesystem directories to check when loading Django templates. It's a search path. +Create a ``mytemplates`` directory in your project directory. Templates can +live anywhere on your filesystem that Django can access. (Django runs as +whatever user your server runs.) However, keeping your templates within the +project is a good convention to follow. + +When you’ve done that, create a directory polls in your template directory. +Within that, create a file called index.html. Note that our +``loader.get_template('polls/index.html')`` code from above maps to +[template_directory]/polls/index.html” on the filesystem. + By default, :setting:`TEMPLATE_DIRS` is empty. So, let's add a line to it, to tell Django where our templates live:: TEMPLATE_DIRS = ( - '/home/my_username/mytemplates', # Change this to your own directory. + '/path/to/mysite/mytemplates', # Change this to your own directory. ) Now copy the template ``admin/base_site.html`` from within the default Django admin template directory in the source code of Django itself (``django/contrib/admin/templates``) into an ``admin`` subdirectory of whichever directory you're using in :setting:`TEMPLATE_DIRS`. For example, if -your :setting:`TEMPLATE_DIRS` includes ``'/home/my_username/mytemplates'``, as +your :setting:`TEMPLATE_DIRS` includes ``'/path/to/mysite/mytemplates'``, as above, then copy ``django/contrib/admin/templates/admin/base_site.html`` to -``/home/my_username/mytemplates/admin/base_site.html``. Don't forget that +``/path/to/mysite/mytemplates/admin/base_site.html``. Don't forget that ``admin`` subdirectory. .. admonition:: Where are the Django source files? diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index f3501026f8..169e6cd59f 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -10,7 +10,7 @@ Philosophy ========== A view is a "type" of Web page in your Django application that generally serves -a specific function and has a specific template. For example, in a Weblog +a specific function and has a specific template. For example, in a blog application, you might have the following views: * Blog homepage -- displays the latest few entries. @@ -41,42 +41,55 @@ In our poll application, we'll have the following four views: In Django, each view is represented by a simple Python function. -Design your URLs -================ +Write your first view +===================== + +Let's write the first view. Open the file ``polls/views.py`` +and put the following Python code in it:: + + from django.http import HttpResponse + + def index(request): + return HttpResponse("Hello, world. You're at the poll index.") -The first step of writing views is to design your URL structure. You do this by -creating a Python module, called a URLconf. URLconfs are how Django associates -a given URL with given Python code. +This is the simplest view possible in Django. Now we have a problem, how does +this view get called? For that we need to map it to a URL, in Django this is +done in a configuration file called a URLconf. -When a user requests a Django-powered page, the system looks at the -:setting:`ROOT_URLCONF` setting, which contains a string in Python dotted -syntax. Django loads that module and looks for a module-level variable called -``urlpatterns``, which is a sequence of tuples in the following format:: +.. admonition:: What is a URLconf? - (regular expression, Python callback function [, optional dictionary]) + In Django, web pages and other content are delivered by views and + determining which view is called is done by Python modules informally + titled 'URLconfs'. These modules are pure Python code and are a simple + mapping between URL patterns (as simple regular expressions) to Python + callback functions (your views). This tutorial provides basic instruction + in their use, and you can refer to :mod:`django.core.urlresolvers` for + more information. -Django starts at the first regular expression and makes its way down the list, -comparing the requested URL against each regular expression until it finds one -that matches. +To create a URLconf in the polls directory, create a file called ``urls.py``. +Your app directory should now look like:: -When it finds a match, Django calls the Python callback function, with an -:class:`~django.http.HttpRequest` object as the first argument, any "captured" -values from the regular expression as keyword arguments, and, optionally, -arbitrary keyword arguments from the dictionary (an optional third item in the -tuple). + polls/ + __init__.py + admin.py + models.py + tests.py + urls.py + views.py -For more on :class:`~django.http.HttpRequest` objects, see the -:doc:`/ref/request-response`. For more details on URLconfs, see the -:doc:`/topics/http/urls`. +In the ``polls/urls.py`` file include the following code:: -When you ran ``django-admin.py startproject mysite`` at the beginning of -Tutorial 1, it created a default URLconf in ``mysite/urls.py``. It also -automatically set your :setting:`ROOT_URLCONF` setting (in ``settings.py``) to -point at that file:: + from django.conf.urls import patterns, url - ROOT_URLCONF = 'mysite.urls' + from polls import views -Time for an example. Edit ``mysite/urls.py`` so it looks like this:: + urlpatterns = patterns('', + url(r'^$', views.index, name='index') + ) + +The next step is to point the root URLconf at the ``polls.urls`` module. In +``mysite/urls.py`` insert an :func:`~django.conf.urls.include`, leaving you +with:: from django.conf.urls import patterns, include, url @@ -84,111 +97,156 @@ Time for an example. Edit ``mysite/urls.py`` so it looks like this:: admin.autodiscover() urlpatterns = patterns('', - url(r'^polls/$', 'polls.views.index'), - url(r'^polls/(?P\d+)/$', 'polls.views.detail'), - url(r'^polls/(?P\d+)/results/$', 'polls.views.results'), - url(r'^polls/(?P\d+)/vote/$', 'polls.views.vote'), + url(r'^polls/', include('polls.urls')), url(r'^admin/', include(admin.site.urls)), ) -This is worth a review. When somebody requests a page from your Web site -- say, -"/polls/23/", Django will load this Python module, because it's pointed to by -the :setting:`ROOT_URLCONF` setting. It finds the variable named ``urlpatterns`` -and traverses the regular expressions in order. When it finds a regular -expression that matches -- ``r'^polls/(?P\d+)/$'`` -- it loads the -function ``detail()`` from ``polls/views.py``. Finally, it calls that -``detail()`` function like so:: +You have now wired an `index` view into the URLconf. Go to +http://localhost:8000/polls/ in your browser, and you should see the text +"*Hello, world. You're at the poll index.*", which you defined in the +``index`` view. - detail(request=, poll_id='23') +The :func:`~django.conf.urls.url` function is passed four arguments, two +required: ``regex`` and ``view``, and two optional: ``kwargs``, and ``name``. +At this point, it's worth reviewing what these arguments are for. -The ``poll_id='23'`` part comes from ``(?P\d+)``. Using parentheses -around a pattern "captures" the text matched by that pattern and sends it as an -argument to the view function; the ``?P`` defines the name that will be -used to identify the matched pattern; and ``\d+`` is a regular expression to -match a sequence of digits (i.e., a number). +:func:`~django.conf.urls.url` argument: regex +--------------------------------------------- -Because the URL patterns are regular expressions, there really is no limit on -what you can do with them. And there's no need to add URL cruft such as ``.php`` --- unless you have a sick sense of humor, in which case you can do something -like this:: - - (r'^polls/latest\.php$', 'polls.views.index'), - -But, don't do that. It's silly. +The term `regex` is a commonly used short form meaning `regular expression`, +which is a syntax for matching patterns in strings, or in this case, url +patterns. Django starts at the first regular expression and makes its way down +the list, comparing the requested URL against each regular expression until it +finds one that matches. Note that these regular expressions do not search GET and POST parameters, or -the domain name. For example, in a request to ``http://www.example.com/myapp/``, -the URLconf will look for ``myapp/``. In a request to -``http://www.example.com/myapp/?page=3``, the URLconf will look for ``myapp/``. +the domain name. For example, in a request to +``http://www.example.com/myapp/``, the URLconf will look for ``myapp/``. In a +request to ``http://www.example.com/myapp/?page=3``, the URLconf will also +look for ``myapp/``. If you need help with regular expressions, see `Wikipedia's entry`_ and the documentation of the :mod:`re` module. Also, the O'Reilly book "Mastering -Regular Expressions" by Jeffrey Friedl is fantastic. +Regular Expressions" by Jeffrey Friedl is fantastic. In practice, however, +you don't need to be an expert on regular expressions, as you really only need +to know how to capture simple patterns. In fact, complex regexes can have poor +lookup performance, so you probably shouldn't rely on the full power of regexes. Finally, a performance note: these regular expressions are compiled the first -time the URLconf module is loaded. They're super fast. +time the URLconf module is loaded. They're super fast (as long as the lookups +aren't too complex as noted above). .. _Wikipedia's entry: http://en.wikipedia.org/wiki/Regular_expression -Write your first view -===================== +:func:`~django.conf.urls.url` argument: view +-------------------------------------------- -Well, we haven't created any views yet -- we just have the URLconf. But let's -make sure Django is following the URLconf properly. +When Django finds a regular expression match, Django calls the specified view +function, with an :class:`~django.http.HttpRequest` object as the first +argument and any “captured” values from the regular expression as other +arguments. If the regex uses simple captures, values are passed as positional +arguments; if it uses named captures, values are passed as keyword arguments. +We'll give an example of this in a bit. -Fire up the Django development Web server: +:func:`~django.conf.urls.url` argument: kwargs +---------------------------------------------- -.. code-block:: bash +Arbitrary keyword arguments can be passed in a dictionary to the target view. We +aren't going to use this feature of Django in the tutorial. - python manage.py runserver +:func:`~django.conf.urls.url` argument: name +--------------------------------------------- -Now go to "http://localhost:8000/polls/" on your domain in your Web browser. -You should get a pleasantly-colored error page with the following message:: +Naming your URL lets you refer to it unambiguously from elsewhere in Django +especially templates. This powerful feature allows you to make global changes +to the url patterns of your project while only touching a single file. - ViewDoesNotExist at /polls/ +Writing more views +================== - Could not import polls.views.index. View does not exist in module polls.views. +Now let's add a few more views to ``polls/views.py``. These views are +slightly different, because they take an argument:: -This error happened because you haven't written a function ``index()`` in the -module ``polls/views.py``. + def detail(request, poll_id): + return HttpResponse("You're looking at poll %s." % poll_id) -Try "/polls/23/", "/polls/23/results/" and "/polls/23/vote/". The error -messages tell you which view Django tried (and failed to find, because you -haven't written any views yet). + def results(request, poll_id): + return HttpResponse("You're looking at the results of poll %s." % poll_id) -Time to write the first view. Open the file ``polls/views.py`` -and put the following Python code in it:: + def vote(request, poll_id): + return HttpResponse("You're voting on poll %s." % poll_id) - from django.http import HttpResponse +Wire these news views into the ``polls.urls`` module by adding the following +:func:`~django.conf.urls.url` calls:: - def index(request): - return HttpResponse("Hello, world. You're at the poll index.") + from django.conf.urls import patterns, url -This is the simplest view possible. Go to "/polls/" in your browser, and you -should see your text. + from polls import views -Now lets add a few more views. These views are slightly different, because -they take an argument (which, remember, is passed in from whatever was -captured by the regular expression in the URLconf):: + urlpatterns = patterns('', + # ex: /polls/ + url(r'^$', views.index, name='index'), + # ex: /polls/5/ + url(r'^(?P\d+)/$', views.detail, name='detail'), + # ex: /polls/5/results/ + url(r'^(?P\d+)/results/$', views.results, name='results'), + # ex: /polls/5/vote/ + url(r'^(?P\d+)/vote/$', views.vote, name='vote'), + ) - def detail(request, poll_id): - return HttpResponse("You're looking at poll %s." % poll_id) +Take a look in your browser, at "/polls/34/". It'll run the ``detail()`` +method and display whatever ID you provide in the URL. Try +"/polls/34/results/" and "/polls/34/vote/" too -- these will display the +placeholder results and voting pages. + +When somebody requests a page from your Web site -- say, "/polls/34/", Django +will load the ``mysite.urls`` Python module because it's pointed to by the +:setting:`ROOT_URLCONF` setting. It finds the variable named ``urlpatterns`` +and traverses the regular expressions in order. The +:func:`~django.conf.urls.include` functions we are using simply reference +other URLconfs. Note that the regular expressions for the +:func:`~django.conf.urls.include` functions don't have a ``$`` (end-of-string +match character) but rather a trailing slash. Whenever Django encounters +: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. - def results(request, poll_id): - return HttpResponse("You're looking at the results of poll %s." % poll_id) +The idea behind :func:`~django.conf.urls.include` is to make it easy to +plug-and-play URLs. Since polls are in their own URLconf +(``polls/urls.py``), they can be placed under "/polls/", or under +"/fun_polls/", or under "/content/polls/", or any other path root, and the +app will still work. - def vote(request, poll_id): - return HttpResponse("You're voting on poll %s." % poll_id) +Here's what happens if a user goes to "/polls/34/" in this system: -Take a look in your browser, at "/polls/34/". It'll run the `detail()` method -and display whatever ID you provide in the URL. Try "/polls/34/results/" and -"/polls/34/vote/" too -- these will display the placeholder results and voting -pages. +* Django will find the match at ``'^polls/'`` + +* Then, Django will strip off the matching text (``"polls/"``) and send the + remaining text -- ``"34/"`` -- to the 'polls.urls' URLconf for + further processing which matches ``r'^(?P\d+)/$'`` resulting in a + call to the ``detail()`` view like so:: + + detail(request=, poll_id='34') + +The ``poll_id='34'`` part comes from ``(?P\d+)``. Using parentheses +around a pattern "captures" the text matched by that pattern and sends it as an +argument to the view function; ``?P`` defines the name that will +be used to identify the matched pattern; and ``\d+`` is a regular expression to +match a sequence of digits (i.e., a number). + +Because the URL patterns are regular expressions, there really is no limit on +what you can do with them. And there's no need to add URL cruft such as ``.php`` +-- unless you have a sick sense of humor, in which case you can do something +like this:: + + (r'^polls/latest\.php$', 'polls.views.index'), + +But, don't do that. It's silly. Write views that actually do something ====================================== -Each view is responsible for doing one of two things: Returning an +Each view is responsible for doing one of two things: returning an :class:`~django.http.HttpResponse` object containing the content for the requested page, or raising an exception such as :exc:`~django.http.Http404`. The rest is up to you. @@ -205,51 +263,21 @@ in :doc:`Tutorial 1 `. Here's one stab at the ``index()`` view, which displays the latest 5 poll questions in the system, separated by commas, according to publication date:: - from polls.models import Poll from django.http import HttpResponse + from polls.models import Poll + def index(request): - latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5] + latest_poll_list = Poll.objects.order_by('-pub_date')[:5] output = ', '.join([p.question for p in latest_poll_list]) return HttpResponse(output) -There's a problem here, though: The page's design is hard-coded in the view. If +There's a problem here, though: the page's design is hard-coded in the view. If you want to change the way the page looks, you'll have to edit this Python code. -So let's use Django's template system to separate the design from Python:: - - from django.template import Context, loader - from polls.models import Poll - from django.http import HttpResponse - - def index(request): - latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5] - t = loader.get_template('polls/index.html') - c = Context({ - 'latest_poll_list': latest_poll_list, - }) - return HttpResponse(t.render(c)) - -That code loads the template called "polls/index.html" and passes it a context. -The context is a dictionary mapping template variable names to Python objects. - -Reload the page. Now you'll see an error:: - - TemplateDoesNotExist at /polls/ - polls/index.html - -Ah. There's no template yet. First, create a directory, somewhere on your -filesystem, whose contents Django can access. (Django runs as whatever user your -server runs.) Don't put them under your document root, though. You probably -shouldn't make them public, just for security's sake. -Then edit :setting:`TEMPLATE_DIRS` in your ``settings.py`` to tell Django where -it can find templates -- just as you did in the "Customize the admin look and -feel" section of Tutorial 2. - -When you've done that, create a directory ``polls`` in your template directory. -Within that, create a file called ``index.html``. Note that our -``loader.get_template('polls/index.html')`` code from above maps to -"[template_directory]/polls/index.html" on the filesystem. +So let's use Django's template system to separate the design from Python. +First, create a directory ``polls`` in your template directory you specified +in setting:`TEMPLATE_DIRS`. Within that, create a file called ``index.html``. Put the following code in that template: .. code-block:: html+django @@ -264,36 +292,58 @@ Put the following code in that template:

    No polls are available.

    {% endif %} +Now let's use that html template in our index view:: + + from django.http import HttpResponse + from django.template import Context, loader + + from polls.models import Poll + + def index(request): + latest_poll_list = Poll.objects.order_by('-pub_date')[:5] + template = loader.get_template('polls/index.html') + context = Context({ + 'latest_poll_list': latest_poll_list, + }) + return HttpResponse(template.render(context)) + +That code loads the template called ``polls/index.html`` and passes it a +context. The context is a dictionary mapping template variable names to Python +objects. + Load the page in your Web browser, and you should see a bulleted-list containing the "What's up" poll from Tutorial 1. The link points to the poll's detail page. -A shortcut: render_to_response() --------------------------------- +A shortcut: :func:`~django.shortcuts.render` +-------------------------------------------- It's a very common idiom to load a template, fill a context and return an :class:`~django.http.HttpResponse` object with the result of the rendered template. Django provides a shortcut. Here's the full ``index()`` view, rewritten:: - from django.shortcuts import render_to_response + from django.shortcuts import render + from polls.models import Poll def index(request): latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5] - return render_to_response('polls/index.html', {'latest_poll_list': latest_poll_list}) + context = {'latest_poll_list': latest_poll_list} + return render(request, 'polls/index.html', context) Note that once we've done this in all these views, we no longer need to import :mod:`~django.template.loader`, :class:`~django.template.Context` and -:class:`~django.http.HttpResponse`. +:class:`~django.http.HttpResponse` (you'll want to keep ``HttpResponse`` if you +still have the stub methods for ``detail``, ``results``, and ``vote``). -The :func:`~django.shortcuts.render_to_response` function takes a template name -as its first argument and a dictionary as its optional second argument. It -returns an :class:`~django.http.HttpResponse` object of the given template -rendered with the given context. +The :func:`~django.shortcuts.render` function takes the request object as its +first argument, a template name as its second argument and a dictionary as its +optional third argument. It returns an :class:`~django.http.HttpResponse` +object of the given template rendered with the given context. -Raising 404 -=========== +Raising a 404 error +=================== Now, let's tackle the poll detail view -- the page that displays the question for a given poll. Here's the view:: @@ -302,10 +352,10 @@ for a given poll. Here's the view:: # ... def detail(request, poll_id): try: - p = Poll.objects.get(pk=poll_id) + poll = Poll.objects.get(pk=poll_id) except Poll.DoesNotExist: raise Http404 - return render_to_response('polls/detail.html', {'poll': p}) + return render(request, 'polls/detail.html', {'poll': poll}) The new concept here: The view raises the :exc:`~django.http.Http404` exception if a poll with the requested ID doesn't exist. @@ -317,18 +367,18 @@ later, but if you'd like to quickly get the above example working, just:: will get you started for now. -A shortcut: get_object_or_404() -------------------------------- +A shortcut: :func:`~django.shortcuts.get_object_or_404` +------------------------------------------------------- It's a very common idiom to use :meth:`~django.db.models.query.QuerySet.get` and raise :exc:`~django.http.Http404` if the object doesn't exist. Django provides a shortcut. Here's the ``detail()`` view, rewritten:: - from django.shortcuts import render_to_response, get_object_or_404 + from django.shortcuts import render, get_object_or_404 # ... def detail(request, poll_id): - p = get_object_or_404(Poll, pk=poll_id) - return render_to_response('polls/detail.html', {'poll': p}) + poll = get_object_or_404(Poll, pk=poll_id) + return render(request, 'polls/detail.html', {'poll': poll}) The :func:`~django.shortcuts.get_object_or_404` function takes a Django model as its first argument and an arbitrary number of keyword arguments, which it @@ -345,7 +395,8 @@ exist. :exc:`~django.core.exceptions.ObjectDoesNotExist`? Because that would couple the model layer to the view layer. One of the - foremost design goals of Django is to maintain loose coupling. + foremost design goals of Django is to maintain loose coupling. Some + controlled coupling is introduced in the :mod:`django.shortcuts` module. There's also a :func:`~django.shortcuts.get_list_or_404` function, which works just as :func:`~django.shortcuts.get_object_or_404` -- except using @@ -369,7 +420,8 @@ You normally won't have to bother with writing 404 views. If you don't set 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). +settings module). If you do create the template, add at least some dummy +content like "Page not found". A couple more things to note about 404 views: @@ -387,11 +439,14 @@ Similarly, your root URLconf may define a ``handler500``, which points to a view to call in case of server errors. Server errors happen when you have runtime errors in view code. +Likewise, you should create a ``500.html`` template at the root of your +template directory and add some content like "Something went wrong". + Use the template system ======================= Back to the ``detail()`` view for our poll application. Given the context -variable ``poll``, here's what the "polls/detail.html" template might look +variable ``poll``, here's what the ``polls/detail.html`` template might look like: .. code-block:: html+django @@ -416,75 +471,67 @@ suitable for use in the :ttag:`{% for %}` tag. See the :doc:`template guide ` for more about templates. -Simplifying the URLconfs -======================== +Removing hardcoded URLs in templates +==================================== -Take some time to play around with the views and template system. As you edit -the URLconf, you may notice there's a fair bit of redundancy in it:: +Remember, when we wrote the link to a poll in the ``polls/index.html`` +template, the link was partially hardcoded like this: - urlpatterns = patterns('', - url(r'^polls/$', 'polls.views.index'), - url(r'^polls/(?P\d+)/$', 'polls.views.detail'), - url(r'^polls/(?P\d+)/results/$', 'polls.views.results'), - url(r'^polls/(?P\d+)/vote/$', 'polls.views.vote'), - ) +.. code-block:: html+django -Namely, ``polls.views`` is in every callback. +
  • {{ poll.question }}
  • -Because this is a common case, the URLconf framework provides a shortcut for -common prefixes. You can factor out the common prefixes and add them as the -first argument to :func:`~django.conf.urls.patterns`, like so:: +The problem with this hardcoded, tightly-coupled approach is that it becomes +challenging to change URLs on projects with a lot of templates. However, since +you defined the name argument in the :func:`~django.conf.urls.url` functions in +the ``polls.urls`` module, you can remove a reliance on specific URL paths +defined in your url configurations by using the ``{% url %}`` template tag: - urlpatterns = patterns('polls.views', - url(r'^polls/$', 'index'), - url(r'^polls/(?P\d+)/$', 'detail'), - url(r'^polls/(?P\d+)/results/$', 'results'), - url(r'^polls/(?P\d+)/vote/$', 'vote'), - ) +.. code-block:: html+django -This is functionally identical to the previous formatting. It's just a bit -tidier. +
  • {{ poll.question }}
  • -Since you generally don't want the prefix for one app to be applied to every -callback in your URLconf, you can concatenate multiple -:func:`~django.conf.urls.patterns`. Your full ``mysite/urls.py`` might -now look like this:: +.. note:: - from django.conf.urls import patterns, include, url + If ``{% url 'detail' poll.id %}`` (with quotes) doesn't work, but + ``{% url detail poll.id %}`` (without quotes) does, that means you're + using a version of Django < 1.5. In this case, add the following + declaration at the top of your template: - from django.contrib import admin - admin.autodiscover() + .. code-block:: html+django - urlpatterns = patterns('polls.views', - url(r'^polls/$', 'index'), - url(r'^polls/(?P\d+)/$', 'detail'), - url(r'^polls/(?P\d+)/results/$', 'results'), - url(r'^polls/(?P\d+)/vote/$', 'vote'), - ) + {% load url from future %} - urlpatterns += patterns('', - url(r'^admin/', include(admin.site.urls)), - ) +The way this works is by looking up the URL definition as specified in the +``polls.urls`` module. You can see exactly where the URL name of 'detail' is +defined below:: -Decoupling the URLconfs -======================= + ... + # the 'name' value as called by the {% url %} template tag + url(r'^(?P\d+)/$', views.detail, name='detail'), + ... -While we're at it, we should take the time to decouple our poll-app URLs from -our Django project configuration. Django apps are meant to be pluggable -- that -is, each particular app should be transferable to another Django installation -with minimal fuss. +If you want to change the URL of the polls detail view to something else, +perhaps to something like ``polls/specifics/12/`` instead of doing it in the +template (or templates) you would change it in ``polls/urls.py``:: -Our poll app is pretty decoupled at this point, thanks to the strict directory -structure that ``python manage.py startapp`` created, but one part of it is -coupled to the Django settings: The URLconf. + ... + # added the word 'specifics' + url(r'^specifics/(?P\d+)/$', views.detail, name='detail'), + ... -We've been editing the URLs in ``mysite/urls.py``, but the URL design of an -app is specific to the app, not to the Django installation -- so let's move the -URLs within the app directory. +Namespacing URL names +====================== -Copy the file ``mysite/urls.py`` to ``polls/urls.py``. Then, change -``mysite/urls.py`` to remove the poll-specific URLs and insert an -:func:`~django.conf.urls.include`, leaving you with:: +The tutorial project has just one app, ``polls``. In real Django projects, +there might be five, ten, twenty apps or more. How does Django differentiate +the URL names between them? For example, the ``polls`` app has a ``detail`` +view, and so might an app on the same project that is for a blog. How does one +make it so that Django knows which app view to create for a url when using the +``{% url %}`` template tag? + +The answer is to add namespaces to your root URLconf. In the +``mysite/urls.py`` file, go ahead and change it to include namespacing:: from django.conf.urls import patterns, include, url @@ -492,74 +539,21 @@ Copy the file ``mysite/urls.py`` to ``polls/urls.py``. Then, change admin.autodiscover() urlpatterns = patterns('', - url(r'^polls/', include('polls.urls')), + url(r'^polls/', include('polls.urls', namespace="polls")), url(r'^admin/', include(admin.site.urls)), ) -:func:`~django.conf.urls.include` simply references another URLconf. -Note that the regular expression doesn't have a ``$`` (end-of-string match -character) but has the trailing slash. Whenever Django encounters -: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. - -Here's what happens if a user goes to "/polls/34/" in this system: - -* Django will find the match at ``'^polls/'`` - -* Then, Django will strip off the matching text (``"polls/"``) and send the - remaining text -- ``"34/"`` -- to the 'polls.urls' URLconf for - further processing. - -Now that we've decoupled that, we need to decouple the ``polls.urls`` -URLconf by removing the leading "polls/" from each line, removing the -lines registering the admin site, and removing the ``include`` import which -is no longer used. Your ``polls/urls.py`` file should now look like -this:: - - from django.conf.urls import patterns, url - - urlpatterns = patterns('polls.views', - url(r'^$', 'index'), - url(r'^(?P\d+)/$', 'detail'), - url(r'^(?P\d+)/results/$', 'results'), - url(r'^(?P\d+)/vote/$', 'vote'), - ) - -The idea behind :func:`~django.conf.urls.include` and URLconf -decoupling is to make it easy to plug-and-play URLs. Now that polls are in their -own URLconf, they can be placed under "/polls/", or under "/fun_polls/", or -under "/content/polls/", or any other path root, and the app will still work. - -All the poll app cares about is its relative path, not its absolute path. - -Removing hardcoded URLs in templates ------------------------------------- - -Remember, when we wrote the link to a poll in our template, the link was -partially hardcoded like this: +Now change your ``polls/index.html`` template from: .. code-block:: html+django -
  • {{ poll.question }}
  • +
  • {{ poll.question }}
  • -To use the decoupled URLs we've just introduced, replace the hardcoded link -with the :ttag:`url` template tag: +to point at the namespaced detail view: .. code-block:: html+django -
  • {{ poll.question }}
  • - -.. note:: - - 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.5. In this case, add the - following declaration at the top of your template: - - .. code-block:: html+django - - {% load url from future %} +
  • {{ poll.question }}
  • When you're comfortable with writing views, read :doc:`part 4 of this tutorial ` to learn about simple form processing and generic views. diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 49e597ca29..8909caf98b 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -18,7 +18,7 @@ tutorial, so that the template contains an HTML ``
    `` element: {% if error_message %}

    {{ error_message }}

    {% endif %} - + {% csrf_token %} {% for choice in poll.choice_set.all %} @@ -35,7 +35,7 @@ A quick rundown: selects one of the radio buttons and submits the form, it'll send the POST data ``choice=3``. This is HTML Forms 101. -* We set the form's ``action`` to ``{% url 'polls.views.vote' poll.id %}``, and we +* We set the form's ``action`` to ``{% url 'polls:vote' poll.id %}``, and we set ``method="post"``. Using ``method="post"`` (as opposed to ``method="get"``) is very important, because the act of submitting this form will alter data server-side. Whenever you create a form that alters @@ -52,34 +52,18 @@ A quick rundown: forms that are targeted at internal URLs should use the :ttag:`{% csrf_token %}` template tag. -The :ttag:`{% csrf_token %}` tag requires information from the -request object, which is not normally accessible from within the template -context. To fix this, a small adjustment needs to be made to the ``detail`` -view, so that it looks like the following:: - - from django.template import RequestContext - # ... - def detail(request, poll_id): - p = get_object_or_404(Poll, pk=poll_id) - return render_to_response('polls/detail.html', {'poll': p}, - context_instance=RequestContext(request)) - -The details of how this works are explained in the documentation for -:ref:`RequestContext `. - Now, let's create a Django view that handles the submitted data and does something with it. Remember, in :doc:`Tutorial 3 `, we created a URLconf for the polls application that includes this line:: - (r'^(?P\d+)/vote/$', 'vote'), + url(r'^(?P\d+)/vote/$', views.vote, name='vote'), We also created a dummy implementation of the ``vote()`` function. Let's create a real version. Add the following to ``polls/views.py``:: - from django.shortcuts import get_object_or_404, render_to_response + from django.shortcuts import get_object_or_404, render from django.http import HttpResponseRedirect, HttpResponse from django.core.urlresolvers import reverse - from django.template import RequestContext from polls.models import Choice, Poll # ... def vote(request, poll_id): @@ -88,17 +72,17 @@ create a real version. Add the following to ``polls/views.py``:: selected_choice = p.choice_set.get(pk=request.POST['choice']) except (KeyError, Choice.DoesNotExist): # Redisplay the poll voting form. - return render_to_response('polls/detail.html', { + return render(request, 'polls/detail.html', { 'poll': p, 'error_message': "You didn't select a choice.", - }, context_instance=RequestContext(request)) + }) else: selected_choice.votes += 1 selected_choice.save() # Always return an HttpResponseRedirect after successfully dealing # with POST data. This prevents data from being posted twice if a # user hits the Back button. - return HttpResponseRedirect(reverse('polls.views.results', args=(p.id,))) + return HttpResponseRedirect(reverse('polls:results', args=(p.id,))) This code includes a few things we haven't covered yet in this tutorial: @@ -142,8 +126,7 @@ This code includes a few things we haven't covered yet in this tutorial: '/polls/3/results/' ... where the ``3`` is the value of ``p.id``. This redirected URL will - then call the ``'results'`` view to display the final page. Note that you - need to use the full name of the view here (including the prefix). + then call the ``'results'`` view to display the final page. As mentioned in Tutorial 3, ``request`` is a :class:`~django.http.HttpRequest` object. For more on :class:`~django.http.HttpRequest` objects, see the @@ -153,14 +136,14 @@ After somebody votes in a poll, the ``vote()`` view redirects to the results page for the poll. Let's write that view:: def results(request, poll_id): - p = get_object_or_404(Poll, pk=poll_id) - return render_to_response('polls/results.html', {'poll': p}) + poll = get_object_or_404(Poll, pk=poll_id) + return render(request, 'polls/results.html', {'poll': poll}) This is almost exactly the same as the ``detail()`` view from :doc:`Tutorial 3 `. The only difference is the template name. We'll fix this redundancy later. -Now, create a ``results.html`` template: +Now, create a ``polls/results.html`` template: .. code-block:: html+django @@ -172,7 +155,7 @@ Now, create a ``results.html`` template: {% endfor %} - Vote again? + Vote again? Now, go to ``/polls/1/`` in your browser and vote in the poll. You should see a results page that gets updated each time you vote. If you submit the form @@ -215,19 +198,7 @@ Read on for details. You should know basic math before you start using a calculator. -First, open the ``polls/urls.py`` URLconf. It looks like this, according to the -tutorial so far:: - - from django.conf.urls import patterns, url - - urlpatterns = patterns('polls.views', - url(r'^$', 'index'), - url(r'^(?P\d+)/$', 'detail'), - url(r'^(?P\d+)/results/$', 'results'), - url(r'^(?P\d+)/vote/$', 'vote'), - ) - -Change it like so:: +First, open the ``polls/urls.py`` URLconf and change it like so:: from django.conf.urls import patterns, url from django.views.generic import DetailView, ListView @@ -239,18 +210,18 @@ Change it like so:: queryset=Poll.objects.order_by('-pub_date')[:5], context_object_name='latest_poll_list', template_name='polls/index.html'), - name='poll_index'), + name='index'), url(r'^(?P\d+)/$', DetailView.as_view( model=Poll, template_name='polls/detail.html'), - name='poll_detail'), + name='detail'), url(r'^(?P\d+)/results/$', DetailView.as_view( model=Poll, template_name='polls/results.html'), - name='poll_results'), - url(r'^(?P\d+)/vote/$', 'polls.views.vote'), + name='results'), + url(r'^(?P\d+)/vote/$', 'polls.views.vote', name='vote'), ) We're using two generic views here: @@ -267,15 +238,6 @@ two views abstract the concepts of "display a list of objects" and ``"pk"``, so we've changed ``poll_id`` to ``pk`` for the generic views. -* We've added the ``name`` argument to the views (e.g. ``name='poll_results'``) - so that we have a way to refer to their URL later on (see the - documentation about :ref:`naming URL patterns - ` for information). We're also using the - :func:`~django.conf.urls.url` function from - :mod:`django.conf.urls` here. It's a good habit to use - :func:`~django.conf.urls.url` when you are providing a - pattern name like this. - By default, the :class:`~django.views.generic.list.DetailView` generic view uses a template called ``/_detail.html``. In our case, it'll use the template ``"polls/poll_detail.html"``. The @@ -308,41 +270,13 @@ You can now delete the ``index()``, ``detail()`` and ``results()`` views from ``polls/views.py``. We don't need them anymore -- they have been replaced by generic views. -The last thing to do is fix the URL handling to account for the use of -generic views. In the vote view above, we used the -:func:`~django.core.urlresolvers.reverse` function to avoid -hard-coding our URLs. Now that we've switched to a generic view, we'll -need to change the :func:`~django.core.urlresolvers.reverse` call to -point back to our new generic view. We can't simply use the view -function anymore -- generic views can be (and are) used multiple times --- but we can use the name we've given:: - - return HttpResponseRedirect(reverse('poll_results', args=(p.id,))) - -The same rule apply for the :ttag:`url` template tag. For example in the -``results.html`` template: - -.. code-block:: html+django - - Vote again? - Run the server, and use your new polling app based on generic views. For full details on generic views, see the :doc:`generic views documentation `. -Coming soon -=========== - -The tutorial ends here for the time being. Future installments of the tutorial -will cover: - -* Advanced form processing -* Using the RSS framework -* Using the cache framework -* Using the comments framework -* Advanced admin features: Permissions -* Advanced admin features: Custom JavaScript +What's next? +============ -In the meantime, you might want to check out some pointers on :doc:`where to go -from here ` +The tutorial ends here for the time being. In the meantime, you might want to +check out some pointers on :doc:`where to go from here `. -- cgit v1.3 From 9190d89829c4e0b9b0f36e1c717ea451a1a13efd Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 15 Oct 2012 19:54:37 -0400 Subject: Fixed #10936 - Tempered recommendation of SQLite - thanks Karen Tracey for the feedback. --- docs/topics/install.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/topics/install.txt b/docs/topics/install.txt index 0ee4113c04..52994ed16a 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -82,10 +82,12 @@ sure a database server is running. Django supports many different database servers and is officially supported with PostgreSQL_, MySQL_, Oracle_ and 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. +If you are developing a simple project or something you don't plan to deploy +in a production environment, SQLite is generally the simplest option as it +doesn't require running a separate server. However, SQLite has many differences +from other databases, so if you are working on something substantial, it's +recommended to develop with the same database as you plan on using in +production. In addition to the officially supported databases, there are backends provided by 3rd parties that allow you to use other databases with Django: -- cgit v1.3 From 8f94d282232db238e4fca9aece6aae5acd6d7d1c Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 16 Oct 2012 09:02:12 +0200 Subject: Fixed #19128 -- Reworded admonition about Jython and Django 1.5 Thanks adam@hopelessgeek.com for the report. --- docs/howto/jython.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/howto/jython.txt b/docs/howto/jython.txt index 762250212a..461a5d3804 100644 --- a/docs/howto/jython.txt +++ b/docs/howto/jython.txt @@ -6,9 +6,10 @@ Running Django on Jython .. admonition:: Python 2.6 support - Django 1.5 has dropped support for Python 2.5. Until Jython provides a new - version that supports 2.6, Django 1.5 is no more compatible with Jython. - Please use Django 1.4 if you want to use Django over Jython. + Django 1.5 has dropped support for Python 2.5. Therefore, you have to use + a Jython 2.7 alpha release if you want to use Django 1.5 with Jython. + Please use Django 1.4 if you want to keep using Django on a stable Jython + version. Jython_ is an implementation of Python that runs on the Java platform (JVM). Django runs cleanly on Jython version 2.5 or later, which means you can deploy -- cgit v1.3 From fd02bcff4aee885d395f2439efdd522c22e40794 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 16 Oct 2012 20:39:13 -0400 Subject: Fixed #18548 - Clarified note regarding reusing model instances when form validation fails. --- docs/topics/forms/modelforms.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index caff03c581..692be7cd7c 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -202,7 +202,7 @@ of cleaning the model you pass to the ``ModelForm`` constructor. For instance, calling ``is_valid()`` on your form will convert any date fields on your model to actual date objects. If form validation fails, only some of the updates may be applied. For this reason, you'll probably want to avoid reusing the -model instance. +model instance passed to the form, especially if validation fails. The ``save()`` method -- cgit v1.3 From 3e0857041b6bfc9deef392315c978abede706c92 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 17 Oct 2012 07:03:40 -0400 Subject: Fixed #18473 - Fixed a suggestion that GZipMiddleware needs to be first in the list of middleware. --- docs/ref/middleware.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 0ce4177e00..b542aee6e2 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -93,8 +93,8 @@ GZip middleware Compresses content for browsers that understand GZip compression (all modern browsers). -It is suggested to place this first in the middleware list, so that the -compression of the response content is the last thing that happens. +This middleware should be placed before any other middleware that need to +read or write the response body so that compression happens afterward. It will NOT compress content if any of the following are true: -- cgit v1.3 From 31dcaf49a0ed6bda13a6d556412b6993a9bd41ba Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Wed, 17 Oct 2012 14:53:21 -0700 Subject: Fixed an error in cookie documentation --- docs/ref/request-response.txt | 6 +----- docs/topics/http/sessions.txt | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'docs') diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 90872a6feb..0a337eba42 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -651,11 +651,7 @@ Methods Returns ``True`` or ``False`` based on a case-insensitive check for a header with the given name. -.. method:: HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=True) - - .. versionchanged:: 1.4 - - The default value for httponly was changed from ``False`` to ``True``. +.. method:: HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False) Sets a cookie. The parameters are the same as in the :class:`Cookie.Morsel` object in the Python standard library. diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index 1f55293413..15f9f7feba 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -524,6 +524,9 @@ consistently by all browsers. However, when it is honored, it can be a useful way to mitigate the risk of client side script accessing the protected cookie data. +.. versionchanged:: 1.4 + The default value of the setting was changed from ``False`` to ``True``. + .. _HTTPOnly: https://www.owasp.org/index.php/HTTPOnly SESSION_COOKIE_NAME -- cgit v1.3 From 0775ab295566ccb306b8ae6340d2690c3d0aa6af Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 18 Oct 2012 08:57:21 +0200 Subject: Fixed #19132 -- Added example for creating custom lazy function Thanks flagzeta@yahoo.it for the report and Luke Plant for his expert assistance. --- docs/topics/i18n/translation.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'docs') diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index aaf728b1af..65c6fe2445 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -427,6 +427,24 @@ In this case, the lazy translations in ``result`` will only be converted to strings when ``result`` itself is used in a string (usually at template rendering time). +Other uses of lazy in delayed translations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For any other case where you would like to delay the translation, but have to +pass the translatable string as argument to another function, you can wrap +this function inside a lazy call yourself. For example:: + + from django.utils import six # Python 3 compatibility + from django.utils.functional import lazy + from django.utils.safestring import mark_safe + from django.utils.translation import ugettext_lazy as _ + + mark_safe_lazy = lazy(mark_safe, six.text_type) + +And then later:: + + lazy_string = mark_safe_lazy(_("

    My string!

    ")) + Localized names of languages ---------------------------- -- cgit v1.3 From db598dd8a053fe17c3308f89cd8f40676e2c479e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 16 Oct 2012 16:12:52 -0400 Subject: Fixed #18046 - Documented than an index is created by default for ForeignKeys; thanks jbauer for the suggestion. --- docs/ref/models/fields.txt | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'docs') diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 02d8453b83..809d56eaf5 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -971,6 +971,12 @@ need to use:: This sort of reference can be useful when resolving circular import dependencies between two applications. +A database index is automatically created on the ``ForeignKey``. You can +disable this by setting :attr:`~Field.db_index` to ``False``. You may want to +avoid the overhead of an index if you are creating a foreign key for +consistency rather than joins, or if you will be creating an alternative index +like a partial or multiple column index. + Database Representation ~~~~~~~~~~~~~~~~~~~~~~~ -- cgit v1.3 From 4cef9a09f9b0d89abf323a1cf8b9e8354e316c18 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 19 Oct 2012 06:52:30 -0400 Subject: Fixed #17388 - Noted in the custom model field docs that field methods need to handle None if the field may be null. --- docs/howto/custom-model-fields.txt | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'docs') diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index 9ff06479c6..1e9d5d8701 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -448,6 +448,13 @@ called when it is created, you should be using `The SubfieldBase metaclass`_ mentioned earlier. Otherwise :meth:`.to_python` won't be called automatically. +.. warning:: + + If your custom field allows ``null=True``, any field method that takes + ``value`` as an argument, like :meth:`~Field.to_python` and + :meth:`~Field.get_prep_value`, should handle the case when ``value`` is + ``None``. + Converting Python objects to query values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- cgit v1.3 From 3084b1cfd6110e2ef57f1a67f8b7dbda309e2e13 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 19 Oct 2012 18:19:17 +0200 Subject: Separated GIS installation docs in sections --- .../ref/contrib/gis/create_template_postgis-1.3.sh | 9 - .../ref/contrib/gis/create_template_postgis-1.4.sh | 9 - .../ref/contrib/gis/create_template_postgis-1.5.sh | 10 - .../contrib/gis/create_template_postgis-debian.sh | 44 - docs/ref/contrib/gis/geodjango_setup.bat | 8 - docs/ref/contrib/gis/index.txt | 2 +- docs/ref/contrib/gis/install.txt | 1311 -------------------- .../gis/install/create_template_postgis-1.3.sh | 9 + .../gis/install/create_template_postgis-1.4.sh | 9 + .../gis/install/create_template_postgis-1.5.sh | 10 + .../gis/install/create_template_postgis-debian.sh | 44 + docs/ref/contrib/gis/install/geodjango_setup.bat | 8 + docs/ref/contrib/gis/install/geolibs.txt | 282 +++++ docs/ref/contrib/gis/install/index.txt | 535 ++++++++ docs/ref/contrib/gis/install/postgis.txt | 175 +++ docs/ref/contrib/gis/install/spatialite.txt | 222 ++++ 16 files changed, 1295 insertions(+), 1392 deletions(-) delete mode 100755 docs/ref/contrib/gis/create_template_postgis-1.3.sh delete mode 100755 docs/ref/contrib/gis/create_template_postgis-1.4.sh delete mode 100755 docs/ref/contrib/gis/create_template_postgis-1.5.sh delete mode 100755 docs/ref/contrib/gis/create_template_postgis-debian.sh delete mode 100644 docs/ref/contrib/gis/geodjango_setup.bat delete mode 100644 docs/ref/contrib/gis/install.txt create mode 100755 docs/ref/contrib/gis/install/create_template_postgis-1.3.sh create mode 100755 docs/ref/contrib/gis/install/create_template_postgis-1.4.sh create mode 100755 docs/ref/contrib/gis/install/create_template_postgis-1.5.sh create mode 100755 docs/ref/contrib/gis/install/create_template_postgis-debian.sh create mode 100644 docs/ref/contrib/gis/install/geodjango_setup.bat create mode 100644 docs/ref/contrib/gis/install/geolibs.txt create mode 100644 docs/ref/contrib/gis/install/index.txt create mode 100644 docs/ref/contrib/gis/install/postgis.txt create mode 100644 docs/ref/contrib/gis/install/spatialite.txt (limited to 'docs') diff --git a/docs/ref/contrib/gis/create_template_postgis-1.3.sh b/docs/ref/contrib/gis/create_template_postgis-1.3.sh deleted file mode 100755 index c9ab4fcebf..0000000000 --- a/docs/ref/contrib/gis/create_template_postgis-1.3.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -POSTGIS_SQL_PATH=`pg_config --sharedir` -createdb -E UTF8 template_postgis # Create the template spatial database. -createlang -d template_postgis plpgsql # Adding PLPGSQL language support. -psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" -psql -d template_postgis -f $POSTGIS_SQL_PATH/lwpostgis.sql # Loading the PostGIS SQL routines -psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql -psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" # Enabling users to alter spatial tables. -psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" diff --git a/docs/ref/contrib/gis/create_template_postgis-1.4.sh b/docs/ref/contrib/gis/create_template_postgis-1.4.sh deleted file mode 100755 index 57a1373f96..0000000000 --- a/docs/ref/contrib/gis/create_template_postgis-1.4.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib -createdb -E UTF8 template_postgis # Create the template spatial database. -createlang -d template_postgis plpgsql # Adding PLPGSQL language support. -psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" -psql -d template_postgis -f $POSTGIS_SQL_PATH/postgis.sql # Loading the PostGIS SQL routines -psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql -psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" # Enabling users to alter spatial tables. -psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" diff --git a/docs/ref/contrib/gis/create_template_postgis-1.5.sh b/docs/ref/contrib/gis/create_template_postgis-1.5.sh deleted file mode 100755 index 081b5f2656..0000000000 --- a/docs/ref/contrib/gis/create_template_postgis-1.5.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-1.5 -createdb -E UTF8 template_postgis # Create the template spatial database. -createlang -d template_postgis plpgsql # Adding PLPGSQL language support. -psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" -psql -d template_postgis -f $POSTGIS_SQL_PATH/postgis.sql # Loading the PostGIS SQL routines -psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql -psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" # Enabling users to alter spatial tables. -psql -d template_postgis -c "GRANT ALL ON geography_columns TO PUBLIC;" -psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" diff --git a/docs/ref/contrib/gis/create_template_postgis-debian.sh b/docs/ref/contrib/gis/create_template_postgis-debian.sh deleted file mode 100755 index 3e621837fa..0000000000 --- a/docs/ref/contrib/gis/create_template_postgis-debian.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -GEOGRAPHY=0 -POSTGIS_SQL=postgis.sql - -# For Ubuntu 8.x and 9.x releases. -if [ -d "/usr/share/postgresql-8.3-postgis" ] -then - POSTGIS_SQL_PATH=/usr/share/postgresql-8.3-postgis - POSTGIS_SQL=lwpostgis.sql -fi - -# For Ubuntu 10.04 -if [ -d "/usr/share/postgresql/8.4/contrib" ] -then - POSTGIS_SQL_PATH=/usr/share/postgresql/8.4/contrib -fi - -# For Ubuntu 10.10 (with PostGIS 1.5) -if [ -d "/usr/share/postgresql/8.4/contrib/postgis-1.5" ] -then - POSTGIS_SQL_PATH=/usr/share/postgresql/8.4/contrib/postgis-1.5 - GEOGRAPHY=1 -fi - -# For Ubuntu 11.10 / Linux Mint 12 (with PostGIS 1.5) -if [ -d "/usr/share/postgresql/9.1/contrib/postgis-1.5" ] -then - POSTGIS_SQL_PATH=/usr/share/postgresql/9.1/contrib/postgis-1.5 - GEOGRAPHY=1 -fi - -createdb -E UTF8 template_postgis && \ -( createlang -d template_postgis -l | grep plpgsql || createlang -d template_postgis plpgsql ) && \ -psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" && \ -psql -d template_postgis -f $POSTGIS_SQL_PATH/$POSTGIS_SQL && \ -psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql && \ -psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" && \ -psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" - -if [ $GEOGRAPHY -eq 1 ] -then - psql -d template_postgis -c "GRANT ALL ON geography_columns TO PUBLIC;" -fi diff --git a/docs/ref/contrib/gis/geodjango_setup.bat b/docs/ref/contrib/gis/geodjango_setup.bat deleted file mode 100644 index b3e6cc6822..0000000000 --- a/docs/ref/contrib/gis/geodjango_setup.bat +++ /dev/null @@ -1,8 +0,0 @@ -set OSGEO4W_ROOT=C:\OSGeo4W -set PYTHON_ROOT=C:\Python27 -set GDAL_DATA=%OSGEO4W_ROOT%\share\gdal -set PROJ_LIB=%OSGEO4W_ROOT%\share\proj -set PATH=%PATH%;%PYTHON_ROOT%;%OSGEO4W_ROOT%\bin -reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path /t REG_EXPAND_SZ /f /d "%PATH%" -reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v GDAL_DATA /t REG_EXPAND_SZ /f /d "%GDAL_DATA%" -reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PROJ_LIB /t REG_EXPAND_SZ /f /d "%PROJ_LIB%" diff --git a/docs/ref/contrib/gis/index.txt b/docs/ref/contrib/gis/index.txt index 1b1e7688d0..6a1402bfab 100644 --- a/docs/ref/contrib/gis/index.txt +++ b/docs/ref/contrib/gis/index.txt @@ -15,7 +15,7 @@ of spatially enabled data. :maxdepth: 2 tutorial - install + install/index model-api db-api geoquerysets diff --git a/docs/ref/contrib/gis/install.txt b/docs/ref/contrib/gis/install.txt deleted file mode 100644 index 355fb55a47..0000000000 --- a/docs/ref/contrib/gis/install.txt +++ /dev/null @@ -1,1311 +0,0 @@ -.. _ref-gis-install: - -====================== -GeoDjango Installation -====================== - -.. highlight:: console - -Overview -======== -In general, GeoDjango installation requires: - -1. :ref:`Python and Django ` -2. :ref:`spatial_database` -3. :ref:`geospatial_libs` - -Details for each of the requirements and installation instructions -are provided in the sections below. In addition, platform-specific -instructions are available for: - -* :ref:`macosx` -* :ref:`ubuntudebian` -* :ref:`windows` - -.. admonition:: Use the Source - - Because GeoDjango takes advantage of the latest in the open source geospatial - software technology, recent versions of the libraries are necessary. - If binary packages aren't available for your platform, - :ref:`installation from source ` - may be required. When compiling the libraries from source, please follow the - directions closely, especially if you're a beginner. - -Requirements -============ - -.. _django: - -Python and Django ------------------ - -Because GeoDjango is included with Django, please refer to Django's -:ref:`installation instructions ` for details on -how to install. - - -.. _spatial_database: - -Spatial database ----------------- -PostgreSQL (with PostGIS), MySQL, Oracle, and SQLite (with SpatiaLite) are -the spatial databases currently supported. - -.. note:: - - PostGIS is recommended, because it is the most mature and feature-rich - open source spatial database. - -The geospatial libraries required for a GeoDjango installation depends -on the spatial database used. The following lists the library requirements, -supported versions, and any notes for each of the supported database backends: - -================== ============================== ================== ========================================= -Database Library Requirements Supported Versions Notes -================== ============================== ================== ========================================= -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+ -================== ============================== ================== ========================================= - -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 --------------------- -GeoDjango uses and/or provides interfaces for the following open source -geospatial libraries: - -======================== ==================================== ================================ ========================== -Program Description Required Supported Versions -======================== ==================================== ================================ ========================== -:ref:`GEOS ` Geometry Engine Open Source Yes 3.3, 3.2, 3.1, 3.0 -`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) 2.0, 1.5, 1.4, 1.3 -`SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 3.0, 2.4, 2.3 -======================== ==================================== ================================ ========================== - -.. admonition:: Install GDAL - - While :ref:`gdalbuild` is technically not required, it is *recommended*. - Important features of GeoDjango (including the :ref:`ref-layermapping`, - geometry reprojection, and the geographic admin) depend on its - functionality. - -.. note:: - - The GeoDjango interfaces to GEOS, GDAL, and GeoIP may be used - independently of Django. In other words, no database or settings file - required -- just import them as normal from :mod:`django.contrib.gis`. - -.. _PROJ.4: http://trac.osgeo.org/proj/ -__ http://postgis.refractions.net/ -__ http://www.gaia-gis.it/gaia-sins/ - -.. _build_from_source: - -Building from source -==================== - -When installing from source on UNIX and GNU/Linux systems, please follow -the installation instructions carefully, and install the libraries in the -given order. If using MySQL or Oracle as the spatial database, only GEOS -is required. - -.. note:: - - On Linux platforms, it may be necessary to run the ``ldconfig`` - command after installing each library. For example:: - - $ sudo make install - $ sudo ldconfig - -.. note:: - - OS X users are required to install `Apple Developer Tools`_ in order - to compile software from source. This is typically included on your - OS X installation DVDs. - -.. _Apple Developer Tools: https://developer.apple.com/technologies/tools/ - -.. _geosbuild: - -GEOS ----- - -GEOS is a C++ library for performing geometric operations, and is the default -internal geometry representation used by GeoDjango (it's behind the "lazy" -geometries). Specifically, the C API library is called (e.g., ``libgeos_c.so``) -directly from Python using ctypes. - -First, download GEOS 3.3.5 from the refractions Web site and untar the source -archive:: - - $ wget http://download.osgeo.org/geos/geos-3.3.5.tar.bz2 - $ tar xjf geos-3.3.5.tar.bz2 - -Next, change into the directory where GEOS was unpacked, run the configure -script, compile, and install:: - - $ cd geos-3.3.5 - $ ./configure - $ make - $ sudo make install - $ cd .. - -Troubleshooting -^^^^^^^^^^^^^^^ - -Can't find GEOS library -~~~~~~~~~~~~~~~~~~~~~~~ - -When GeoDjango can't find GEOS, this error is raised: - -.. code-block:: text - - ImportError: Could not find the GEOS library (tried "geos_c"). Try setting GEOS_LIBRARY_PATH in your settings. - -The most common solution is to properly configure your :ref:`libsettings` *or* set -:ref:`geoslibrarypath` in your settings. - -If using a binary package of GEOS (e.g., on Ubuntu), you may need to :ref:`binutils`. - -.. _geoslibrarypath: - -``GEOS_LIBRARY_PATH`` -~~~~~~~~~~~~~~~~~~~~~ - -If your GEOS library is in a non-standard location, or you don't want to -modify the system's library path then the :setting:`GEOS_LIBRARY_PATH` -setting may be added to your Django settings file with the full path to the -GEOS C library. For example: - -.. code-block:: python - - GEOS_LIBRARY_PATH = '/home/bob/local/lib/libgeos_c.so' - -.. note:: - - The setting must be the *full* path to the **C** shared library; in - other words you want to use ``libgeos_c.so``, not ``libgeos.so``. - -See also :ref:`My logs are filled with GEOS-related errors `. - -.. _proj4: - -PROJ.4 ------- - -`PROJ.4`_ is a library for converting geospatial data to different coordinate -reference systems. - -First, download the PROJ.4 source code and datum shifting files [#]_:: - - $ wget http://download.osgeo.org/proj/proj-4.8.0.tar.gz - $ wget http://download.osgeo.org/proj/proj-datumgrid-1.5.tar.gz - -Next, untar the source code archive, and extract the datum shifting files in the -``nad`` subdirectory. This must be done *prior* to configuration:: - - $ tar xzf proj-4.8.0.tar.gz - $ cd proj-4.8.0/nad - $ tar xzf ../../proj-datumgrid-1.5.tar.gz - $ cd .. - -Finally, configure, make and install PROJ.4:: - - $ ./configure - $ make - $ sudo make install - $ cd .. - -.. _gdalbuild: - -GDAL ----- - -`GDAL`__ is an excellent open source geospatial library that has support for -reading most vector and raster spatial data formats. Currently, GeoDjango only -supports :ref:`GDAL's vector data ` capabilities [#]_. -:ref:`geosbuild` and :ref:`proj4` should be installed prior to building GDAL. - -First download the latest GDAL release version and untar the archive:: - - $ wget http://download.osgeo.org/gdal/gdal-1.9.1.tar.gz - $ tar xzf gdal-1.9.1.tar.gz - $ cd gdal-1.9.1 - -Configure, make and install:: - - $ ./configure - $ make # Go get some coffee, this takes a while. - $ sudo make install - $ cd .. - -.. note:: - - Because GeoDjango has it's own Python interface, the preceding instructions - do not build GDAL's own Python bindings. The bindings may be built by - adding the ``--with-python`` flag when running ``configure``. See - `GDAL/OGR In Python`__ for more information on GDAL's bindings. - -If you have any problems, please see the troubleshooting section below for -suggestions and solutions. - -__ http://trac.osgeo.org/gdal/ -__ http://trac.osgeo.org/gdal/wiki/GdalOgrInPython - -.. _gdaltrouble: - -Troubleshooting -^^^^^^^^^^^^^^^ - -Can't find GDAL library -~~~~~~~~~~~~~~~~~~~~~~~ - -When GeoDjango can't find the GDAL library, the ``HAS_GDAL`` flag -will be false: - -.. code-block:: pycon - - >>> from django.contrib.gis import gdal - >>> gdal.HAS_GDAL - False - -The solution is to properly configure your :ref:`libsettings` *or* set -:ref:`gdallibrarypath` in your settings. - -.. _gdallibrarypath: - -``GDAL_LIBRARY_PATH`` -~~~~~~~~~~~~~~~~~~~~~ - -If your GDAL library is in a non-standard location, or you don't want to -modify the system's library path then the :setting:`GDAL_LIBRARY_PATH` -setting may be added to your Django settings file with the full path to -the GDAL library. For example: - -.. code-block:: python - - GDAL_LIBRARY_PATH = '/home/sue/local/lib/libgdal.so' - -.. _gdaldata: - -Can't find GDAL data files (``GDAL_DATA``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When installed from source, GDAL versions 1.5.1 and below have an autoconf bug -that places data in the wrong location. [#]_ This can lead to error messages -like this: - -.. code-block:: text - - ERROR 4: Unable to open EPSG support file gcs.csv. - ... - OGRException: OGR failure. - -The solution is to set the ``GDAL_DATA`` environment variable to the location of the -GDAL data files before invoking Python (typically ``/usr/local/share``; use -``gdal-config --datadir`` to find out). For example:: - - $ export GDAL_DATA=`gdal-config --datadir` - $ python manage.py shell - -If using Apache, you may need to add this environment variable to your configuration -file: - -.. code-block:: apache - - 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 ----------- - -.. note:: - - Mac OS X users should follow the instructions in the :ref:`kyngchaos` section, - as it is much easier than building from source. - -`SpatiaLite`__ adds spatial support to SQLite, turning it into a full-featured -spatial database. Because SpatiaLite has special requirements, it typically -requires SQLite and pysqlite2 (the Python SQLite DB-API adaptor) to be built from -source. :ref:`geosbuild` and :ref:`proj4` should be installed prior to building -SpatiaLite. - -After installation is complete, don't forget to read the post-installation -docs on :ref:`create_spatialite_db`. - -__ http://www.gaia-gis.it/gaia-sins/ - -.. _sqlite: - -SQLite -^^^^^^ - -Typically, SQLite packages are not compiled to include the `R*Tree module`__ -- -thus it must be compiled from source. First download the latest amalgamation -source archive from the `SQLite download page`__, and extract:: - - $ wget http://sqlite.org/sqlite-amalgamation-3.6.23.1.tar.gz - $ tar xzf sqlite-amalgamation-3.6.23.1.tar.gz - $ cd sqlite-3.6.23.1 - -Next, run the ``configure`` script -- however the ``CFLAGS`` environment variable -needs to be customized so that SQLite knows to build the R*Tree module:: - - $ CFLAGS="-DSQLITE_ENABLE_RTREE=1" ./configure - $ make - $ sudo make install - $ cd .. - -.. note:: - - If using Ubuntu, installing a newer SQLite from source can be very difficult - because it links to the existing ``libsqlite3.so`` in ``/usr/lib`` which - many other packages depend on. Unfortunately, the best solution at this time - is to overwrite the existing library by adding ``--prefix=/usr`` to the - ``configure`` command. - -__ http://www.sqlite.org/rtree.html -__ http://www.sqlite.org/download.html - -.. _spatialitebuild : - -SpatiaLite library (``libspatialite``) and tools (``spatialite``) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -After SQLite has been built with the R*Tree module enabled, get the latest -SpatiaLite library source and tools bundle from the `download page`__:: - - $ wget http://www.gaia-gis.it/gaia-sins/libspatialite-sources/libspatialite-amalgamation-2.3.1.tar.gz - $ wget http://www.gaia-gis.it/gaia-sins/spatialite-tools-sources/spatialite-tools-2.3.1.tar.gz - $ tar xzf libspatialite-amalgamation-2.3.1.tar.gz - $ tar xzf spatialite-tools-2.3.1.tar.gz - -Prior to attempting to build, please read the important notes below to see if -customization of the ``configure`` command is necessary. If not, then run the -``configure`` script, make, and install for the SpatiaLite library:: - - $ cd libspatialite-amalgamation-2.3.1 - $ ./configure # May need to modified, see notes below. - $ make - $ sudo make install - $ cd .. - -Finally, do the same for the SpatiaLite tools:: - - $ cd spatialite-tools-2.3.1 - $ ./configure # May need to modified, see notes below. - $ make - $ sudo make install - $ cd .. - -.. note:: - - If you've installed GEOS and PROJ.4 from binary packages, you will have to specify - their paths when running the ``configure`` scripts for *both* the library and the - tools (the configure scripts look, by default, in ``/usr/local``). For example, - on Debian/Ubuntu distributions that have GEOS and PROJ.4 packages, the command would be:: - - $ ./configure --with-proj-include=/usr/include --with-proj-lib=/usr/lib --with-geos-include=/usr/include --with-geos-lib=/usr/lib - -.. note:: - - For Mac OS X users building from source, the SpatiaLite library *and* tools - need to have their ``target`` configured:: - - $ ./configure --target=macosx - -__ http://www.gaia-gis.it/gaia-sins/libspatialite-sources/ - -.. _pysqlite2: - -pysqlite2 -^^^^^^^^^ - -Because SpatiaLite must be loaded as an external extension, it requires the -``enable_load_extension`` method, which is only available in versions 2.5+ of -pysqlite2. Thus, download pysqlite2 2.6, and untar:: - - $ wget http://pysqlite.googlecode.com/files/pysqlite-2.6.0.tar.gz - $ tar xzf pysqlite-2.6.0.tar.gz - $ cd pysqlite-2.6.0 - -Next, use a text editor (e.g., ``emacs`` or ``vi``) to edit the ``setup.cfg`` file -to look like the following: - -.. code-block:: ini - - [build_ext] - #define= - include_dirs=/usr/local/include - library_dirs=/usr/local/lib - libraries=sqlite3 - #define=SQLITE_OMIT_LOAD_EXTENSION - -.. note:: - - The important thing here is to make sure you comment out the - ``define=SQLITE_OMIT_LOAD_EXTENSION`` flag and that the ``include_dirs`` - and ``library_dirs`` settings are uncommented and set to the appropriate - path if the SQLite header files and libraries are not in ``/usr/include`` - and ``/usr/lib``, respectively. - -After modifying ``setup.cfg`` appropriately, then run the ``setup.py`` script -to build and install:: - - $ sudo python setup.py install - -Post-installation -================= - -.. _spatialdb_template: -.. _spatialdb_template91: - -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; - -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 ---------------------------------------------------------- - -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 -the steps in this process, it's better to create a database template that -can be reused later. - -First, you need to be able to execute the commands as a privileged database -user. For example, you can use the following to become the ``postgres`` user:: - - $ sudo su - postgres - -.. note:: - - The location *and* name of the PostGIS SQL files (e.g., from - ``POSTGIS_SQL_PATH`` below) depends on the version of PostGIS. - PostGIS versions 1.3 and below use ``/contrib/lwpostgis.sql``; - whereas version 1.4 uses ``/contrib/postgis.sql`` and - version 1.5 uses ``/contrib/postgis-1.5/postgis.sql``. - - To complicate matters, :ref:`ubuntudebian` distributions have their - own separate directory naming system that changes each release. - - The example below assumes PostGIS 1.5, thus you may need to modify - ``POSTGIS_SQL_PATH`` and the name of the SQL file for the specific - version of PostGIS you are using. - -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-2.0 - # Creating the template spatial database. - $ createdb -E UTF8 template_postgis - $ createlang -d template_postgis plpgsql # Adding PLPGSQL language support. - # Allows non-superusers the ability to create from this template - $ psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" - # Loading the PostGIS SQL routines - $ psql -d template_postgis -f $POSTGIS_SQL_PATH/postgis.sql - $ psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql - # Enabling users to alter spatial tables. - $ psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" - $ psql -d template_postgis -c "GRANT ALL ON geography_columns TO PUBLIC;" - $ psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" - -These commands may be placed in a shell script for later use; for convenience -the following scripts are available: - -=============== ============================================= -PostGIS version Bash shell script -=============== ============================================= -1.3 :download:`create_template_postgis-1.3.sh` -1.4 :download:`create_template_postgis-1.4.sh` -1.5 :download:`create_template_postgis-1.5.sh` -Debian/Ubuntu :download:`create_template_postgis-debian.sh` -=============== ============================================= - -Afterwards, you may create a spatial database by simply specifying -``template_postgis`` as the template to use (via the ``-T`` option):: - - $ createdb -T template_postgis - -.. note:: - - While the ``createdb`` command does not require database super-user privileges, - it must be executed by a database user that has permissions to create databases. - You can create such a user with the following command:: - - $ createuser --createdb - -.. _create_spatialite_db: - -Creating a spatial database for SpatiaLite ------------------------------------------- - -After you've installed SpatiaLite, you'll need to create a number of spatial -metadata tables in your database in order to perform spatial queries. - -If you're using SpatiaLite 2.4 or newer, use the ``spatialite`` utility to -call the ``InitSpatialMetaData()`` function, like this:: - - $ spatialite geodjango.db "SELECT InitSpatialMetaData();" - the SPATIAL_REF_SYS table already contains some row(s) - InitSpatiaMetaData ()error:"table spatial_ref_sys already exists" - 0 - -You can safely ignore the error messages shown. When you've done this, you can -skip the rest of this section. - -If you're using SpatiaLite 2.3, you'll need to download a -database-initialization file and execute its SQL queries in your database. - -First, get it from the `SpatiaLite Resources`__ page:: - - $ wget http://www.gaia-gis.it/spatialite-2.3.1/init_spatialite-2.3.sql.gz - $ gunzip init_spatialite-2.3.sql.gz - -Then, use the ``spatialite`` command to initialize a spatial database:: - - $ spatialite geodjango.db < init_spatialite-2.3.sql - -.. note:: - - The parameter ``geodjango.db`` is the *filename* of the SQLite database - you want to use. Use the same in the :setting:`DATABASES` ``"name"`` key - inside your ``settings.py``. - -__ http://www.gaia-gis.it/spatialite-2.3.1/resources.html - -Add ``django.contrib.gis`` to :setting:`INSTALLED_APPS` -------------------------------------------------------- - -Like other Django contrib applications, you will *only* need to add -:mod:`django.contrib.gis` to :setting:`INSTALLED_APPS` in your settings. -This is the so that ``gis`` templates can be located -- if not done, then -features such as the geographic admin or KML sitemaps will not function properly. - -.. _addgoogleprojection: - -Add Google projection to ``spatial_ref_sys`` table --------------------------------------------------- - -.. note:: - - If you're running PostGIS 1.4 or above, you can skip this step. The entry - is already included in the default ``spatial_ref_sys`` table. - -In order to conduct database transformations to the so-called "Google" -projection (a spherical mercator projection used by Google Maps), -an entry must be added to your spatial database's ``spatial_ref_sys`` table. -Invoke the Django shell from your project and execute the -``add_srs_entry`` function: - -.. code-block:: pycon - - $ python manage shell - >>> from django.contrib.gis.utils import add_srs_entry - >>> add_srs_entry(900913) - -This adds an entry for the 900913 SRID to the ``spatial_ref_sys`` (or equivalent) -table, making it possible for the spatial database to transform coordinates in -this projection. You only need to execute this command *once* per spatial database. - -Troubleshooting -=============== - -If you can't find the solution to your problem here then participate in the -community! You can: - -* Join the ``#geodjango`` IRC channel on FreeNode. Please be patient and polite - -- while you may not get an immediate response, someone will attempt to answer - your question as soon as they see it. -* Ask your question on the `GeoDjango`__ mailing list. -* File a ticket on the `Django trac`__ if you think there's a bug. Make - sure to provide a complete description of the problem, versions used, - and specify the component as "GIS". - -__ http://groups.google.com/group/geodjango -__ https://code.djangoproject.com/newticket - -.. _libsettings: - -Library environment settings ----------------------------- - -By far, the most common problem when installing GeoDjango is that the -external shared libraries (e.g., for GEOS and GDAL) cannot be located. [#]_ -Typically, the cause of this problem is that the operating system isn't aware -of the directory where the libraries built from source were installed. - -In general, the library path may be set on a per-user basis by setting -an environment variable, or by configuring the library path for the entire -system. - -``LD_LIBRARY_PATH`` environment variable -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A user may set this environment variable to customize the library paths -they want to use. The typical library directory for software -built from source is ``/usr/local/lib``. Thus, ``/usr/local/lib`` needs -to be included in the ``LD_LIBRARY_PATH`` variable. For example, the user -could place the following in their bash profile:: - - export LD_LIBRARY_PATH=/usr/local/lib - -Setting system library path -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -On GNU/Linux systems, there is typically a file in ``/etc/ld.so.conf``, which may include -additional paths from files in another directory, such as ``/etc/ld.so.conf.d``. -As the root user, add the custom library path (like ``/usr/local/lib``) on a -new line in ``ld.so.conf``. This is *one* example of how to do so:: - - $ sudo echo /usr/local/lib >> /etc/ld.so.conf - $ sudo ldconfig - -For OpenSolaris users, the system library path may be modified using the -``crle`` utility. Run ``crle`` with no options to see the current configuration -and use ``crle -l`` to set with the new library path. Be *very* careful when -modifying the system library path:: - - # crle -l $OLD_PATH:/usr/local/lib - -.. _binutils: - -Install ``binutils`` -^^^^^^^^^^^^^^^^^^^^ - -GeoDjango uses the ``find_library`` function (from the ``ctypes.util`` Python -module) to discover libraries. The ``find_library`` routine uses a program -called ``objdump`` (part of the ``binutils`` package) to verify a shared -library on GNU/Linux systems. Thus, if ``binutils`` is not installed on your -Linux system then Python's ctypes may not be able to find your library even if -your library path is set correctly and geospatial libraries were built perfectly. - -The ``binutils`` package may be installed on Debian and Ubuntu systems using the -following command:: - - $ sudo apt-get install binutils - -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 -============================== - -.. _macosx: - -Mac OS X --------- - -Because of the variety of packaging systems available for OS X, users have -several different options for installing GeoDjango. These options are: - -* :ref:`homebrew` -* :ref:`kyngchaos` -* :ref:`fink` -* :ref:`macports` -* :ref:`build_from_source` - -.. note:: - - Currently, the easiest and recommended approach for installing GeoDjango - on OS X is to use the KyngChaos packages. - -This section also includes instructions for installing an upgraded version -of :ref:`macosx_python` from packages provided by the Python Software -Foundation, however, this is not required. - -.. _macosx_python: - -Python -^^^^^^ - -Although OS X comes with Python installed, users can use framework -installers (`2.6`__ and `2.7`__ are available) provided by -the Python Software Foundation. An advantage to using the installer is -that OS X's Python will remain "pristine" for internal operating system -use. - -__ http://python.org/ftp/python/2.6.6/python-2.6.6-macosx10.3.dmg -__ http://python.org/ftp/python/2.7.3/ - -.. note:: - - You will need to modify the ``PATH`` environment variable in your - ``.profile`` file so that the new version of Python is used when - ``python`` is entered at the command-line:: - - export PATH=/Library/Frameworks/Python.framework/Versions/Current/bin:$PATH - -.. _homebrew: - -Homebrew -^^^^^^^^ - -`Homebrew`__ provides "recipes" for building binaries and packages from source. -It provides recipes for the GeoDjango prerequisites on Macintosh computers -running OS X. Because Homebrew still builds the software from source, the -`Apple Developer Tools`_ are required. - -Summary:: - - $ brew install postgresql - $ brew install postgis - $ brew install gdal - $ brew install libgeoip - -__ http://mxcl.github.com/homebrew/ - -.. _kyngchaos: - -KyngChaos packages -^^^^^^^^^^^^^^^^^^ - -William Kyngesburye provides a number of `geospatial library binary packages`__ -that make it simple to get GeoDjango installed on OS X without compiling -them from source. However, the `Apple Developer Tools`_ are still necessary -for compiling the Python database adapters :ref:`psycopg2_kyngchaos` (for PostGIS) -and :ref:`pysqlite2_kyngchaos` (for SpatiaLite). - -.. note:: - - SpatiaLite users should consult the :ref:`spatialite_kyngchaos` section - after installing the packages for additional instructions. - -Download the framework packages for: - -* UnixImageIO -* PROJ -* GEOS -* SQLite3 (includes the SpatiaLite library) -* GDAL - -Install the packages in the order they are listed above, as the GDAL and SQLite -packages require the packages listed before them. - -Afterwards, you can also install the KyngChaos binary packages for `PostgreSQL -and PostGIS`__. - -After installing the binary packages, you'll want to add the following to -your ``.profile`` to be able to run the package programs from the command-line:: - - export PATH=/Library/Frameworks/UnixImageIO.framework/Programs:$PATH - export PATH=/Library/Frameworks/PROJ.framework/Programs:$PATH - export PATH=/Library/Frameworks/GEOS.framework/Programs:$PATH - export PATH=/Library/Frameworks/SQLite3.framework/Programs:$PATH - export PATH=/Library/Frameworks/GDAL.framework/Programs:$PATH - export PATH=/usr/local/pgsql/bin:$PATH - -__ http://www.kyngchaos.com/software/frameworks -__ http://www.kyngchaos.com/software/postgres - -.. _psycopg2_kyngchaos: - -psycopg2 -~~~~~~~~ - -After you've installed the KyngChaos binaries and modified your ``PATH``, as -described above, ``psycopg2`` may be installed using the following command:: - - $ sudo pip install psycopg2 - -.. note:: - - If you don't have ``pip``, follow the the :ref:`installation instructions - ` to install it. - -.. _pysqlite2_kyngchaos: - -pysqlite2 -~~~~~~~~~ - -Follow the :ref:`pysqlite2` source install instructions, however, -when editing the ``setup.cfg`` use the following instead: - -.. code-block:: ini - - [build_ext] - #define= - include_dirs=/Library/Frameworks/SQLite3.framework/unix/include - library_dirs=/Library/Frameworks/SQLite3.framework/unix/lib - libraries=sqlite3 - #define=SQLITE_OMIT_LOAD_EXTENSION - -.. _spatialite_kyngchaos: - -SpatiaLite -~~~~~~~~~~ - -When :ref:`create_spatialite_db`, the ``spatialite`` program is required. -However, instead of attempting to compile the SpatiaLite tools from source, -download the `SpatiaLite Binaries`__ for OS X, and install ``spatialite`` in a -location available in your ``PATH``. For example:: - - $ curl -O http://www.gaia-gis.it/spatialite/spatialite-tools-osx-x86-2.3.1.tar.gz - $ tar xzf spatialite-tools-osx-x86-2.3.1.tar.gz - $ cd spatialite-tools-osx-x86-2.3.1/bin - $ sudo cp spatialite /Library/Frameworks/SQLite3.framework/Programs - -Finally, for GeoDjango to be able to find the KyngChaos SpatiaLite library, -add the following to your ``settings.py``: - -.. code-block:: python - - SPATIALITE_LIBRARY_PATH='/Library/Frameworks/SQLite3.framework/SQLite3' - -__ http://www.gaia-gis.it/spatialite-2.3.1/binaries.html - -.. _fink: - -Fink -^^^^ - -`Kurt Schwehr`__ has been gracious enough to create GeoDjango packages for users -of the `Fink`__ package system. The following packages are available, depending -on which version of Python you want to use: - -* ``django-gis-py26`` -* ``django-gis-py25`` -* ``django-gis-py24`` - -__ http://schwehr.org/blog/ -__ http://www.finkproject.org/ - -.. _macports: - -MacPorts -^^^^^^^^ - -`MacPorts`__ may be used to install GeoDjango prerequisites on Macintosh -computers running OS X. Because MacPorts still builds the software from source, -the `Apple Developer Tools`_ are required. - -Summary:: - - $ sudo port install postgresql83-server - $ sudo port install geos - $ sudo port install proj - $ sudo port install postgis - $ sudo port install gdal +geos - $ sudo port install libgeoip - -.. note:: - - You will also have to modify the ``PATH`` in your ``.profile`` so - that the MacPorts programs are accessible from the command-line:: - - export PATH=/opt/local/bin:/opt/local/lib/postgresql83/bin - - In addition, add the ``DYLD_FALLBACK_LIBRARY_PATH`` setting so that - the libraries can be found by Python:: - - export DYLD_FALLBACK_LIBRARY_PATH=/opt/local/lib:/opt/local/lib/postgresql83 - -__ http://www.macports.org/ - -.. _ubuntudebian: - -Ubuntu & Debian GNU/Linux -------------------------- - -.. note:: - - The PostGIS SQL files are not placed in the PostgreSQL share directory in - the Debian and Ubuntu packages. Instead, they're located in a special - directory depending on the release. In this case, use the - :download:`create_template_postgis-debian.sh` script - -.. _ubuntu: - -Ubuntu -^^^^^^ - -11.10 through 12.04 -~~~~~~~~~~~~~~~~~~~ - -In Ubuntu 11.10, PostgreSQL was upgraded to 9.1. The installation command is: - -.. code-block:: bash - - $ sudo apt-get install binutils gdal-bin libproj-dev \ - postgresql-9.1-postgis postgresql-server-dev-9.1 python-psycopg2 - -.. _ubuntu10: - -10.04 through 11.04 -~~~~~~~~~~~~~~~~~~~ - -In Ubuntu 10.04, PostgreSQL was upgraded to 8.4 and GDAL was upgraded to 1.6. -Ubuntu 10.04 uses PostGIS 1.4, while Ubuntu 10.10 uses PostGIS 1.5 (with -geography support). The installation command is: - -.. code-block:: bash - - $ sudo apt-get install binutils gdal-bin libproj-dev postgresql-8.4-postgis \ - postgresql-server-dev-8.4 python-psycopg2 - -.. _ibex: - -8.10 -~~~~ - -Use the synaptic package manager to install the following packages: - -.. code-block:: bash - - $ sudo apt-get install binutils gdal-bin postgresql-8.3-postgis \ - postgresql-server-dev-8.3 python-psycopg2 - -That's it! For the curious, the required binary prerequisites packages are: - -* ``binutils``: for ctypes to find libraries -* ``postgresql-8.3`` -* ``postgresql-server-dev-8.3``: for ``pg_config`` -* ``postgresql-8.3-postgis``: for PostGIS 1.3.3 -* ``libgeos-3.0.0``, and ``libgeos-c1``: for GEOS 3.0.0 -* ``libgdal1-1.5.0``: for GDAL 1.5.0 library -* ``proj``: for PROJ 4.6.0 -- but no datum shifting files, see note below -* ``python-psycopg2`` - -Optional packages to consider: - -* ``libgeoip1``: for :ref:`GeoIP ` support -* ``gdal-bin``: for GDAL command line programs like ``ogr2ogr`` -* ``python-gdal`` for GDAL's own Python bindings -- includes interfaces for raster manipulation - -.. note:: - - On this version of Ubuntu the ``proj`` package does not come with the - datum shifting files installed, which will cause problems with the - geographic admin because the ``null`` datum grid is not available for - transforming geometries to the spherical mercator projection. A solution - is to download the datum-shifting files, create the grid file, and - install it yourself: - - .. code-block:: bash - - $ wget http://download.osgeo.org/proj/proj-datumgrid-1.4.tar.gz - $ mkdir nad - $ cd nad - $ tar xzf ../proj-datumgrid-1.4.tar.gz - $ nad2bin null < null.lla - $ sudo cp null /usr/share/proj - - Otherwise, the Ubuntu ``proj`` package is fine for general use as long as you - do not plan on doing any database transformation of geometries to the - Google projection (900913). - -.. _debian: - -Debian ------- - -.. _lenny: - -5.0 (Lenny) -^^^^^^^^^^^ - -This version is comparable to Ubuntu :ref:`ibex`, so the command -is very similar: - -.. code-block:: bash - - $ sudo apt-get install binutils libgdal1-1.5.0 postgresql-8.3 \ - postgresql-8.3-postgis postgresql-server-dev-8.3 \ - python-psycopg2 python-setuptools - -This assumes that you are using PostgreSQL version 8.3. Else, replace ``8.3`` -in the above command with the appropriate PostgreSQL version. - -.. note:: - - Please read the note in the Ubuntu :ref:`ibex` install documentation - about the ``proj`` package -- it also applies here because the package does - not include the datum shifting files. - -.. _post_install: - -Post-installation notes -~~~~~~~~~~~~~~~~~~~~~~~ - -If the PostgreSQL database cluster was not initiated after installing, then it -can be created (and started) with the following command: - -.. code-block:: bash - - $ sudo pg_createcluster --start 8.3 main - -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_earlier` either: - -* Create a symbolic link to these files: - - .. code-block:: bash - - $ sudo ln -s /usr/share/postgresql-8.3-postgis/{lwpostgis,spatial_ref_sys}.sql \ - /usr/share/postgresql/8.3 - - If not running PostgreSQL 8.3, then replace ``8.3`` in the command above with - the correct version. - -* Or use the :download:`create_template_postgis-debian.sh` to create the spatial database. - -.. _windows: - -Windows -------- - -Proceed through the following sections sequentially in order to install -GeoDjango on Windows. - -.. note:: - - These instructions assume that you are using 32-bit versions of - all programs. While 64-bit versions of Python and PostgreSQL 9.0 - are available, 64-bit versions of spatial libraries, like - GEOS and GDAL, are not yet provided by the :ref:`OSGeo4W` installer. - -Python -^^^^^^ - -First, download the latest `Python 2.7 installer`__ from the Python Web site. -Next, run the installer and keep the defaults -- for example, keep -'Install for all users' checked and the installation path set as -``C:\Python27``. - -.. note:: - - You may already have a version of Python installed in ``C:\python`` as ESRI - products sometimes install a copy there. *You should still install a - fresh version of Python 2.7.* - -__ http://python.org/download/ - -PostgreSQL -^^^^^^^^^^ - -First, download the latest `PostgreSQL 9.0 installer`__ from the -`EnterpriseDB`__ Web site. After downloading, simply run the installer, -follow the on-screen directions, and keep the default options unless -you know the consequences of changing them. - -.. note:: - - The PostgreSQL installer creates both a new Windows user to be the - 'postgres service account' and a ``postgres`` database superuser - You will be prompted once to set the password for both accounts -- - make sure to remember it! - -When the installer completes, it will ask to launch the Application Stack -Builder (ASB) on exit -- keep this checked, as it is necessary to -install :ref:`postgisasb`. - -.. note:: - - If installed successfully, the PostgreSQL server will run in the - background each time the system as started as a Windows service. - A :menuselection:`PostgreSQL 9.0` start menu group will created - and contains shortcuts for the ASB as well as the 'SQL Shell', - which will launch a ``psql`` command window. - -__ http://www.enterprisedb.com/products-services-training/pgdownload -__ http://www.enterprisedb.com - -.. _postgisasb: - -PostGIS -^^^^^^^ - -From within the Application Stack Builder (to run outside of the installer, -:menuselection:`Start --> Programs --> PostgreSQL 9.0`), select -:menuselection:`PostgreSQL Database Server 9.0 on port 5432` from the drop down -menu. Next, expand the :menuselection:`Categories --> Spatial Extensions` menu -tree and select :menuselection:`PostGIS 1.5 for PostgreSQL 9.0`. - -After clicking next, you will be prompted to select your mirror, PostGIS -will be downloaded, and the PostGIS installer will begin. Select only the -default options during install (e.g., do not uncheck the option to create a -default PostGIS database). - -.. note:: - - You will be prompted to enter your ``postgres`` database superuser - password in the 'Database Connection Information' dialog. - -psycopg2 -^^^^^^^^ - -The ``psycopg2`` Python module provides the interface between Python and the -PostgreSQL database. Download the latest `Windows installer`__ for your version -of Python and PostgreSQL and run using the default settings. [#]_ - -__ http://www.stickpeople.com/projects/python/win-psycopg/ - -.. _osgeo4w: - -OSGeo4W -^^^^^^^ - -The `OSGeo4W installer`_ makes it simple to install the PROJ.4, GDAL, and GEOS -libraries required by GeoDjango. First, download the `OSGeo4W installer`_, -and run it. Select :menuselection:`Express Web-GIS Install` and click next. -In the 'Select Packages' list, ensure that GDAL is selected; MapServer and -Apache are also enabled by default, but are not required by GeoDjango and -may be unchecked safely. After clicking next, the packages will be -automatically downloaded and installed, after which you may exit the -installer. - -.. _OSGeo4W installer: http://trac.osgeo.org/osgeo4w/ - -Modify Windows environment -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In order to use GeoDjango, you will need to add your Python and OSGeo4W -directories to your Windows system ``Path``, as well as create ``GDAL_DATA`` -and ``PROJ_LIB`` environment variables. The following set of commands, -executable with ``cmd.exe``, will set this up: - -.. code-block:: bat - - set OSGEO4W_ROOT=C:\OSGeo4W - set PYTHON_ROOT=C:\Python27 - set GDAL_DATA=%OSGEO4W_ROOT%\share\gdal - set PROJ_LIB=%OSGEO4W_ROOT%\share\proj - set PATH=%PATH%;%PYTHON_ROOT%;%OSGEO4W_ROOT%\bin - reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path /t REG_EXPAND_SZ /f /d "%PATH%" - reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v GDAL_DATA /t REG_EXPAND_SZ /f /d "%GDAL_DATA%" - reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PROJ_LIB /t REG_EXPAND_SZ /f /d "%PROJ_LIB%" - -For your convenience, these commands are available in the executable batch -script, :download:`geodjango_setup.bat`. - -.. note:: - - Administrator privileges are required to execute these commands. - To do this, right-click on :download:`geodjango_setup.bat` and select - :menuselection:`Run as administrator`. You need to log out and log back in again - for the settings to take effect. - -.. note:: - - If you customized the Python or OSGeo4W installation directories, - then you will need to modify the ``OSGEO4W_ROOT`` and/or ``PYTHON_ROOT`` - variables accordingly. - -Install Django and set up database -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Finally, :ref:`install Django ` on your system. -You do not need to create a spatial database template, as one named -``template_postgis`` is created for you when installing PostGIS. - -To administer the database, you can either use the pgAdmin III program -(:menuselection:`Start --> PostgreSQL 9.0 --> pgAdmin III`) or the -SQL Shell (:menuselection:`Start --> PostgreSQL 9.0 --> SQL Shell`). -For example, to create a ``geodjango`` spatial database and user, the following -may be executed from the SQL Shell as the ``postgres`` user:: - - postgres# CREATE USER geodjango PASSWORD 'my_passwd'; - postgres# CREATE DATABASE geodjango OWNER geodjango TEMPLATE template_postgis ENCODING 'utf8'; - -.. rubric:: Footnotes -.. [#] The datum shifting files are needed for converting data to and from - certain projections. - For example, the PROJ.4 string for the `Google projection (900913 or 3857) - `_ requires the - ``null`` grid file only included in the extra datum shifting files. - It is easier to install the shifting files now, then to have debug a - problem caused by their absence later. -.. [#] Specifically, GeoDjango provides support for the `OGR - `_ library, a component of GDAL. -.. [#] See `GDAL ticket #2382 `_. -.. [#] GeoDjango uses the :func:`~ctypes.util.find_library` routine from - :mod:`ctypes.util` to locate shared libraries. -.. [#] The ``psycopg2`` Windows installers are packaged and maintained by - `Jason Erickson `_. diff --git a/docs/ref/contrib/gis/install/create_template_postgis-1.3.sh b/docs/ref/contrib/gis/install/create_template_postgis-1.3.sh new file mode 100755 index 0000000000..c9ab4fcebf --- /dev/null +++ b/docs/ref/contrib/gis/install/create_template_postgis-1.3.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +POSTGIS_SQL_PATH=`pg_config --sharedir` +createdb -E UTF8 template_postgis # Create the template spatial database. +createlang -d template_postgis plpgsql # Adding PLPGSQL language support. +psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" +psql -d template_postgis -f $POSTGIS_SQL_PATH/lwpostgis.sql # Loading the PostGIS SQL routines +psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql +psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" # Enabling users to alter spatial tables. +psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" diff --git a/docs/ref/contrib/gis/install/create_template_postgis-1.4.sh b/docs/ref/contrib/gis/install/create_template_postgis-1.4.sh new file mode 100755 index 0000000000..57a1373f96 --- /dev/null +++ b/docs/ref/contrib/gis/install/create_template_postgis-1.4.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib +createdb -E UTF8 template_postgis # Create the template spatial database. +createlang -d template_postgis plpgsql # Adding PLPGSQL language support. +psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" +psql -d template_postgis -f $POSTGIS_SQL_PATH/postgis.sql # Loading the PostGIS SQL routines +psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql +psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" # Enabling users to alter spatial tables. +psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" diff --git a/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh b/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh new file mode 100755 index 0000000000..081b5f2656 --- /dev/null +++ b/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-1.5 +createdb -E UTF8 template_postgis # Create the template spatial database. +createlang -d template_postgis plpgsql # Adding PLPGSQL language support. +psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" +psql -d template_postgis -f $POSTGIS_SQL_PATH/postgis.sql # Loading the PostGIS SQL routines +psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql +psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" # Enabling users to alter spatial tables. +psql -d template_postgis -c "GRANT ALL ON geography_columns TO PUBLIC;" +psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" diff --git a/docs/ref/contrib/gis/install/create_template_postgis-debian.sh b/docs/ref/contrib/gis/install/create_template_postgis-debian.sh new file mode 100755 index 0000000000..3e621837fa --- /dev/null +++ b/docs/ref/contrib/gis/install/create_template_postgis-debian.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +GEOGRAPHY=0 +POSTGIS_SQL=postgis.sql + +# For Ubuntu 8.x and 9.x releases. +if [ -d "/usr/share/postgresql-8.3-postgis" ] +then + POSTGIS_SQL_PATH=/usr/share/postgresql-8.3-postgis + POSTGIS_SQL=lwpostgis.sql +fi + +# For Ubuntu 10.04 +if [ -d "/usr/share/postgresql/8.4/contrib" ] +then + POSTGIS_SQL_PATH=/usr/share/postgresql/8.4/contrib +fi + +# For Ubuntu 10.10 (with PostGIS 1.5) +if [ -d "/usr/share/postgresql/8.4/contrib/postgis-1.5" ] +then + POSTGIS_SQL_PATH=/usr/share/postgresql/8.4/contrib/postgis-1.5 + GEOGRAPHY=1 +fi + +# For Ubuntu 11.10 / Linux Mint 12 (with PostGIS 1.5) +if [ -d "/usr/share/postgresql/9.1/contrib/postgis-1.5" ] +then + POSTGIS_SQL_PATH=/usr/share/postgresql/9.1/contrib/postgis-1.5 + GEOGRAPHY=1 +fi + +createdb -E UTF8 template_postgis && \ +( createlang -d template_postgis -l | grep plpgsql || createlang -d template_postgis plpgsql ) && \ +psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" && \ +psql -d template_postgis -f $POSTGIS_SQL_PATH/$POSTGIS_SQL && \ +psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql && \ +psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" && \ +psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" + +if [ $GEOGRAPHY -eq 1 ] +then + psql -d template_postgis -c "GRANT ALL ON geography_columns TO PUBLIC;" +fi diff --git a/docs/ref/contrib/gis/install/geodjango_setup.bat b/docs/ref/contrib/gis/install/geodjango_setup.bat new file mode 100644 index 0000000000..b3e6cc6822 --- /dev/null +++ b/docs/ref/contrib/gis/install/geodjango_setup.bat @@ -0,0 +1,8 @@ +set OSGEO4W_ROOT=C:\OSGeo4W +set PYTHON_ROOT=C:\Python27 +set GDAL_DATA=%OSGEO4W_ROOT%\share\gdal +set PROJ_LIB=%OSGEO4W_ROOT%\share\proj +set PATH=%PATH%;%PYTHON_ROOT%;%OSGEO4W_ROOT%\bin +reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path /t REG_EXPAND_SZ /f /d "%PATH%" +reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v GDAL_DATA /t REG_EXPAND_SZ /f /d "%GDAL_DATA%" +reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PROJ_LIB /t REG_EXPAND_SZ /f /d "%PROJ_LIB%" diff --git a/docs/ref/contrib/gis/install/geolibs.txt b/docs/ref/contrib/gis/install/geolibs.txt new file mode 100644 index 0000000000..c78f0c0e62 --- /dev/null +++ b/docs/ref/contrib/gis/install/geolibs.txt @@ -0,0 +1,282 @@ +.. _geospatial_libs: + +=============================== +Installing Geospatial libraries +=============================== + +GeoDjango uses and/or provides interfaces for the following open source +geospatial libraries: + +======================== ==================================== ================================ ========================== +Program Description Required Supported Versions +======================== ==================================== ================================ ========================== +:ref:`GEOS ` Geometry Engine Open Source Yes 3.3, 3.2, 3.1, 3.0 +`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) 2.0, 1.5, 1.4, 1.3 +`SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 3.0, 2.4, 2.3 +======================== ==================================== ================================ ========================== + +.. admonition:: Install GDAL + + While :ref:`gdalbuild` is technically not required, it is *recommended*. + Important features of GeoDjango (including the :ref:`ref-layermapping`, + geometry reprojection, and the geographic admin) depend on its + functionality. + +.. note:: + + The GeoDjango interfaces to GEOS, GDAL, and GeoIP may be used + independently of Django. In other words, no database or settings file + required -- just import them as normal from :mod:`django.contrib.gis`. + +.. _PROJ.4: http://trac.osgeo.org/proj/ +__ http://postgis.refractions.net/ +__ http://www.gaia-gis.it/gaia-sins/ + + +On Debian/Ubuntu, you are advised to install the following packages which will +install, directly or by dependency, the required geospatial libraries: + +.. code-block:: bash + + $ sudo apt-get install binutils libproj-dev gdal-bin + +Optional packages to consider: + +* ``libgeoip1``: for :ref:`GeoIP ` support +* ``gdal-bin``: for GDAL command line programs like ``ogr2ogr`` +* ``python-gdal`` for GDAL's own Python bindings -- includes interfaces for raster manipulation + +Please also consult platform-specific instructions if you are on :ref:`macosx` +or :ref:`windows`. + +.. _build_from_source: + +Building from source +==================== + +When installing from source on UNIX and GNU/Linux systems, please follow +the installation instructions carefully, and install the libraries in the +given order. If using MySQL or Oracle as the spatial database, only GEOS +is required. + +.. note:: + + On Linux platforms, it may be necessary to run the ``ldconfig`` + command after installing each library. For example:: + + $ sudo make install + $ sudo ldconfig + +.. note:: + + OS X users are required to install `Apple Developer Tools`_ in order + to compile software from source. This is typically included on your + OS X installation DVDs. + +.. _Apple Developer Tools: https://developer.apple.com/technologies/tools/ + +.. _geosbuild: + +GEOS +---- + +GEOS is a C++ library for performing geometric operations, and is the default +internal geometry representation used by GeoDjango (it's behind the "lazy" +geometries). Specifically, the C API library is called (e.g., ``libgeos_c.so``) +directly from Python using ctypes. + +First, download GEOS 3.3.5 from the refractions Web site and untar the source +archive:: + + $ wget http://download.osgeo.org/geos/geos-3.3.5.tar.bz2 + $ tar xjf geos-3.3.5.tar.bz2 + +Next, change into the directory where GEOS was unpacked, run the configure +script, compile, and install:: + + $ cd geos-3.3.5 + $ ./configure + $ make + $ sudo make install + $ cd .. + +Troubleshooting +^^^^^^^^^^^^^^^ + +Can't find GEOS library +~~~~~~~~~~~~~~~~~~~~~~~ + +When GeoDjango can't find GEOS, this error is raised: + +.. code-block:: text + + ImportError: Could not find the GEOS library (tried "geos_c"). Try setting GEOS_LIBRARY_PATH in your settings. + +The most common solution is to properly configure your :ref:`libsettings` *or* set +:ref:`geoslibrarypath` in your settings. + +If using a binary package of GEOS (e.g., on Ubuntu), you may need to :ref:`binutils`. + +.. _geoslibrarypath: + +``GEOS_LIBRARY_PATH`` +~~~~~~~~~~~~~~~~~~~~~ + +If your GEOS library is in a non-standard location, or you don't want to +modify the system's library path then the :setting:`GEOS_LIBRARY_PATH` +setting may be added to your Django settings file with the full path to the +GEOS C library. For example: + +.. code-block:: python + + GEOS_LIBRARY_PATH = '/home/bob/local/lib/libgeos_c.so' + +.. note:: + + The setting must be the *full* path to the **C** shared library; in + other words you want to use ``libgeos_c.so``, not ``libgeos.so``. + +See also :ref:`My logs are filled with GEOS-related errors `. + +.. _proj4: + +PROJ.4 +------ + +`PROJ.4`_ is a library for converting geospatial data to different coordinate +reference systems. + +First, download the PROJ.4 source code and datum shifting files [#]_:: + + $ wget http://download.osgeo.org/proj/proj-4.8.0.tar.gz + $ wget http://download.osgeo.org/proj/proj-datumgrid-1.5.tar.gz + +Next, untar the source code archive, and extract the datum shifting files in the +``nad`` subdirectory. This must be done *prior* to configuration:: + + $ tar xzf proj-4.8.0.tar.gz + $ cd proj-4.8.0/nad + $ tar xzf ../../proj-datumgrid-1.5.tar.gz + $ cd .. + +Finally, configure, make and install PROJ.4:: + + $ ./configure + $ make + $ sudo make install + $ cd .. + +.. _gdalbuild: + +GDAL +---- + +`GDAL`__ is an excellent open source geospatial library that has support for +reading most vector and raster spatial data formats. Currently, GeoDjango only +supports :ref:`GDAL's vector data ` capabilities [#]_. +:ref:`geosbuild` and :ref:`proj4` should be installed prior to building GDAL. + +First download the latest GDAL release version and untar the archive:: + + $ wget http://download.osgeo.org/gdal/gdal-1.9.1.tar.gz + $ tar xzf gdal-1.9.1.tar.gz + $ cd gdal-1.9.1 + +Configure, make and install:: + + $ ./configure + $ make # Go get some coffee, this takes a while. + $ sudo make install + $ cd .. + +.. note:: + + Because GeoDjango has it's own Python interface, the preceding instructions + do not build GDAL's own Python bindings. The bindings may be built by + adding the ``--with-python`` flag when running ``configure``. See + `GDAL/OGR In Python`__ for more information on GDAL's bindings. + +If you have any problems, please see the troubleshooting section below for +suggestions and solutions. + +__ http://trac.osgeo.org/gdal/ +__ http://trac.osgeo.org/gdal/wiki/GdalOgrInPython + +.. _gdaltrouble: + +Troubleshooting +^^^^^^^^^^^^^^^ + +Can't find GDAL library +~~~~~~~~~~~~~~~~~~~~~~~ + +When GeoDjango can't find the GDAL library, the ``HAS_GDAL`` flag +will be false: + +.. code-block:: pycon + + >>> from django.contrib.gis import gdal + >>> gdal.HAS_GDAL + False + +The solution is to properly configure your :ref:`libsettings` *or* set +:ref:`gdallibrarypath` in your settings. + +.. _gdallibrarypath: + +``GDAL_LIBRARY_PATH`` +~~~~~~~~~~~~~~~~~~~~~ + +If your GDAL library is in a non-standard location, or you don't want to +modify the system's library path then the :setting:`GDAL_LIBRARY_PATH` +setting may be added to your Django settings file with the full path to +the GDAL library. For example: + +.. code-block:: python + + GDAL_LIBRARY_PATH = '/home/sue/local/lib/libgdal.so' + +.. _gdaldata: + +Can't find GDAL data files (``GDAL_DATA``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When installed from source, GDAL versions 1.5.1 and below have an autoconf bug +that places data in the wrong location. [#]_ This can lead to error messages +like this: + +.. code-block:: text + + ERROR 4: Unable to open EPSG support file gcs.csv. + ... + OGRException: OGR failure. + +The solution is to set the ``GDAL_DATA`` environment variable to the location of the +GDAL data files before invoking Python (typically ``/usr/local/share``; use +``gdal-config --datadir`` to find out). For example:: + + $ export GDAL_DATA=`gdal-config --datadir` + $ python manage.py shell + +If using Apache, you may need to add this environment variable to your configuration +file: + +.. code-block:: apache + + SetEnv GDAL_DATA /usr/local/share + +.. rubric:: Footnotes +.. [#] The datum shifting files are needed for converting data to and from + certain projections. + For example, the PROJ.4 string for the `Google projection (900913 or 3857) + `_ requires the + ``null`` grid file only included in the extra datum shifting files. + It is easier to install the shifting files now, then to have debug a + problem caused by their absence later. +.. [#] Specifically, GeoDjango provides support for the `OGR + `_ library, a component of GDAL. +.. [#] See `GDAL ticket #2382 `_. + diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt new file mode 100644 index 0000000000..c710866813 --- /dev/null +++ b/docs/ref/contrib/gis/install/index.txt @@ -0,0 +1,535 @@ +.. _ref-gis-install: + +====================== +GeoDjango Installation +====================== + +.. highlight:: console + +Overview +======== +In general, GeoDjango installation requires: + +1. :ref:`Python and Django ` +2. :ref:`spatial_database` +3. :ref:`geospatial_libs` + +Details for each of the requirements and installation instructions +are provided in the sections below. In addition, platform-specific +instructions are available for: + +* :ref:`macosx` +* :ref:`windows` + +.. admonition:: Use the Source + + Because GeoDjango takes advantage of the latest in the open source geospatial + software technology, recent versions of the libraries are necessary. + If binary packages aren't available for your platform, installation from + source may be required. When compiling the libraries from source, please + follow the directions closely, especially if you're a beginner. + +Requirements +============ + +.. _django: + +Python and Django +----------------- + +Because GeoDjango is included with Django, please refer to Django's +:ref:`installation instructions ` for details on +how to install. + + +.. _spatial_database: + +Spatial database +---------------- +PostgreSQL (with PostGIS), MySQL, Oracle, and SQLite (with SpatiaLite) are +the spatial databases currently supported. + +.. note:: + + PostGIS is recommended, because it is the most mature and feature-rich + open source spatial database. + +The geospatial libraries required for a GeoDjango installation depends +on the spatial database used. The following lists the library requirements, +supported versions, and any notes for each of the supported database backends: + +================== ============================== ================== ========================================= +Database Library Requirements Supported Versions Notes +================== ============================== ================== ========================================= +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+ +================== ============================== ================== ========================================= + +See also `this comparison matrix`__ on the OSGeo Wiki for +PostgreSQL/PostGIS/GEOS/GDAL possible combinations. + +__ http://trac.osgeo.org/postgis/wiki/UsersWikiPostgreSQLPostGIS + +Installation +============ + +Geospatial libraries +-------------------- + +.. toctree:: + :maxdepth: 1 + + geolibs + +Database installation +--------------------- + +.. toctree:: + :maxdepth: 1 + + postgis + spatialite + +Add ``django.contrib.gis`` to :setting:`INSTALLED_APPS` +------------------------------------------------------- + +Like other Django contrib applications, you will *only* need to add +:mod:`django.contrib.gis` to :setting:`INSTALLED_APPS` in your settings. +This is the so that ``gis`` templates can be located -- if not done, then +features such as the geographic admin or KML sitemaps will not function properly. + +.. _addgoogleprojection: + +Add Google projection to ``spatial_ref_sys`` table +-------------------------------------------------- + +.. note:: + + If you're running PostGIS 1.4 or above, you can skip this step. The entry + is already included in the default ``spatial_ref_sys`` table. + +In order to conduct database transformations to the so-called "Google" +projection (a spherical mercator projection used by Google Maps), +an entry must be added to your spatial database's ``spatial_ref_sys`` table. +Invoke the Django shell from your project and execute the +``add_srs_entry`` function: + +.. code-block:: pycon + + $ python manage shell + >>> from django.contrib.gis.utils import add_srs_entry + >>> add_srs_entry(900913) + +This adds an entry for the 900913 SRID to the ``spatial_ref_sys`` (or equivalent) +table, making it possible for the spatial database to transform coordinates in +this projection. You only need to execute this command *once* per spatial database. + +Troubleshooting +=============== + +If you can't find the solution to your problem here then participate in the +community! You can: + +* Join the ``#geodjango`` IRC channel on FreeNode. Please be patient and polite + -- while you may not get an immediate response, someone will attempt to answer + your question as soon as they see it. +* Ask your question on the `GeoDjango`__ mailing list. +* File a ticket on the `Django trac`__ if you think there's a bug. Make + sure to provide a complete description of the problem, versions used, + and specify the component as "GIS". + +__ http://groups.google.com/group/geodjango +__ https://code.djangoproject.com/newticket + +.. _libsettings: + +Library environment settings +---------------------------- + +By far, the most common problem when installing GeoDjango is that the +external shared libraries (e.g., for GEOS and GDAL) cannot be located. [#]_ +Typically, the cause of this problem is that the operating system isn't aware +of the directory where the libraries built from source were installed. + +In general, the library path may be set on a per-user basis by setting +an environment variable, or by configuring the library path for the entire +system. + +``LD_LIBRARY_PATH`` environment variable +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A user may set this environment variable to customize the library paths +they want to use. The typical library directory for software +built from source is ``/usr/local/lib``. Thus, ``/usr/local/lib`` needs +to be included in the ``LD_LIBRARY_PATH`` variable. For example, the user +could place the following in their bash profile:: + + export LD_LIBRARY_PATH=/usr/local/lib + +Setting system library path +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +On GNU/Linux systems, there is typically a file in ``/etc/ld.so.conf``, which may include +additional paths from files in another directory, such as ``/etc/ld.so.conf.d``. +As the root user, add the custom library path (like ``/usr/local/lib``) on a +new line in ``ld.so.conf``. This is *one* example of how to do so:: + + $ sudo echo /usr/local/lib >> /etc/ld.so.conf + $ sudo ldconfig + +For OpenSolaris users, the system library path may be modified using the +``crle`` utility. Run ``crle`` with no options to see the current configuration +and use ``crle -l`` to set with the new library path. Be *very* careful when +modifying the system library path:: + + # crle -l $OLD_PATH:/usr/local/lib + +.. _binutils: + +Install ``binutils`` +^^^^^^^^^^^^^^^^^^^^ + +GeoDjango uses the ``find_library`` function (from the ``ctypes.util`` Python +module) to discover libraries. The ``find_library`` routine uses a program +called ``objdump`` (part of the ``binutils`` package) to verify a shared +library on GNU/Linux systems. Thus, if ``binutils`` is not installed on your +Linux system then Python's ctypes may not be able to find your library even if +your library path is set correctly and geospatial libraries were built perfectly. + +The ``binutils`` package may be installed on Debian and Ubuntu systems using the +following command:: + + $ sudo apt-get install binutils + +Similarly, on Red Hat and CentOS systems:: + + $ sudo yum install binutils + +Platform-specific instructions +============================== + +.. _macosx: + +Mac OS X +-------- + +Because of the variety of packaging systems available for OS X, users have +several different options for installing GeoDjango. These options are: + +* :ref:`homebrew` +* :ref:`kyngchaos` +* :ref:`fink` +* :ref:`macports` +* :ref:`build_from_source` + +.. note:: + + Currently, the easiest and recommended approach for installing GeoDjango + on OS X is to use the KyngChaos packages. + +This section also includes instructions for installing an upgraded version +of :ref:`macosx_python` from packages provided by the Python Software +Foundation, however, this is not required. + +.. _macosx_python: + +Python +^^^^^^ + +Although OS X comes with Python installed, users can use framework +installers (`2.6`__ and `2.7`__ are available) provided by +the Python Software Foundation. An advantage to using the installer is +that OS X's Python will remain "pristine" for internal operating system +use. + +__ http://python.org/ftp/python/2.6.6/python-2.6.6-macosx10.3.dmg +__ http://python.org/ftp/python/2.7.3/ + +.. note:: + + You will need to modify the ``PATH`` environment variable in your + ``.profile`` file so that the new version of Python is used when + ``python`` is entered at the command-line:: + + export PATH=/Library/Frameworks/Python.framework/Versions/Current/bin:$PATH + +.. _homebrew: + +Homebrew +^^^^^^^^ + +`Homebrew`__ provides "recipes" for building binaries and packages from source. +It provides recipes for the GeoDjango prerequisites on Macintosh computers +running OS X. Because Homebrew still builds the software from source, the +`Apple Developer Tools`_ are required. + +Summary:: + + $ brew install postgresql + $ brew install postgis + $ brew install gdal + $ brew install libgeoip + +__ http://mxcl.github.com/homebrew/ +.. _Apple Developer Tools: https://developer.apple.com/technologies/tools/ + +.. _kyngchaos: + +KyngChaos packages +^^^^^^^^^^^^^^^^^^ + +William Kyngesburye provides a number of `geospatial library binary packages`__ +that make it simple to get GeoDjango installed on OS X without compiling +them from source. However, the `Apple Developer Tools`_ are still necessary +for compiling the Python database adapters :ref:`psycopg2_kyngchaos` (for PostGIS) +and :ref:`pysqlite2` (for SpatiaLite). + +.. note:: + + SpatiaLite users should consult the :ref:`spatialite_macosx` section + after installing the packages for additional instructions. + +Download the framework packages for: + +* UnixImageIO +* PROJ +* GEOS +* SQLite3 (includes the SpatiaLite library) +* GDAL + +Install the packages in the order they are listed above, as the GDAL and SQLite +packages require the packages listed before them. + +Afterwards, you can also install the KyngChaos binary packages for `PostgreSQL +and PostGIS`__. + +After installing the binary packages, you'll want to add the following to +your ``.profile`` to be able to run the package programs from the command-line:: + + export PATH=/Library/Frameworks/UnixImageIO.framework/Programs:$PATH + export PATH=/Library/Frameworks/PROJ.framework/Programs:$PATH + export PATH=/Library/Frameworks/GEOS.framework/Programs:$PATH + export PATH=/Library/Frameworks/SQLite3.framework/Programs:$PATH + export PATH=/Library/Frameworks/GDAL.framework/Programs:$PATH + export PATH=/usr/local/pgsql/bin:$PATH + +__ http://www.kyngchaos.com/software/frameworks +__ http://www.kyngchaos.com/software/postgres + +.. _psycopg2_kyngchaos: + +psycopg2 +~~~~~~~~ + +After you've installed the KyngChaos binaries and modified your ``PATH``, as +described above, ``psycopg2`` may be installed using the following command:: + + $ sudo pip install psycopg2 + +.. note:: + + If you don't have ``pip``, follow the the :ref:`installation instructions + ` to install it. + +.. _fink: + +Fink +^^^^ + +`Kurt Schwehr`__ has been gracious enough to create GeoDjango packages for users +of the `Fink`__ package system. The following packages are available, depending +on which version of Python you want to use: + +* ``django-gis-py26`` +* ``django-gis-py25`` +* ``django-gis-py24`` + +__ http://schwehr.org/blog/ +__ http://www.finkproject.org/ + +.. _macports: + +MacPorts +^^^^^^^^ + +`MacPorts`__ may be used to install GeoDjango prerequisites on Macintosh +computers running OS X. Because MacPorts still builds the software from source, +the `Apple Developer Tools`_ are required. + +Summary:: + + $ sudo port install postgresql83-server + $ sudo port install geos + $ sudo port install proj + $ sudo port install postgis + $ sudo port install gdal +geos + $ sudo port install libgeoip + +.. note:: + + You will also have to modify the ``PATH`` in your ``.profile`` so + that the MacPorts programs are accessible from the command-line:: + + export PATH=/opt/local/bin:/opt/local/lib/postgresql83/bin + + In addition, add the ``DYLD_FALLBACK_LIBRARY_PATH`` setting so that + the libraries can be found by Python:: + + export DYLD_FALLBACK_LIBRARY_PATH=/opt/local/lib:/opt/local/lib/postgresql83 + +__ http://www.macports.org/ + +.. _windows: + +Windows +------- + +Proceed through the following sections sequentially in order to install +GeoDjango on Windows. + +.. note:: + + These instructions assume that you are using 32-bit versions of + all programs. While 64-bit versions of Python and PostgreSQL 9.0 + are available, 64-bit versions of spatial libraries, like + GEOS and GDAL, are not yet provided by the :ref:`OSGeo4W` installer. + +Python +^^^^^^ + +First, download the latest `Python 2.7 installer`__ from the Python Web site. +Next, run the installer and keep the defaults -- for example, keep +'Install for all users' checked and the installation path set as +``C:\Python27``. + +.. note:: + + You may already have a version of Python installed in ``C:\python`` as ESRI + products sometimes install a copy there. *You should still install a + fresh version of Python 2.7.* + +__ http://python.org/download/ + +PostgreSQL +^^^^^^^^^^ + +First, download the latest `PostgreSQL 9.0 installer`__ from the +`EnterpriseDB`__ Web site. After downloading, simply run the installer, +follow the on-screen directions, and keep the default options unless +you know the consequences of changing them. + +.. note:: + + The PostgreSQL installer creates both a new Windows user to be the + 'postgres service account' and a ``postgres`` database superuser + You will be prompted once to set the password for both accounts -- + make sure to remember it! + +When the installer completes, it will ask to launch the Application Stack +Builder (ASB) on exit -- keep this checked, as it is necessary to +install :ref:`postgisasb`. + +.. note:: + + If installed successfully, the PostgreSQL server will run in the + background each time the system as started as a Windows service. + A :menuselection:`PostgreSQL 9.0` start menu group will created + and contains shortcuts for the ASB as well as the 'SQL Shell', + which will launch a ``psql`` command window. + +__ http://www.enterprisedb.com/products-services-training/pgdownload +__ http://www.enterprisedb.com + +.. _postgisasb: + +PostGIS +^^^^^^^ + +From within the Application Stack Builder (to run outside of the installer, +:menuselection:`Start --> Programs --> PostgreSQL 9.0`), select +:menuselection:`PostgreSQL Database Server 9.0 on port 5432` from the drop down +menu. Next, expand the :menuselection:`Categories --> Spatial Extensions` menu +tree and select :menuselection:`PostGIS 1.5 for PostgreSQL 9.0`. + +After clicking next, you will be prompted to select your mirror, PostGIS +will be downloaded, and the PostGIS installer will begin. Select only the +default options during install (e.g., do not uncheck the option to create a +default PostGIS database). + +.. note:: + + You will be prompted to enter your ``postgres`` database superuser + password in the 'Database Connection Information' dialog. + +psycopg2 +^^^^^^^^ + +The ``psycopg2`` Python module provides the interface between Python and the +PostgreSQL database. Download the latest `Windows installer`__ for your version +of Python and PostgreSQL and run using the default settings. [#]_ + +__ http://www.stickpeople.com/projects/python/win-psycopg/ + +.. _osgeo4w: + +OSGeo4W +^^^^^^^ + +The `OSGeo4W installer`_ makes it simple to install the PROJ.4, GDAL, and GEOS +libraries required by GeoDjango. First, download the `OSGeo4W installer`_, +and run it. Select :menuselection:`Express Web-GIS Install` and click next. +In the 'Select Packages' list, ensure that GDAL is selected; MapServer and +Apache are also enabled by default, but are not required by GeoDjango and +may be unchecked safely. After clicking next, the packages will be +automatically downloaded and installed, after which you may exit the +installer. + +.. _OSGeo4W installer: http://trac.osgeo.org/osgeo4w/ + +Modify Windows environment +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In order to use GeoDjango, you will need to add your Python and OSGeo4W +directories to your Windows system ``Path``, as well as create ``GDAL_DATA`` +and ``PROJ_LIB`` environment variables. The following set of commands, +executable with ``cmd.exe``, will set this up: + +.. code-block:: bat + + set OSGEO4W_ROOT=C:\OSGeo4W + set PYTHON_ROOT=C:\Python27 + set GDAL_DATA=%OSGEO4W_ROOT%\share\gdal + set PROJ_LIB=%OSGEO4W_ROOT%\share\proj + set PATH=%PATH%;%PYTHON_ROOT%;%OSGEO4W_ROOT%\bin + reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path /t REG_EXPAND_SZ /f /d "%PATH%" + reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v GDAL_DATA /t REG_EXPAND_SZ /f /d "%GDAL_DATA%" + reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PROJ_LIB /t REG_EXPAND_SZ /f /d "%PROJ_LIB%" + +For your convenience, these commands are available in the executable batch +script, :download:`geodjango_setup.bat`. + +.. note:: + + Administrator privileges are required to execute these commands. + To do this, right-click on :download:`geodjango_setup.bat` and select + :menuselection:`Run as administrator`. You need to log out and log back in again + for the settings to take effect. + +.. note:: + + If you customized the Python or OSGeo4W installation directories, + then you will need to modify the ``OSGEO4W_ROOT`` and/or ``PYTHON_ROOT`` + variables accordingly. + +Install Django and set up database +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Finally, :ref:`install Django ` on your system. + +.. rubric:: Footnotes +.. [#] GeoDjango uses the :func:`~ctypes.util.find_library` routine from + :mod:`ctypes.util` to locate shared libraries. +.. [#] The ``psycopg2`` Windows installers are packaged and maintained by + `Jason Erickson `_. diff --git a/docs/ref/contrib/gis/install/postgis.txt b/docs/ref/contrib/gis/install/postgis.txt new file mode 100644 index 0000000000..6d7fe88203 --- /dev/null +++ b/docs/ref/contrib/gis/install/postgis.txt @@ -0,0 +1,175 @@ +.. _postgis: + +================== +Installing 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 + +On Debian/Ubuntu, you are advised to install the following packages: +postgresql-x.x, postgresql-x.x-postgis, postgresql-server-dev-x.x, +python-psycopg2 (x.x matching the PostgreSQL version you want to install). +Please also consult platform-specific instructions if you are on :ref:`macosx` +or :ref:`windows`. + +Building from source +==================== + +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/ + +Post-installation +================= + +.. _spatialdb_template: +.. _spatialdb_template91: + +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; + +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 +--------------------------------------------------------- + +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 +the steps in this process, it's better to create a database template that +can be reused later. + +First, you need to be able to execute the commands as a privileged database +user. For example, you can use the following to become the ``postgres`` user:: + + $ sudo su - postgres + +.. note:: + + The location *and* name of the PostGIS SQL files (e.g., from + ``POSTGIS_SQL_PATH`` below) depends on the version of PostGIS. + PostGIS versions 1.3 and below use ``/contrib/lwpostgis.sql``; + whereas version 1.4 uses ``/contrib/postgis.sql`` and + version 1.5 uses ``/contrib/postgis-1.5/postgis.sql``. + + To complicate matters, Debian/Ubuntu distributions have their own separate + directory naming system that might change with time. In this case, use the + :download:`create_template_postgis-debian.sh` script. + + The example below assumes PostGIS 1.5, thus you may need to modify + ``POSTGIS_SQL_PATH`` and the name of the SQL file for the specific + version of PostGIS you are using. + +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-2.0 + # Creating the template spatial database. + $ createdb -E UTF8 template_postgis + $ createlang -d template_postgis plpgsql # Adding PLPGSQL language support. + # Allows non-superusers the ability to create from this template + $ psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" + # Loading the PostGIS SQL routines + $ psql -d template_postgis -f $POSTGIS_SQL_PATH/postgis.sql + $ psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql + # Enabling users to alter spatial tables. + $ psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" + $ psql -d template_postgis -c "GRANT ALL ON geography_columns TO PUBLIC;" + $ psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" + +These commands may be placed in a shell script for later use; for convenience +the following scripts are available: + +=============== ============================================= +PostGIS version Bash shell script +=============== ============================================= +1.3 :download:`create_template_postgis-1.3.sh` +1.4 :download:`create_template_postgis-1.4.sh` +1.5 :download:`create_template_postgis-1.5.sh` +Debian/Ubuntu :download:`create_template_postgis-debian.sh` +=============== ============================================= + +Afterwards, you may create a spatial database by simply specifying +``template_postgis`` as the template to use (via the ``-T`` option):: + + $ createdb -T template_postgis + +.. note:: + + While the ``createdb`` command does not require database super-user privileges, + it must be executed by a database user that has permissions to create databases. + You can create such a user with the following command:: + + $ createuser --createdb + +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/ + +Managing the database +--------------------- + +To administer the database, you can either use the pgAdmin III program +(:menuselection:`Start --> PostgreSQL 9.0 --> pgAdmin III`) or the +SQL Shell (:menuselection:`Start --> PostgreSQL 9.0 --> SQL Shell`). +For example, to create a ``geodjango`` spatial database and user, the following +may be executed from the SQL Shell as the ``postgres`` user:: + + postgres# CREATE USER geodjango PASSWORD 'my_passwd'; + postgres# CREATE DATABASE geodjango OWNER geodjango TEMPLATE template_postgis ENCODING 'utf8'; diff --git a/docs/ref/contrib/gis/install/spatialite.txt b/docs/ref/contrib/gis/install/spatialite.txt new file mode 100644 index 0000000000..941d559272 --- /dev/null +++ b/docs/ref/contrib/gis/install/spatialite.txt @@ -0,0 +1,222 @@ +.. _spatialite: + +===================== +Installing Spatialite +===================== + +`SpatiaLite`__ adds spatial support to SQLite, turning it into a full-featured +spatial database. + +Check first if you can install Spatialite from system packages or binaries. For +example, on Debian-based distributions, try to install the ``spatialite-bin`` +package. For Mac OS X, follow the +:ref:`specific instructions below`. For Windows, you may +find binaries on `Gaia-SINS`__ home page. In any case, you should always +be able to :ref:`install from source`. + +When you are done with the installation process, skip to :ref:`create_spatialite_db`. + +__ https://www.gaia-gis.it/fossil/libspatialite +__ http://www.gaia-gis.it/gaia-sins/ + +.. _spatialite_source: + +Installing from source +~~~~~~~~~~~~~~~~~~~~~~ + +:ref:`GEOS and PROJ.4` should be installed prior to building +SpatiaLite. + +SQLite +^^^^^^ + +Check first if SQLite is compiled with the `R*Tree module`__. Run the sqlite3 +command line interface and enter the following query:: + + sqlite> CREATE VIRTUAL TABLE testrtree USING rtree(id,minX,maxX,minY,maxY); + +If you obtain an error, you will have to recompile SQLite from source. Otherwise, +just skip this section. + +To install from sources, download the latest amalgamation source archive from +the `SQLite download page`__, and extract:: + + $ wget http://sqlite.org/sqlite-amalgamation-3.6.23.1.tar.gz + $ tar xzf sqlite-amalgamation-3.6.23.1.tar.gz + $ cd sqlite-3.6.23.1 + +Next, run the ``configure`` script -- however the ``CFLAGS`` environment variable +needs to be customized so that SQLite knows to build the R*Tree module:: + + $ CFLAGS="-DSQLITE_ENABLE_RTREE=1" ./configure + $ make + $ sudo make install + $ cd .. + +__ http://www.sqlite.org/rtree.html +__ http://www.sqlite.org/download.html + +.. _spatialitebuild : + +SpatiaLite library (``libspatialite``) and tools (``spatialite``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Get the latest SpatiaLite library source and tools bundle from the +`download page`__:: + + $ wget http://www.gaia-gis.it/gaia-sins/libspatialite-sources/libspatialite-amalgamation-2.4.0-5.tar.gz + $ wget http://www.gaia-gis.it/gaia-sins/spatialite-tools-sources/spatialite-tools-2.4.0-5.tar.gz + $ tar xzf libspatialite-amalgamation-2.4.0-5.tar.gz + $ tar xzf spatialite-tools-2.4.0-5.tar.gz + +Prior to attempting to build, please read the important notes below to see if +customization of the ``configure`` command is necessary. If not, then run the +``configure`` script, make, and install for the SpatiaLite library:: + + $ cd libspatialite-amalgamation-2.3.1 + $ ./configure # May need to modified, see notes below. + $ make + $ sudo make install + $ cd .... _spatialite + +Finally, do the same for the SpatiaLite tools:: + + $ cd spatialite-tools-2.3.1 + $ ./configure # May need to modified, see notes below. + $ make + $ sudo make install + $ cd .. + +.. note:: + + If you've installed GEOS and PROJ.4 from binary packages, you will have to specify + their paths when running the ``configure`` scripts for *both* the library and the + tools (the configure scripts look, by default, in ``/usr/local``). For example, + on Debian/Ubuntu distributions that have GEOS and PROJ.4 packages, the command would be:: + + $ ./configure --with-proj-include=/usr/include --with-proj-lib=/usr/lib --with-geos-include=/usr/include --with-geos-lib=/usr/lib + +.. note:: + + For Mac OS X users building from source, the SpatiaLite library *and* tools + need to have their ``target`` configured:: + + $ ./configure --target=macosx + +__ http://www.gaia-gis.it/gaia-sins/libspatialite-sources/ + +.. _pysqlite2: + +pysqlite2 +^^^^^^^^^ + +If you are on Python 2.6, you will also have to compile pysqlite2, because +``SpatiaLite`` must be loaded as an external extension, and the required +``enable_load_extension`` method is only available in versions 2.5+ of +pysqlite2. Thus, download pysqlite2 2.6, and untar:: + + $ wget http://pysqlite.googlecode.com/files/pysqlite-2.6.3.tar.gz + $ tar xzf pysqlite-2.6.3.tar.gz + $ cd pysqlite-2.6.3 + +Next, use a text editor (e.g., ``emacs`` or ``vi``) to edit the ``setup.cfg`` file +to look like the following: + +.. code-block:: ini + + [build_ext] + #define= + include_dirs=/usr/local/include + library_dirs=/usr/local/lib + libraries=sqlite3 + #define=SQLITE_OMIT_LOAD_EXTENSION + +or if you are on Mac OS X: + +.. code-block:: ini + + [build_ext] + #define= + include_dirs=/Library/Frameworks/SQLite3.framework/unix/include + library_dirs=/Library/Frameworks/SQLite3.framework/unix/lib + libraries=sqlite3 + #define=SQLITE_OMIT_LOAD_EXTENSION + +.. note:: + + The important thing here is to make sure you comment out the + ``define=SQLITE_OMIT_LOAD_EXTENSION`` flag and that the ``include_dirs`` + and ``library_dirs`` settings are uncommented and set to the appropriate + path if the SQLite header files and libraries are not in ``/usr/include`` + and ``/usr/lib``, respectively. + +After modifying ``setup.cfg`` appropriately, then run the ``setup.py`` script +to build and install:: + + $ sudo python setup.py install + +.. _spatialite_macosx: + +Mac OS X-specific instructions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Mac OS X users should follow the instructions in the :ref:`kyngchaos` section, +as it is much easier than building from source. + +When :ref:`create_spatialite_db`, the ``spatialite`` program is required. +However, instead of attempting to compile the SpatiaLite tools from source, +download the `SpatiaLite Binaries`__ for OS X, and install ``spatialite`` in a +location available in your ``PATH``. For example:: + + $ curl -O http://www.gaia-gis.it/spatialite/spatialite-tools-osx-x86-2.3.1.tar.gz + $ tar xzf spatialite-tools-osx-x86-2.3.1.tar.gz + $ cd spatialite-tools-osx-x86-2.3.1/bin + $ sudo cp spatialite /Library/Frameworks/SQLite3.framework/Programs + +Finally, for GeoDjango to be able to find the KyngChaos SpatiaLite library, +add the following to your ``settings.py``: + +.. code-block:: python + + SPATIALITE_LIBRARY_PATH='/Library/Frameworks/SQLite3.framework/SQLite3' + +__ http://www.gaia-gis.it/spatialite-2.3.1/binaries.html + +.. _create_spatialite_db: + +Creating a spatial database for SpatiaLite +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After you've installed SpatiaLite, you'll need to create a number of spatial +metadata tables in your database in order to perform spatial queries. + +If you're using SpatiaLite 2.4 or newer, use the ``spatialite`` utility to +call the ``InitSpatialMetaData()`` function, like this:: + + $ spatialite geodjango.db "SELECT InitSpatialMetaData();" + the SPATIAL_REF_SYS table already contains some row(s) + InitSpatiaMetaData ()error:"table spatial_ref_sys already exists" + 0 + +You can safely ignore the error messages shown. When you've done this, you can +skip the rest of this section. + +If you're using SpatiaLite 2.3, you'll need to download a +database-initialization file and execute its SQL queries in your database. + +First, get it from the `SpatiaLite Resources`__ page:: + + $ wget http://www.gaia-gis.it/spatialite-2.3.1/init_spatialite-2.3.sql.gz + $ gunzip init_spatialite-2.3.sql.gz + +Then, use the ``spatialite`` command to initialize a spatial database:: + + $ spatialite geodjango.db < init_spatialite-2.3.sql + +.. note:: + + The parameter ``geodjango.db`` is the *filename* of the SQLite database + you want to use. Use the same in the :setting:`DATABASES` ``"name"`` key + inside your ``settings.py``. + +__ http://www.gaia-gis.it/spatialite-2.3.1/resources.html -- cgit v1.3 From eed4faf16f37a8b0af06a52eada05b84dead4c0d Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 18 Oct 2012 20:12:41 -0400 Subject: Fixed #17006 - Documented ModelAdmin get_form() and get_formsets() --- docs/ref/contrib/admin/index.txt | 41 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 06751df879..971db19925 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -307,7 +307,9 @@ subclass:: By default a ``ModelForm`` is dynamically created for your model. It is used to create the form presented on both the add/change pages. You can easily provide your own ``ModelForm`` to override any default form behavior - on the add/change pages. + on the add/change pages. Alternatively, you can customize the default + form rather than specifying an entirely new one by using the + :meth:`ModelAdmin.get_form` method. For an example see the section `Adding custom validation to the admin`_. @@ -373,7 +375,8 @@ subclass:: .. attribute:: ModelAdmin.inlines - See :class:`InlineModelAdmin` objects below. + See :class:`InlineModelAdmin` objects below as well as + :meth:`ModelAdmin.get_formsets`. .. attribute:: ModelAdmin.list_display @@ -1109,6 +1112,38 @@ templates used by the :class:`ModelAdmin` views: (r'^my_view/$', self.admin_site.admin_view(self.my_view, cacheable=True)) +.. method:: ModelAdmin.get_form(self, request, obj=None, **kwargs) + + Returns a :class:`~django.forms.ModelForm` class for use in the admin add + and change views, see :meth:`add_view` and :meth:`change_view`. + + If you wanted to hide a field from non-superusers, for example, you could + override ``get_form`` as follows:: + + class MyModelAdmin(admin.ModelAdmin): + def get_form(self, request, obj=None, **kwargs): + self.exclude = [] + if not request.user.is_superuser: + self.exclude.append('field_to_hide') + return super(MyModelAdmin, self).get_form(request, obj, **kwargs) + +.. method:: ModelAdmin.get_formsets(self, request, obj=None) + + Yields :class:`InlineModelAdmin`\s for use in admin add and change views. + + For example if you wanted to display a particular inline only in the change + view, you could override ``get_formsets`` as follows:: + + class MyModelAdmin(admin.ModelAdmin): + inlines = [MyInline, SomeOtherInline] + + def get_formsets(self, request, obj=None): + for inline in self.get_inline_instances(): + # hide MyInline in the add view + if isinstance(inline, MyInline) and obj is None: + continue + yield inline.get_formset(request, obj) + .. method:: ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs) The ``formfield_for_foreignkey`` method on a ``ModelAdmin`` allows you to @@ -1423,8 +1458,6 @@ The ``InlineModelAdmin`` class adds: through to ``inlineformset_factory`` when creating the formset for this inline. - .. _ref-contrib-admin-inline-extra: - .. attribute:: InlineModelAdmin.extra This controls the number of extra forms the formset will display in -- cgit v1.3 From dfd4a7175119ddb422d8426dcc15902265d5a428 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 20 Oct 2012 14:33:57 +0200 Subject: Fixed #5611 -- Restricted accepted content types in parsing POST data Thanks paulegan for the report and Preston Holmes for the review. --- django/http/__init__.py | 8 +++++--- docs/ref/request-response.txt | 10 ++++++++-- docs/releases/1.5.txt | 12 ++++++++++++ tests/regressiontests/requests/tests.py | 25 +++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) (limited to 'docs') diff --git a/django/http/__init__.py b/django/http/__init__.py index b385b450ee..b67c182c37 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -315,7 +315,7 @@ class HttpRequest(object): self._post_parse_error = True def _load_post_and_files(self): - # Populates self._post and self._files + """Populate self._post and self._files if the content-type is a form type""" if self.method != 'POST': self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict() return @@ -323,7 +323,7 @@ class HttpRequest(object): self._mark_post_parse_error() return - if self.META.get('CONTENT_TYPE', '').startswith('multipart'): + if self.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'): if hasattr(self, '_body'): # Use already read data data = BytesIO(self._body) @@ -341,8 +341,10 @@ class HttpRequest(object): # empty POST self._mark_post_parse_error() raise - else: + elif self.META.get('CONTENT_TYPE', '').startswith('application/x-www-form-urlencoded'): self._post, self._files = QueryDict(self.body, encoding=self._encoding), MultiValueDict() + else: + self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict() ## File-like and iterator interface. ## diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 0a337eba42..d7266f0aff 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -92,8 +92,14 @@ All attributes should be considered read-only, unless stated otherwise below. .. attribute:: HttpRequest.POST - A dictionary-like object containing all given HTTP POST parameters. See the - :class:`QueryDict` documentation below. + A dictionary-like object containing all given HTTP POST parameters, + providing that the request contains form data. See the + :class:`QueryDict` documentation below. If you need to access raw or + non-form data posted in the request, access this through the + :attr:`HttpRequest.body` attribute instead. + + .. versionchanged:: 1.5 + Before Django 1.5, HttpRequest.POST contained non-form data. It's possible that a request can come in via POST with an empty ``POST`` dictionary -- if, say, a form is requested via the POST HTTP method but diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index d49bae801d..d30bd5ff7e 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -245,6 +245,18 @@ For consistency with the design of the other generic views, dictionary into the context, instead passing the variables from the URLconf directly into the context. +Non-form data in HTTP requests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:attr:`request.POST ` will no longer include data +posted via HTTP requests with non form-specific content-types in the header. +In prior versions, data posted with content-types other than +``multipart/form-data`` or ``application/x-www-form-urlencoded`` would still +end up represented in the :attr:`request.POST ` +attribute. Developers wishing to access the raw POST data for these cases, +should use the :attr:`request.body ` attribute +instead. + OPTIONS, PUT and DELETE requests in the test client ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py index d80161371e..378b4cf6d9 100644 --- a/tests/regressiontests/requests/tests.py +++ b/tests/regressiontests/requests/tests.py @@ -330,6 +330,7 @@ class RequestsTests(unittest.TestCase): def test_stream(self): payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': len(payload), 'wsgi.input': BytesIO(payload)}) self.assertEqual(request.read(), b'name=value') @@ -341,6 +342,7 @@ class RequestsTests(unittest.TestCase): """ payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': len(payload), 'wsgi.input': BytesIO(payload)}) self.assertEqual(request.POST, {'name': ['value']}) @@ -354,6 +356,7 @@ class RequestsTests(unittest.TestCase): """ payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': len(payload), 'wsgi.input': BytesIO(payload)}) self.assertEqual(request.read(2), b'na') @@ -402,9 +405,28 @@ class RequestsTests(unittest.TestCase): 'wsgi.input': BytesIO(payload)}) self.assertEqual(request.POST, {}) + def test_POST_binary_only(self): + payload = b'\r\n\x01\x00\x00\x00ab\x00\x00\xcd\xcc,@' + environ = {'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'application/octet-stream', + 'CONTENT_LENGTH': len(payload), + 'wsgi.input': BytesIO(payload)} + request = WSGIRequest(environ) + self.assertEqual(request.POST, {}) + self.assertEqual(request.FILES, {}) + self.assertEqual(request.body, payload) + + # Same test without specifying content-type + environ.update({'CONTENT_TYPE': '', 'wsgi.input': BytesIO(payload)}) + request = WSGIRequest(environ) + self.assertEqual(request.POST, {}) + self.assertEqual(request.FILES, {}) + self.assertEqual(request.body, payload) + def test_read_by_lines(self): payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': len(payload), 'wsgi.input': BytesIO(payload)}) self.assertEqual(list(request), [b'name=value']) @@ -415,6 +437,7 @@ class RequestsTests(unittest.TestCase): """ payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': len(payload), 'wsgi.input': BytesIO(payload)}) raw_data = request.body @@ -427,6 +450,7 @@ class RequestsTests(unittest.TestCase): """ payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': len(payload), 'wsgi.input': BytesIO(payload)}) raw_data = request.body @@ -479,6 +503,7 @@ class RequestsTests(unittest.TestCase): payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': len(payload), 'wsgi.input': ExplodingBytesIO(payload)}) -- cgit v1.3 From c2e19e26bc33d34eff57079bd1a6838ff64d9e81 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 20 Oct 2012 15:48:38 +0200 Subject: Fixed #17856 -- Passed obj to get_inline_instances Thanks ybon, quinode and sjaensch for the patch, and Tim Graham for the review. --- django/contrib/admin/options.py | 12 ++++++------ docs/ref/contrib/admin/index.txt | 10 ++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) (limited to 'docs') diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index bbd7939d3f..19c212db9a 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -346,14 +346,14 @@ class ModelAdmin(BaseModelAdmin): self.admin_site = admin_site super(ModelAdmin, self).__init__() - def get_inline_instances(self, request): + def get_inline_instances(self, request, obj=None): inline_instances = [] for inline_class in self.inlines: inline = inline_class(self.model, self.admin_site) if request: if not (inline.has_add_permission(request) or - inline.has_change_permission(request) or - inline.has_delete_permission(request)): + inline.has_change_permission(request, obj) or + inline.has_delete_permission(request, obj)): continue if not inline.has_add_permission(request): inline.max_num = 0 @@ -506,7 +506,7 @@ class ModelAdmin(BaseModelAdmin): fields=self.list_editable, **defaults) def get_formsets(self, request, obj=None): - for inline in self.get_inline_instances(request): + for inline in self.get_inline_instances(request, obj): yield inline.get_formset(request, obj) def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True): @@ -994,7 +994,7 @@ class ModelAdmin(BaseModelAdmin): ModelForm = self.get_form(request) formsets = [] - inline_instances = self.get_inline_instances(request) + inline_instances = self.get_inline_instances(request, None) if request.method == 'POST': form = ModelForm(request.POST, request.FILES) if form.is_valid(): @@ -1091,7 +1091,7 @@ class ModelAdmin(BaseModelAdmin): ModelForm = self.get_form(request, obj) formsets = [] - inline_instances = self.get_inline_instances(request) + inline_instances = self.get_inline_instances(request, obj) if request.method == 'POST': form = ModelForm(request.POST, request.FILES, instance=obj) if form.is_valid(): diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 971db19925..72066ca799 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1046,6 +1046,16 @@ templates used by the :class:`ModelAdmin` views: changelist that will be linked to the change view, as described in the :attr:`ModelAdmin.list_display_links` section. +.. method:: ModelAdmin.get_inline_instances(self, request, obj=None) + + .. versionadded:: 1.5 + + The ``get_inline_instances`` method is given the ``HttpRequest`` and the + ``obj`` being edited (or ``None`` on an add form) and is expected to return + a ``list`` or ``tuple`` of :class:`~django.contrib.admin.InlineModelAdmin` + objects, as described below in the :class:`~django.contrib.admin.InlineModelAdmin` + section. + .. method:: ModelAdmin.get_urls(self) The ``get_urls`` method on a ``ModelAdmin`` returns the URLs to be used for -- cgit v1.3 From 300d052713a4312bcfca334ea34b348d57549950 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 20 Oct 2012 09:57:15 -0400 Subject: Fixed arguments for get_inline_instances example; refs #17856 --- docs/ref/contrib/admin/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 72066ca799..e9b573e3a0 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1148,7 +1148,7 @@ templates used by the :class:`ModelAdmin` views: inlines = [MyInline, SomeOtherInline] def get_formsets(self, request, obj=None): - for inline in self.get_inline_instances(): + for inline in self.get_inline_instances(request, obj): # hide MyInline in the add view if isinstance(inline, MyInline) and obj is None: continue -- cgit v1.3 From 4b27813198ae31892f1159d437e492f7745761a0 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 20 Oct 2012 17:40:14 +0200 Subject: Fixed #7581 -- Added streaming responses. Thanks mrmachine and everyone else involved on this long-standing ticket. --- django/http/__init__.py | 178 +++++++++++++++++++++++----- django/http/utils.py | 12 +- django/middleware/common.py | 16 ++- django/middleware/gzip.py | 24 ++-- django/middleware/http.py | 2 +- django/test/testcases.py | 4 +- django/utils/cache.py | 3 +- django/utils/text.py | 31 +++++ django/views/generic/base.py | 2 +- django/views/static.py | 6 +- docs/ref/request-response.txt | 87 +++++++++++++- docs/releases/1.5.txt | 18 +++ docs/topics/http/middleware.txt | 17 +++ tests/regressiontests/cache/tests.py | 26 +++- tests/regressiontests/httpwrappers/abc.txt | 1 + tests/regressiontests/httpwrappers/tests.py | 111 ++++++++++++++++- tests/regressiontests/middleware/tests.py | 43 ++++++- tests/regressiontests/views/tests/static.py | 27 +++-- 18 files changed, 533 insertions(+), 75 deletions(-) create mode 100644 tests/regressiontests/httpwrappers/abc.txt (limited to 'docs') diff --git a/django/http/__init__.py b/django/http/__init__.py index b67c182c37..49acd57af3 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -528,18 +528,23 @@ def parse_cookie(cookie): class BadHeaderError(ValueError): pass -class HttpResponse(object): - """A basic HTTP response, with content and dictionary-accessed headers.""" +class HttpResponseBase(object): + """ + An HTTP response base class with dictionary-accessed headers. + + This class doesn't handle content. It should not be used directly. + Use the HttpResponse and StreamingHttpResponse subclasses instead. + """ status_code = 200 - def __init__(self, content='', content_type=None, status=None, - mimetype=None): + def __init__(self, content_type=None, status=None, mimetype=None): # _headers is a mapping of the lower-case name to the original case of # the header (required for working with legacy systems) and the header # value. Both the name of the header and its value are ASCII strings. self._headers = {} self._charset = settings.DEFAULT_CHARSET + self._closable_objects = [] if mimetype: warnings.warn("Using mimetype keyword argument is deprecated, use" " content_type instead", PendingDeprecationWarning) @@ -547,26 +552,24 @@ class HttpResponse(object): if not content_type: content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, self._charset) - # content is a bytestring. See the content property methods. - self.content = content self.cookies = SimpleCookie() if status: self.status_code = status self['Content-Type'] = content_type - def serialize(self): - """Full HTTP message, including headers, as a bytestring.""" + def serialize_headers(self): + """HTTP headers as a bytestring.""" headers = [ ('%s: %s' % (key, value)).encode('us-ascii') for key, value in self._headers.values() ] - return b'\r\n'.join(headers) + b'\r\n\r\n' + self.content + return b'\r\n'.join(headers) if six.PY3: - __bytes__ = serialize + __bytes__ = serialize_headers else: - __str__ = serialize + __str__ = serialize_headers def _convert_to_charset(self, value, charset, mime_encode=False): """Converts headers key/value to ascii/latin1 native strings. @@ -690,24 +693,75 @@ class HttpResponse(object): self.set_cookie(key, max_age=0, path=path, domain=domain, expires='Thu, 01-Jan-1970 00:00:00 GMT') + # Common methods used by subclasses + + def make_bytes(self, value): + """Turn a value into a bytestring encoded in the output charset.""" + # For backwards compatibility, this method supports values that are + # unlikely to occur in real applications. It has grown complex and + # should be refactored. It also overlaps __next__. See #18796. + if self.has_header('Content-Encoding'): + if isinstance(value, int): + value = six.text_type(value) + if isinstance(value, six.text_type): + value = value.encode('ascii') + # force conversion to bytes in case chunk is a subclass + return bytes(value) + else: + return force_bytes(value, self._charset) + + # These methods partially implement the file-like object interface. + # See http://docs.python.org/lib/bltin-file-objects.html + + # The WSGI server must call this method upon completion of the request. + # See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html + def close(self): + for closable in self._closable_objects: + closable.close() + + def write(self, content): + raise Exception("This %s instance is not writable" % self.__class__.__name__) + + def flush(self): + pass + + def tell(self): + raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) + +class HttpResponse(HttpResponseBase): + """ + An HTTP response class with a string as content. + + This content that can be read, appended to or replaced. + """ + + streaming = False + + def __init__(self, content='', *args, **kwargs): + super(HttpResponse, self).__init__(*args, **kwargs) + # Content is a bytestring. See the `content` property methods. + self.content = content + + def serialize(self): + """Full HTTP message, including headers, as a bytestring.""" + return self.serialize_headers() + b'\r\n\r\n' + self.content + + if six.PY3: + __bytes__ = serialize + else: + __str__ = serialize + @property def content(self): - if self.has_header('Content-Encoding'): - def make_bytes(value): - if isinstance(value, int): - value = six.text_type(value) - if isinstance(value, six.text_type): - value = value.encode('ascii') - # force conversion to bytes in case chunk is a subclass - return bytes(value) - return b''.join(make_bytes(e) for e in self._container) - return b''.join(force_bytes(e, self._charset) for e in self._container) + return b''.join(self.make_bytes(e) for e in self._container) @content.setter def content(self, value): if hasattr(value, '__iter__') and not isinstance(value, (bytes, six.string_types)): self._container = value self._base_content_is_iter = True + if hasattr(value, 'close'): + self._closable_objects.append(value) else: self._container = [value] self._base_content_is_iter = False @@ -727,25 +781,85 @@ class HttpResponse(object): next = __next__ # Python 2 compatibility - def close(self): - if hasattr(self._container, 'close'): - self._container.close() - - # The remaining methods partially implement the file-like object interface. - # See http://docs.python.org/lib/bltin-file-objects.html def write(self, content): if self._base_content_is_iter: - raise Exception("This %s instance is not writable" % self.__class__) + raise Exception("This %s instance is not writable" % self.__class__.__name__) self._container.append(content) - def flush(self): - pass - def tell(self): if self._base_content_is_iter: - raise Exception("This %s instance cannot tell its position" % self.__class__) + raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) return sum([len(chunk) for chunk in self]) +class StreamingHttpResponse(HttpResponseBase): + """ + A streaming HTTP response class with an iterator as content. + + This should only be iterated once, when the response is streamed to the + client. However, it can be appended to or replaced with a new iterator + that wraps the original content (or yields entirely new content). + """ + + streaming = True + + def __init__(self, streaming_content=(), *args, **kwargs): + super(StreamingHttpResponse, self).__init__(*args, **kwargs) + # `streaming_content` should be an iterable of bytestrings. + # See the `streaming_content` property methods. + self.streaming_content = streaming_content + + @property + def content(self): + raise AttributeError("This %s instance has no `content` attribute. " + "Use `streaming_content` instead." % self.__class__.__name__) + + @property + def streaming_content(self): + return self._iterator + + @streaming_content.setter + def streaming_content(self, value): + # Ensure we can never iterate on "value" more than once. + self._iterator = iter(value) + if hasattr(value, 'close'): + self._closable_objects.append(value) + + def __iter__(self): + return self + + def __next__(self): + return self.make_bytes(next(self._iterator)) + + next = __next__ # Python 2 compatibility + +class CompatibleStreamingHttpResponse(StreamingHttpResponse): + """ + This class maintains compatibility with middleware that doesn't know how + to handle the content of a streaming response by exposing a `content` + attribute that will consume and cache the content iterator when accessed. + + These responses will stream only if no middleware attempts to access the + `content` attribute. Otherwise, they will behave like a regular response, + and raise a `PendingDeprecationWarning`. + """ + @property + def content(self): + warnings.warn( + 'Accessing the `content` attribute on a streaming response is ' + 'deprecated. Use the `streaming_content` attribute instead.', + PendingDeprecationWarning) + content = b''.join(self) + self.streaming_content = [content] + return content + + @content.setter + def content(self, content): + warnings.warn( + 'Accessing the `content` attribute on a streaming response is ' + 'deprecated. Use the `streaming_content` attribute instead.', + PendingDeprecationWarning) + self.streaming_content = [content] + class HttpResponseRedirectBase(HttpResponse): allowed_schemes = ['http', 'https', 'ftp'] diff --git a/django/http/utils.py b/django/http/utils.py index 01808648ba..f7ff477f09 100644 --- a/django/http/utils.py +++ b/django/http/utils.py @@ -26,10 +26,16 @@ def conditional_content_removal(request, response): responses. Ensures compliance with RFC 2616, section 4.3. """ if 100 <= response.status_code < 200 or response.status_code in (204, 304): - response.content = '' - response['Content-Length'] = 0 + if response.streaming: + response.streaming_content = [] + else: + response.content = '' + response['Content-Length'] = '0' if request.method == 'HEAD': - response.content = '' + if response.streaming: + response.streaming_content = [] + else: + response.content = '' return response def fix_IE_for_attach(request, response): diff --git a/django/middleware/common.py b/django/middleware/common.py index 0ec17fbe92..6fbbf43044 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -113,14 +113,18 @@ class CommonMiddleware(object): if settings.USE_ETAGS: if response.has_header('ETag'): etag = response['ETag'] + elif response.streaming: + etag = None else: etag = '"%s"' % hashlib.md5(response.content).hexdigest() - if response.status_code >= 200 and response.status_code < 300 and request.META.get('HTTP_IF_NONE_MATCH') == etag: - cookies = response.cookies - response = http.HttpResponseNotModified() - response.cookies = cookies - else: - response['ETag'] = etag + if etag is not None: + if (200 <= response.status_code < 300 + and request.META.get('HTTP_IF_NONE_MATCH') == etag): + cookies = response.cookies + response = http.HttpResponseNotModified() + response.cookies = cookies + else: + response['ETag'] = etag return response diff --git a/django/middleware/gzip.py b/django/middleware/gzip.py index 69f938cf0a..fb54501a03 100644 --- a/django/middleware/gzip.py +++ b/django/middleware/gzip.py @@ -1,6 +1,6 @@ import re -from django.utils.text import compress_string +from django.utils.text import compress_sequence, compress_string from django.utils.cache import patch_vary_headers re_accepts_gzip = re.compile(r'\bgzip\b') @@ -13,7 +13,7 @@ class GZipMiddleware(object): """ def process_response(self, request, response): # It's not worth attempting to compress really short responses. - if len(response.content) < 200: + if not response.streaming and len(response.content) < 200: return response patch_vary_headers(response, ('Accept-Encoding',)) @@ -32,15 +32,21 @@ class GZipMiddleware(object): if not re_accepts_gzip.search(ae): return response - # Return the compressed content only if it's actually shorter. - compressed_content = compress_string(response.content) - if len(compressed_content) >= len(response.content): - return response + if response.streaming: + # Delete the `Content-Length` header for streaming content, because + # we won't know the compressed size until we stream it. + response.streaming_content = compress_sequence(response.streaming_content) + del response['Content-Length'] + else: + # Return the compressed content only if it's actually shorter. + compressed_content = compress_string(response.content) + if len(compressed_content) >= len(response.content): + return response + response.content = compressed_content + response['Content-Length'] = str(len(response.content)) if response.has_header('ETag'): response['ETag'] = re.sub('"$', ';gzip"', response['ETag']) - - response.content = compressed_content response['Content-Encoding'] = 'gzip' - response['Content-Length'] = str(len(response.content)) + return response diff --git a/django/middleware/http.py b/django/middleware/http.py index 86e46cea82..5a46e04946 100644 --- a/django/middleware/http.py +++ b/django/middleware/http.py @@ -10,7 +10,7 @@ class ConditionalGetMiddleware(object): """ def process_response(self, request, response): response['Date'] = http_date() - if not response.has_header('Content-Length'): + if not response.streaming and not response.has_header('Content-Length'): response['Content-Length'] = str(len(response.content)) if response.has_header('ETag'): diff --git a/django/test/testcases.py b/django/test/testcases.py index 1d52fed69f..cfa2cde643 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -596,7 +596,9 @@ class TransactionTestCase(SimpleTestCase): 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) + content = b''.join(response).decode(response._charset) + # Avoid ResourceWarning about unclosed files. + response.close() if html: content = assert_and_parse_html(self, content, None, "Response's content is not valid HTML:") diff --git a/django/utils/cache.py b/django/utils/cache.py index 91c4796988..0fceaa96e6 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -95,7 +95,8 @@ def get_max_age(response): pass def _set_response_etag(response): - response['ETag'] = '"%s"' % hashlib.md5(response.content).hexdigest() + if not response.streaming: + response['ETag'] = '"%s"' % hashlib.md5(response.content).hexdigest() return response def patch_response_headers(response, cache_timeout=None): diff --git a/django/utils/text.py b/django/utils/text.py index c19708458b..d75ca8dbca 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -288,6 +288,37 @@ def compress_string(s): zfile.close() return zbuf.getvalue() +class StreamingBuffer(object): + def __init__(self): + self.vals = [] + + def write(self, val): + self.vals.append(val) + + def read(self): + ret = b''.join(self.vals) + self.vals = [] + return ret + + def flush(self): + return + + def close(self): + return + +# Like compress_string, but for iterators of strings. +def compress_sequence(sequence): + buf = StreamingBuffer() + zfile = GzipFile(mode='wb', compresslevel=6, fileobj=buf) + # Output headers... + yield buf.read() + for item in sequence: + zfile.write(item) + zfile.flush() + yield buf.read() + zfile.close() + yield buf.read() + ustring_re = re.compile("([\u0080-\uffff])") def javascript_quote(s, quote_double_quotes=False): diff --git a/django/views/generic/base.py b/django/views/generic/base.py index d2349e1fca..23e18c54a0 100644 --- a/django/views/generic/base.py +++ b/django/views/generic/base.py @@ -99,7 +99,7 @@ class View(object): """ response = http.HttpResponse() response['Allow'] = ', '.join(self._allowed_methods()) - response['Content-Length'] = 0 + response['Content-Length'] = '0' return response def _allowed_methods(self): diff --git a/django/views/static.py b/django/views/static.py index 7dd44c5772..f61ba28bd5 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -14,7 +14,8 @@ try: except ImportError: # Python 2 from urllib import unquote -from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified +from django.http import (CompatibleStreamingHttpResponse, Http404, + HttpResponse, HttpResponseRedirect, HttpResponseNotModified) from django.template import loader, Template, Context, TemplateDoesNotExist from django.utils.http import http_date, parse_http_date from django.utils.translation import ugettext as _, ugettext_noop @@ -62,8 +63,7 @@ def serve(request, path, document_root=None, show_indexes=False): if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), statobj.st_mtime, statobj.st_size): return HttpResponseNotModified() - with open(fullpath, 'rb') as f: - response = HttpResponse(f.read(), content_type=mimetype) + response = CompatibleStreamingHttpResponse(open(fullpath, 'rb'), content_type=mimetype) response["Last-Modified"] = http_date(statobj.st_mtime) if stat.S_ISREG(statobj.st_mode): response["Content-Length"] = statobj.st_size diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index d7266f0aff..89d0fe847c 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -566,13 +566,21 @@ file-like object:: Passing iterators ~~~~~~~~~~~~~~~~~ -Finally, you can pass ``HttpResponse`` an iterator rather than passing it -hard-coded strings. If you use this technique, follow these guidelines: +Finally, you can pass ``HttpResponse`` an iterator rather than strings. If you +use this technique, the iterator should return strings. -* The iterator should return strings. -* If an :class:`HttpResponse` has been initialized with an iterator as its - content, you can't use the :class:`HttpResponse` instance as a file-like - object. Doing so will raise ``Exception``. +.. versionchanged:: 1.5 + + Passing an iterator as content to :class:`HttpResponse` creates a + streaming response if (and only if) no middleware accesses the + :attr:`HttpResponse.content` attribute before the response is returned. + + If you want to guarantee that your response will stream to the client, you + should use the new :class:`StreamingHttpResponse` class instead. + +If an :class:`HttpResponse` instance has been initialized with an iterator as +its content, you can't use it as a file-like object. Doing so will raise an +exception. Setting headers ~~~~~~~~~~~~~~~ @@ -614,6 +622,13 @@ Attributes The `HTTP Status code`_ for the response. +.. attribute:: HttpResponse.streaming + + This is always ``False``. + + This attribute exists so middleware can treat streaming responses + differently from regular responses. + Methods ------- @@ -781,3 +796,63 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in method, Django will treat it as emulating a :class:`~django.template.response.SimpleTemplateResponse`, and the ``render`` method must itself return a valid response object. + +StreamingHttpResponse objects +============================= + +.. versionadded:: 1.5 + +.. class:: StreamingHttpResponse + +The :class:`StreamingHttpResponse` class is used to stream a response from +Django to the browser. You might want to do this if generating the response +takes too long or uses too much memory. For instance, it's useful for +generating large CSV files. + +.. admonition:: Performance considerations + + Django is designed for short-lived requests. Streaming responses will tie + a worker process and keep a database connection idle in transaction for + the entire duration of the response. This may result in poor performance. + + Generally speaking, you should perform expensive tasks outside of the + request-response cycle, rather than resorting to a streamed response. + +The :class:`StreamingHttpResponse` is not a subclass of :class:`HttpResponse`, +because it features a slightly different API. However, it is almost identical, +with the following notable differences: + +* It should be given an iterator that yields strings as content. + +* You cannot access its content, except by iterating the response object + itself. This should only occur when the response is returned to the client. + +* It has no ``content`` attribute. Instead, it has a + :attr:`~StreamingHttpResponse.streaming_content` attribute. + +* You cannot use the file-like object ``tell()`` or ``write()`` methods. + Doing so will raise an exception. + +* Any iterators that have a ``close()`` method and are assigned as content will + be closed automatically after the response has been iterated. + +:class:`StreamingHttpResponse` should only be used in situations where it is +absolutely required that the whole content isn't iterated before transferring +the data to the client. Because the content can't be accessed, many +middlewares can't function normally. For example the ``ETag`` and ``Content- +Length`` headers can't be generated for streaming responses. + +Attributes +---------- + +.. attribute:: StreamingHttpResponse.streaming_content + + An iterator of strings representing the content. + +.. attribute:: HttpResponse.status_code + + The `HTTP Status code`_ for the response. + +.. attribute:: HttpResponse.streaming + + This is always ``True``. diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index d30bd5ff7e..f7467bc06a 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -84,6 +84,24 @@ For one-to-one relationships, both sides can be cached. For many-to-one relationships, only the single side of the relationship can be cached. This is particularly helpful in combination with ``prefetch_related``. +Explicit support for streaming responses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before Django 1.5, it was possible to create a streaming response by passing +an iterator to :class:`~django.http.HttpResponse`. But this was unreliable: +any middleware that accessed the :attr:`~django.http.HttpResponse.content` +attribute would consume the iterator prematurely. + +You can now explicitly generate a streaming response with the new +:class:`~django.http.StreamingHttpResponse` class. This class exposes a +:class:`~django.http.StreamingHttpResponse.streaming_content` attribute which +is an iterator. + +Since :class:`~django.http.StreamingHttpResponse` does not have a ``content`` +attribute, middleware that need access to the response content must test for +streaming responses and behave accordingly. See :ref:`response-middleware` for +more information. + ``{% verbatim %}`` template tag ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index a8347e52a0..c27e7e8690 100644 --- a/docs/topics/http/middleware.txt +++ b/docs/topics/http/middleware.txt @@ -164,6 +164,23 @@ an earlier middleware method returned an :class:`~django.http.HttpResponse` classes are applied in reverse order, from the bottom up. This means classes defined at the end of :setting:`MIDDLEWARE_CLASSES` will be run first. +.. versionchanged:: 1.5 + ``response`` may also be an :class:`~django.http.StreamingHttpResponse` + object. + +Unlike :class:`~django.http.HttpResponse`, +:class:`~django.http.StreamingHttpResponse` does not have a ``content`` +attribute. As a result, middleware can no longer assume that all responses +will have a ``content`` attribute. If they need access to the content, they +must test for streaming responses and adjust their behavior accordingly:: + + if response.streaming: + response.streaming_content = wrap_streaming_content(response.streaming_content) + else: + response.content = wrap_content(response.content) + +``streaming_content`` should be assumed to be too large to hold in memory. +Middleware may wrap it in a new generator, but must not consume it. .. _exception-middleware: diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index de27bc9476..a6eff8950b 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -19,7 +19,8 @@ from django.core.cache import get_cache from django.core.cache.backends.base import (CacheKeyWarning, InvalidCacheBackendError) from django.db import router -from django.http import HttpResponse, HttpRequest, QueryDict +from django.http import (HttpResponse, HttpRequest, StreamingHttpResponse, + QueryDict) from django.middleware.cache import (FetchFromCacheMiddleware, UpdateCacheMiddleware, CacheMiddleware) from django.template import Template @@ -1416,6 +1417,29 @@ class CacheI18nTest(TestCase): # reset the language translation.deactivate() + @override_settings( + CACHE_MIDDLEWARE_KEY_PREFIX="test", + CACHE_MIDDLEWARE_SECONDS=60, + USE_ETAGS=True, + ) + def test_middleware_with_streaming_response(self): + # cache with non empty request.GET + request = self._get_request_cache(query_string='foo=baz&other=true') + + # first access, cache must return None + get_cache_data = FetchFromCacheMiddleware().process_request(request) + self.assertEqual(get_cache_data, None) + + # pass streaming response through UpdateCacheMiddleware. + content = 'Check for cache with QUERY_STRING and streaming content' + response = StreamingHttpResponse(content) + UpdateCacheMiddleware().process_response(request, response) + + # second access, cache must still return None, because we can't cache + # streaming response. + get_cache_data = FetchFromCacheMiddleware().process_request(request) + self.assertEqual(get_cache_data, None) + @override_settings( CACHES={ diff --git a/tests/regressiontests/httpwrappers/abc.txt b/tests/regressiontests/httpwrappers/abc.txt new file mode 100644 index 0000000000..6bac42b3ad --- /dev/null +++ b/tests/regressiontests/httpwrappers/abc.txt @@ -0,0 +1 @@ +random content diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index 4c6aed1b97..bfb4ae1fd5 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -2,12 +2,13 @@ from __future__ import unicode_literals import copy +import os import pickle from django.core.exceptions import SuspiciousOperation from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseNotAllowed, - HttpResponseNotModified, + HttpResponseNotModified, StreamingHttpResponse, SimpleCookie, BadHeaderError, parse_cookie) from django.test import TestCase @@ -351,7 +352,6 @@ class HttpResponseTests(unittest.TestCase): self.assertRaises(SuspiciousOperation, HttpResponsePermanentRedirect, url) - class HttpResponseSubclassesTests(TestCase): def test_redirect(self): response = HttpResponseRedirect('/redirected/') @@ -379,6 +379,113 @@ class HttpResponseSubclassesTests(TestCase): content_type='text/html') self.assertContains(response, 'Only the GET method is allowed', status_code=405) +class StreamingHttpResponseTests(TestCase): + def test_streaming_response(self): + r = StreamingHttpResponse(iter(['hello', 'world'])) + + # iterating over the response itself yields bytestring chunks. + chunks = list(r) + self.assertEqual(chunks, [b'hello', b'world']) + for chunk in chunks: + self.assertIsInstance(chunk, six.binary_type) + + # and the response can only be iterated once. + self.assertEqual(list(r), []) + + # even when a sequence that can be iterated many times, like a list, + # is given as content. + r = StreamingHttpResponse(['abc', 'def']) + self.assertEqual(list(r), [b'abc', b'def']) + self.assertEqual(list(r), []) + + # streaming responses don't have a `content` attribute. + self.assertFalse(hasattr(r, 'content')) + + # and you can't accidentally assign to a `content` attribute. + with self.assertRaises(AttributeError): + r.content = 'xyz' + + # but they do have a `streaming_content` attribute. + self.assertTrue(hasattr(r, 'streaming_content')) + + # that exists so we can check if a response is streaming, and wrap or + # replace the content iterator. + r.streaming_content = iter(['abc', 'def']) + r.streaming_content = (chunk.upper() for chunk in r.streaming_content) + self.assertEqual(list(r), [b'ABC', b'DEF']) + + # coercing a streaming response to bytes doesn't return a complete HTTP + # message like a regular response does. it only gives us the headers. + r = StreamingHttpResponse(iter(['hello', 'world'])) + self.assertEqual( + six.binary_type(r), b'Content-Type: text/html; charset=utf-8') + + # and this won't consume its content. + self.assertEqual(list(r), [b'hello', b'world']) + + # additional content cannot be written to the response. + r = StreamingHttpResponse(iter(['hello', 'world'])) + with self.assertRaises(Exception): + r.write('!') + + # and we can't tell the current position. + with self.assertRaises(Exception): + r.tell() + +class FileCloseTests(TestCase): + def test_response(self): + filename = os.path.join(os.path.dirname(__file__), 'abc.txt') + + # file isn't closed until we close the response. + file1 = open(filename) + r = HttpResponse(file1) + self.assertFalse(file1.closed) + r.close() + self.assertTrue(file1.closed) + + # don't automatically close file when we finish iterating the response. + file1 = open(filename) + r = HttpResponse(file1) + self.assertFalse(file1.closed) + list(r) + self.assertFalse(file1.closed) + r.close() + self.assertTrue(file1.closed) + + # when multiple file are assigned as content, make sure they are all + # closed with the response. + file1 = open(filename) + file2 = open(filename) + r = HttpResponse(file1) + r.content = file2 + self.assertFalse(file1.closed) + self.assertFalse(file2.closed) + r.close() + self.assertTrue(file1.closed) + self.assertTrue(file2.closed) + + def test_streaming_response(self): + filename = os.path.join(os.path.dirname(__file__), 'abc.txt') + + # file isn't closed until we close the response. + file1 = open(filename) + r = StreamingHttpResponse(file1) + self.assertFalse(file1.closed) + r.close() + self.assertTrue(file1.closed) + + # when multiple file are assigned as content, make sure they are all + # closed with the response. + file1 = open(filename) + file2 = open(filename) + r = StreamingHttpResponse(file1) + r.streaming_content = file2 + self.assertFalse(file1.closed) + self.assertFalse(file2.closed) + r.close() + self.assertTrue(file1.closed) + self.assertTrue(file2.closed) + class CookieTests(unittest.TestCase): def test_encode(self): """ diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py index eb66f2bbf3..de901f4a80 100644 --- a/tests/regressiontests/middleware/tests.py +++ b/tests/regressiontests/middleware/tests.py @@ -8,7 +8,7 @@ from io import BytesIO from django.conf import settings from django.core import mail from django.http import HttpRequest -from django.http import HttpResponse +from django.http import HttpResponse, StreamingHttpResponse from django.middleware.clickjacking import XFrameOptionsMiddleware from django.middleware.common import CommonMiddleware from django.middleware.http import ConditionalGetMiddleware @@ -322,6 +322,12 @@ class ConditionalGetMiddlewareTest(TestCase): self.assertTrue('Content-Length' in self.resp) self.assertEqual(int(self.resp['Content-Length']), content_length) + def test_content_length_header_not_added(self): + resp = StreamingHttpResponse('content') + self.assertFalse('Content-Length' in resp) + resp = ConditionalGetMiddleware().process_response(self.req, resp) + self.assertFalse('Content-Length' in resp) + def test_content_length_header_not_changed(self): bad_content_length = len(self.resp.content) + 10 self.resp['Content-Length'] = bad_content_length @@ -351,6 +357,29 @@ class ConditionalGetMiddlewareTest(TestCase): self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp) self.assertEqual(self.resp.status_code, 200) + @override_settings(USE_ETAGS=True) + def test_etag(self): + req = HttpRequest() + res = HttpResponse('content') + self.assertTrue( + CommonMiddleware().process_response(req, res).has_header('ETag')) + + @override_settings(USE_ETAGS=True) + def test_etag_streaming_response(self): + req = HttpRequest() + res = StreamingHttpResponse(['content']) + res['ETag'] = 'tomatoes' + self.assertEqual( + CommonMiddleware().process_response(req, res).get('ETag'), + 'tomatoes') + + @override_settings(USE_ETAGS=True) + def test_no_etag_streaming_response(self): + req = HttpRequest() + res = StreamingHttpResponse(['content']) + self.assertFalse( + CommonMiddleware().process_response(req, res).has_header('ETag')) + # Tests for the Last-Modified header def test_if_modified_since_and_no_last_modified(self): @@ -511,6 +540,7 @@ class GZipMiddlewareTest(TestCase): short_string = b"This string is too short to be worth compressing." compressible_string = b'a' * 500 uncompressible_string = b''.join(six.int2byte(random.randint(0, 255)) for _ in xrange(500)) + sequence = [b'a' * 500, b'b' * 200, b'a' * 300] def setUp(self): self.req = HttpRequest() @@ -525,6 +555,8 @@ class GZipMiddlewareTest(TestCase): self.resp.status_code = 200 self.resp.content = self.compressible_string self.resp['Content-Type'] = 'text/html; charset=UTF-8' + self.stream_resp = StreamingHttpResponse(self.sequence) + self.stream_resp['Content-Type'] = 'text/html; charset=UTF-8' @staticmethod def decompress(gzipped_string): @@ -539,6 +571,15 @@ class GZipMiddlewareTest(TestCase): self.assertEqual(r.get('Content-Encoding'), 'gzip') self.assertEqual(r.get('Content-Length'), str(len(r.content))) + def test_compress_streaming_response(self): + """ + Tests that compression is performed on responses with streaming content. + """ + r = GZipMiddleware().process_response(self.req, self.stream_resp) + self.assertEqual(self.decompress(b''.join(r)), b''.join(self.sequence)) + self.assertEqual(r.get('Content-Encoding'), 'gzip') + self.assertFalse(r.has_header('Content-Length')) + def test_compress_non_200_response(self): """ Tests that compression is performed on responses with a status other than 200. diff --git a/tests/regressiontests/views/tests/static.py b/tests/regressiontests/views/tests/static.py index 38cf38ce46..221244a4a5 100644 --- a/tests/regressiontests/views/tests/static.py +++ b/tests/regressiontests/views/tests/static.py @@ -31,28 +31,35 @@ class StaticTests(TestCase): media_files = ['file.txt', 'file.txt.gz'] for filename in media_files: response = self.client.get('/views/%s/%s' % (self.prefix, filename)) + response_content = b''.join(response) + response.close() file_path = path.join(media_dir, filename) with open(file_path, 'rb') as fp: - self.assertEqual(fp.read(), response.content) - self.assertEqual(len(response.content), int(response['Content-Length'])) + self.assertEqual(fp.read(), response_content) + self.assertEqual(len(response_content), int(response['Content-Length'])) self.assertEqual(mimetypes.guess_type(file_path)[1], response.get('Content-Encoding', None)) def test_unknown_mime_type(self): response = self.client.get('/views/%s/file.unknown' % self.prefix) + response.close() self.assertEqual('application/octet-stream', response['Content-Type']) def test_copes_with_empty_path_component(self): file_name = 'file.txt' response = self.client.get('/views/%s//%s' % (self.prefix, file_name)) + response_content = b''.join(response) + response.close() with open(path.join(media_dir, file_name), 'rb') as fp: - self.assertEqual(fp.read(), response.content) + self.assertEqual(fp.read(), response_content) def test_is_modified_since(self): file_name = 'file.txt' response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE='Thu, 1 Jan 1970 00:00:00 GMT') + response_content = b''.join(response) + response.close() with open(path.join(media_dir, file_name), 'rb') as fp: - self.assertEqual(fp.read(), response.content) + self.assertEqual(fp.read(), response_content) def test_not_modified_since(self): file_name = 'file.txt' @@ -74,9 +81,11 @@ class StaticTests(TestCase): invalid_date = 'Mon, 28 May 999999999999 28:25:26 GMT' response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE=invalid_date) + response_content = b''.join(response) + response.close() with open(path.join(media_dir, file_name), 'rb') as fp: - self.assertEqual(fp.read(), response.content) - self.assertEqual(len(response.content), + self.assertEqual(fp.read(), response_content) + self.assertEqual(len(response_content), int(response['Content-Length'])) def test_invalid_if_modified_since2(self): @@ -89,9 +98,11 @@ class StaticTests(TestCase): invalid_date = ': 1291108438, Wed, 20 Oct 2010 14:05:00 GMT' response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE=invalid_date) + response_content = b''.join(response) + response.close() with open(path.join(media_dir, file_name), 'rb') as fp: - self.assertEqual(fp.read(), response.content) - self.assertEqual(len(response.content), + self.assertEqual(fp.read(), response_content) + self.assertEqual(len(response_content), int(response['Content-Length'])) -- cgit v1.3 From 2f722d9728c1946d5d800b3e0b24de7f566a755d Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 20 Oct 2012 15:21:19 -0400 Subject: Fixed #13869 - Warned that QuerySet.iterator() doesn't affect DB driver caching; thanks jtiai for the suggestion. --- docs/ref/models/querysets.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'docs') diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 858371978a..7138cd0e74 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1456,6 +1456,16 @@ evaluated will force it to evaluate again, repeating the query. Also, use of ``iterator()`` causes previous ``prefetch_related()`` calls to be ignored since these two optimizations do not make sense together. +.. warning:: + + Some Python database drivers like ``psycopg2`` perform caching if using + client side cursors (instantiated with ``connection.cursor()`` and what + Django's ORM uses). Using ``iterator()`` does not affect caching at the + database driver level. To disable this caching, look at `server side + cursors`_. + +.. _server side cursors: http://initd.org/psycopg/docs/usage.html#server-side-cursors + latest ~~~~~~ -- cgit v1.3 From e987d20ac92ceef382cc02dfab38aac64d84dc5d Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Thu, 18 Oct 2012 09:55:14 -0700 Subject: Added 1.4.2 release notes --- docs/releases/1.4.2.txt | 43 ++++++++++++++++++++++++++++++++++++++++++- docs/releases/index.txt | 2 +- 2 files changed, 43 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/releases/1.4.2.txt b/docs/releases/1.4.2.txt index 6f2e9aca2e..07eec39764 100644 --- a/docs/releases/1.4.2.txt +++ b/docs/releases/1.4.2.txt @@ -2,13 +2,54 @@ Django 1.4.2 release notes ========================== -*TO BE RELEASED* +*October 17, 2012* This is the second security release in the Django 1.4 series. +Host header poisoning +--------------------- + +Some parts of Django -- independent of end-user-written applications -- make +use of full URLs, including domain name, which are generated from the HTTP Host +header. Some attacks against this are beyond Django's ability to control, and +require the web server to be properly configured; Django's documentation has +for some time contained notes advising users on such configuration. + +Django's own built-in parsing of the Host header is, however, still vulnerable, +as was reported to us recently. The Host header parsing in Django 1.3.3 and +Django 1.4.1 -- specifically, django.http.HttpRequest.get_host() -- was +incorrectly handling username/password information in the header. Thus, for +example, the following Host header would be accepted by Django when running on +"validsite.com":: + + Host: validsite.com:random@evilsite.com + +Using this, an attacker can cause parts of Django -- particularly the +password-reset mechanism -- to generate and display arbitrary URLs to users. + +To remedy this, the parsing in HttpRequest.get_host() is being modified; Host +headers which contain potentially dangerous content (such as username/password +pairs) now raise the exception django.core.exceptions.SuspiciousOperation + +Details of this issue were initially posted online as a `security advisory`_. + +.. _security advisory: https://www.djangoproject.com/weblog/2012/oct/17/security/ + Backwards incompatible changes ============================== * The newly introduced :class:`~django.db.models.GenericIPAddressField` constructor arguments have been adapted to match those of all other model fields. The first two keyword arguments are now verbose_name and name. + +Other bugfixes and changes +========================== + +* Subclass HTMLParser only for appropriate Python versions (#18239). +* Added batch_size argument to qs.bulk_create() (#17788). +* Fixed a small regression in the admin filters where wrongly formatted dates passed as url parameters caused an unhandled ValidationError (#18530). +* Fixed an endless loop bug when accessing permissions in templates (#18979) +* Fixed some Python 2.5 compatibility issues +* Fixed an issue with quoted filenames in Content-Disposition header (#19006) +* Made the context option in ``trans`` and ``blocktrans`` tags accept literals wrapped in single quotes (#18881). +* Numerous documentation improvements and fixes. diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 2329d1effa..efcba11df3 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -28,7 +28,7 @@ Final releases .. toctree:: :maxdepth: 1 - .. 1.4.2 (uncomment on release) + 1.4.2 1.4.1 1.4 -- cgit v1.3 From 104ca49c57e3e48fe518985e2eee60ce6969d7ab Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 21 Oct 2012 22:44:02 +0200 Subject: Removed inaccurate statement from the StreamingHttpResponse docs. Iterators will be closed for both regular and streaming responses; this shouldn't be described as a difference. --- docs/ref/request-response.txt | 3 --- 1 file changed, 3 deletions(-) (limited to 'docs') diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 89d0fe847c..a4b8e9aa66 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -833,9 +833,6 @@ with the following notable differences: * You cannot use the file-like object ``tell()`` or ``write()`` methods. Doing so will raise an exception. -* Any iterators that have a ``close()`` method and are assigned as content will - be closed automatically after the response has been iterated. - :class:`StreamingHttpResponse` should only be used in situations where it is absolutely required that the whole content isn't iterated before transferring the data to the client. Because the content can't be accessed, many -- cgit v1.3 From 495a8b8107dbd4fb511954bcd2322d125addd94e Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 23 Oct 2012 22:25:38 +0200 Subject: Fixed #6527 -- Provided repeatable content access in HttpResponses instantiated with iterators. --- django/http/response.py | 30 +++++++++++++--- docs/internals/deprecation.txt | 4 +++ docs/ref/request-response.txt | 23 +++++++----- docs/releases/1.5.txt | 54 ++++++++++++++++++----------- tests/regressiontests/httpwrappers/tests.py | 31 ++++++++++++++--- 5 files changed, 104 insertions(+), 38 deletions(-) (limited to 'docs') diff --git a/django/http/response.py b/django/http/response.py index 4a5c479419..e9cc3f70a9 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -246,8 +246,18 @@ class HttpResponse(HttpResponseBase): else: __str__ = serialize + def _consume_content(self): + # If the response was instantiated with an iterator, when its content + # is accessed, the iterator is going be exhausted and the content + # loaded in memory. At this point, it's better to abandon the original + # iterator and save the content for later reuse. This is a temporary + # solution. See the comment in __iter__ below for the long term plan. + if self._base_content_is_iter: + self.content = b''.join(self.make_bytes(e) for e in self._container) + @property def content(self): + self._consume_content() return b''.join(self.make_bytes(e) for e in self._container) @content.setter @@ -262,6 +272,17 @@ class HttpResponse(HttpResponseBase): self._base_content_is_iter = False def __iter__(self): + # Raise a deprecation warning only if the content wasn't consumed yet, + # because the response may be intended to be streamed. + # Once the deprecation completes, iterators should be consumed upon + # assignment rather than upon access. The _consume_content method + # should be removed. See #6527. + if self._base_content_is_iter: + warnings.warn( + 'Creating streaming responses with `HttpResponse` is ' + 'deprecated. Use `StreamingHttpResponse` instead ' + 'if you need the streaming behavior.', + PendingDeprecationWarning, stacklevel=2) self._iterator = iter(self._container) return self @@ -277,14 +298,12 @@ class HttpResponse(HttpResponseBase): next = __next__ # Python 2 compatibility def write(self, content): - if self._base_content_is_iter: - raise Exception("This %s instance is not writable" % self.__class__.__name__) + self._consume_content() self._container.append(content) def tell(self): - if self._base_content_is_iter: - raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) - return sum([len(chunk) for chunk in self]) + self._consume_content() + return sum(len(chunk) for chunk in self) class StreamingHttpResponse(HttpResponseBase): @@ -389,6 +408,7 @@ class HttpResponseNotModified(HttpResponse): if value: raise AttributeError("You cannot set content to a 304 (Not Modified) response") self._container = [] + self._base_content_is_iter = False class HttpResponseBadRequest(HttpResponse): diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 6387c87d1d..014ea05a51 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -286,6 +286,10 @@ these changes. * The ``mimetype`` argument to :class:`~django.http.HttpResponse` ``__init__`` will be removed (``content_type`` should be used instead). +* When :class:`~django.http.HttpResponse` is instantiated with an iterator, + or when :attr:`~django.http.HttpResponse.content` is set to an iterator, + that iterator will be immediately consumed. + * The ``AUTH_PROFILE_MODULE`` setting, and the ``get_profile()`` method on the User model, will be removed. diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index a4b8e9aa66..c3ba99168d 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -569,18 +569,25 @@ Passing iterators Finally, you can pass ``HttpResponse`` an iterator rather than strings. If you use this technique, the iterator should return strings. +Passing an iterator as content to :class:`HttpResponse` creates a +streaming response if (and only if) no middleware accesses the +:attr:`HttpResponse.content` attribute before the response is returned. + .. versionchanged:: 1.5 - Passing an iterator as content to :class:`HttpResponse` creates a - streaming response if (and only if) no middleware accesses the - :attr:`HttpResponse.content` attribute before the response is returned. +This technique is fragile and was deprecated in Django 1.5. If you need the +response to be streamed from the iterator to the client, you should use the +:class:`StreamingHttpResponse` class instead. + +As of Django 1.7, when :class:`HttpResponse` is instantiated with an +iterator, it will consume it immediately, store the response content as a +string, and discard the iterator. - If you want to guarantee that your response will stream to the client, you - should use the new :class:`StreamingHttpResponse` class instead. +.. versionchanged:: 1.5 -If an :class:`HttpResponse` instance has been initialized with an iterator as -its content, you can't use it as a file-like object. Doing so will raise an -exception. +You can now use :class:`HttpResponse` as a file-like object even if it was +instantiated with an iterator. Django will consume and save the content of +the iterator on first access. Setting headers ~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index f7467bc06a..ac61fb363b 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -84,6 +84,8 @@ For one-to-one relationships, both sides can be cached. For many-to-one relationships, only the single side of the relationship can be cached. This is particularly helpful in combination with ``prefetch_related``. +.. _explicit-streaming-responses: + Explicit support for streaming responses ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -98,7 +100,7 @@ You can now explicitly generate a streaming response with the new is an iterator. Since :class:`~django.http.StreamingHttpResponse` does not have a ``content`` -attribute, middleware that need access to the response content must test for +attribute, middleware that needs access to the response content must test for streaming responses and behave accordingly. See :ref:`response-middleware` for more information. @@ -483,6 +485,30 @@ Features deprecated in 1.5 .. _simplejson-deprecation: +: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. + +Streaming behavior of :class:`HttpResponse` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django 1.5 deprecates the ability to stream a response by passing an iterator +to :class:`~django.http.HttpResponse`. If you rely on this behavior, switch to +:class:`~django.http.StreamingHttpResponse`. See :ref:`explicit-streaming- +responses` above. + +In Django 1.7 and above, the iterator will be consumed immediately by +:class:`~django.http.HttpResponse`. + ``django.utils.simplejson`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -497,12 +523,6 @@ incompatibilities between versions of :mod:`simplejson` -- see the If you rely on features added to :mod:`simplejson` after it became Python's :mod:`json`, you should import :mod:`simplejson` explicitly. -``itercompat.product`` -~~~~~~~~~~~~~~~~~~~~~~ - -The :func:`~django.utils.itercompat.product` function has been deprecated. Use -the built-in :func:`itertools.product` instead. - ``django.utils.encoding.StrAndUnicode`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -510,6 +530,13 @@ The :class:`~django.utils.encoding.StrAndUnicode` mix-in has been deprecated. Define a ``__str__`` method and apply the :func:`~django.utils.encoding.python_2_unicode_compatible` decorator instead. +``django.utils.itercompat.product`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :func:`~django.utils.itercompat.product` function has been deprecated. Use +the built-in :func:`itertools.product` instead. + + ``django.utils.markup`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -517,16 +544,3 @@ 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/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index bfb4ae1fd5..7f61a3074f 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import copy import os import pickle +import warnings from django.core.exceptions import SuspiciousOperation from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, @@ -313,11 +314,17 @@ class HttpResponseTests(unittest.TestCase): r.content = [1, 2, 3] self.assertEqual(r.content, b'123') - #test retrieval explicitly using iter and odd inputs + #test retrieval explicitly using iter (deprecated) and odd inputs r = HttpResponse() r.content = ['1', '2', 3, '\u079e'] - my_iter = r.__iter__() - result = list(my_iter) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", PendingDeprecationWarning) + my_iter = iter(r) + self.assertEqual(w[0].category, PendingDeprecationWarning) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", PendingDeprecationWarning) + result = list(my_iter) + self.assertEqual(w[0].category, PendingDeprecationWarning) #'\xde\x9e' == unichr(1950).encode('utf-8') self.assertEqual(result, [b'1', b'2', b'3', b'\xde\x9e']) self.assertEqual(r.content, b'123\xde\x9e') @@ -330,6 +337,16 @@ class HttpResponseTests(unittest.TestCase): self.assertRaises(UnicodeEncodeError, getattr, r, 'content') + # content can safely be accessed multiple times. + r = HttpResponse(iter(['hello', 'world'])) + self.assertEqual(r.content, r.content) + self.assertEqual(r.content, b'helloworld') + + # additional content can be written to the response. + r.write('!') + self.assertEqual(r.content, b'helloworld!') + + def test_file_interface(self): r = HttpResponse() r.write(b"hello") @@ -338,7 +355,9 @@ class HttpResponseTests(unittest.TestCase): self.assertEqual(r.tell(), 17) r = HttpResponse(['abc']) - self.assertRaises(Exception, r.write, 'def') + r.write('def') + self.assertEqual(r.tell(), 6) + self.assertEqual(r.content, b'abcdef') def test_unsafe_redirect(self): bad_urls = [ @@ -447,7 +466,9 @@ class FileCloseTests(TestCase): file1 = open(filename) r = HttpResponse(file1) self.assertFalse(file1.closed) - list(r) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", PendingDeprecationWarning) + list(r) self.assertFalse(file1.closed) r.close() self.assertTrue(file1.closed) -- cgit v1.3 From da958eb2098372c20cc3aaf905777b1a8d3144eb Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 24 Oct 2012 16:30:23 -0400 Subject: Fixed #9471 - Expanded ModelAdmin.raw_id_fields docs; thanks adroffne for the suggestion. --- docs/ref/contrib/admin/_images/raw_id_fields.png | Bin 0 -> 1871 bytes docs/ref/contrib/admin/index.txt | 8 ++++++++ 2 files changed, 8 insertions(+) create mode 100644 docs/ref/contrib/admin/_images/raw_id_fields.png (limited to 'docs') diff --git a/docs/ref/contrib/admin/_images/raw_id_fields.png b/docs/ref/contrib/admin/_images/raw_id_fields.png new file mode 100644 index 0000000000..0774c40469 Binary files /dev/null and b/docs/ref/contrib/admin/_images/raw_id_fields.png differ diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index e9b573e3a0..6ed929cb7d 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -803,6 +803,14 @@ subclass:: class ArticleAdmin(admin.ModelAdmin): raw_id_fields = ("newspaper",) + The ``raw_id_fields`` ``Input`` widget should contain a primary key if the + field is a ``ForeignKey`` or a comma separated list of values if the field + is a ``ManyToManyField``. The ``raw_id_fields`` widget shows a magnifying + glass button next to the field which allows users to search for and select + a value: + + .. image:: _images/raw_id_fields.png + .. attribute:: ModelAdmin.readonly_fields By default the admin shows all fields as editable. Any fields in this -- cgit v1.3 From 137fdbeebd83e50d8c24c0b6be091e7d7088ef9a Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Thu, 25 Oct 2012 13:59:13 -0500 Subject: Added release notes for Django 1.5 alpha 1. Also updated 1.5-proper release notes a bit. --- docs/releases/1.5-alpha-1.txt | 631 ++++++++++++++++++++++++++++++++++++++++++ docs/releases/1.5.txt | 97 ++++++- docs/releases/index.txt | 1 + 3 files changed, 719 insertions(+), 10 deletions(-) create mode 100644 docs/releases/1.5-alpha-1.txt (limited to 'docs') diff --git a/docs/releases/1.5-alpha-1.txt b/docs/releases/1.5-alpha-1.txt new file mode 100644 index 0000000000..8f027c6859 --- /dev/null +++ b/docs/releases/1.5-alpha-1.txt @@ -0,0 +1,631 @@ +============================================ +Django 1.5 release notes - UNDER DEVELOPMENT +============================================ + +October 25, 2012. + +Welcome to Django 1.5 alpha! + +This is the first in a series of preview/development releases leading up to the +eventual release of Django 1.5, scheduled for December 2012. This release is +primarily targeted at developers who are interested in trying out new features +and testing the Django codebase to help identify and resolve bugs prior to the +final 1.5 release. + +As such, this release is *not* intended for production use, and any such use +is discouraged. + +In particular, we need the community's help to test Django 1.5's new `Python 3 +support`_ -- not just to report bugs on Python 3, but also regressions on Python +2. While Django is very conservative with regards to backwards compatibility, +mistakes are always possible, and it's likely that the Python 3 refactoring work +introduced some regressions. + +Django 1.5 alpha includes various `new features`_ and some minor `backwards +incompatible changes`_. There are also some features that have been dropped, +which are detailed in :doc:`our deprecation plan `, +and we've `begun the deprecation process for some features`_. + +.. _`new features`: `What's new in Django 1.5`_ +.. _`backwards incompatible changes`: `Backwards incompatible changes in 1.5`_ +.. _`begun the deprecation process for some features`: `Features deprecated in 1.5`_ + +Overview +======== + +The biggest new feature in Django 1.5 is the `configurable User model`_. Before +Django 1.5, applications that wanted to use Django's auth framework +(:mod:`django.contrib.auth`) were forced to use Django's definition of a "user". +In Django 1.5, you can now swap out the ``User`` model for one that you write +yourself. This could be a simple extension to the existing ``User`` model -- for +example, you could add a Twitter or Facebook ID field -- or you could completely +replace the ``User`` with one totally customized for your site. + +Django 1.5 is also the first release with `Python 3 support`_! We're labeling +this support "experimental" because we don't yet consider it production-ready, +but everything's in place for you to start porting your apps to Python 3. +Our next release, Django 1.6, will support Python 3 without reservations. + +Other notable new features in Django 1.5 include: + +* `Support for saving a subset of model's fields`_ - + :meth:`Model.save() ` now accepts an + ``update_fields`` argument, letting you specify which fields are + written back to the databse when you call ``save()``. This can help + in high-concurrancy operations, and can improve performance. + +* Better `support for streaming responses <#explicit-streaming-responses>`_ via + the new :class:`~django.http.StreamingHttpResponse` response class. + +* `GeoDjango`_ now supports PostGIS 2.0. + +* ... and more; `see below <#what-s-new-in-django-1-5>`_. + +Wherever possible we try to introduce new features in a backwards-compatible +manner per :doc:`our API stability policy ` policy. +However, as with previous releases, Django 1.5 ships with some minor +`backwards incompatible changes`_; people upgrading from previous versions +of Django should read that list carefully. + +One deprecated feature worth noting is the shift to "new-style" :ttag:`url` tag. +Prior to Django 1.3, syntax like ``{% url myview %}`` was interpreted +incorrectly (Django considered ``"myview"`` to be a literal name of a view, not +a template variable named ``myview``). Django 1.3 and above introduced the +``{% load url from future %}`` syntax to bring in the corrected behavior where +``myview`` was seen as a variable. + +The upshot of this is that if you are not using ``{% load url from future %}`` +in your templates, you'll need to change tags like ``{% url myview %}`` to +``{% url "myview" %}``. If you *were* using ``{% load url from future %}`` you +can simply remove that line under Django 1.5 + +Python compatibility +==================== + +Django 1.5 requires Python 2.6.5 or above, though we **highly recommended** +Python 2.7.3 or above. Support for Python 2.5 and below as been dropped. + +This change should affect only a small number of Django users, as most +operating-system vendors today are shipping Python 2.6 or newer as their default +version. If you're still using Python 2.5, however, you'll need to stick to +Django 1.4 until you can upgrade your Python version. Per :doc:`our support +policy `, Django 1.4 will continue to receive +security support until the release of Django 1.6. + +Django 1.5 does not run on a Jython final release, because Jython's latest +release doesn't currently support Python 2.6. However, Jython currently does +offer an alpha release featuring 2.7 support, and Django 1.5 supports that alpha +release. + +Python 3 support +~~~~~~~~~~~~~~~~ + +Django 1.5 introduces support for Python 3 - specifically, Python +3.2 and above. This comes in the form of a **single** codebase; you don't +need to install a different version of Django on Python 3. This means that +you can write application targeted for just Python 2, just Python 3, or single +applications that support both platforms. + +However, we're labling this support "experimental" for now: although it's +receved extensive testing via our automated test suite, it's recieved very +little real-world testing. We've done our best to eliminate bugs, but we can't +be sure we covered all possible uses of Django. Further, Django's more than a +web framework; it's an ecosystem of pluggable components. At this point, very +few third-party applications have been ported to Python 3, so it's unliukely +that a real-world application will have all its dependecies satisfied under +Python 3. + +Thus, we're recommending that Django 1.5 not be used in production under Python +3. Instead, use this oportunity to begin :doc:`porting applications to Python 3 +`. If you're an author of a pluggable component, we encourage you +to start porting now. + +We plan to offer first-class, production-ready support for Python 3 in our next +release, Django 1.6. + +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 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The method :meth:`Model.save() ` has a new +keyword argument ``update_fields``. By using this argument it is possible to +save only a select list of model's fields. This can be useful for performance +reasons or when trying to avoid overwriting concurrent changes. + +Deferred instances (those loaded by .only() or .defer()) will automatically +save just the loaded fields. If any field is set manually after load, that +field will also get updated on save. + +See the :meth:`Model.save() ` documentation for +more details. + +Caching of related model instances +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When traversing relations, the ORM will avoid re-fetching objects that were +previously loaded. For example, with the tutorial's models:: + + >>> first_poll = Poll.objects.all()[0] + >>> first_choice = first_poll.choice_set.all()[0] + >>> first_choice.poll is first_poll + True + +In Django 1.5, the third line no longer triggers a new SQL query to fetch +``first_choice.poll``; it was set by the second line. + +For one-to-one relationships, both sides can be cached. For many-to-one +relationships, only the single side of the relationship can be cached. This +is particularly helpful in combination with ``prefetch_related``. + +Explicit support for streaming responses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before Django 1.5, it was possible to create a streaming response by passing +an iterator to :class:`~django.http.HttpResponse`. But this was unreliable: +any middleware that accessed the :attr:`~django.http.HttpResponse.content` +attribute would consume the iterator prematurely. + +You can now explicitly generate a streaming response with the new +:class:`~django.http.StreamingHttpResponse` class. This class exposes a +:class:`~django.http.StreamingHttpResponse.streaming_content` attribute which +is an iterator. + +Since :class:`~django.http.StreamingHttpResponse` does not have a ``content`` +attribute, middleware that needs access to the response content must test for +streaming responses and behave accordingly. See :ref:`response-middleware` for +more information. + +``{% verbatim %}`` template tag +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To make it easier to deal with javascript templates which collide with Django's +syntax, you can now use the :ttag:`verbatim` block tag to avoid parsing the +tag's content. + +Retrieval of ``ContentType`` instances associated with proxy models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The methods :meth:`ContentTypeManager.get_for_model() ` +and :meth:`ContentTypeManager.get_for_models() ` +have a new keyword argument – respectively ``for_concrete_model`` and ``for_concrete_models``. +By passing ``False`` using this argument it is now possible to retreive the +:class:`ContentType ` +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). + +* The wkb and hex properties of `GEOSGeometry` objects preserve the Z dimension. + +* Support for PostGIS 2.0 has been added and support for GDAL < 1.5 has been + dropped. + +Minor features +~~~~~~~~~~~~~~ + +Django 1.5 also includes several smaller improvements worth noting: + +* The template engine now interprets ``True``, ``False`` and ``None`` as the + corresponding Python objects. + +* :mod:`django.utils.timezone` provides a helper for converting aware + datetimes between time zones. See :func:`~django.utils.timezone.localtime`. + +* The generic views support OPTIONS requests. + +* Management commands do not raise ``SystemExit`` any more when called by code + from :ref:`call_command `. Any exception raised by the command + (mostly :ref:`CommandError `) is propagated. + +* The dumpdata management command outputs one row at a time, preventing + out-of-memory errors when dumping large datasets. + +* In the localflavor for Canada, "pq" was added to the acceptable codes for + Quebec. It's an old abbreviation. + +* 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 + single batch is limited so that 999 parameters per query isn't exceeded. + +* The :setting:`LOGIN_URL` and :setting:`LOGIN_REDIRECT_URL` settings now also + accept view function names and + :ref:`named URL patterns `. This allows you to reduce + configuration duplication. More information can be found in the + :func:`~django.contrib.auth.decorators.login_required` documentation. + +* 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. + +* 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). + +* When using :class:`~django.template.RequestContext`, it is now possible to + 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. + +* :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` + +* 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 +===================================== + +.. warning:: + + In addition to the changes outlined in this section, be sure to review the + :doc:`deprecation plan ` for any features that + have been removed. If you haven't updated your code within the + deprecation timeline for a given feature, its removal may appear as a + backwards incompatible change. + +Context in year archive class-based views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For consistency with the other date-based generic views, +:class:`~django.views.generic.dates.YearArchiveView` now passes ``year`` in +the context as a :class:`datetime.date` rather than a string. If you are +using ``{{ year }}`` in your templates, you must replace it with ``{{ +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 +~~~~~~~~~~~~~~~~~~~~~~~ + +For consistency with the design of the other generic views, +:class:`~django.views.generic.base.TemplateView` no longer passes a ``params`` +dictionary into the context, instead passing the variables from the URLconf +directly into the context. + +Non-form data in HTTP requests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:attr:`request.POST ` will no longer include data +posted via HTTP requests with non form-specific content-types in the header. +In prior versions, data posted with content-types other than +``multipart/form-data`` or ``application/x-www-form-urlencoded`` would still +end up represented in the :attr:`request.POST ` +attribute. Developers wishing to access the raw POST data for these cases, +should use the :attr:`request.body ` attribute +instead. + +OPTIONS, PUT and DELETE requests in the test client +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unlike GET and POST, these HTTP methods aren't implemented by web browsers. +Rather, they're used in APIs, which transfer data in various formats such as +JSON or XML. Since such requests may contain arbitrary data, Django doesn't +attempt to decode their body. + +However, the test client used to build a query string for OPTIONS and DELETE +requests like for GET, and a request body for PUT requests like for POST. This +encoding was arbitrary and inconsistent with Django's behavior when it +receives the requests, so it was removed in Django 1.5. + +If you were using the ``data`` parameter in an OPTIONS or a DELETE request, +you must convert it to a query string and append it to the ``path`` parameter. + +If you were using the ``data`` parameter in a PUT request without a +``content_type``, you must encode your data before passing it to the test +client and set the ``content_type`` argument. + +System version of :mod:`simplejson` no longer used +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As explained below, Django 1.5 deprecates +:mod:`django.utils.simplejson` in favor of Python 2.6's built-in :mod:`json` +module. In theory, this change is harmless. Unfortunately, because of +incompatibilities between versions of :mod:`simplejson`, it may trigger errors +in some circumstances. + +JSON-related features in Django 1.4 always used :mod:`django.utils.simplejson`. +This module was actually: + +- A system version of :mod:`simplejson`, if one was available (ie. ``import + simplejson`` works), if it was more recent than Django's built-in copy or it + had the C speedups, or +- The :mod:`json` module from the standard library, if it was available (ie. + Python 2.6 or greater), or +- A built-in copy of version 2.0.7 of :mod:`simplejson`. + +In Django 1.5, those features use Python's :mod:`json` module, which is based +on version 2.0.9 of :mod:`simplejson`. + +There are no known incompatibilities between Django's copy of version 2.0.7 and +Python's copy of version 2.0.9. However, there are some incompatibilities +between other versions of :mod:`simplejson`: + +- While the :mod:`simplejson` API is documented as always returning unicode + strings, the optional C implementation can return a byte string. This was + fixed in Python 2.7. +- :class:`simplejson.JSONEncoder` gained a ``namedtuple_as_object`` keyword + argument in version 2.2. + +More information on these incompatibilities is available in `ticket #18023`_. + +The net result is that, if you have installed :mod:`simplejson` and your code +uses Django's serialization internals directly -- for instance +:class:`django.core.serializers.json.DjangoJSONEncoder`, the switch from +:mod:`simplejson` to :mod:`json` could break your code. (In general, changes to +internals aren't documented; we're making an exception here.) + +At this point, the maintainers of Django believe that using :mod:`json` from +the standard library offers the strongest guarantee of backwards-compatibility. +They recommend to use it from now on. + +.. _ticket #18023: https://code.djangoproject.com/ticket/18023#comment:10 + +String types of hasher method parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have written a :ref:`custom password hasher `, +your ``encode()``, ``verify()`` or ``safe_summary()`` methods should accept +Unicode parameters (``password``, ``salt`` or ``encoded``). If any of the +hashing methods need byte strings, you can use the +:func:`~django.utils.encoding.force_bytes` utility to encode the strings. + +Validation of previous_page_number and next_page_number +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using :doc:`object pagination `, +the ``previous_page_number()`` and ``next_page_number()`` methods of the +:class:`~django.core.paginator.Page` object did not check if the returned +number was inside the existing page range. +It does check it now and raises an :exc:`InvalidPage` exception when the number +is either too low or too high. + +Behavior of autocommit database option on PostgreSQL changed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PostgreSQL's autocommit option didn't work as advertised previously. It did +work for single transaction block, but after the first block was left the +autocommit behavior was never restored. This bug is now fixed in 1.5. While +this is only a bug fix, it is worth checking your applications behavior if +you are using PostgreSQL together with the autocommit option. + +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 +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some changes have been introduced in the execution of tests that might be +backward-incompatible for some testing setups: + +Database flushing in ``django.test.TransactionTestCase`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, the test database was truncated *before* each test run in a +:class:`~django.test.TransactionTestCase`. + +In order to be able to run unit tests in any order and to make sure they are +always isolated from each other, :class:`~django.test.TransactionTestCase` will +now reset the database *after* each test run instead. + +No more implict DB sequences reset +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:class:`~django.test.TransactionTestCase` tests used to reset primary key +sequences automatically together with the database flushing actions described +above. + +This has been changed so no sequences are implicitly reset. This can cause +:class:`~django.test.TransactionTestCase` tests that depend on hard-coded +primary key values to break. + +The new :attr:`~django.test.TransactionTestCase.reset_sequences` attribute can +be used to force the old behavior for :class:`~django.test.TransactionTestCase` +that might need it. + +Ordering of tests +^^^^^^^^^^^^^^^^^ + +In order to make sure all ``TestCase`` code starts with a clean database, +tests are now executed in the following order: + +* First, all unittests (including :class:`unittest.TestCase`, + :class:`~django.test.SimpleTestCase`, :class:`~django.test.TestCase` and + :class:`~django.test.TransactionTestCase`) are run with no particular ordering + guaranteed nor enforced among them. + +* Then any other tests (e.g. doctests) that may alter the database without + restoring it to its original state are run. + +This should not cause any problems unless you have existing doctests which +assume a :class:`~django.test.TransactionTestCase` executed earlier left some +database state behind or unit tests that rely on some form of state being +preserved after the execution of other tests. Such tests are already very +fragile, and must now be changed to be able to run independently. + +`cleaned_data` dictionary kept for invalid forms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :attr:`~django.forms.Form.cleaned_data` dictionary is now always present +after form validation. When the form doesn't validate, it contains only the +fields that passed validation. You should test the success of the validation +with the :meth:`~django.forms.Form.is_valid()` method and not with the +presence or absence of the :attr:`~django.forms.Form.cleaned_data` attribute +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. + +* The ``slugify`` template filter is now available as a standard python + 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. + +* 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. + +* The :ttag:`csrf_token` template tag is no longer enclosed in a div. If you need + HTML validation against pre-HTML5 Strict DTDs, you should add a div around it + in your pages. + +Features deprecated in 1.5 +========================== + +: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. + +Streaming behavior of :class:`HttpResponse` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django 1.5 deprecates the ability to stream a response by passing an iterator +to :class:`~django.http.HttpResponse`. If you rely on this behavior, switch to +:class:`~django.http.StreamingHttpResponse`. See above for more details. + +In Django 1.7 and above, the iterator will be consumed immediately by +:class:`~django.http.HttpResponse`. + +``django.utils.simplejson`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since Django 1.5 drops support for Python 2.5, we can now rely on the +:mod:`json` module being available in Python's standard library, so we've +removed our own copy of :mod:`simplejson`. You should now import :mod:`json` +instead :mod:`django.utils.simplejson`. + +Unfortunately, this change might have unwanted side-effects, because of +incompatibilities between versions of :mod:`simplejson` -- see the backwards- +incompatible changes section. If you rely on features added to :mod:`simplejson` +after it became Python's :mod:`json`, you should import :mod:`simplejson` +explicitly. + +``django.utils.encoding.StrAndUnicode`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`~django.utils.encoding.StrAndUnicode` mix-in has been deprecated. +Define a ``__str__`` method and apply the +:func:`~django.utils.encoding.python_2_unicode_compatible` decorator instead. + +``django.utils.itercompat.product`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :func:`~django.utils.itercompat.product` function has been deprecated. Use +the built-in :func:`itertools.product` instead. + + +``django.utils.markup`` +~~~~~~~~~~~~~~~~~~~~~~~ + +The markup contrib module has been deprecated and will follow an accelerated +deprecation schedule. Direct use of python markup libraries or 3rd party tag +libraries is preferred to Django maintaining this functionality in the +framework. diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index ac61fb363b..f68f93d958 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -2,6 +2,8 @@ Django 1.5 release notes - UNDER DEVELOPMENT ============================================ +Welcome to Django 1.5! + These release notes cover the `new features`_, as well as some `backwards incompatible changes`_ you'll want to be aware of when upgrading from Django 1.4 or older versions. We've also dropped some @@ -13,23 +15,98 @@ features`_. .. _`backwards incompatible changes`: `Backwards incompatible changes in 1.5`_ .. _`begun the deprecation process for some features`: `Features deprecated in 1.5`_ +Overview +======== + +The biggest new feature in Django 1.5 is the `configurable User model`_. Before +Django 1.5, applications that wanted to use Django's auth framework +(:mod:`django.contrib.auth`) were forced to use Django's definition of a "user". +In Django 1.5, you can now swap out the ``User`` model for one that you write +yourself. This could be a simple extension to the existing ``User`` model -- for +example, you could add a Twitter or Facebook ID field -- or you could completely +replace the ``User`` with one totally customized for your site. + +Django 1.5 is also the first release with `Python 3 support`_! We're labling +this support "experimental" because we don't yet consider it production-ready, +but everything's in place for you to start porting your apps to Python 3. +Our next release, Django 1.6, will support Python 3 without reservations. + +Other notable new features in Django 1.5 include: + +* `Support for saving a subset of model's fields`_ - + :meth:`Model.save() ` now accepts an + ``update_fields`` argument, letting you specify which fields are + written back to the databse when you call ``save()``. This can help + in high-concurrancy operations, and can improve performance. + +* Better `support for streaming responses <#explicit-streaming-responses>`_ via + the new :class:`~django.http.StreamingHttpResponse` response class. + +* `GeoDjango`_ now supports PostGIS 2.0. + +* ... and more; `see below <#what-s-new-in-django-1-5>`_. + +Wherever possible we try to introduce new features in a backwards-compatible +manner per :doc:`our API stability policy ` policy. +However, as with previous releases, Django 1.5 ships with some minor +`backwards incompatible changes`_; people upgrading from previous versions +of Django should read that list carefully. + +One deprecated feature worth noting is the shift to "new-style" :ttag:`url` tag. +Prior to Django 1.3, syntax like ``{% url myview %}`` was interpreted +incorrectly (Django considered ``"myview"`` to be a literal name of a view, not +a template variable named ``myview``). Django 1.3 and above introduced the +``{% load url from future %}`` syntax to bring in the corrected behavior where +``myview`` was seen as a variable. + +The upshot of this is that if you are not using ``{% load url from future %}`` +in your templates, you'll need to change tags like ``{% url myview %}`` to +``{% url "myview" %}``. If you *were* using ``{% load url from future %}`` you +can simply remove that line under Django 1.5 + Python compatibility ==================== -Django 1.5 has dropped support for Python 2.5. Python 2.6.5 is now the minimum -required Python version. Django is tested and supported on Python 2.6 and -2.7. +Django 1.5 requires Python 2.6.5 or above, though we **highly recommended** +Python 2.7.3 or above. Support for Python 2.5 and below as been dropped. This change should affect only a small number of Django users, as most operating-system vendors today are shipping Python 2.6 or newer as their default version. If you're still using Python 2.5, however, you'll need to stick to -Django 1.4 until you can upgrade your Python version. Per :doc:`our support policy -`, Django 1.4 will continue to receive security -support until the release of Django 1.6. - -Django 1.5 does not run on a Jython final release, because Jython's latest release -doesn't currently support Python 2.6. However, Jython currently does offer an alpha -release featuring 2.7 support. +Django 1.4 until you can upgrade your Python version. Per :doc:`our support +policy `, Django 1.4 will continue to receive +security support until the release of Django 1.6. + +Django 1.5 does not run on a Jython final release, because Jython's latest +release doesn't currently support Python 2.6. However, Jython currently does +offer an alpha release featuring 2.7 support, and Django 1.5 supports that alpha +release. + +Python 3 support +~~~~~~~~~~~~~~~~ + +Django 1.5 introduces support for Python 3 - specifically, Python +3.2 and above. This comes in the form of a **single** codebase; you don't +need to install a different version of Django on Python 3. This means that +you can write application targeted for just Python 2, just Python 3, or single +applications that support both platforms. + +However, we're labeling this support "experimental" for now: although it's +receved extensive testing via our automated test suite, it's recieved very +little real-world testing. We've done our best to eliminate bugs, but we can't +be sure we covered all possible uses of Django. Further, Django's more than a +web framework; it's an ecosystem of pluggable components. At this point, very +few third-party applications have been ported to Python 3, so it's unliukely +that a real-world application will have all its dependecies satisfied under +Python 3. + +Thus, we're recommending that Django 1.5 not be used in production under Python +3. Instead, use this oportunity to begin :doc:`porting applications to Python 3 +`. If you're an author of a pluggable component, we encourage you +to start porting now. + +We plan to offer first-class, production-ready support for Python 3 in our next +release, Django 1.6. What's new in Django 1.5 ======================== diff --git a/docs/releases/index.txt b/docs/releases/index.txt index efcba11df3..6df9821f56 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -92,6 +92,7 @@ notes. .. toctree:: :maxdepth: 1 + 1.5-alpha-1 1.4-beta-1 1.4-alpha-1 1.3-beta-1 -- cgit v1.3 From e8b258895adec3d892ada30722517976b298e420 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Thu, 25 Oct 2012 12:02:22 -0700 Subject: Tweaked tense of URL tag changes for clarity --- docs/internals/deprecation.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 014ea05a51..10bbfe1a91 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -141,9 +141,9 @@ these changes. :class:`~django.contrib.staticfiles.handlers.StaticFilesHandler`. * The :ttag:`url` and :ttag:`ssi` template tags will be - modified so that the first argument to each tag is a - template variable, not an implied string. Until then, the new-style - behavior is provided in the ``future`` template tag library. + modified so that the first argument to each tag is a template variable, not + an implied string. In 1.4, this behavior is provided by a version of the tag + in the ``future`` template tag library. * The :djadmin:`reset` and :djadmin:`sqlreset` management commands will be removed. -- cgit v1.3 From 9912a30b062fa62a4a8e7d598942dbf2cf2738af Mon Sep 17 00:00:00 2001 From: Eric Florenzano Date: Thu, 25 Oct 2012 13:26:22 -0700 Subject: Update docs/releases/1.5.txt Fix typo. --- docs/releases/1.5.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index f68f93d958..5b2e836675 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -116,8 +116,8 @@ 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 +you want to store user's names 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, -- cgit v1.3 From 48be78cf088e6fca87d1e4da7c527e17305f9fe2 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 25 Oct 2012 17:20:55 -0400 Subject: Fixed broken links + spell check in 1.5 release notes. --- docs/releases/1.5.txt | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) (limited to 'docs') diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index f68f93d958..b71a9cd9df 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -26,7 +26,7 @@ yourself. This could be a simple extension to the existing ``User`` model -- for example, you could add a Twitter or Facebook ID field -- or you could completely replace the ``User`` with one totally customized for your site. -Django 1.5 is also the first release with `Python 3 support`_! We're labling +Django 1.5 is also the first release with `Python 3 support`_! We're labeling this support "experimental" because we don't yet consider it production-ready, but everything's in place for you to start porting your apps to Python 3. Our next release, Django 1.6, will support Python 3 without reservations. @@ -36,8 +36,8 @@ Other notable new features in Django 1.5 include: * `Support for saving a subset of model's fields`_ - :meth:`Model.save() ` now accepts an ``update_fields`` argument, letting you specify which fields are - written back to the databse when you call ``save()``. This can help - in high-concurrancy operations, and can improve performance. + written back to the database when you call ``save()``. This can help + in high-concurrency operations, and can improve performance. * Better `support for streaming responses <#explicit-streaming-responses>`_ via the new :class:`~django.http.StreamingHttpResponse` response class. @@ -92,17 +92,17 @@ you can write application targeted for just Python 2, just Python 3, or single applications that support both platforms. However, we're labeling this support "experimental" for now: although it's -receved extensive testing via our automated test suite, it's recieved very +received extensive testing via our automated test suite, it's received very little real-world testing. We've done our best to eliminate bugs, but we can't be sure we covered all possible uses of Django. Further, Django's more than a web framework; it's an ecosystem of pluggable components. At this point, very -few third-party applications have been ported to Python 3, so it's unliukely -that a real-world application will have all its dependecies satisfied under +few third-party applications have been ported to Python 3, so it's unlikely +that a real-world application will have all its dependencies satisfied under Python 3. Thus, we're recommending that Django 1.5 not be used in production under Python -3. Instead, use this oportunity to begin :doc:`porting applications to Python 3 -`. If you're an author of a pluggable component, we encourage you +3. Instead, use this opportunity to begin :doc:`porting applications to Python 3 +`. If you're an author of a pluggable component, we encourage you to start porting now. We plan to offer first-class, production-ready support for Python 3 in our next @@ -194,7 +194,7 @@ Retrieval of ``ContentType`` instances associated with proxy models The methods :meth:`ContentTypeManager.get_for_model() ` and :meth:`ContentTypeManager.get_for_models() ` have a new keyword argument – respectively ``for_concrete_model`` and ``for_concrete_models``. -By passing ``False`` using this argument it is now possible to retreive the +By passing ``False`` using this argument it is now possible to retrieve the :class:`ContentType ` associated with proxy models. @@ -484,8 +484,8 @@ In order to be able to run unit tests in any order and to make sure they are always isolated from each other, :class:`~django.test.TransactionTestCase` will now reset the database *after* each test run instead. -No more implict DB sequences reset -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +No more implicit DB sequences reset +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :class:`~django.test.TransactionTestCase` tests used to reset primary key sequences automatically together with the database flushing actions described @@ -543,7 +543,7 @@ Miscellaneous 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 + them to be executable change :setting:`FILE_UPLOAD_PERMISSIONS` to your needs. The new default value is `0666` (octal) and the current umask value is first masked out. @@ -580,8 +580,8 @@ Streaming behavior of :class:`HttpResponse` Django 1.5 deprecates the ability to stream a response by passing an iterator to :class:`~django.http.HttpResponse`. If you rely on this behavior, switch to -:class:`~django.http.StreamingHttpResponse`. See :ref:`explicit-streaming- -responses` above. +:class:`~django.http.StreamingHttpResponse`. See +:ref:`explicit-streaming-responses` above. In Django 1.7 and above, the iterator will be consumed immediately by :class:`~django.http.HttpResponse`. -- cgit v1.3 From 5c143cb340df6825714a6317901b6df03d4ef4b0 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 25 Oct 2012 06:51:19 -0400 Subject: Fixed #19180 - Clarified policy regarding older versions of the docs. --- docs/internals/contributing/writing-documentation.txt | 5 +++-- docs/intro/whatsnext.txt | 14 ++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) (limited to 'docs') diff --git a/docs/internals/contributing/writing-documentation.txt b/docs/internals/contributing/writing-documentation.txt index c8d7039a68..469f8614b9 100644 --- a/docs/internals/contributing/writing-documentation.txt +++ b/docs/internals/contributing/writing-documentation.txt @@ -30,8 +30,9 @@ If you'd like to start contributing to our docs, get the development version of Django from the source code repository (see :ref:`installing-development-version`). The development version has the latest-and-greatest documentation, just as it has latest-and-greatest code. -Generally, we only revise documentation in the development version, as our -policy is to freeze documentation for existing releases (see +We also backport documentation fixes and improvements, at the discretion of the +committer, to the last release branch. That's because it's highly advantageous +to have the docs for the last release be up-to-date and correct (see :ref:`differences-between-doc-versions`). Getting started with Sphinx diff --git a/docs/intro/whatsnext.txt b/docs/intro/whatsnext.txt index ea4b18de03..500a858d47 100644 --- a/docs/intro/whatsnext.txt +++ b/docs/intro/whatsnext.txt @@ -216,15 +216,13 @@ We follow this policy: "New in version X.Y", being X.Y the next release version (hence, the one being developed). -* Documentation for a particular Django release is frozen once the version - has been released officially. It remains a snapshot of the docs as of the - moment of the release. We will make exceptions to this rule in - the case of retroactive security updates or other such retroactive - changes. Once documentation is frozen, we add a note to the top of each - frozen document that says "These docs are frozen for Django version XXX" - and links to the current version of that document. +* Documentation fixes and improvements may be backported to the last release + branch, at the discretion of the committer, however, once a version of + Django is :ref:`no longer supported`, that + version of the docs won't get any further updates. * The `main documentation Web page`_ includes links to documentation for - all previous versions. + all previous versions. Be sure you are using the version of the docs + corresponding to the version of Django you are using! .. _main documentation Web page: https://docs.djangoproject.com/en/dev/ -- cgit v1.3