From fb052b528ad5f0a92f7420ab8ade16462b6435fd Mon Sep 17 00:00:00 2001 From: Pedro Mourelle Date: Mon, 25 Feb 2013 03:18:27 -0300 Subject: Fixed #19900 -- Updated admin buttons to use CSS3 rounded corners. --- docs/releases/1.7.txt | 3 +++ 1 file changed, 3 insertions(+) (limited to 'docs') diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 8c5a0fb585..a7ab751dd4 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -72,6 +72,9 @@ Minor features allowing the ``published`` element to be included in the feed (which relies on ``pubdate``). +* Buttons in :mod:`django.contrib.admin` now use the ``border-radius`` CSS + property for rounded corners rather than GIF background images. + Backwards incompatible changes in 1.7 ===================================== -- cgit v1.3 From 65e03a424e82e157b4513cdebb500891f5c78363 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 12 Jul 2013 09:52:18 -0400 Subject: Fixed #10284 -- ModelFormSet.save(commit=False) no longer deletes objects Thanks laureline.guerin@ and Wedg. --- django/forms/models.py | 3 ++- docs/releases/1.7.txt | 5 +++++ docs/topics/forms/formsets.txt | 34 ++++++++++++++++++++++++++-------- docs/topics/forms/modelforms.txt | 7 +++++++ tests/model_formsets/tests.py | 3 +++ 5 files changed, 43 insertions(+), 9 deletions(-) (limited to 'docs') diff --git a/django/forms/models.py b/django/forms/models.py index 1d3bec1635..2d1ec96306 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -717,7 +717,8 @@ class BaseModelFormSet(BaseFormSet): obj = self._existing_object(pk_value) if form in forms_to_delete: self.deleted_objects.append(obj) - obj.delete() + if commit: + obj.delete() continue if form.has_changed(): self.changed_objects.append((obj, form.changed_data)) diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index a7ab751dd4..bec24c94dc 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -94,6 +94,11 @@ Miscellaneous have a custom :class:`~django.core.files.uploadhandler.FileUploadHandler` that implements ``new_file()``, be sure it accepts this new parameter. +* :class:`ModelFormSet`'s no longer + delete instances when ``save(commit=False)`` is called. See + :attr:`~django.forms.formsets.BaseFormSet.can_delete` for instructions on how + to manually delete objects from deleted forms. + Features deprecated in 1.7 ========================== diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 470d9f52e4..8be6e10ff6 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -3,7 +3,10 @@ Formsets ======== -.. class:: django.forms.formsets.BaseFormSet +.. module:: django.forms.formsets + :synopsis: An abstraction for working with multiple forms on the same page. + +.. class:: BaseFormSet A formset is a layer of abstraction to work with multiple forms on the same page. It can be best compared to a data grid. Let's say you have the following @@ -164,9 +167,7 @@ As we can see, ``formset.errors`` is a list whose entries correspond to the forms in the formset. Validation was performed for each of the two forms, and the expected error message appears for the second item. -.. currentmodule:: django.forms.formsets.BaseFormSet - -.. method:: total_error_count(self) +.. method:: BaseFormSet.total_error_count(self) .. versionadded:: 1.6 @@ -353,6 +354,8 @@ formsets and deletion of forms from a formset. ``can_order`` ~~~~~~~~~~~~~ +.. attribute:: BaseFormSet.can_order + Default: ``False`` Lets you create a formset with the ability to order:: @@ -411,6 +414,8 @@ happen when the user changes these values:: ``can_delete`` ~~~~~~~~~~~~~~ +.. attribute:: BaseFormSet.can_delete + Default: ``False`` Lets you create a formset with the ability to select forms for deletion:: @@ -463,10 +468,23 @@ delete fields you can access them with ``deleted_forms``:: If you are using a :class:`ModelFormSet`, model instances for deleted forms will be deleted when you call -``formset.save()``. On the other hand, if you are using a plain ``FormSet``, -it's up to you to handle ``formset.deleted_forms``, perhaps in your formset's -``save()`` method, as there's no general notion of what it means to delete a -form. +``formset.save()``. + +.. versionchanged:: 1.7 + + If you call ``formset.save(commit=False)``, objects will not be deleted + automatically. You'll need to call ``delete()`` on each of the + :attr:`formset.deleted_objects + ` to actually delete + them:: + + >>> instances = formset.save(commit=False) + >>> for obj in formset.deleted_objects: + ... obj.delete() + +On the other hand, if you are using a plain ``FormSet``, it's up to you to +handle ``formset.deleted_forms``, perhaps in your formset's ``save()`` method, +as there's no general notion of what it means to delete a form. Adding additional fields to a formset ------------------------------------- diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 0f3c5bb815..4ef7a6f074 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -825,6 +825,13 @@ to the database. If your formset contains a ``ManyToManyField``, you'll also need to call ``formset.save_m2m()`` to ensure the many-to-many relationships are saved properly. +After calling ``save()``, your model formset will have three new attributes +containing the formset's changes: + +.. attribute:: models.BaseModelFormSet.changed_objects +.. attribute:: models.BaseModelFormSet.deleted_objects +.. attribute:: models.BaseModelFormSet.new_objects + .. _model-formsets-max-num: Limiting the number of editable objects diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index 4411bbf59a..f66b5abb63 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -32,6 +32,9 @@ class DeletionTests(TestCase): 'form-0-DELETE': 'on', } formset = PoetFormSet(data, queryset=Poet.objects.all()) + formset.save(commit=False) + self.assertEqual(Poet.objects.count(), 1) + formset.save() self.assertTrue(formset.is_valid()) self.assertEqual(Poet.objects.count(), 0) -- cgit v1.3 From c928725b933ff479c04e8a7fb74c4dc2ba138aa7 Mon Sep 17 00:00:00 2001 From: Dominic Rodger Date: Tue, 23 Jul 2013 21:58:43 +0100 Subject: Fixed #20794 -- Documented changes to validate_email 4e2e8f39d changed the way validate_email behaves for foo@localhost email addresses, but wasn't listed in the release notes. --- docs/releases/1.6.txt | 3 +++ 1 file changed, 3 insertions(+) (limited to 'docs') diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index b8babe1843..5b3c7903c5 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -801,6 +801,9 @@ Miscellaneous of the admin views. You should update your custom templates if they use the previous parameter name. +* :meth:`~django.core.validators.validate_email` now accepts email addresses + with ``localhost`` as the domain. + Features deprecated in 1.6 ========================== -- cgit v1.3 From 31c13a99bb9ebdaf12ccab4e880c5da930d86e79 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 16 Jul 2013 09:10:04 -0400 Subject: Fixed #14300 -- Fixed initial SQL location if models is a package. Thanks al_the_x for the report and fheinz for the draft patch. --- django/core/management/commands/loaddata.py | 2 +- django/core/management/sql.py | 20 +++++++++++++++++--- django/db/models/__init__.py | 2 +- django/db/models/loading.py | 16 ++++++++++++---- docs/internals/deprecation.txt | 4 ++++ docs/releases/1.7.txt | 9 +++++++++ tests/fixtures_model_package/models/sql/book.sql | 2 ++ tests/fixtures_model_package/sql/book.sql | 1 + tests/fixtures_model_package/tests.py | 17 +++++++++++++++++ 9 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 tests/fixtures_model_package/models/sql/book.sql create mode 100644 tests/fixtures_model_package/sql/book.sql (limited to 'docs') diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 6856e85e45..aa879f6acc 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -233,7 +233,7 @@ class Command(BaseCommand): """ dirs = [] for path in get_app_paths(): - d = os.path.join(os.path.dirname(path), 'fixtures') + d = os.path.join(path, 'fixtures') if os.path.isdir(d): dirs.append(d) dirs.extend(list(settings.FIXTURE_DIRS)) diff --git a/django/core/management/sql.py b/django/core/management/sql.py index b58d89f60a..c5806086f9 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import codecs import os import re +import warnings from django.conf import settings from django.core.management.base import CommandError @@ -168,7 +169,18 @@ def _split_statements(content): def custom_sql_for_model(model, style, connection): opts = model._meta - app_dir = os.path.normpath(os.path.join(os.path.dirname(upath(models.get_app(model._meta.app_label).__file__)), 'sql')) + app_dirs = [] + app_dir = models.get_app_path(model._meta.app_label) + app_dirs.append(os.path.normpath(os.path.join(app_dir, 'sql'))) + + # Deprecated location -- remove in Django 1.9 + old_app_dir = os.path.normpath(os.path.join(app_dir, 'models/sql')) + if os.path.exists(old_app_dir): + warnings.warn("Custom SQL location '/models/sql' is " + "deprecated, use '/sql' instead.", + PendingDeprecationWarning) + app_dirs.append(old_app_dir) + output = [] # Post-creation SQL should come before any initial SQL data is loaded. @@ -181,8 +193,10 @@ def custom_sql_for_model(model, style, connection): # Find custom SQL, if it's available. backend_name = connection.settings_dict['ENGINE'].split('.')[-1] - sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.model_name, backend_name)), - os.path.join(app_dir, "%s.sql" % opts.model_name)] + sql_files = [] + for app_dir in app_dirs: + sql_files.append(os.path.join(app_dir, "%s.%s.sql" % (opts.model_name, backend_name))) + sql_files.append(os.path.join(app_dir, "%s.sql" % opts.model_name)) for sql_file in sql_files: if os.path.exists(sql_file): with codecs.open(sql_file, 'U', encoding=settings.FILE_CHARSET) as fp: diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 4d310e480b..33151e068d 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -1,7 +1,7 @@ from functools import wraps from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured -from django.db.models.loading import get_apps, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp +from django.db.models.loading import get_apps, get_app_path, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp from django.db.models.query import Q from django.db.models.expressions import F from django.db.models.manager import Manager diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 7280051bd8..9a0cadaf37 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -154,6 +154,16 @@ class AppCache(object): return [elt[0] for elt in apps] + def _get_app_path(self, app): + if hasattr(app, '__path__'): # models/__init__.py package + app_path = app.__path__[0] + else: # models.py module + app_path = app.__file__ + return os.path.dirname(upath(app_path)) + + def get_app_path(self, app_label): + return self._get_app_path(self.get_app(app_label)) + def get_app_paths(self): """ Returns a list of paths to all installed apps. @@ -165,10 +175,7 @@ class AppCache(object): app_paths = [] for app in self.get_apps(): - if hasattr(app, '__path__'): # models/__init__.py package - app_paths.extend([upath(path) for path in app.__path__]) - else: # models.py module - app_paths.append(upath(app.__file__)) + app_paths.append(self._get_app_path(app)) return app_paths def get_app(self, app_label, emptyOK=False): @@ -321,6 +328,7 @@ cache = AppCache() # These methods were always module level, so are kept that way for backwards # compatibility. get_apps = cache.get_apps +get_app_path = cache.get_app_path get_app_paths = cache.get_app_paths get_app = cache.get_app get_app_errors = cache.get_app_errors diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index b0f5566cb3..7f93e1dc58 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -414,6 +414,10 @@ these changes. * ``django.utils.unittest`` will be removed. +* If models are organized in a package, Django will no longer look for + :ref:`initial SQL data` in ``myapp/models/sql/``. Move your + custom SQL files to ``myapp/sql/``. + 2.0 --- diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index bec24c94dc..bec5aaa12a 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -116,3 +116,12 @@ on all Python versions. Since ``unittest2`` became the standard library's :mod:`unittest` module in Python 2.7, and Django 1.7 drops support for older Python versions, this module isn't useful anymore. It has been deprecated. Use :mod:`unittest` instead. + +Custom SQL location for models package +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, if models were organized in a package (``myapp/models/``) rather +than simply ``myapp/models.py``, Django would look for :ref:`initial SQL data +` in ``myapp/models/sql/``. This bug has been fixed so that Django +will search ``myapp/sql/`` as documented. The old location will continue to +work until Django 1.9. diff --git a/tests/fixtures_model_package/models/sql/book.sql b/tests/fixtures_model_package/models/sql/book.sql new file mode 100644 index 0000000000..9b3918f4d7 --- /dev/null +++ b/tests/fixtures_model_package/models/sql/book.sql @@ -0,0 +1,2 @@ +-- Deprecated search path for custom SQL -- remove in Django 1.9 +INSERT INTO fixtures_model_package_book (name) VALUES ('My Deprecated Book'); diff --git a/tests/fixtures_model_package/sql/book.sql b/tests/fixtures_model_package/sql/book.sql new file mode 100644 index 0000000000..21b1d9465b --- /dev/null +++ b/tests/fixtures_model_package/sql/book.sql @@ -0,0 +1 @@ +INSERT INTO fixtures_model_package_book (name) VALUES ('My Book'); diff --git a/tests/fixtures_model_package/tests.py b/tests/fixtures_model_package/tests.py index ad82267da3..1e22ac9833 100644 --- a/tests/fixtures_model_package/tests.py +++ b/tests/fixtures_model_package/tests.py @@ -5,6 +5,7 @@ import warnings from django.core import management from django.db import transaction from django.test import TestCase, TransactionTestCase +from django.utils.six import StringIO from .models import Article, Book @@ -110,3 +111,19 @@ class FixtureTestCase(TestCase): ], lambda a: a.headline, ) + + +class InitialSQLTests(TestCase): + + def test_custom_sql(self): + """ + #14300 -- Verify that custom_sql_for_model searches `app/sql` and not + `app/models/sql` (the old location will work until Django 1.9) + """ + out = StringIO() + management.call_command("sqlcustom", "fixtures_model_package", stdout=out) + output = out.getvalue() + self.assertTrue("INSERT INTO fixtures_model_package_book (name) " + "VALUES ('My Book')" in output) + # value from deprecated search path models/sql (remove in Django 1.9) + self.assertTrue("Deprecated Book" in output) -- cgit v1.3 From 5a5d59471719af520ddc203a6b81cd2f6ef81a20 Mon Sep 17 00:00:00 2001 From: Jon Lønne Date: Wed, 24 Jul 2013 13:14:32 +0200 Subject: Fixed typo in Custom management commands documentation. --- docs/howto/custom-management-commands.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt index 34e68d3700..04ab9bae1b 100644 --- a/docs/howto/custom-management-commands.txt +++ b/docs/howto/custom-management-commands.txt @@ -245,7 +245,7 @@ All attributes can be set in your derived class and can be used in Make sure you know what you are doing if you decide to change the value of this option in your custom command if it creates database content that is locale-sensitive and such content shouldn't contain any translations (like - it happens e.g. with django.contrim.auth permissions) as making the locale + it happens e.g. with django.contrib.auth permissions) as making the locale differ from the de facto default 'en-us' might cause unintended effects. See the `Management commands and locales`_ section above for further details. -- cgit v1.3 From b2314d9e1e08749f2c05151f9cd44520d2b3a03a Mon Sep 17 00:00:00 2001 From: Anssi Kääriäinen Date: Thu, 28 Feb 2013 10:00:38 +0200 Subject: Fixed #19941 -- Modified runtests.py to make running the tests easier. 1. Automatically use tests/../django as the Django version. 2. If settings aren't provided through --settings or DJANGO_SETTINGS_MODULE) then use test_sqlite. --- .../contributing/writing-code/unit-tests.txt | 16 +++++++++++++--- tests/runtests.py | 20 ++++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) (limited to 'docs') diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 7ee65cd6fa..b38c6f6783 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -25,16 +25,26 @@ Quickstart ~~~~~~~~~~ Running the tests requires a Django settings module that defines the -databases to use. To make it easy to get started, Django provides a -sample settings module that uses the SQLite database. To run the tests -with this sample ``settings`` module: +databases to use. To make it easy to get started, Django provides and uses a +sample settings module that uses the SQLite database. To run the tests: .. code-block:: bash git clone git@github.com:django/django.git django-repo cd django-repo/tests + ./runtests.py + +.. versionchanged:: 1.7 + +Older versions of Django required running the tests like this:: + PYTHONPATH=..:$PYTHONPATH python ./runtests.py --settings=test_sqlite +``runtests.py`` now uses the Django package found at ``tests/../django`` (there +isn't a need to add this on your ``PYTHONPATH``) and ``test_sqlite`` for the +settings if settings aren't provided through either ``--settings`` or +:envvar:`DJANGO_SETTINGS_MODULE`. + .. _running-unit-tests-settings: Using another ``settings`` module diff --git a/tests/runtests.py b/tests/runtests.py index 8d5b375fb3..53318a7461 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -7,6 +7,20 @@ import sys import tempfile import warnings +def upath(path): + """ + Separate version of django.utils._os.upath. The django.utils version isn't + usable here, as upath is needed for RUNTESTS_DIR which is needed before + django can be imported. + """ + if sys.version_info[0] != 3 and not isinstance(path, bytes): + fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() + return path.decode(fs_encoding) + return path + +RUNTESTS_DIR = os.path.abspath(os.path.dirname(upath(__file__))) +sys.path.insert(0, os.path.dirname(RUNTESTS_DIR)) # 'tests/../' + from django import contrib from django.utils._os import upath from django.utils import six @@ -15,7 +29,6 @@ CONTRIB_MODULE_PATH = 'django.contrib' TEST_TEMPLATE_DIR = 'templates' -RUNTESTS_DIR = os.path.abspath(os.path.dirname(upath(__file__))) CONTRIB_DIR = os.path.dirname(upath(contrib.__file__)) TEMP_DIR = tempfile.mkdtemp(prefix='django_') @@ -331,10 +344,9 @@ if __name__ == "__main__": options, args = parser.parse_args() if options.settings: os.environ['DJANGO_SETTINGS_MODULE'] = options.settings - elif "DJANGO_SETTINGS_MODULE" not in os.environ: - parser.error("DJANGO_SETTINGS_MODULE is not set in the environment. " - "Set it or use --settings.") else: + if "DJANGO_SETTINGS_MODULE" not in os.environ: + os.environ['DJANGO_SETTINGS_MODULE'] = 'test_sqlite' options.settings = os.environ['DJANGO_SETTINGS_MODULE'] if options.liveserver is not None: -- cgit v1.3 From bd0dcc6c89e262780df3c17f18b2462f50b48137 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 18 Jul 2013 11:10:49 -0400 Subject: Fixed #20766 -- Deprecated FastCGI support. --- django/core/management/commands/runfcgi.py | 8 +++++++- docs/howto/deployment/fastcgi.txt | 3 +++ docs/howto/deployment/index.txt | 8 +++++++- docs/index.txt | 2 +- docs/internals/deprecation.txt | 3 +++ docs/ref/django-admin.txt | 3 +++ docs/topics/install.txt | 9 ++++----- 7 files changed, 28 insertions(+), 8 deletions(-) (limited to 'docs') diff --git a/django/core/management/commands/runfcgi.py b/django/core/management/commands/runfcgi.py index a60d4ebc59..4e9331fc80 100644 --- a/django/core/management/commands/runfcgi.py +++ b/django/core/management/commands/runfcgi.py @@ -1,3 +1,5 @@ +import warnings + from django.core.management.base import BaseCommand class Command(BaseCommand): @@ -5,6 +7,10 @@ class Command(BaseCommand): args = '[various KEY=val options, use `runfcgi help` for help]' def handle(self, *args, **options): + warnings.warn( + "FastCGI support has been deprecated and will be removed in Django 1.9.", + PendingDeprecationWarning) + from django.conf import settings from django.utils import translation # Activate the current language, because it won't get activated later. @@ -14,7 +20,7 @@ class Command(BaseCommand): pass from django.core.servers.fastcgi import runfastcgi runfastcgi(args) - + def usage(self, subcommand): from django.core.servers.fastcgi import FASTCGI_HELP return FASTCGI_HELP diff --git a/docs/howto/deployment/fastcgi.txt b/docs/howto/deployment/fastcgi.txt index 507e50d1a2..cc8d00966c 100644 --- a/docs/howto/deployment/fastcgi.txt +++ b/docs/howto/deployment/fastcgi.txt @@ -2,6 +2,9 @@ How to use Django with FastCGI, SCGI, or AJP ============================================ +.. deprecated:: 1.7 + FastCGI support is deprecated and will be removed in Django 1.9. + .. highlight:: bash Although :doc:`WSGI` is the preferred deployment diff --git a/docs/howto/deployment/index.txt b/docs/howto/deployment/index.txt index ed4bcf3d4a..8b0368ac67 100644 --- a/docs/howto/deployment/index.txt +++ b/docs/howto/deployment/index.txt @@ -10,9 +10,15 @@ ways to easily deploy Django: :maxdepth: 1 wsgi/index - fastcgi checklist +FastCGI support is deprecated and will be removed in Django 1.9. + +.. toctree:: + :maxdepth: 1 + + fastcgi + If you're new to deploying Django and/or Python, we'd recommend you try :doc:`mod_wsgi ` first. In most cases it'll be the easiest, fastest, and most stable deployment choice. diff --git a/docs/index.txt b/docs/index.txt index 7f9d1bd032..8f46db8eb9 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -190,7 +190,7 @@ testing of Django applications: * **Deployment:** :doc:`Overview ` | :doc:`WSGI servers ` | - :doc:`FastCGI/SCGI/AJP ` | + :doc:`FastCGI/SCGI/AJP ` (deprecated) | :doc:`Deploying static files ` | :doc:`Tracking code errors by email ` diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 7f93e1dc58..f7036d13bd 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -418,6 +418,9 @@ these changes. :ref:`initial SQL data` in ``myapp/models/sql/``. Move your custom SQL files to ``myapp/sql/``. +* FastCGI support via the ``runfcgi`` management command will be + removed. Please deploy your project using WSGI. + 2.0 --- diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index d16766618a..31c6a0f660 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -577,6 +577,9 @@ runfcgi [options] .. django-admin:: runfcgi +.. deprecated:: 1.7 + FastCGI support is deprecated and will be removed in Django 1.9. + Starts a set of FastCGI processes suitable for use with any Web server that supports the FastCGI protocol. See the :doc:`FastCGI deployment documentation ` for details. Requires the Python FastCGI module from diff --git a/docs/topics/install.txt b/docs/topics/install.txt index 9cf02d96de..5bcc3f64ec 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -59,11 +59,10 @@ installed. If you can't use mod_wsgi for some reason, fear not: Django supports many other deployment options. One is :doc:`uWSGI `; it works -very well with `nginx`_. Another is :doc:`FastCGI `, -perfect for using Django with servers other than Apache. Additionally, Django -follows the WSGI spec (:pep:`3333`), which allows it to run on a variety of -server platforms. See the `server-arrangements wiki page`_ for specific -installation instructions for each platform. +very well with `nginx`_. Additionally, Django follows the WSGI spec +(:pep:`3333`), which allows it to run on a variety of server platforms. See the +`server-arrangements wiki page`_ for specific installation instructions for +each platform. .. _Apache: http://httpd.apache.org/ .. _nginx: http://nginx.org/ -- cgit v1.3 From 10f8a2100279621ca0e0fa47d99ee744741a05e7 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Wed, 24 Jul 2013 14:58:14 -0700 Subject: Fixed #18168 -- clarified precedence of validation any choices set by formfield_for_choice_field are still subject to model validation of the model field's choices attribute --- docs/ref/contrib/admin/index.txt | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'docs') diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index e5e9428805..137d20a351 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1389,6 +1389,15 @@ templates used by the :class:`ModelAdmin` views: kwargs['choices'] += (('ready', 'Ready for deployment'),) return super(MyModelAdmin, self).formfield_for_choice_field(db_field, request, **kwargs) + .. admonition:: Note + + Any ``choices`` attribute set on the formfield will limited to the form + field only. If the corresponding field on the model has choices set, + the choices provided to the form must be a valid subset of those + choices, otherwise the form submission will fail with + a :exc:`~django.core.exceptions.ValidationError` when the model itself + is validated before saving. + .. method:: ModelAdmin.get_changelist(self, request, **kwargs) Returns the ``Changelist`` class to be used for listing. By default, -- cgit v1.3 From dab52d99fc821f31ab64177551b90d0a513f1eee Mon Sep 17 00:00:00 2001 From: Brenton Cleeland Date: Thu, 25 Jul 2013 20:57:49 +1000 Subject: Fixed #20792 -- Corrected DISALLOWED_USER_AGENTS docs. Thanks simonb for the report. --- docs/ref/middleware.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 4898bab636..d011f054ac 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -37,7 +37,7 @@ defines. See the :doc:`cache documentation `. Adds a few conveniences for perfectionists: * Forbids access to user agents in the :setting:`DISALLOWED_USER_AGENTS` - setting, which should be a list of strings. + setting, which should be a list of compiled regular expression objects. * Performs URL rewriting based on the :setting:`APPEND_SLASH` and :setting:`PREPEND_WWW` settings. -- cgit v1.3 From 8c9240222fd882d9ae3819ff289989df1bcd05d7 Mon Sep 17 00:00:00 2001 From: mark hellewell Date: Thu, 25 Jul 2013 22:48:22 +1000 Subject: Fixed #18315 -- Documented QueryDict.popitem and QueryDict.pop Thanks gcbirzan for the report. --- docs/ref/request-response.txt | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'docs') diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 060ec02e91..c57a1470d6 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -495,6 +495,26 @@ In addition, ``QueryDict`` has the following methods: >>> q.lists() [(u'a', [u'1', u'2', u'3'])] +.. method:: QueryDict.pop(key) + + Returns a list of values for the given key and removes them from the + dictionary. Raises ``KeyError`` if the key does not exist. For example:: + + >>> q = QueryDict('a=1&a=2&a=3', mutable=True) + >>> q.pop('a') + [u'1', u'2', u'3'] + +.. method:: QueryDict.popitem() + + Removes an arbitrary member of the dictionary (since there's no concept + of ordering), and returns a two value tuple containing the key and a list + of all values for the key. Raises ``KeyError`` when called on an empty + dictionary. For example:: + + >>> q = QueryDict('a=1&a=2&a=3', mutable=True) + >>> q.popitem() + (u'a', [u'1', u'2', u'3']) + .. method:: QueryDict.dict() Returns ``dict`` representation of ``QueryDict``. For every (key, list) -- cgit v1.3 From bddb4a68181f773f9f8d479b1afb1453439739ba Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 25 Jul 2013 13:03:15 -0400 Subject: Fixed #20769 -- Added "Python compatibility" section to the 1.6 release notes. --- docs/releases/1.6.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'docs') diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 5b3c7903c5..73b48edc85 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -32,6 +32,16 @@ deprecation process for some features`_. .. _`backwards incompatible changes`: `Backwards incompatible changes in 1.6`_ .. _`begun the deprecation process for some features`: `Features deprecated in 1.6`_ +Python compatibility +==================== + +Django 1.6, like Django 1.5, requires Python 2.6.5 or above. Python 3 is also +officially supported. We **highly recommend** the latest minor release for each +supported Python series (2.6.X, 2.7.X, 3.2.X, and 3.3.X). + +Django 1.6 will be the final release series to support Python 2.6; beginning +with Django 1.7, the minimum supported Python version will be 2.7. + What's new in Django 1.6 ======================== -- cgit v1.3 From 5ed7ec99b6cef20b941d2f35838864d1e440ac6b Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 25 Jul 2013 20:14:18 +0200 Subject: Added versionadded directive missing from b7bd708. --- docs/ref/class-based-views/base.txt | 2 ++ 1 file changed, 2 insertions(+) (limited to 'docs') diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 4ad017c342..24058311a4 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -222,6 +222,8 @@ RedirectView .. attribute:: pattern_name + .. versionadded:: 1.6 + The name of the URL pattern to redirect to. Reversing will be done using the same args and kwargs as are passed in for this view. -- cgit v1.3 From 31fadc120213284da76801cc7bc56e9f32d7281b Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Fri, 26 Jul 2013 11:59:40 +0300 Subject: Fixed #20625 -- Chainable Manager/QuerySet methods. Additionally this patch solves the orthogonal problem that specialized `QuerySet` like `ValuesQuerySet` didn't inherit from the current `QuerySet` type. This wasn't an issue until now because we didn't officially support custom `QuerySet` but it became necessary with the introduction of this new feature. Thanks aaugustin, akaariai, carljm, charettes, mjtamlyn, shaib and timgraham for the reviews. --- django/db/models/__init__.py | 2 +- django/db/models/manager.py | 168 +++++++++++++--------------------------- django/db/models/query.py | 53 ++++++++++++- docs/ref/models/querysets.txt | 15 +++- docs/releases/1.7.txt | 7 ++ docs/topics/db/managers.txt | 119 ++++++++++++++++++++++++++++ tests/basic/tests.py | 55 +++++++++++++ tests/custom_managers/models.py | 52 +++++++++++-- tests/custom_managers/tests.py | 42 ++++++++++ tests/queryset_pickle/tests.py | 4 + 10 files changed, 390 insertions(+), 127 deletions(-) (limited to 'docs') diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 33151e068d..2ee525faf1 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -2,7 +2,7 @@ from functools import wraps from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured from django.db.models.loading import get_apps, get_app_path, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp -from django.db.models.query import Q +from django.db.models.query import Q, QuerySet from django.db.models.expressions import F from django.db.models.manager import Manager from django.db.models.base import Model diff --git a/django/db/models/manager.py b/django/db/models/manager.py index b369aedb64..f57944ebbc 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -1,4 +1,6 @@ import copy +import inspect + from django.db import router from django.db.models.query import QuerySet, insert_query, RawQuerySet from django.db.models import signals @@ -56,17 +58,51 @@ class RenameManagerMethods(RenameMethodsBase): ) -class Manager(six.with_metaclass(RenameManagerMethods)): +class BaseManager(six.with_metaclass(RenameManagerMethods)): # Tracks each time a Manager instance is created. Used to retain order. creation_counter = 0 def __init__(self): - super(Manager, self).__init__() + super(BaseManager, self).__init__() self._set_creation_counter() self.model = None self._inherited = False self._db = None + @classmethod + def _get_queryset_methods(cls, queryset_class): + def create_method(name, method): + def manager_method(self, *args, **kwargs): + return getattr(self.get_queryset(), name)(*args, **kwargs) + manager_method.__name__ = method.__name__ + manager_method.__doc__ = method.__doc__ + return manager_method + + new_methods = {} + # Refs http://bugs.python.org/issue1785. + predicate = inspect.isfunction if six.PY3 else inspect.ismethod + for name, method in inspect.getmembers(queryset_class, predicate=predicate): + # Only copy missing methods. + if hasattr(cls, name): + continue + # Only copy public methods or methods with the attribute `queryset_only=False`. + queryset_only = getattr(method, 'queryset_only', None) + if queryset_only or (queryset_only is None and name.startswith('_')): + continue + # Copy the method onto the manager. + new_methods[name] = create_method(name, method) + return new_methods + + @classmethod + def from_queryset(cls, queryset_class, class_name=None): + if class_name is None: + class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__) + class_dict = { + '_queryset_class': queryset_class, + } + class_dict.update(cls._get_queryset_methods(queryset_class)) + return type(class_name, (cls,), class_dict) + def contribute_to_class(self, model, name): # TODO: Use weakref because of possible memory leak / circular reference. self.model = model @@ -92,8 +128,8 @@ class Manager(six.with_metaclass(RenameManagerMethods)): Sets the creation counter value for this instance and increments the class-level copy. """ - self.creation_counter = Manager.creation_counter - Manager.creation_counter += 1 + self.creation_counter = BaseManager.creation_counter + BaseManager.creation_counter += 1 def _copy_to_model(self, model): """ @@ -117,130 +153,30 @@ class Manager(six.with_metaclass(RenameManagerMethods)): def db(self): return self._db or router.db_for_read(self.model) - ####################### - # PROXIES TO QUERYSET # - ####################### - def get_queryset(self): - """Returns a new QuerySet object. Subclasses can override this method - to easily customize the behavior of the Manager. """ - return QuerySet(self.model, using=self._db) - - def none(self): - return self.get_queryset().none() + Returns a new QuerySet object. Subclasses can override this method to + easily customize the behavior of the Manager. + """ + return self._queryset_class(self.model, using=self._db) def all(self): + # We can't proxy this method through the `QuerySet` like we do for the + # rest of the `QuerySet` methods. This is because `QuerySet.all()` + # works by creating a "copy" of the current queryset and in making said + # copy, all the cached `prefetch_related` lookups are lost. See the + # implementation of `RelatedManager.get_queryset()` for a better + # understanding of how this comes into play. return self.get_queryset() - def count(self): - return self.get_queryset().count() - - def dates(self, *args, **kwargs): - return self.get_queryset().dates(*args, **kwargs) - - def datetimes(self, *args, **kwargs): - return self.get_queryset().datetimes(*args, **kwargs) - - def distinct(self, *args, **kwargs): - return self.get_queryset().distinct(*args, **kwargs) - - def extra(self, *args, **kwargs): - return self.get_queryset().extra(*args, **kwargs) - - def get(self, *args, **kwargs): - return self.get_queryset().get(*args, **kwargs) - - def get_or_create(self, **kwargs): - return self.get_queryset().get_or_create(**kwargs) - - def update_or_create(self, **kwargs): - return self.get_queryset().update_or_create(**kwargs) - - def create(self, **kwargs): - return self.get_queryset().create(**kwargs) - - def bulk_create(self, *args, **kwargs): - return self.get_queryset().bulk_create(*args, **kwargs) - - def filter(self, *args, **kwargs): - return self.get_queryset().filter(*args, **kwargs) - - def aggregate(self, *args, **kwargs): - return self.get_queryset().aggregate(*args, **kwargs) - - def annotate(self, *args, **kwargs): - return self.get_queryset().annotate(*args, **kwargs) - - def complex_filter(self, *args, **kwargs): - return self.get_queryset().complex_filter(*args, **kwargs) - - def exclude(self, *args, **kwargs): - return self.get_queryset().exclude(*args, **kwargs) - - def in_bulk(self, *args, **kwargs): - return self.get_queryset().in_bulk(*args, **kwargs) - - def iterator(self, *args, **kwargs): - return self.get_queryset().iterator(*args, **kwargs) - - def earliest(self, *args, **kwargs): - return self.get_queryset().earliest(*args, **kwargs) - - def latest(self, *args, **kwargs): - return self.get_queryset().latest(*args, **kwargs) - - def first(self): - return self.get_queryset().first() - - def last(self): - return self.get_queryset().last() - - def order_by(self, *args, **kwargs): - return self.get_queryset().order_by(*args, **kwargs) - - def select_for_update(self, *args, **kwargs): - return self.get_queryset().select_for_update(*args, **kwargs) - - def select_related(self, *args, **kwargs): - return self.get_queryset().select_related(*args, **kwargs) - - def prefetch_related(self, *args, **kwargs): - return self.get_queryset().prefetch_related(*args, **kwargs) - - def values(self, *args, **kwargs): - return self.get_queryset().values(*args, **kwargs) - - def values_list(self, *args, **kwargs): - return self.get_queryset().values_list(*args, **kwargs) - - def update(self, *args, **kwargs): - return self.get_queryset().update(*args, **kwargs) - - def reverse(self, *args, **kwargs): - return self.get_queryset().reverse(*args, **kwargs) - - def defer(self, *args, **kwargs): - return self.get_queryset().defer(*args, **kwargs) - - def only(self, *args, **kwargs): - return self.get_queryset().only(*args, **kwargs) - - def using(self, *args, **kwargs): - return self.get_queryset().using(*args, **kwargs) - - def exists(self, *args, **kwargs): - return self.get_queryset().exists(*args, **kwargs) - def _insert(self, objs, fields, **kwargs): return insert_query(self.model, objs, fields, **kwargs) - def _update(self, values, **kwargs): - return self.get_queryset()._update(values, **kwargs) - 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) +Manager = BaseManager.from_queryset(QuerySet, class_name='Manager') + class ManagerDescriptor(object): # This class ensures managers aren't accessible via model instances. diff --git a/django/db/models/query.py b/django/db/models/query.py index 087c10de8e..406838f907 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -10,7 +10,7 @@ from django.conf import settings from django.core import exceptions from django.db import connections, router, transaction, DatabaseError, IntegrityError from django.db.models.constants import LOOKUP_SEP -from django.db.models.fields import AutoField +from django.db.models.fields import AutoField, Empty from django.db.models.query_utils import (Q, select_related_descend, deferred_class_factory, InvalidQuery) from django.db.models.deletion import Collector @@ -30,10 +30,23 @@ REPR_OUTPUT_SIZE = 20 EmptyResultSet = sql.EmptyResultSet +def _pickle_queryset(class_bases, class_dict): + """ + Used by `__reduce__` to create the initial version of the `QuerySet` class + onto which the output of `__getstate__` will be applied. + + See `__reduce__` for more details. + """ + new = Empty() + new.__class__ = type(class_bases[0].__name__, class_bases, class_dict) + return new + + class QuerySet(object): """ Represents a lazy database lookup for a set of objects. """ + def __init__(self, model=None, query=None, using=None): self.model = model self._db = using @@ -45,6 +58,13 @@ class QuerySet(object): self._prefetch_done = False self._known_related_objects = {} # {rel_field, {pk: rel_obj}} + def as_manager(cls): + # Address the circular dependency between `Queryset` and `Manager`. + from django.db.models.manager import Manager + return Manager.from_queryset(cls)() + as_manager.queryset_only = True + as_manager = classmethod(as_manager) + ######################## # PYTHON MAGIC METHODS # ######################## @@ -70,6 +90,26 @@ class QuerySet(object): obj_dict = self.__dict__.copy() return obj_dict + def __reduce__(self): + """ + Used by pickle to deal with the types that we create dynamically when + specialized queryset such as `ValuesQuerySet` are used in conjunction + with querysets that are *subclasses* of `QuerySet`. + + See `_clone` implementation for more details. + """ + if hasattr(self, '_specialized_queryset_class'): + class_bases = ( + self._specialized_queryset_class, + self._base_queryset_class, + ) + class_dict = { + '_specialized_queryset_class': self._specialized_queryset_class, + '_base_queryset_class': self._base_queryset_class, + } + return _pickle_queryset, (class_bases, class_dict), self.__getstate__() + return super(QuerySet, self).__reduce__() + def __repr__(self): data = list(self[:REPR_OUTPUT_SIZE + 1]) if len(data) > REPR_OUTPUT_SIZE: @@ -528,6 +568,7 @@ class QuerySet(object): # Clear the result cache, in case this QuerySet gets reused. self._result_cache = None delete.alters_data = True + delete.queryset_only = True def _raw_delete(self, using): """ @@ -567,6 +608,7 @@ class QuerySet(object): self._result_cache = None return query.get_compiler(self.db).execute_sql(None) _update.alters_data = True + _update.queryset_only = False def exists(self): if self._result_cache is None: @@ -886,6 +928,15 @@ class QuerySet(object): def _clone(self, klass=None, setup=False, **kwargs): if klass is None: klass = self.__class__ + elif not issubclass(self.__class__, klass): + base_queryset_class = getattr(self, '_base_queryset_class', self.__class__) + class_bases = (klass, base_queryset_class) + class_dict = { + '_base_queryset_class': base_queryset_class, + '_specialized_queryset_class': klass, + } + klass = type(klass.__name__, class_bases, class_dict) + query = self.query.clone() if self._sticky_filter: query.filter_is_sticky = True diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 3963785733..c3f6a660b4 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -121,9 +121,7 @@ described here. QuerySet API ============ -Though you usually won't create one manually — you'll go through a -:class:`~django.db.models.Manager` — here's the formal declaration of a -``QuerySet``: +Here's the formal declaration of a ``QuerySet``: .. class:: QuerySet([model=None, query=None, using=None]) @@ -1866,6 +1864,17 @@ 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. +as_manager +~~~~~~~~~~ + +.. classmethod:: as_manager() + +.. versionadded:: 1.7 + +Class method that returns an instance of :class:`~django.db.models.Manager` +with a copy of the ``QuerySet``'s methods. See +:ref:`create-manager-with-queryset-methods` for more details. + .. _field-lookups: Field lookups diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index bec5aaa12a..3526b2bce7 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -30,6 +30,13 @@ security support until the release of Django 1.8. What's new in Django 1.7 ======================== +Calling custom ``QuerySet`` methods from the ``Manager`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :meth:`QuerySet.as_manager() ` +class method has been added to :ref:`create Manager with QuerySet methods +`. + Admin shortcuts support time zones ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/db/managers.txt b/docs/topics/db/managers.txt index b940b09d33..3b83865e60 100644 --- a/docs/topics/db/managers.txt +++ b/docs/topics/db/managers.txt @@ -201,6 +201,125 @@ attribute on the manager class. This is documented fully below_. .. _below: manager-types_ +.. _calling-custom-queryset-methods-from-manager: + +Calling custom ``QuerySet`` methods from the ``Manager`` +-------------------------------------------------------- + +While most methods from the standard ``QuerySet`` are accessible directly from +the ``Manager``, this is only the case for the extra methods defined on a +custom ``QuerySet`` if you also implement them on the ``Manager``:: + + class PersonQuerySet(models.QuerySet): + def male(self): + return self.filter(sex='M') + + def female(self): + return self.filter(sex='F') + + class PersonManager(models.Manager): + def get_queryset(self): + return PersonQuerySet() + + def male(self): + return self.get_queryset().male() + + def female(self): + return self.get_queryset().female() + + class Person(models.Model): + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + sex = models.CharField(max_length=1, choices=(('M', 'Male'), ('F', 'Female'))) + people = PersonManager() + +This example allows you to call both ``male()`` and ``female()`` directly from +the manager ``Person.people``. + +.. _create-manager-with-queryset-methods: + +Creating ``Manager`` with ``QuerySet`` methods +---------------------------------------------- + +.. versionadded:: 1.7 + +In lieu of the above approach which requires duplicating methods on both the +``QuerySet`` and the ``Manager``, :meth:`QuerySet.as_manager() +` can be used to create an instance +of ``Manager`` with a copy of a custom ``QuerySet``'s methods:: + + class Person(models.Model): + ... + people = PersonQuerySet.as_manager() + +The ``Manager`` instance created by :meth:`QuerySet.as_manager() +` will be virtually +identical to the ``PersonManager`` from the previous example. + +Not every ``QuerySet`` method makes sense at the ``Manager`` level; for +instance we intentionally prevent the :meth:`QuerySet.delete() +` method from being copied onto +the ``Manager`` class. + +Methods are copied according to the following rules: + +- Public methods are copied by default. +- Private methods (starting with an underscore) are not copied by default. +- Methods with a `queryset_only` attribute set to `False` are always copied. +- Methods with a `queryset_only` attribute set to `True` are never copied. + +For example:: + + class CustomQuerySet(models.QuerySet): + # Available on both Manager and QuerySet. + def public_method(self): + return + + # Available only on QuerySet. + def _private_method(self): + return + + # Available only on QuerySet. + def opted_out_public_method(self): + return + opted_out_public_method.queryset_only = True + + # Available on both Manager and QuerySet. + def _opted_in_private_method(self): + return + _opted_in_private_method.queryset_only = False + +from_queryset +~~~~~~~~~~~~~ + +.. classmethod:: from_queryset(queryset_class) + +For advance usage you might want both a custom ``Manager`` and a custom +``QuerySet``. You can do that by calling ``Manager.from_queryset()`` which +returns a *subclass* of your base ``Manager`` with a copy of the custom +``QuerySet`` methods:: + + class BaseManager(models.Manager): + def __init__(self, *args, **kwargs): + ... + + def manager_only_method(self): + return + + class CustomQuerySet(models.QuerySet): + def manager_and_queryset_method(self): + return + + class MyModel(models.Model): + objects = BaseManager.from_queryset(CustomQueryset)(*args, **kwargs) + +You may also store the generated class into a variable:: + + CustomManager = BaseManager.from_queryset(CustomQueryset) + + class MyModel(models.Model): + objects = CustomManager(*args, **kwargs) + .. _custom-managers-and-inheritance: Custom managers and model inheritance diff --git a/tests/basic/tests.py b/tests/basic/tests.py index fb21b11279..9d4490afc6 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -6,6 +6,7 @@ import threading from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db import connections, DEFAULT_DB_ALIAS from django.db.models.fields import Field, FieldDoesNotExist +from django.db.models.manager import BaseManager from django.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet, MAX_GET_RESULTS from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature from django.utils import six @@ -734,3 +735,57 @@ class ConcurrentSaveTests(TransactionTestCase): t.join() a.save() self.assertEqual(Article.objects.get(pk=a.pk).headline, 'foo') + + +class ManagerTest(TestCase): + QUERYSET_PROXY_METHODS = [ + 'none', + 'count', + 'dates', + 'datetimes', + 'distinct', + 'extra', + 'get', + 'get_or_create', + 'update_or_create', + 'create', + 'bulk_create', + 'filter', + 'aggregate', + 'annotate', + 'complex_filter', + 'exclude', + 'in_bulk', + 'iterator', + 'earliest', + 'latest', + 'first', + 'last', + 'order_by', + 'select_for_update', + 'select_related', + 'prefetch_related', + 'values', + 'values_list', + 'update', + 'reverse', + 'defer', + 'only', + 'using', + 'exists', + '_update', + ] + + def test_manager_methods(self): + """ + This test ensures that the correct set of methods from `QuerySet` + are copied onto `Manager`. + + It's particularly useful to prevent accidentally leaking new methods + into `Manager`. New `QuerySet` methods that should also be copied onto + `Manager` will need to be added to `ManagerTest.QUERYSET_PROXY_METHODS`. + """ + self.assertEqual( + sorted(BaseManager._get_queryset_methods(QuerySet).keys()), + sorted(self.QUERYSET_PROXY_METHODS), + ) diff --git a/tests/custom_managers/models.py b/tests/custom_managers/models.py index 2f5e62fc7a..44d5eb70da 100644 --- a/tests/custom_managers/models.py +++ b/tests/custom_managers/models.py @@ -20,6 +20,49 @@ class PersonManager(models.Manager): def get_fun_people(self): return self.filter(fun=True) +# An example of a custom manager that sets get_queryset(). + +class PublishedBookManager(models.Manager): + def get_queryset(self): + return super(PublishedBookManager, self).get_queryset().filter(is_published=True) + +# An example of a custom queryset that copies its methods onto the manager. + +class CustomQuerySet(models.QuerySet): + def filter(self, *args, **kwargs): + queryset = super(CustomQuerySet, self).filter(fun=True) + queryset._filter_CustomQuerySet = True + return queryset + + def public_method(self, *args, **kwargs): + return self.all() + + def _private_method(self, *args, **kwargs): + return self.all() + + def optout_public_method(self, *args, **kwargs): + return self.all() + optout_public_method.queryset_only = True + + def _optin_private_method(self, *args, **kwargs): + return self.all() + _optin_private_method.queryset_only = False + +class BaseCustomManager(models.Manager): + def __init__(self, arg): + super(BaseCustomManager, self).__init__() + self.init_arg = arg + + def filter(self, *args, **kwargs): + queryset = super(BaseCustomManager, self).filter(fun=True) + queryset._filter_CustomManager = True + return queryset + + def manager_only(self): + return self.all() + +CustomManager = BaseCustomManager.from_queryset(CustomQuerySet) + @python_2_unicode_compatible class Person(models.Model): first_name = models.CharField(max_length=30) @@ -27,15 +70,12 @@ class Person(models.Model): fun = models.BooleanField() objects = PersonManager() + custom_queryset_default_manager = CustomQuerySet.as_manager() + custom_queryset_custom_manager = CustomManager('hello') + def __str__(self): return "%s %s" % (self.first_name, self.last_name) -# An example of a custom manager that sets get_queryset(). - -class PublishedBookManager(models.Manager): - def get_queryset(self): - return super(PublishedBookManager, self).get_queryset().filter(is_published=True) - @python_2_unicode_compatible class Book(models.Model): title = models.CharField(max_length=50) diff --git a/tests/custom_managers/tests.py b/tests/custom_managers/tests.py index 4fe79fe3fb..7fa58b2a88 100644 --- a/tests/custom_managers/tests.py +++ b/tests/custom_managers/tests.py @@ -11,12 +11,54 @@ class CustomManagerTests(TestCase): p1 = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) p2 = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) + # Test a custom `Manager` method. self.assertQuerysetEqual( Person.objects.get_fun_people(), [ "Bugs Bunny" ], six.text_type ) + + # Test that the methods of a custom `QuerySet` are properly + # copied onto the default `Manager`. + for manager in ['custom_queryset_default_manager', + 'custom_queryset_custom_manager']: + manager = getattr(Person, manager) + + # Copy public methods. + manager.public_method() + # Don't copy private methods. + with self.assertRaises(AttributeError): + manager._private_method() + # Copy methods with `manager=True` even if they are private. + manager._optin_private_method() + # Don't copy methods with `manager=False` even if they are public. + with self.assertRaises(AttributeError): + manager.optout_public_method() + + # Test that the overriden method is called. + queryset = manager.filter() + self.assertQuerysetEqual(queryset, ["Bugs Bunny"], six.text_type) + self.assertEqual(queryset._filter_CustomQuerySet, True) + + # Test that specialized querysets inherit from our custom queryset. + queryset = manager.values_list('first_name', flat=True).filter() + self.assertEqual(list(queryset), [six.text_type("Bugs")]) + self.assertEqual(queryset._filter_CustomQuerySet, True) + + # Test that the custom manager `__init__()` argument has been set. + self.assertEqual(Person.custom_queryset_custom_manager.init_arg, 'hello') + + # Test that the custom manager method is only available on the manager. + Person.custom_queryset_custom_manager.manager_only() + with self.assertRaises(AttributeError): + Person.custom_queryset_custom_manager.all().manager_only() + + # Test that the queryset method doesn't override the custom manager method. + queryset = Person.custom_queryset_custom_manager.filter() + self.assertQuerysetEqual(queryset, ["Bugs Bunny"], six.text_type) + self.assertEqual(queryset._filter_CustomManager, True) + # The RelatedManager used on the 'books' descriptor extends the default # manager self.assertIsInstance(p2.books, PublishedBookManager) diff --git a/tests/queryset_pickle/tests.py b/tests/queryset_pickle/tests.py index b4b540c80d..d2f333a9b3 100644 --- a/tests/queryset_pickle/tests.py +++ b/tests/queryset_pickle/tests.py @@ -90,3 +90,7 @@ class PickleabilityTestCase(TestCase): reloaded = pickle.loads(dumped) self.assertEqual(original, reloaded) self.assertIs(reloaded.__class__, dynclass) + + def test_specialized_queryset(self): + self.assert_pickles(Happening.objects.values('name')) + self.assert_pickles(Happening.objects.values('name').dates('when', 'year')) -- cgit v1.3 From 8676318d2dae9a570d2314e4e6da8c00aaf2e2a0 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 26 Jul 2013 14:43:46 -0400 Subject: Fixed #20805 -- Removed an extra colon beside checkboxes in the admin. Thanks CollinAnderson for the report. --- django/contrib/admin/helpers.py | 8 +++++--- django/forms/forms.py | 9 ++++++--- docs/ref/forms/api.txt | 15 ++++++++++++++- docs/releases/1.6.txt | 4 +++- tests/admin_util/tests.py | 4 ++-- tests/forms_tests/tests/test_forms.py | 10 ++++++++++ 6 files changed, 40 insertions(+), 10 deletions(-) (limited to 'docs') diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index 3ffb85e6c6..b6d5bde932 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -125,14 +125,16 @@ class AdminField(object): contents = conditional_escape(force_text(self.field.label)) if self.is_checkbox: classes.append('vCheckboxLabel') - else: - contents += ':' + if self.field.field.required: classes.append('required') if not self.is_first: classes.append('inline') attrs = {'class': ' '.join(classes)} if classes else {} - return self.field.label_tag(contents=mark_safe(contents), attrs=attrs) + # checkboxes should not have a label suffix as the checkbox appears + # to the left of the label. + return self.field.label_tag(contents=mark_safe(contents), attrs=attrs, + label_suffix='' if self.is_checkbox else None) def errors(self): return mark_safe(self.field.errors.as_ul()) diff --git a/django/forms/forms.py b/django/forms/forms.py index e144eb60f8..ad5daf4416 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -509,20 +509,23 @@ class BoundField(object): ) return self.field.prepare_value(data) - def label_tag(self, contents=None, attrs=None): + def label_tag(self, contents=None, attrs=None, label_suffix=None): """ Wraps the given contents in a ` for various deployment + setups as well as a :doc:`deployment checklist` + for some things you'll need to think about. + * Finally, there's some "specialized" documentation not usually relevant to most developers. This includes the :doc:`release notes ` and :doc:`internals documentation ` for those who want to add diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index 5c27c9c958..ebe5302ef8 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -140,18 +140,18 @@ The 404 (page not found) view .. function:: django.views.defaults.page_not_found(request, template_name='404.html') -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 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. +When you raise :exc:`~django.http.Http404` from within a view, Django loads a +special view devoted to handling 404 errors. By default, it's the view +:func:`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. 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 -so:: +you want to override it, you can specify ``handler404`` in your root URLconf +(setting ``handler404`` anywhere else will have no effect), like so:: handler404 = 'mysite.views.my_custom_404_view' -- cgit v1.3 From 920b242e307297778c6e5010f23502c4f41f299c Mon Sep 17 00:00:00 2001 From: Dominic Rodger Date: Thu, 1 Aug 2013 22:27:11 +0100 Subject: Fixed #20786 -- Cleaned up docs/ref/exceptions.txt Thanks Daniele Procida for the suggestion and edits. --- docs/ref/exceptions.txt | 48 +++++++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 17 deletions(-) (limited to 'docs') diff --git a/docs/ref/exceptions.txt b/docs/ref/exceptions.txt index b15bbea8fa..bee8559ab0 100644 --- a/docs/ref/exceptions.txt +++ b/docs/ref/exceptions.txt @@ -6,24 +6,29 @@ Django Exceptions Django raises some Django specific exceptions as well as many standard Python exceptions. -Django-specific Exceptions -========================== +Django Core Exceptions +====================== .. module:: django.core.exceptions - :synopsis: Django specific exceptions + :synopsis: Django core exceptions + +Django core exception classes are defined in :mod:`django.core.exceptions`. ObjectDoesNotExist and DoesNotExist ----------------------------------- .. exception:: DoesNotExist -.. exception:: ObjectDoesNotExist - The :exc:`DoesNotExist` exception is raised when an object is not found - for the given parameters of a query. + The ``DoesNotExist`` exception is raised when an object is not found for + the given parameters of a query. Django provides a ``DoesNotExist`` + exception as an attribute of each model class to identify the class of + object that could not be found and to allow you to catch a particular model + class with ``try/except``. + +.. exception:: ObjectDoesNotExist - :exc:`ObjectDoesNotExist` is defined in :mod:`django.core.exceptions`. - :exc:`DoesNotExist` is a subclass of the base :exc:`ObjectDoesNotExist` - exception that is provided on every model class as a way of - identifying the specific type of object that could not be found. + The base class for ``DoesNotExist`` exceptions; a ``try/except`` for + ``ObjectDoesNotExist`` will catch ``DoesNotExist`` exceptions for all + models. See :meth:`~django.db.models.query.QuerySet.get()` for further information on :exc:`ObjectDoesNotExist` and :exc:`DoesNotExist`. @@ -121,6 +126,11 @@ ValidationError .. currentmodule:: django.core.urlresolvers +URL Resolver exceptions +======================= + +URL Resolver exceptions are defined in :mod:`django.core.urlresolvers`. + NoReverseMatch -------------- .. exception:: NoReverseMatch @@ -134,9 +144,10 @@ NoReverseMatch Database Exceptions =================== +Database exceptions are provided in :mod:`django.db`. + Django wraps the standard database exceptions so that your Django code has a -guaranteed common implementation of these classes. These database exceptions -are provided in :mod:`django.db`. +guaranteed common implementation of these classes. .. exception:: Error .. exception:: InterfaceError @@ -160,34 +171,37 @@ to Python 3.) .. versionchanged:: 1.6 - Previous version of Django only wrapped ``DatabaseError`` and + Previous versions of Django only wrapped ``DatabaseError`` and ``IntegrityError``, and did not provide ``__cause__``. .. exception:: models.ProtectedError Raised to prevent deletion of referenced objects when using -:attr:`django.db.models.PROTECT`. Subclass of :exc:`IntegrityError`. +:attr:`django.db.models.PROTECT`. :exc:`models.ProtectedError` is a subclass +of :exc:`IntegrityError`. .. currentmodule:: django.http Http Exceptions =============== +Http exceptions are provided in :mod:`django.http`. + .. exception:: UnreadablePostError The :exc:`UnreadablePostError` is raised when a user cancels an upload. - It is available from :mod:`django.http`. .. currentmodule:: django.db.transaction Transaction Exceptions ====================== +Transaction exceptions are defined in :mod:`django.db.transaction`. + .. exception:: TransactionManagementError The :exc:`TransactionManagementError` is raised for any and all problems - related to database transactions. It is available from - :mod:`django.db.transaction`. + related to database transactions. Python Exceptions ================= -- cgit v1.3 From a0c58113b9de072482a5fee2a085218e67971b41 Mon Sep 17 00:00:00 2001 From: Alasdair Nicol Date: Sat, 3 Aug 2013 20:42:02 +0100 Subject: Added missing request argument to example in URL dispatcher 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 65c1f83168..83610e99c0 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -412,7 +412,7 @@ For example:: ) In this example, for a request to ``/blog/2005/``, Django will call -``blog.views.year_archive(year='2005', foo='bar')``. +``blog.views.year_archive(request, year='2005', foo='bar')``. This technique is used in the :doc:`syndication framework ` to pass metadata and -- cgit v1.3 From 0bcdcc7eb9ca2265cb83225257df65a8def3e14a Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Sat, 3 Aug 2013 23:15:15 +0700 Subject: Added ModelAdmin.get_search_fields. --- django/contrib/admin/options.py | 19 ++++++++++++++----- docs/ref/contrib/admin/index.txt | 8 ++++++++ docs/releases/1.7.txt | 3 +++ tests/admin_changelist/admin.py | 7 +++++++ tests/admin_changelist/tests.py | 10 +++++++++- 5 files changed, 41 insertions(+), 6 deletions(-) (limited to 'docs') diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 0b941df091..94e6548650 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -277,9 +277,10 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): return "%s__icontains" % field_name use_distinct = False - if self.search_fields and search_term: + search_fields = self.get_search_fields(request) + if search_fields and search_term: orm_lookups = [construct_search(str(search_field)) - for search_field in self.search_fields] + for search_field in search_fields] for bit in search_term.split(): or_queries = [models.Q(**{orm_lookup: bit}) for orm_lookup in orm_lookups] @@ -767,6 +768,13 @@ class ModelAdmin(BaseModelAdmin): """ return self.list_filter + def get_search_fields(self, request): + """ + Returns a sequence containing the fields to be searched whenever + somebody submits a search query. + """ + return self.search_fields + def get_preserved_filters(self, request): """ Returns the preserved filters querystring. @@ -1243,6 +1251,7 @@ class ModelAdmin(BaseModelAdmin): list_display = self.get_list_display(request) list_display_links = self.get_list_display_links(request, list_display) list_filter = self.get_list_filter(request) + search_fields = self.get_search_fields(request) # Check actions to see if any are available on this changelist actions = self.get_actions(request) @@ -1254,9 +1263,9 @@ class ModelAdmin(BaseModelAdmin): try: cl = ChangeList(request, self.model, list_display, list_display_links, list_filter, self.date_hierarchy, - self.search_fields, self.list_select_related, - self.list_per_page, self.list_max_show_all, self.list_editable, - self) + search_fields, self.list_select_related, self.list_per_page, + self.list_max_show_all, self.list_editable, self) + except IncorrectLookupParameters: # Wacky lookup parameters were given, so redirect to the main # changelist page, without parameters, and pass an 'invalid=1' diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 946f915797..40ec514331 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1233,6 +1233,14 @@ templates used by the :class:`ModelAdmin` views: to return the same kind of sequence type as for the :attr:`~ModelAdmin.list_filter` attribute. +.. method:: ModelAdmin.get_search_fields(self, request) + + .. versionadded:: 1.7 + + The ``get_search_fields`` method is given the ``HttpRequest`` and is expected + to return the same kind of sequence type as for the + :attr:`~ModelAdmin.search_fields` attribute. + .. method:: ModelAdmin.get_inline_instances(self, request, obj=None) .. versionadded:: 1.5 diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index c47a392dff..d21cb45428 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -110,6 +110,9 @@ Minor features or ``None`` for the key and the custom label as the value. The default blank option ``"----------"`` will be omitted in this case. +* The admin's search fields can now be customized per-request thanks to the new + :meth:`django.contrib.admin.ModelAdmin.get_search_fields` method. + Backwards incompatible changes in 1.7 ===================================== diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py index db836cdf7d..a63f2b60ce 100644 --- a/tests/admin_changelist/admin.py +++ b/tests/admin_changelist/admin.py @@ -105,3 +105,10 @@ class DynamicListFilterChildAdmin(admin.ModelAdmin): my_list_filter.remove('parent') return my_list_filter +class DynamicSearchFieldsChildAdmin(admin.ModelAdmin): + search_fields = ('name',) + + def get_search_fields(self, request): + search_fields = super(DynamicSearchFieldsChildAdmin, self).get_search_fields(request) + search_fields += ('age',) + return search_fields diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index a65de46490..cb1ac5039f 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -18,7 +18,8 @@ from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin, GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin, DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin, FilteredChildAdmin, CustomPaginator, site as custom_site, - SwallowAdmin, DynamicListFilterChildAdmin, InvitationAdmin) + SwallowAdmin, DynamicListFilterChildAdmin, InvitationAdmin, + DynamicSearchFieldsChildAdmin) from .models import (Event, Child, Parent, Genre, Band, Musician, Group, Quartet, Membership, ChordsMusician, ChordsBand, Invitation, Swallow, UnorderedObject, OrderedObject, CustomIdUser) @@ -588,6 +589,13 @@ class ChangeListTests(TestCase): response = m.changelist_view(request) self.assertEqual(response.context_data['cl'].list_filter, ('parent', 'name', 'age')) + def test_dynamic_search_fields(self): + child = self._create_superuser('child') + m = DynamicSearchFieldsChildAdmin(Child, admin.site) + request = self._mocked_authenticated_request('/child/', child) + response = m.changelist_view(request) + self.assertEqual(response.context_data['cl'].search_fields, ('name', 'age')) + def test_pagination_page_range(self): """ Regression tests for ticket #15653: ensure the number of pages -- cgit v1.3 From b278f7478d37d3620e5addf7cc2070bc38c10871 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sun, 4 Aug 2013 05:57:11 -0400 Subject: Fixed #20858 -- Removed erroneous import in tutorial 2. Thanks AtomicSpark. --- docs/intro/tutorial02.txt | 6 ------ 1 file changed, 6 deletions(-) (limited to 'docs') diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index dd3e86d8ae..c5c5f8f288 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -352,12 +352,6 @@ representation of the output. You can improve that by giving that method (in :file:`polls/models.py`) a few attributes, as follows:: - import datetime - from django.utils import timezone - from django.db import models - - from polls.models import Poll - class Poll(models.Model): # ... def was_published_recently(self): -- cgit v1.3 From 07876cf02b6db453ca0397c29c225668872fa96d Mon Sep 17 00:00:00 2001 From: Curtis Maloney Date: Sat, 3 Aug 2013 15:41:15 +1000 Subject: Deprecated SortedDict (replaced with collections.OrderedDict) Thanks Loic Bistuer for the review. --- django/contrib/admin/options.py | 8 ++--- django/contrib/admin/views/main.py | 6 ++-- django/contrib/auth/forms.py | 5 +-- django/contrib/auth/hashers.py | 16 ++++----- django/contrib/formtools/wizard/views.py | 20 +++++++----- django/contrib/staticfiles/finders.py | 9 ++--- .../management/commands/collectstatic.py | 4 +-- django/contrib/staticfiles/storage.py | 6 ++-- django/core/management/commands/dumpdata.py | 9 ++--- django/core/management/commands/inspectdb.py | 6 ++-- django/core/management/commands/syncdb.py | 4 +-- django/db/models/deletion.py | 4 +-- django/db/models/loading.py | 21 ++++++++---- django/db/models/options.py | 10 +++--- django/db/models/sql/query.py | 20 ++++++------ django/forms/forms.py | 4 +-- django/forms/models.py | 8 ++--- django/middleware/locale.py | 5 +-- django/utils/datastructures.py | 6 +++- django/utils/translation/trans_real.py | 8 ++--- docs/internals/deprecation.txt | 3 ++ docs/ref/models/querysets.txt | 7 ++-- docs/ref/utils.txt | 4 +++ docs/releases/1.7.txt | 7 ++++ tests/extra_regress/tests.py | 38 +++++++++++----------- tests/queries/tests.py | 8 ++--- 26 files changed, 139 insertions(+), 107 deletions(-) (limited to 'docs') diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 94e6548650..8a6b65358e 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1,3 +1,4 @@ +from collections import OrderedDict import copy import operator from functools import partial, reduce, update_wrapper @@ -29,7 +30,6 @@ from django.http.response import HttpResponseBase from django.shortcuts import get_object_or_404 from django.template.response import SimpleTemplateResponse, TemplateResponse from django.utils.decorators import method_decorator -from django.utils.datastructures import SortedDict from django.utils.html import escape, escapejs from django.utils.safestring import mark_safe from django.utils import six @@ -672,7 +672,7 @@ class ModelAdmin(BaseModelAdmin): # want *any* actions enabled on this page. from django.contrib.admin.views.main import _is_changelist_popup if self.actions is None or _is_changelist_popup(request): - return SortedDict() + return OrderedDict() actions = [] @@ -693,8 +693,8 @@ class ModelAdmin(BaseModelAdmin): # get_action might have returned None, so filter any of those out. actions = filter(None, actions) - # Convert the actions into a SortedDict keyed by name. - actions = SortedDict([ + # Convert the actions into an OrderedDict keyed by name. + actions = OrderedDict([ (name, (func, name, desc)) for func, name, desc in actions ]) diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index f766031bf8..3325747a9f 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,3 +1,4 @@ +from collections import OrderedDict import sys import warnings @@ -7,7 +8,6 @@ from django.core.urlresolvers import reverse from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils import six -from django.utils.datastructures import SortedDict from django.utils.deprecation import RenameMethodsBase from django.utils.encoding import force_str, force_text from django.utils.translation import ugettext, ugettext_lazy @@ -319,13 +319,13 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): def get_ordering_field_columns(self): """ - Returns a SortedDict of ordering field column numbers and asc/desc + Returns an OrderedDict of ordering field column numbers and asc/desc """ # We must cope with more than one column having the same underlying sort # field, so we base things on column numbers. ordering = self._get_default_ordering() - ordering_fields = SortedDict() + ordering_fields = OrderedDict() if ORDER_VAR not in self.params: # for ordering specified on ModelAdmin or model Meta, we don't know # the right column numbers absolutely, because there might be more diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index e3f03cc536..c5c2db456e 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals +from collections import OrderedDict + from django import forms from django.forms.util import flatatt from django.template import loader -from django.utils.datastructures import SortedDict from django.utils.encoding import force_bytes from django.utils.html import format_html, format_html_join from django.utils.http import urlsafe_base64_encode @@ -324,7 +325,7 @@ class PasswordChangeForm(SetPasswordForm): ) return old_password -PasswordChangeForm.base_fields = SortedDict([ +PasswordChangeForm.base_fields = OrderedDict([ (k, PasswordChangeForm.base_fields[k]) for k in ['old_password', 'new_password1', 'new_password2'] ]) diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index 45ebc0f980..06aec64636 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -2,13 +2,13 @@ from __future__ import unicode_literals import base64 import binascii +from collections import OrderedDict import hashlib import importlib from django.dispatch import receiver from django.conf import settings from django.test.signals import setting_changed -from django.utils.datastructures import SortedDict from django.utils.encoding import force_bytes, force_str, force_text from django.core.exceptions import ImproperlyConfigured from django.utils.crypto import ( @@ -243,7 +243,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher): def safe_summary(self, encoded): algorithm, iterations, salt, hash = encoded.split('$', 3) assert algorithm == self.algorithm - return SortedDict([ + return OrderedDict([ (_('algorithm'), algorithm), (_('iterations'), iterations), (_('salt'), mask_hash(salt)), @@ -320,7 +320,7 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher): algorithm, empty, algostr, work_factor, data = encoded.split('$', 4) assert algorithm == self.algorithm salt, checksum = data[:22], data[22:] - return SortedDict([ + return OrderedDict([ (_('algorithm'), algorithm), (_('work factor'), work_factor), (_('salt'), mask_hash(salt)), @@ -368,7 +368,7 @@ class SHA1PasswordHasher(BasePasswordHasher): def safe_summary(self, encoded): algorithm, salt, hash = encoded.split('$', 2) assert algorithm == self.algorithm - return SortedDict([ + return OrderedDict([ (_('algorithm'), algorithm), (_('salt'), mask_hash(salt, show=2)), (_('hash'), mask_hash(hash)), @@ -396,7 +396,7 @@ class MD5PasswordHasher(BasePasswordHasher): def safe_summary(self, encoded): algorithm, salt, hash = encoded.split('$', 2) assert algorithm == self.algorithm - return SortedDict([ + return OrderedDict([ (_('algorithm'), algorithm), (_('salt'), mask_hash(salt, show=2)), (_('hash'), mask_hash(hash)), @@ -429,7 +429,7 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher): def safe_summary(self, encoded): assert encoded.startswith('sha1$$') hash = encoded[6:] - return SortedDict([ + return OrderedDict([ (_('algorithm'), self.algorithm), (_('hash'), mask_hash(hash)), ]) @@ -462,7 +462,7 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher): return constant_time_compare(encoded, encoded_2) def safe_summary(self, encoded): - return SortedDict([ + return OrderedDict([ (_('algorithm'), self.algorithm), (_('hash'), mask_hash(encoded, show=3)), ]) @@ -496,7 +496,7 @@ class CryptPasswordHasher(BasePasswordHasher): def safe_summary(self, encoded): algorithm, salt, data = encoded.split('$', 2) assert algorithm == self.algorithm - return SortedDict([ + return OrderedDict([ (_('algorithm'), algorithm), (_('salt'), salt), (_('hash'), mask_hash(data, show=3)), diff --git a/django/contrib/formtools/wizard/views.py b/django/contrib/formtools/wizard/views.py index c137f1aeaf..9c81f77ed2 100644 --- a/django/contrib/formtools/wizard/views.py +++ b/django/contrib/formtools/wizard/views.py @@ -1,3 +1,4 @@ +from collections import OrderedDict import re from django import forms @@ -5,7 +6,6 @@ from django.shortcuts import redirect from django.core.urlresolvers import reverse from django.forms import formsets, ValidationError from django.views.generic import TemplateView -from django.utils.datastructures import SortedDict from django.utils.decorators import classonlymethod from django.utils.translation import ugettext as _ from django.utils import six @@ -158,7 +158,7 @@ class WizardView(TemplateView): form_list = form_list or kwargs.pop('form_list', getattr(cls, 'form_list', None)) or [] - computed_form_list = SortedDict() + computed_form_list = OrderedDict() assert len(form_list) > 0, 'at least one form is needed' @@ -206,7 +206,7 @@ class WizardView(TemplateView): The form_list is always generated on the fly because condition methods could use data from other (maybe previous forms). """ - form_list = SortedDict() + form_list = OrderedDict() for form_key, form_class in six.iteritems(self.form_list): # try to fetch the value from condition list, by default, the form # gets passed to the new list. @@ -498,9 +498,10 @@ class WizardView(TemplateView): if step is None: step = self.steps.current form_list = self.get_form_list() - key = form_list.keyOrder.index(step) + 1 - if len(form_list.keyOrder) > key: - return form_list.keyOrder[key] + keys = list(form_list.keys()) + key = keys.index(step) + 1 + if len(keys) > key: + return keys[key] return None def get_prev_step(self, step=None): @@ -512,9 +513,10 @@ class WizardView(TemplateView): if step is None: step = self.steps.current form_list = self.get_form_list() - key = form_list.keyOrder.index(step) - 1 + keys = list(form_list.keys()) + key = keys.index(step) - 1 if key >= 0: - return form_list.keyOrder[key] + return keys[key] return None def get_step_index(self, step=None): @@ -524,7 +526,7 @@ class WizardView(TemplateView): """ if step is None: step = self.steps.current - return self.get_form_list().keyOrder.index(step) + return list(self.get_form_list().keys()).index(step) def get_context_data(self, form, **kwargs): """ diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py index 7d266d95a0..d4efd1a8d8 100644 --- a/django/contrib/staticfiles/finders.py +++ b/django/contrib/staticfiles/finders.py @@ -1,8 +1,9 @@ +from collections import OrderedDict import os + from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.files.storage import default_storage, Storage, FileSystemStorage -from django.utils.datastructures import SortedDict from django.utils.functional import empty, memoize, LazyObject from django.utils.module_loading import import_by_path from django.utils._os import safe_join @@ -11,7 +12,7 @@ from django.utils import six from django.contrib.staticfiles import utils from django.contrib.staticfiles.storage import AppStaticStorage -_finders = SortedDict() +_finders = OrderedDict() class BaseFinder(object): @@ -47,7 +48,7 @@ class FileSystemFinder(BaseFinder): # List of locations with static files self.locations = [] # Maps dir paths to an appropriate storage instance - self.storages = SortedDict() + self.storages = OrderedDict() if not isinstance(settings.STATICFILES_DIRS, (list, tuple)): raise ImproperlyConfigured( "Your STATICFILES_DIRS setting is not a tuple or list; " @@ -118,7 +119,7 @@ class AppDirectoriesFinder(BaseFinder): # The list of apps that are handled self.apps = [] # Mapping of app module paths to storage instances - self.storages = SortedDict() + self.storages = OrderedDict() if apps is None: apps = settings.INSTALLED_APPS for app in apps: diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index 7c3de80e93..c1e9fa811b 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -2,12 +2,12 @@ from __future__ import unicode_literals import os import sys +from collections import OrderedDict from optparse import make_option from django.core.files.storage import FileSystemStorage from django.core.management.base import CommandError, NoArgsCommand from django.utils.encoding import smart_text -from django.utils.datastructures import SortedDict from django.utils.six.moves import input from django.contrib.staticfiles import finders, storage @@ -97,7 +97,7 @@ class Command(NoArgsCommand): else: handler = self.copy_file - found_files = SortedDict() + found_files = OrderedDict() for finder in finders.get_finders(): for path, storage in finder.list(self.ignore_patterns): # Prefix the relative path if the source storage contains it diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index f6d0a37a11..1242afe411 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from collections import OrderedDict import hashlib from importlib import import_module import os @@ -16,7 +17,6 @@ from django.core.cache import (get_cache, InvalidCacheBackendError, from django.core.exceptions import ImproperlyConfigured from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage, get_storage_class -from django.utils.datastructures import SortedDict from django.utils.encoding import force_bytes, force_text from django.utils.functional import LazyObject from django.utils._os import upath @@ -64,7 +64,7 @@ class CachedFilesMixin(object): except InvalidCacheBackendError: # Use the default backend self.cache = default_cache - self._patterns = SortedDict() + self._patterns = OrderedDict() for extension, patterns in self.patterns: for pattern in patterns: if isinstance(pattern, (tuple, list)): @@ -202,7 +202,7 @@ class CachedFilesMixin(object): def post_process(self, paths, dry_run=False, **options): """ - Post process the given SortedDict of files (called from collectstatic). + Post process the given OrderedDict of files (called from collectstatic). Processing is actually two separate operations: diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index 100c1872a7..fd9418a728 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -1,10 +1,11 @@ +from collections import OrderedDict +from optparse import make_option + from django.core.exceptions import ImproperlyConfigured from django.core.management.base import BaseCommand, CommandError from django.core import serializers from django.db import router, DEFAULT_DB_ALIAS -from django.utils.datastructures import SortedDict -from optparse import make_option class Command(BaseCommand): option_list = BaseCommand.option_list + ( @@ -66,11 +67,11 @@ class Command(BaseCommand): if len(app_labels) == 0: if primary_keys: raise CommandError("You can only use --pks option with one model") - app_list = SortedDict((app, None) for app in get_apps() if app not in excluded_apps) + app_list = OrderedDict((app, None) for app in get_apps() if app not in excluded_apps) else: if len(app_labels) > 1 and primary_keys: raise CommandError("You can only use --pks option with one model") - app_list = SortedDict() + app_list = OrderedDict() for label in app_labels: try: app_label, model_label = label.split('.') diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index dc26bb11d2..2cfea028ec 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -1,12 +1,12 @@ from __future__ import unicode_literals +from collections import OrderedDict import keyword import re from optparse import make_option from django.core.management.base import NoArgsCommand, CommandError from django.db import connections, DEFAULT_DB_ALIAS -from django.utils.datastructures import SortedDict class Command(NoArgsCommand): @@ -69,7 +69,7 @@ class Command(NoArgsCommand): used_column_names = [] # Holds column names used in the table so far for i, row in enumerate(connection.introspection.get_table_description(cursor, table_name)): comment_notes = [] # Holds Field notes, to be displayed in a Python comment. - extra_params = SortedDict() # Holds Field parameters such as 'db_column'. + extra_params = OrderedDict() # Holds Field parameters such as 'db_column'. column_name = row[0] is_relation = i in relations @@ -193,7 +193,7 @@ class Command(NoArgsCommand): description, this routine will return the given field type name, as well as any additional keyword parameters and notes for the field. """ - field_params = SortedDict() + field_params = OrderedDict() field_notes = [] try: diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index 7c8d8b5d8f..d51699e95a 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -1,3 +1,4 @@ +from collections import OrderedDict from importlib import import_module from optparse import make_option import itertools @@ -9,7 +10,6 @@ from django.core.management.base import NoArgsCommand from django.core.management.color import no_style from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal, emit_pre_sync_signal from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS -from django.utils.datastructures import SortedDict class Command(NoArgsCommand): @@ -76,7 +76,7 @@ class Command(NoArgsCommand): return not ((converter(opts.db_table) in tables) or (opts.auto_created and converter(opts.auto_created._meta.db_table) in tables)) - manifest = SortedDict( + manifest = OrderedDict( (app_name, list(filter(model_installed, model_list))) for app_name, model_list in all_models ) diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index e0bfb9d879..f4c64f7b7d 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -1,8 +1,8 @@ +from collections import OrderedDict from operator import attrgetter from django.db import connections, transaction, IntegrityError from django.db.models import signals, sql -from django.utils.datastructures import SortedDict from django.utils import six @@ -234,7 +234,7 @@ class Collector(object): found = True if not found: return - self.data = SortedDict([(model, self.data[model]) + self.data = OrderedDict([(model, self.data[model]) for model in sorted_models]) def delete(self): diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 83682035d2..f858f25c1d 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -1,5 +1,7 @@ "Utilities for loading models and the modules that contain them." +from collections import OrderedDict +import copy import imp from importlib import import_module import os @@ -7,7 +9,6 @@ import sys from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils.datastructures import SortedDict from django.utils.module_loading import module_has_submodule from django.utils._os import upath from django.utils import six @@ -17,6 +18,14 @@ __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models', MODELS_MODULE_NAME = 'models' +class ModelDict(OrderedDict): + """ + We need to special-case the deepcopy for this, as the keys are modules, + which can't be deep copied. + """ + def __deepcopy__(self, memo): + return self.__class__([(key, copy.deepcopy(value, memo)) + for key, value in self.items()]) class UnavailableApp(Exception): pass @@ -31,14 +40,14 @@ class AppCache(object): # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531. __shared_state = dict( # Keys of app_store are the model modules for each application. - app_store=SortedDict(), + app_store=ModelDict(), # Mapping of installed app_labels to model modules for that app. app_labels={}, # Mapping of app_labels to a dictionary of model names to model code. # May contain apps that are not installed. - app_models=SortedDict(), + app_models=ModelDict(), # Mapping of app_labels to errors raised when trying to import the app. app_errors={}, @@ -244,12 +253,12 @@ class AppCache(object): if app_mod: if app_mod in self.app_store: app_list = [self.app_models.get(self._label_for(app_mod), - SortedDict())] + ModelDict())] else: app_list = [] else: if only_installed: - app_list = [self.app_models.get(app_label, SortedDict()) + app_list = [self.app_models.get(app_label, ModelDict()) for app_label in six.iterkeys(self.app_labels)] else: app_list = six.itervalues(self.app_models) @@ -298,7 +307,7 @@ class AppCache(object): # Store as 'name: model' pair in a dictionary # in the app_models dictionary model_name = model._meta.model_name - model_dict = self.app_models.setdefault(app_label, SortedDict()) + model_dict = self.app_models.setdefault(app_label, ModelDict()) if model_name in model_dict: # The same model may be imported via different paths (e.g. # appname.models and project.appname.models). We use the source diff --git a/django/db/models/options.py b/django/db/models/options.py index d072d67fb3..95cefec1bf 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from collections import OrderedDict import re from bisect import bisect import warnings @@ -11,7 +12,6 @@ from django.db.models.fields.proxy import OrderWrt from django.db.models.loading import get_models, app_cache_ready from django.utils import six from django.utils.functional import cached_property -from django.utils.datastructures import SortedDict from django.utils.encoding import force_text, smart_text, python_2_unicode_compatible from django.utils.translation import activate, deactivate_all, get_language, string_concat @@ -58,7 +58,7 @@ class Options(object): # concrete models, the concrete_model is always the class itself. self.concrete_model = None self.swappable = None - self.parents = SortedDict() + self.parents = OrderedDict() self.auto_created = False # To handle various inheritance situations, we need to track where @@ -332,7 +332,7 @@ class Options(object): return list(six.iteritems(self._m2m_cache)) def _fill_m2m_cache(self): - cache = SortedDict() + cache = OrderedDict() for parent in self.parents: for field, model in parent._meta.get_m2m_with_model(): if model: @@ -474,7 +474,7 @@ class Options(object): return [t for t in cache.items() if all(p(*t) for p in predicates)] def _fill_related_objects_cache(self): - cache = SortedDict() + cache = OrderedDict() parent_list = self.get_parent_list() for parent in self.parents: for obj, model in parent._meta.get_all_related_objects_with_model(include_hidden=True): @@ -519,7 +519,7 @@ class Options(object): return list(six.iteritems(cache)) def _fill_related_many_to_many_cache(self): - cache = SortedDict() + cache = OrderedDict() parent_list = self.get_parent_list() for parent in self.parents: for obj, model in parent._meta.get_all_related_m2m_objects_with_model(): diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index fa7ae51d9c..de431204bd 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -7,9 +7,9 @@ databases). The abstraction barrier only works one way: this module has to know all about the internals of models in order to get the information it needs. """ +from collections import OrderedDict import copy -from django.utils.datastructures import SortedDict from django.utils.encoding import force_text from django.utils.tree import Node from django.utils import six @@ -142,7 +142,7 @@ class Query(object): self.select_related = False # SQL aggregate-related attributes - self.aggregates = SortedDict() # Maps alias -> SQL aggregate function + self.aggregates = OrderedDict() # Maps alias -> SQL aggregate function self.aggregate_select_mask = None self._aggregate_select_cache = None @@ -152,7 +152,7 @@ class Query(object): # These are for extensions. The contents are more or less appended # verbatim to the appropriate clause. - self.extra = SortedDict() # Maps col_alias -> (col_sql, params). + self.extra = OrderedDict() # Maps col_alias -> (col_sql, params). self.extra_select_mask = None self._extra_select_cache = None @@ -741,7 +741,7 @@ class Query(object): self.group_by = [relabel_column(col) for col in self.group_by] self.select = [SelectInfo(relabel_column(s.col), s.field) for s in self.select] - self.aggregates = SortedDict( + self.aggregates = OrderedDict( (key, relabel_column(col)) for key, col in self.aggregates.items()) # 2. Rename the alias in the internal table/alias datastructures. @@ -795,7 +795,7 @@ class Query(object): assert current < ord('Z') prefix = chr(current + 1) self.alias_prefix = prefix - change_map = SortedDict() + change_map = OrderedDict() for pos, alias in enumerate(self.tables): if alias in exceptions: continue @@ -1638,7 +1638,7 @@ class Query(object): # dictionary with their parameters in 'select_params' so that # subsequent updates to the select dictionary also adjust the # parameters appropriately. - select_pairs = SortedDict() + select_pairs = OrderedDict() if select_params: param_iter = iter(select_params) else: @@ -1651,7 +1651,7 @@ class Query(object): entry_params.append(next(param_iter)) pos = entry.find("%s", pos + 2) select_pairs[name] = (entry, entry_params) - # This is order preserving, since self.extra_select is a SortedDict. + # This is order preserving, since self.extra_select is an OrderedDict. self.extra.update(select_pairs) if where or params: self.where.add(ExtraWhere(where, params), AND) @@ -1760,7 +1760,7 @@ class Query(object): self._extra_select_cache = None def _aggregate_select(self): - """The SortedDict of aggregate columns that are not masked, and should + """The OrderedDict of aggregate columns that are not masked, and should be used in the SELECT clause. This result is cached for optimization purposes. @@ -1768,7 +1768,7 @@ class Query(object): if self._aggregate_select_cache is not None: return self._aggregate_select_cache elif self.aggregate_select_mask is not None: - self._aggregate_select_cache = SortedDict([ + self._aggregate_select_cache = OrderedDict([ (k, v) for k, v in self.aggregates.items() if k in self.aggregate_select_mask ]) @@ -1781,7 +1781,7 @@ class Query(object): if self._extra_select_cache is not None: return self._extra_select_cache elif self.extra_select_mask is not None: - self._extra_select_cache = SortedDict([ + self._extra_select_cache = OrderedDict([ (k, v) for k, v in self.extra.items() if k in self.extra_select_mask ]) diff --git a/django/forms/forms.py b/django/forms/forms.py index 31e51e7c34..9905eee7c7 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -4,6 +4,7 @@ Form classes from __future__ import unicode_literals +from collections import OrderedDict import copy import warnings @@ -11,7 +12,6 @@ from django.core.exceptions import ValidationError from django.forms.fields import Field, FileField from django.forms.util import flatatt, ErrorDict, ErrorList from django.forms.widgets import Media, media_property, TextInput, Textarea -from django.utils.datastructures import SortedDict from django.utils.html import conditional_escape, format_html from django.utils.encoding import smart_text, force_text, python_2_unicode_compatible from django.utils.safestring import mark_safe @@ -55,7 +55,7 @@ def get_declared_fields(bases, attrs, with_base_fields=True): if hasattr(base, 'declared_fields'): fields = list(six.iteritems(base.declared_fields)) + fields - return SortedDict(fields) + return OrderedDict(fields) class DeclarativeFieldsMetaclass(type): """ diff --git a/django/forms/models.py b/django/forms/models.py index 83954b0b22..a5b82e521d 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -5,6 +5,7 @@ and database field objects. from __future__ import unicode_literals +from collections import OrderedDict import warnings from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError @@ -15,7 +16,6 @@ from django.forms.util import ErrorList from django.forms.widgets import (SelectMultiple, HiddenInput, MultipleHiddenInput, media_property, CheckboxSelectMultiple) from django.utils.encoding import smart_text, force_text -from django.utils.datastructures import SortedDict from django.utils import six from django.utils.text import get_text_list, capfirst from django.utils.translation import ugettext_lazy as _, ugettext, string_concat @@ -142,7 +142,7 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None, localized_fields=None, labels=None, help_texts=None, error_messages=None): """ - Returns a ``SortedDict`` containing form fields for the given model. + Returns a ``OrderedDict`` containing form fields for the given model. ``fields`` is an optional list of field names. If provided, only the named fields will be included in the returned fields. @@ -199,9 +199,9 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, field_list.append((f.name, formfield)) else: ignored.append(f.name) - field_dict = SortedDict(field_list) + field_dict = OrderedDict(field_list) if fields: - field_dict = SortedDict( + field_dict = OrderedDict( [(f, field_dict.get(f)) for f in fields if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored)] ) diff --git a/django/middleware/locale.py b/django/middleware/locale.py index 4e0a4753ce..cd5d2cce71 100644 --- a/django/middleware/locale.py +++ b/django/middleware/locale.py @@ -1,12 +1,13 @@ "This is the locale selecting middleware that will look at accept headers" +from collections import OrderedDict + from django.conf import settings from django.core.urlresolvers import (is_valid_path, get_resolver, LocaleRegexURLResolver) from django.http import HttpResponseRedirect from django.utils.cache import patch_vary_headers from django.utils import translation -from django.utils.datastructures import SortedDict class LocaleMiddleware(object): @@ -19,7 +20,7 @@ class LocaleMiddleware(object): """ def __init__(self): - self._supported_languages = SortedDict(settings.LANGUAGES) + self._supported_languages = OrderedDict(settings.LANGUAGES) self._is_language_prefix_patterns_used = False for url_pattern in get_resolver(None).url_patterns: if isinstance(url_pattern, LocaleRegexURLResolver): diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index 3b1638392c..d6447ec0c7 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -1,7 +1,7 @@ import copy +import warnings from django.utils import six - class MergeDict(object): """ A simple class for creating new "virtual" dictionaries that actually look @@ -124,6 +124,10 @@ class SortedDict(dict): return instance def __init__(self, data=None): + warnings.warn( + "SortedDict is deprecated and will be removed in Django 1.9.", + PendingDeprecationWarning, stacklevel=2 + ) if data is None or isinstance(data, dict): data = data or [] super(SortedDict, self).__init__(data) diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 91a73f1e68..11463d8f9b 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -1,6 +1,7 @@ """Translation helper functions.""" from __future__ import unicode_literals +from collections import OrderedDict import locale import os import re @@ -10,7 +11,6 @@ from importlib import import_module from threading import local import warnings -from django.utils.datastructures import SortedDict from django.utils.encoding import force_str, force_text from django.utils.functional import memoize from django.utils._os import upath @@ -369,7 +369,7 @@ def get_supported_language_variant(lang_code, supported=None, strict=False): """ if supported is None: from django.conf import settings - supported = SortedDict(settings.LANGUAGES) + supported = OrderedDict(settings.LANGUAGES) if lang_code: # if fr-CA is not supported, try fr-ca; if that fails, fallback to fr. generic_lang_code = lang_code.split('-')[0] @@ -396,7 +396,7 @@ def get_language_from_path(path, supported=None, strict=False): """ if supported is None: from django.conf import settings - supported = SortedDict(settings.LANGUAGES) + supported = OrderedDict(settings.LANGUAGES) regex_match = language_code_prefix_re.match(path) if not regex_match: return None @@ -418,7 +418,7 @@ def get_language_from_request(request, check_path=False): """ global _accepted from django.conf import settings - supported = SortedDict(settings.LANGUAGES) + supported = OrderedDict(settings.LANGUAGES) if check_path: lang_code = get_language_from_path(request.path_info, supported) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 3d6cd48b00..9b0dbf8ffa 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -423,6 +423,9 @@ these changes. * FastCGI support via the ``runfcgi`` management command will be removed. Please deploy your project using WSGI. +* ``django.utils.datastructures.SortedDict`` will be removed. Use + :class:`collections.OrderedDict` from the Python standard library instead. + 2.0 --- diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 95dad202fa..c29ac987a6 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -977,14 +977,13 @@ of the arguments is required, but you should use at least one of them. ``select_params`` parameter. Since ``select_params`` is a sequence and the ``select`` attribute is a dictionary, some care is required so that the parameters are matched up correctly with the extra select pieces. - In this situation, you should use a - :class:`django.utils.datastructures.SortedDict` for the ``select`` - value, not just a normal Python dictionary. + In this situation, you should use a :class:`collections.OrderedDict` for + the ``select`` value, not just a normal Python dictionary. This will work, for example:: Blog.objects.extra( - select=SortedDict([('a', '%s'), ('b', '%s')]), + select=OrderedDict([('a', '%s'), ('b', '%s')]), select_params=('one', 'two')) The only thing to be careful about when using select parameters in diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 6b614e0c83..599797bedb 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -105,6 +105,10 @@ to distinguish caches by the ``Accept-language`` header. .. class:: SortedDict +.. deprecated:: 1.7 + ``SortedDict`` is deprecated and will be removed in Django 1.9. Use + :class:`collections.OrderedDict` instead. + The :class:`django.utils.datastructures.SortedDict` class is a dictionary that keeps its keys in the order in which they're inserted. diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index d21cb45428..91b932aff1 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -167,6 +167,13 @@ on all Python versions. Since ``unittest2`` became the standard library's Python versions, this module isn't useful anymore. It has been deprecated. Use :mod:`unittest` instead. +``django.utils.datastructures.SortedDict`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As :class:`~collections.OrderedDict` was added to the standard library in +Python 2.7, :class:`~django.utils.datastructures.SortedDict` is no longer +needed and has been deprecated. + Custom SQL location for models package ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/extra_regress/tests.py b/tests/extra_regress/tests.py index 921f33aab9..ae8f1b4423 100644 --- a/tests/extra_regress/tests.py +++ b/tests/extra_regress/tests.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals +from collections import OrderedDict import datetime from django.contrib.auth.models import User from django.test import TestCase -from django.utils.datastructures import SortedDict from .models import TestObject, Order, RevisionableModel @@ -74,7 +74,7 @@ class ExtraRegressTests(TestCase): # select portions. Applies when portions are updated or otherwise # moved around. qs = User.objects.extra( - select=SortedDict((("alpha", "%s"), ("beta", "2"), ("gamma", "%s"))), + select=OrderedDict((("alpha", "%s"), ("beta", "2"), ("gamma", "%s"))), select_params=(1, 3) ) qs = qs.extra(select={"beta": 4}) @@ -180,100 +180,100 @@ class ExtraRegressTests(TestCase): obj.save() self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values()), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values()), [{'bar': 'second', 'third': 'third', 'second': 'second', 'whiz': 'third', 'foo': 'first', 'id': obj.pk, 'first': 'first'}] ) # Extra clauses after an empty values clause are still included self.assertEqual( - list(TestObject.objects.values().extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + list(TestObject.objects.values().extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), [{'bar': 'second', 'third': 'third', 'second': 'second', 'whiz': 'third', 'foo': 'first', 'id': obj.pk, 'first': 'first'}] ) # Extra columns are ignored if not mentioned in the values() clause self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('first', 'second')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('first', 'second')), [{'second': 'second', 'first': 'first'}] ) # Extra columns after a non-empty values() clause are ignored self.assertEqual( - list(TestObject.objects.values('first', 'second').extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + list(TestObject.objects.values('first', 'second').extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), [{'second': 'second', 'first': 'first'}] ) # Extra columns can be partially returned self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('first', 'second', 'foo')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('first', 'second', 'foo')), [{'second': 'second', 'foo': 'first', 'first': 'first'}] ) # Also works if only extra columns are included self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('foo', 'whiz')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('foo', 'whiz')), [{'foo': 'first', 'whiz': 'third'}] ) # Values list works the same way # All columns are returned for an empty values_list() self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list()), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list()), [('first', 'second', 'third', obj.pk, 'first', 'second', 'third')] ) # Extra columns after an empty values_list() are still included self.assertEqual( - list(TestObject.objects.values_list().extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + list(TestObject.objects.values_list().extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), [('first', 'second', 'third', obj.pk, 'first', 'second', 'third')] ) # Extra columns ignored completely if not mentioned in values_list() self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first', 'second')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first', 'second')), [('first', 'second')] ) # Extra columns after a non-empty values_list() clause are ignored completely self.assertEqual( - list(TestObject.objects.values_list('first', 'second').extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + list(TestObject.objects.values_list('first', 'second').extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), [('first', 'second')] ) self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('second', flat=True)), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('second', flat=True)), ['second'] ) # Only the extra columns specified in the values_list() are returned self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first', 'second', 'whiz')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first', 'second', 'whiz')), [('first', 'second', 'third')] ) # ...also works if only extra columns are included self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('foo','whiz')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('foo','whiz')), [('first', 'third')] ) self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz', flat=True)), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz', flat=True)), ['third'] ) # ... and values are returned in the order they are specified self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz','foo')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz','foo')), [('third', 'first')] ) self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first','id')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first','id')), [('first', obj.pk)] ) self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz', 'first', 'bar', 'id')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz', 'first', 'bar', 'id')), [('third', 'first', 'second', obj.pk)] ) diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 025fcd8608..6e03b0d7f6 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from collections import OrderedDict import datetime from operator import attrgetter import pickle @@ -14,7 +15,6 @@ from django.db.models.sql.where import WhereNode, EverythingNode, NothingNode from django.db.models.sql.datastructures import EmptyResultSet from django.test import TestCase, skipUnlessDBFeature from django.test.utils import str_prefix -from django.utils.datastructures import SortedDict from .models import ( Annotation, Article, Author, Celebrity, Child, Cover, Detail, DumbCategory, @@ -499,7 +499,7 @@ class Queries1Tests(BaseQuerysetTest): ) def test_ticket2902(self): - # Parameters can be given to extra_select, *if* you use a SortedDict. + # Parameters can be given to extra_select, *if* you use an OrderedDict. # (First we need to know which order the keys fall in "naturally" on # your system, so we can put things in the wrong way around from @@ -513,7 +513,7 @@ class Queries1Tests(BaseQuerysetTest): # This slightly odd comparison works around the fact that PostgreSQL will # return 'one' and 'two' as strings, not Unicode objects. It's a side-effect of # using constants here and not a real concern. - d = Item.objects.extra(select=SortedDict(s), select_params=params).values('a', 'b')[0] + d = Item.objects.extra(select=OrderedDict(s), select_params=params).values('a', 'b')[0] self.assertEqual(d, {'a': 'one', 'b': 'two'}) # Order by the number of tags attached to an item. @@ -1987,7 +1987,7 @@ class ValuesQuerysetTests(BaseQuerysetTest): def test_extra_values(self): # testing for ticket 14930 issues - qs = Number.objects.extra(select=SortedDict([('value_plus_x', 'num+%s'), + qs = Number.objects.extra(select=OrderedDict([('value_plus_x', 'num+%s'), ('value_minus_x', 'num-%s')]), select_params=(1, 2)) qs = qs.order_by('value_minus_x') -- cgit v1.3 From 61ecb5f48a4732f1471f858c64904ec1c4763925 Mon Sep 17 00:00:00 2001 From: Justin Michalicek Date: Fri, 2 Aug 2013 14:24:26 -0400 Subject: Fixed #20855 -- Added documentation of current_app and extra_context params to django.contrib.auth views refs #5298 and refs #8342 --- docs/topics/auth/default.txt | 101 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 9 deletions(-) (limited to 'docs') diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 25fe262b56..4ffafc1720 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -561,13 +561,33 @@ Most built-in authentication views provide a URL name for easier reference. See patterns. -.. function:: login(request, [template_name, redirect_field_name, authentication_form]) +.. function:: login(request, [template_name, redirect_field_name, authentication_form, current_app, extra_context]) **URL name:** ``login`` See :doc:`the URL documentation ` for details on using named URL patterns. + **Optional arguments:** + + * ``template_name``: The name of a template to display for the view used to + log the user in. Defaults to :file:`registration/login.html`. + + * ``redirect_field_name``: The name of a ``GET`` field containing the + URL to redirect to after login. Overrides ``next`` if the given + ``GET`` parameter is passed. + + * ``authentication_form``: A callable (typically just a form class) to + use for authentication. Defaults to + :class:`~django.contrib.auth.forms.AuthenticationForm`. + + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + ` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + Here's what ``django.contrib.auth.views.login`` does: * If called via ``GET``, it displays a login form that POSTs to the @@ -657,7 +677,7 @@ patterns. .. _site framework docs: ../sites/ -.. function:: logout(request, [next_page, template_name, redirect_field_name]) +.. function:: logout(request, [next_page, template_name, redirect_field_name, current_app, extra_context]) Logs a user out. @@ -675,6 +695,13 @@ patterns. URL to redirect to after log out. Overrides ``next_page`` if the given ``GET`` parameter is passed. + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + ` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + **Template context:** * ``title``: The string "Logged out", localized. @@ -691,7 +718,14 @@ patterns. :attr:`request.META['SERVER_NAME'] `. For more on sites, see :doc:`/ref/contrib/sites`. -.. function:: logout_then_login(request[, login_url]) + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + ` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + +.. function:: logout_then_login(request[, login_url, current_app, extra_context]) Logs a user out, then redirects to the login page. @@ -702,7 +736,14 @@ patterns. * ``login_url``: The URL of the login page to redirect to. Defaults to :setting:`settings.LOGIN_URL ` if not supplied. -.. function:: password_change(request[, template_name, post_change_redirect, password_change_form]) + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + ` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + +.. function:: password_change(request[, template_name, post_change_redirect, password_change_form, current_app, extra_context]) Allows a user to change their password. @@ -722,11 +763,18 @@ patterns. actually changing the user's password. Defaults to :class:`~django.contrib.auth.forms.PasswordChangeForm`. + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + ` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + **Template context:** * ``form``: The password change form (see ``password_change_form`` above). -.. function:: password_change_done(request[, template_name]) +.. function:: password_change_done(request[, template_name, current_app, extra_context]) The page shown after a user has changed their password. @@ -738,7 +786,14 @@ patterns. Defaults to :file:`registration/password_change_done.html` if not supplied. -.. function:: password_reset(request[, is_admin_site, template_name, email_template_name, password_reset_form, token_generator, post_reset_redirect, from_email]) + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + ` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + +.. function:: password_reset(request[, is_admin_site, template_name, email_template_name, password_reset_form, token_generator, post_reset_redirect, from_email, current_app, extra_context]) Allows a user to reset their password by generating a one-time use link that can be used to reset the password, and sending that link to the @@ -794,6 +849,13 @@ patterns. * ``from_email``: A valid email address. By default Django uses the :setting:`DEFAULT_FROM_EMAIL`. + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + ` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + **Template context:** * ``form``: The form (see ``password_reset_form`` above) for resetting @@ -838,7 +900,7 @@ patterns. single line plain text string. -.. function:: password_reset_done(request[, template_name]) +.. function:: password_reset_done(request[, template_name, current_app, extra_context]) The page shown after a user has been emailed a link to reset their password. This view is called by default if the :func:`password_reset` view @@ -852,7 +914,14 @@ patterns. Defaults to :file:`registration/password_reset_done.html` if not supplied. -.. function:: password_reset_confirm(request[, uidb64, token, template_name, token_generator, set_password_form, post_reset_redirect]) + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + ` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + +.. function:: password_reset_confirm(request[, uidb64, token, template_name, token_generator, set_password_form, post_reset_redirect, current_app, extra_context]) Presents a form for entering a new password. @@ -883,6 +952,13 @@ patterns. * ``post_reset_redirect``: URL to redirect after the password reset done. Defaults to ``None``. + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + ` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + **Template context:** * ``form``: The form (see ``set_password_form`` above) for setting the @@ -891,7 +967,7 @@ patterns. * ``validlink``: Boolean, True if the link (combination of ``uidb64`` and ``token``) is valid or unused yet. -.. function:: password_reset_complete(request[,template_name]) +.. function:: password_reset_complete(request[,template_name, current_app, extra_context]) Presents a view which informs the user that the password has been successfully changed. @@ -903,6 +979,13 @@ patterns. * ``template_name``: The full name of a template to display the view. Defaults to :file:`registration/password_reset_complete.html`. + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + ` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + Helper functions ---------------- -- cgit v1.3 From ebb3e50243448545c7314a1932a9067ddca5960b Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Wed, 31 Jul 2013 12:52:11 +0700 Subject: Introduced ModelAdmin.get_fields() and refactored get_fieldsets() to use it. Refs #18681. This also starts the deprecation of ModelAdmin.declared_fieldsets --- django/contrib/admin/options.py | 60 +++++++++++++++++++++++++++++++--------- docs/internals/deprecation.txt | 2 ++ docs/ref/contrib/admin/index.txt | 8 ++++++ docs/releases/1.7.txt | 13 +++++++++ tests/modeladmin/tests.py | 10 +++++++ 5 files changed, 80 insertions(+), 13 deletions(-) (limited to 'docs') diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index cb50d0e749..b475868598 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -2,6 +2,7 @@ from collections import OrderedDict import copy import operator from functools import partial, reduce, update_wrapper +import warnings from django import forms from django.conf import settings @@ -238,13 +239,49 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): return db_field.formfield(**kwargs) - def _declared_fieldsets(self): + @property + def declared_fieldsets(self): + warnings.warn( + "ModelAdmin.declared_fieldsets is deprecated and " + "will be removed in Django 1.9.", + PendingDeprecationWarning, stacklevel=2 + ) + if self.fieldsets: return self.fieldsets elif self.fields: return [(None, {'fields': self.fields})] return None - declared_fieldsets = property(_declared_fieldsets) + + def get_fields(self, request, obj=None): + """ + Hook for specifying fields. + """ + return self.fields + + def get_fieldsets(self, request, obj=None): + """ + Hook for specifying fieldsets. + """ + # We access the property and check if it triggers a warning. + # If it does, then it's ours and we can safely ignore it, but if + # it doesn't then it has been overriden so we must warn about the + # deprecation. + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + declared_fieldsets = self.declared_fieldsets + if len(w) != 1 or not issubclass(w[0].category, PendingDeprecationWarning): + warnings.warn( + "ModelAdmin.declared_fieldsets is deprecated and " + "will be removed in Django 1.9.", + PendingDeprecationWarning + ) + if declared_fieldsets: + return declared_fieldsets + + if self.fieldsets: + return self.fieldsets + return [(None, {'fields': self.get_fields(request, obj)})] def get_ordering(self, request): """ @@ -478,13 +515,11 @@ class ModelAdmin(BaseModelAdmin): 'delete': self.has_delete_permission(request), } - def get_fieldsets(self, request, obj=None): - "Hook for specifying fieldsets for the add form." - if self.declared_fieldsets: - return self.declared_fieldsets + def get_fields(self, request, obj=None): + if self.fields: + return self.fields form = self.get_form(request, obj, fields=None) - fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) - return [(None, {'fields': fields})] + return list(form.base_fields) + list(self.get_readonly_fields(request, obj)) def get_form(self, request, obj=None, **kwargs): """ @@ -1657,12 +1692,11 @@ class InlineModelAdmin(BaseModelAdmin): return inlineformset_factory(self.parent_model, self.model, **defaults) - def get_fieldsets(self, request, obj=None): - if self.declared_fieldsets: - return self.declared_fieldsets + def get_fields(self, request, obj=None): + if self.fields: + return self.fields form = self.get_formset(request, obj, fields=None).form - fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) - return [(None, {'fields': fields})] + return list(form.base_fields) + list(self.get_readonly_fields(request, obj)) def get_queryset(self, request): queryset = super(InlineModelAdmin, self).get_queryset(request) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 9b0dbf8ffa..fe8b48a5a9 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -426,6 +426,8 @@ these changes. * ``django.utils.datastructures.SortedDict`` will be removed. Use :class:`collections.OrderedDict` from the Python standard library instead. +* ``ModelAdmin.declared_fieldsets`` will be removed. + 2.0 --- diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 40ec514331..04dc52a3a1 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1218,6 +1218,14 @@ 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_fields(self, request, obj=None) + + .. versionadded:: 1.7 + + The ``get_fields`` method is given the ``HttpRequest`` and the ``obj`` + being edited (or ``None`` on an add form) and is expected to return a list + of fields, as described above in the :attr:`ModelAdmin.fields` section. + .. method:: ModelAdmin.get_fieldsets(self, request, obj=None) The ``get_fieldsets`` method is given the ``HttpRequest`` and the ``obj`` diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 91b932aff1..970f362949 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -113,6 +113,11 @@ Minor features * The admin's search fields can now be customized per-request thanks to the new :meth:`django.contrib.admin.ModelAdmin.get_search_fields` method. +* The :meth:`ModelAdmin.get_fields() + ` method may be overridden to + customize the value of :attr:`ModelAdmin.fields + `. + Backwards incompatible changes in 1.7 ===================================== @@ -182,3 +187,11 @@ than simply ``myapp/models.py``, Django would look for :ref:`initial SQL data ` in ``myapp/models/sql/``. This bug has been fixed so that Django will search ``myapp/sql/`` as documented. The old location will continue to work until Django 1.9. + +``declared_fieldsets`` attribute on ``ModelAdmin.`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``ModelAdmin.declared_fieldsets`` was deprecated. Despite being a private API, +it will go through a regular deprecation path. This attribute was mostly used +by methods that bypassed ``ModelAdmin.get_fieldsets()`` but this was considered +a bug and has been addressed. diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 616b0889b9..424588cf49 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -51,6 +51,12 @@ class ModelAdminTests(TestCase): self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'bio', 'sign_date']) + self.assertEqual(list(ma.get_fields(request)), + ['name', 'bio', 'sign_date']) + + self.assertEqual(list(ma.get_fields(request, self.band)), + ['name', 'bio', 'sign_date']) + def test_default_fieldsets(self): # fieldsets_add and fieldsets_change should return a special data structure that # is used in the templates. They should generate the "right thing" whether we @@ -97,6 +103,10 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) + self.assertEqual(list(ma.get_fields(request)), ['name']) + + self.assertEqual(list(ma.get_fields(request, self.band)), ['name']) + self.assertEqual(ma.get_fieldsets(request), [(None, {'fields': ['name']})]) -- cgit v1.3 From e8183a8193ebc80fdc6fef789813c2c0c3615e5c Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Fri, 2 Aug 2013 14:26:19 +0100 Subject: Fixed #20842 and #20845 - Added a note on order_by() and improved prefetch_related() docs. --- docs/ref/models/querysets.txt | 103 +++++++++++++++++++++++++++--------------- 1 file changed, 67 insertions(+), 36 deletions(-) (limited to 'docs') diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index c29ac987a6..570682be04 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -110,7 +110,7 @@ described here. .. admonition:: You can't share pickles between versions - Pickles of QuerySets are only valid for the version of Django that + Pickles of ``QuerySets`` are only valid for the version of Django that was used to generate them. If you generate a pickle using Django version N, there is no guarantee that pickle will be readable with Django version N+1. Pickles should not be used as part of a long-term @@ -298,14 +298,30 @@ Be cautious when ordering by fields in related models if you are also using :meth:`distinct()`. See the note in :meth:`distinct` for an explanation of how related model ordering can change the expected results. -It is permissible to specify a multi-valued field to order the results by (for -example, a :class:`~django.db.models.ManyToManyField` field). Normally -this won't be a sensible thing to do and it's really an advanced usage -feature. However, if you know that your queryset's filtering or available data -implies that there will only be one ordering piece of data for each of the main -items you are selecting, the ordering may well be exactly what you want to do. -Use ordering on multi-valued fields with care and make sure the results are -what you expect. +.. note:: + It is permissible to specify a multi-valued field to order the results by + (for example, a :class:`~django.db.models.ManyToManyField` field, or the + reverse relation of a :class:`~django.db.models.ForeignKey` field). + + Consider this case:: + + class Event(Model): + parent = models.ForeignKey('self', related_name='children') + date = models.DateField() + + Event.objects.order_by('children__date') + + Here, there could potentially be multiple ordering data for each ``Event``; + each ``Event`` with multiple ``children`` will be returned multiple times + into the new ``QuerySet`` that ``order_by()`` creates. In other words, + using ``order_by()`` on the ``QuerySet`` could return more items than you + were working on to begin with - which is probably neither expected nor + useful. + + Thus, take care when using multi-valued field to order the results. **If** + you can be sure that there will only be one ordering piece of data for each + of the items you're ordering, this approach should not present problems. If + not, make sure the results are what you expect. There's no way to specify whether ordering should be case sensitive. With respect to case-sensitivity, Django will order results however your database @@ -386,7 +402,7 @@ field names, the database will only compare the specified field names. .. note:: When you specify field names, you *must* provide an ``order_by()`` in the - QuerySet, and the fields in ``order_by()`` must start with the fields in + ``QuerySet``, and the fields in ``order_by()`` must start with the fields in ``distinct()``, in the same order. For example, ``SELECT DISTINCT ON (a)`` gives you the first row for each @@ -787,8 +803,8 @@ stop the deluge of database queries that is caused by accessing related objects, but the strategy is quite different. ``select_related`` works by creating a SQL join and including the fields of the -related object in the SELECT statement. For this reason, ``select_related`` gets -the related objects in the same database query. However, to avoid the much +related object in the ``SELECT`` statement. For this reason, ``select_related`` +gets the related objects in the same database query. However, to avoid the much larger result set that would result from joining across a 'many' relationship, ``select_related`` is limited to single-valued relationships - foreign key and one-to-one. @@ -817,39 +833,54 @@ For example, suppose you have these models:: return u"%s (%s)" % (self.name, u", ".join([topping.name for topping in self.toppings.all()])) -and run this code:: +and run:: >>> Pizza.objects.all() [u"Hawaiian (ham, pineapple)", u"Seafood (prawns, smoked salmon)"... -The problem with this code is that it will run a query on the Toppings table for -**every** item in the Pizza ``QuerySet``. Using ``prefetch_related``, this can -be reduced to two: +The problem with this is that every time ``Pizza.__unicode__()`` asks for +``self.toppings.all()`` it has to query the database, so +``Pizza.objects.all()`` will run a query on the Toppings table for **every** +item in the Pizza ``QuerySet``. + +We can reduce to just two queries using ``prefetch_related``: >>> Pizza.objects.all().prefetch_related('toppings') -All the relevant toppings will be fetched in a single query, and used to make -``QuerySets`` that have a pre-filled cache of the relevant results. These -``QuerySets`` are then used in the ``self.toppings.all()`` calls. +This implies a ``self.toppings.all()`` for each ``Pizza``; now each time +``self.toppings.all()`` is called, instead of having to go to the database for +the items, it will find them in a prefetched ``QuerySet`` cache that was +populated in a single query. + +That is, all the relevant toppings will have been fetched in a single query, +and used to make ``QuerySets`` that have a pre-filled cache of the relevant +results; these ``QuerySets`` are then used in the ``self.toppings.all()`` calls. + +The additional queries in ``prefetch_related()`` are executed after the +``QuerySet`` has begun to be evaluated and the primary query has been executed. -The additional queries are executed after the QuerySet has begun to be evaluated -and the primary query has been executed. Note that the result cache of the -primary QuerySet and all specified related objects will then be fully loaded -into memory, which is often avoided in other cases - even after a query has been -executed in the database, QuerySet normally tries to make uses of chunking -between the database to avoid loading all objects into memory before you need -them. +Note that the result cache of the primary ``QuerySet`` and all specified related +objects will then be fully loaded into memory. This changes the typical +behavior of ``QuerySets``, which normally try to avoid loading all objects into +memory before they are needed, even after a query has been executed in the +database. + +.. note:: -Also remember that, as always with QuerySets, any subsequent chained methods -which imply a different database query will ignore previously cached results, -and retrieve data using a fresh database query. So, if you write the following: + Remember that, as always with ``QuerySets``, any subsequent chained methods + which imply a different database query will ignore previously cached + results, and retrieve data using a fresh database query. So, if you write + the following: - >>> pizzas = Pizza.objects.prefetch_related('toppings') - >>> [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas] + >>> pizzas = Pizza.objects.prefetch_related('toppings') + >>> [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas] -...then the fact that ``pizza.toppings.all()`` has been prefetched will not help -you - in fact it hurts performance, since you have done a database query that -you haven't used. So use this feature with caution! + ...then the fact that ``pizza.toppings.all()`` has been prefetched will not + help you. The ``prefetch_related('toppings')`` implied + ``pizza.toppings.all()``, but ``pizza.toppings.filter()`` is a new and + different query. The prefetched cache can't help here; in fact it hurts + performance, since you have done a database query that you haven't used. So + use this feature with caution! You can also use the normal join syntax to do related fields of related fields. Suppose we have an additional model to the example above:: @@ -902,7 +933,7 @@ additional queries on the ``ContentType`` table if the relevant rows have not already been fetched. ``prefetch_related`` in most cases will be implemented using a SQL query that -uses the 'IN' operator. This means that for a large QuerySet a large 'IN' clause +uses the 'IN' operator. This means that for a large ``QuerySet`` a large 'IN' clause could be generated, which, depending on the database, might have performance problems of its own when it comes to parsing or executing the SQL query. Always profile for your use case! @@ -1273,7 +1304,7 @@ raw Takes a raw SQL query, executes it, and returns a ``django.db.models.query.RawQuerySet`` instance. This ``RawQuerySet`` instance -can be iterated over just like an normal QuerySet to provide object instances. +can be iterated over just like an normal ``QuerySet`` to provide object instances. See the :ref:`executing-raw-queries` for more information. -- cgit v1.3 From 1593a86494f246f5410a7ad2f96bef67fb778778 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sun, 4 Aug 2013 14:45:29 -0400 Subject: Fixed #20860 -- Removed references to defunct chicagocrime.org --- docs/ref/contrib/syndication.txt | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) (limited to 'docs') diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index 9a0fb5830e..996950ef56 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -49,17 +49,17 @@ are views which can be used in your :doc:`URLconf `. A simple example ---------------- -This simple example, taken from `chicagocrime.org`_, describes a feed of the -latest five news items:: +This simple example, taken from a hypothetical police beat news site describes +a feed of the latest five news items:: from django.contrib.syndication.views import Feed from django.core.urlresolvers import reverse - from chicagocrime.models import NewsItem + from policebeat.models import NewsItem class LatestEntriesFeed(Feed): - title = "Chicagocrime.org site news" + title = "Police beat site news" link = "/sitenews/" - description = "Updates on changes and additions to chicagocrime.org." + description = "Updates on changes and additions to police beat central." def items(self): return NewsItem.objects.order_by('-pub_date')[:5] @@ -199,22 +199,20 @@ into those elements. are responsible for doing all necessary URL quoting and conversion to ASCII inside the method itself. -.. _chicagocrime.org: http://www.chicagocrime.org/ - A complex example ----------------- The framework also supports more complex feeds, via arguments. -For example, `chicagocrime.org`_ offers an RSS feed of recent crimes for every -police beat in Chicago. It'd be silly to create a separate +For example, a website could offer an RSS feed of recent crimes for every +police beat in a city. It'd be silly to create a separate :class:`~django.contrib.syndication.views.Feed` class for each police beat; that would violate the :ref:`DRY principle ` and would couple data to programming logic. Instead, the syndication framework lets you access the arguments passed from your :doc:`URLconf ` so feeds can output items based on information in the feed's URL. -On chicagocrime.org, the police-beat feeds are accessible via URLs like this: +The police beat feeds could be accessible via URLs like this: * :file:`/beats/613/rss/` -- Returns recent crimes for beat 613. * :file:`/beats/1424/rss/` -- Returns recent crimes for beat 1424. @@ -238,7 +236,7 @@ Here's the code for these beat-specific feeds:: return get_object_or_404(Beat, pk=beat_id) def title(self, obj): - return "Chicagocrime.org: Crimes for beat %s" % obj.beat + return "Police beat central: Crimes for beat %s" % obj.beat def link(self, obj): return obj.get_absolute_url() @@ -339,13 +337,13 @@ URLconf to add the extra versions. Here's a full example:: from django.contrib.syndication.views import Feed - from chicagocrime.models import NewsItem + from policebeat.models import NewsItem from django.utils.feedgenerator import Atom1Feed class RssSiteNewsFeed(Feed): - title = "Chicagocrime.org site news" + title = "Police beat site news" link = "/sitenews/" - description = "Updates on changes and additions to chicagocrime.org." + description = "Updates on changes and additions to police beat central." def items(self): return NewsItem.objects.order_by('-pub_date')[:5] -- cgit v1.3 From 28d3b33c04cc2e2250059039c5ebbab97869d525 Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Sun, 4 Aug 2013 17:18:10 -0700 Subject: Added a note to the 1.6 release about the new `--keep-pot` option for `makemessages`. Refs #17008. --- docs/releases/1.6.txt | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'docs') diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index ab70be7cfe..b545cbcd64 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -812,6 +812,10 @@ Miscellaneous * :meth:`~django.core.validators.validate_email` now accepts email addresses with ``localhost`` as the domain. +* The :djadminopt:`--keep-pot` option was added to :djadmin:`makemessages` + to prevent django from deleting the temporary .pot file it generates before + creating the .po file. + Features deprecated in 1.6 ========================== -- cgit v1.3 From 0b071ba7df7394b9caa040aca314b8015ebad776 Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Sun, 4 Aug 2013 17:29:55 -0700 Subject: Fixed a small formatting issue. --- docs/ref/django-admin.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 6be9d0f441..ac02b4b5fb 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -253,8 +253,8 @@ prompts. The :djadminopt:`--database` option may be used to specify the database to flush. ---no-initial-data -~~~~~~~~~~~~~~~~~ +``--no-initial-data`` +~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 1.5 @@ -1145,8 +1145,8 @@ prompts. The :djadminopt:`--database` option can be used to specify the database to synchronize. ---no-initial-data -~~~~~~~~~~~~~~~~~ +``--no-initial-data`` +~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 1.5 -- cgit v1.3 From 26c4bd38acd4c1b5125eaa381420c2ca5603bd39 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 5 Aug 2013 08:14:27 -0400 Subject: Fixed #20862 -- Updated startproject MIDDLEWARE_CLASSES in docs. Thanks Keryn Knight. --- docs/topics/http/middleware.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index 503d4322e0..6bb7ccb8f8 100644 --- a/docs/topics/http/middleware.txt +++ b/docs/topics/http/middleware.txt @@ -28,11 +28,12 @@ here's the default value created by :djadmin:`django-admin.py startproject `:: MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) A Django installation doesn't require any middleware — -- cgit v1.3 From 94d7fed7750322e8b80402c1e731bab6d4509f2e Mon Sep 17 00:00:00 2001 From: Jimmy Song Date: Sun, 4 Aug 2013 19:40:16 +0000 Subject: Fixed #20859 - Clarified Model.clean() example. --- AUTHORS | 1 + docs/ref/models/instances.txt | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 9 deletions(-) (limited to 'docs') diff --git a/AUTHORS b/AUTHORS index ade95df06a..130122c5ea 100644 --- a/AUTHORS +++ b/AUTHORS @@ -543,6 +543,7 @@ answer newbie questions, and generally made Django that much better: smurf@smurf.noris.de Vsevolod Solovyov George Song + Jimmy Song sopel Leo Soto Thomas Sorrel diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 1e7ad1ffdf..cb8570afdc 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -140,15 +140,19 @@ attributes on your model if desired. For instance, you could use it to automatically provide a value for a field, or to do validation that requires access to more than a single field:: - def clean(self): - import datetime - from django.core.exceptions import ValidationError - # Don't allow draft entries to have a pub_date. - if self.status == 'draft' and self.pub_date is not None: - raise ValidationError('Draft entries may not have a publication date.') - # Set the pub_date for published items if it hasn't been set already. - if self.status == 'published' and self.pub_date is None: - self.pub_date = datetime.date.today() + import datetime + from django.core.exceptions import ValidationError + from django.db import models + + class Article(models.Model): + ... + def clean(self): + # Don't allow draft entries to have a pub_date. + if self.status == 'draft' and self.pub_date is not None: + raise ValidationError('Draft entries may not have a publication date.') + # Set the pub_date for published items if it hasn't been set already. + if self.status == 'published' and self.pub_date is None: + self.pub_date = datetime.date.today() Any :exc:`~django.core.exceptions.ValidationError` exceptions raised by ``Model.clean()`` will be stored in a special key error dictionary key, -- cgit v1.3 From 6d88d47be6d37234aab86d0e863e371f28347d12 Mon Sep 17 00:00:00 2001 From: Justin Michalicek Date: Tue, 30 Jul 2013 22:29:34 -0400 Subject: Fixed #20832 -- Enabled HTML password reset email Added optional html_email_template_name parameter to password_reset view and PasswordResetForm. --- AUTHORS | 1 + django/contrib/auth/forms.py | 9 +++- .../registration/html_password_reset_email.html | 1 + django/contrib/auth/tests/test_forms.py | 55 ++++++++++++++++++++++ django/contrib/auth/tests/test_views.py | 19 ++++++++ django/contrib/auth/tests/urls.py | 1 + django/contrib/auth/views.py | 4 +- docs/releases/1.7.txt | 4 ++ docs/topics/auth/default.txt | 10 +++- 9 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 django/contrib/auth/tests/templates/registration/html_password_reset_email.html (limited to 'docs') diff --git a/AUTHORS b/AUTHORS index 130122c5ea..15f3e8dbf4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -417,6 +417,7 @@ answer newbie questions, and generally made Django that much better: Zain Memon Christian Metts michal@plovarna.cz + Justin Michalicek Slawek Mikula Katie Miller Shawn Milochik diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index c5c2db456e..3eba8abd51 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -230,7 +230,7 @@ class PasswordResetForm(forms.Form): subject_template_name='registration/password_reset_subject.txt', email_template_name='registration/password_reset_email.html', use_https=False, token_generator=default_token_generator, - from_email=None, request=None): + from_email=None, request=None, html_email_template_name=None): """ Generates a one-use only link for resetting password and sends to the user. @@ -263,7 +263,12 @@ class PasswordResetForm(forms.Form): # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) email = loader.render_to_string(email_template_name, c) - send_mail(subject, email, from_email, [user.email]) + + if html_email_template_name: + html_email = loader.render_to_string(html_email_template_name, c) + else: + html_email = None + send_mail(subject, email, from_email, [user.email], html_message=html_email) class SetPasswordForm(forms.Form): diff --git a/django/contrib/auth/tests/templates/registration/html_password_reset_email.html b/django/contrib/auth/tests/templates/registration/html_password_reset_email.html new file mode 100644 index 0000000000..1ebb550048 --- /dev/null +++ b/django/contrib/auth/tests/templates/registration/html_password_reset_email.html @@ -0,0 +1 @@ +Link diff --git a/django/contrib/auth/tests/test_forms.py b/django/contrib/auth/tests/test_forms.py index ae2ec93428..eef366f184 100644 --- a/django/contrib/auth/tests/test_forms.py +++ b/django/contrib/auth/tests/test_forms.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os +import re from django import forms from django.contrib.auth import get_user_model @@ -452,6 +453,60 @@ class PasswordResetFormTest(TestCase): form.save() self.assertEqual(len(mail.outbox), 0) + @override_settings( + TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',), + TEMPLATE_DIRS=( + os.path.join(os.path.dirname(upath(__file__)), 'templates'), + ), + ) + def test_save_plaintext_email(self): + """ + Test the PasswordResetForm.save() method with no html_email_template_name + parameter passed in. + Test to ensure original behavior is unchanged after the parameter was added. + """ + (user, username, email) = self.create_dummy_user() + form = PasswordResetForm({"email": email}) + self.assertTrue(form.is_valid()) + form.save() + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0].message() + self.assertFalse(message.is_multipart()) + self.assertEqual(message.get_content_type(), 'text/plain') + self.assertEqual(message.get('subject'), 'Custom password reset on example.com') + self.assertEqual(len(mail.outbox[0].alternatives), 0) + self.assertEqual(message.get_all('to'), [email]) + self.assertTrue(re.match(r'^http://example.com/reset/[\w+/-]', message.get_payload())) + + @override_settings( + TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',), + TEMPLATE_DIRS=( + os.path.join(os.path.dirname(upath(__file__)), 'templates'), + ), + ) + def test_save_html_email_template_name(self): + """ + Test the PasswordResetFOrm.save() method with html_email_template_name + parameter specified. + Test to ensure that a multipart email is sent with both text/plain + and text/html parts. + """ + (user, username, email) = self.create_dummy_user() + form = PasswordResetForm({"email": email}) + self.assertTrue(form.is_valid()) + form.save(html_email_template_name='registration/html_password_reset_email.html') + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(len(mail.outbox[0].alternatives), 1) + message = mail.outbox[0].message() + self.assertEqual(message.get('subject'), 'Custom password reset on example.com') + self.assertEqual(len(message.get_payload()), 2) + self.assertTrue(message.is_multipart()) + self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain') + self.assertEqual(message.get_payload(1).get_content_type(), 'text/html') + self.assertEqual(message.get_all('to'), [email]) + self.assertTrue(re.match(r'^http://example.com/reset/[\w/-]+', message.get_payload(0).get_payload())) + self.assertTrue(re.match(r'^Link$', message.get_payload(1).get_payload())) + class ReadOnlyPasswordHashTest(TestCase): diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py index 4820116c68..22ccbfd225 100644 --- a/django/contrib/auth/tests/test_views.py +++ b/django/contrib/auth/tests/test_views.py @@ -128,6 +128,25 @@ class PasswordResetTest(AuthViewsTestCase): self.assertEqual(len(mail.outbox), 1) self.assertTrue("http://" in mail.outbox[0].body) self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email) + # optional multipart text/html email has been added. Make sure original, + # default functionality is 100% the same + self.assertFalse(mail.outbox[0].message().is_multipart()) + + def test_html_mail_template(self): + """ + A multipart email with text/plain and text/html is sent + if the html_email_template parameter is passed to the view + """ + response = self.client.post('/password_reset/html_email_template/', {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0].message() + self.assertEqual(len(message.get_payload()), 2) + self.assertTrue(message.is_multipart()) + self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain') + self.assertEqual(message.get_payload(1).get_content_type(), 'text/html') + self.assertTrue('' not in message.get_payload(0).get_payload()) + self.assertTrue('' in message.get_payload(1).get_payload()) def test_email_found_custom_from(self): "Email is sent if a valid email address is provided for password reset when a custom from_email is provided." diff --git a/django/contrib/auth/tests/urls.py b/django/contrib/auth/tests/urls.py index 502fc659d4..2af83d21ea 100644 --- a/django/contrib/auth/tests/urls.py +++ b/django/contrib/auth/tests/urls.py @@ -67,6 +67,7 @@ urlpatterns = urlpatterns + patterns('', (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')), (r'^password_reset/custom_redirect/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='/custom/')), (r'^password_reset/custom_redirect/named/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='password_reset')), + (r'^password_reset/html_email_template/$', 'django.contrib.auth.views.password_reset', dict(html_email_template_name='registration/html_password_reset_email.html')), (r'^reset/custom/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', 'django.contrib.auth.views.password_reset_confirm', dict(post_reset_redirect='/custom/')), diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 1d2838e9b0..d852106a4e 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -140,7 +140,8 @@ def password_reset(request, is_admin_site=False, post_reset_redirect=None, from_email=None, current_app=None, - extra_context=None): + extra_context=None, + html_email_template_name=None): if post_reset_redirect is None: post_reset_redirect = reverse('password_reset_done') else: @@ -155,6 +156,7 @@ def password_reset(request, is_admin_site=False, 'email_template_name': email_template_name, 'subject_template_name': subject_template_name, 'request': request, + 'html_email_template_name': html_email_template_name, } if is_admin_site: opts = dict(opts, domain_override=request.get_host()) diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 970f362949..28d9c9e1f1 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -118,6 +118,10 @@ Minor features customize the value of :attr:`ModelAdmin.fields `. +* :func:`django.contrib.auth.views.password_reset` takes an optional + ``html_email_template_name`` parameter used to send a multipart HTML email + for password resets. + Backwards incompatible changes in 1.7 ===================================== diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 4ffafc1720..7dff9cdca7 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -793,7 +793,7 @@ patterns. * ``extra_context``: A dictionary of context data that will be added to the default context data passed to the template. -.. function:: password_reset(request[, is_admin_site, template_name, email_template_name, password_reset_form, token_generator, post_reset_redirect, from_email, current_app, extra_context]) +.. function:: password_reset(request[, is_admin_site, template_name, email_template_name, password_reset_form, token_generator, post_reset_redirect, from_email, current_app, extra_context, html_email_template_name]) Allows a user to reset their password by generating a one-time use link that can be used to reset the password, and sending that link to the @@ -856,6 +856,14 @@ patterns. * ``extra_context``: A dictionary of context data that will be added to the default context data passed to the template. + * ``html_email_template_name``: The full name of a template to use + for generating a ``text/html`` multipart email with the password reset + link. By default, HTML email is not sent. + + .. versionadded:: 1.7 + + ``html_email_template_name`` was added. + **Template context:** * ``form``: The form (see ``password_reset_form`` above) for resetting -- cgit v1.3 From c33d1ca1d98003de29cdecb6080b52c5c52139bd Mon Sep 17 00:00:00 2001 From: Dominic Rodger Date: Mon, 5 Aug 2013 17:23:26 +0100 Subject: Fixed #20852 - Fixed incorrectly generated left quotes in docs. Sphinx generates left single quotes for apostrophes after code markup, when right single quotes are required. The easiest way to fix this is just by inserting the unicode character for a right single quote. Instances of the problem were found by looking for ">‘" in the generated HTML. --- docs/howto/custom-management-commands.txt | 4 ++-- docs/howto/error-reporting.txt | 8 ++++---- docs/intro/tutorial05.txt | 2 +- docs/ref/class-based-views/mixins-date-based.txt | 2 +- docs/ref/contrib/admin/index.txt | 2 +- docs/ref/contrib/auth.txt | 2 +- docs/ref/contrib/comments/moderation.txt | 2 +- docs/ref/django-admin.txt | 2 +- docs/ref/models/options.txt | 2 +- docs/ref/models/querysets.txt | 2 +- docs/ref/settings.txt | 4 ++-- docs/ref/templates/api.txt | 2 +- docs/releases/1.3.txt | 4 ++-- docs/releases/1.6.txt | 4 ++-- docs/releases/1.7.txt | 2 +- docs/topics/class-based-views/mixins.txt | 2 +- docs/topics/db/managers.txt | 6 +++--- docs/topics/db/queries.txt | 2 +- docs/topics/db/tablespaces.txt | 2 +- docs/topics/forms/modelforms.txt | 4 ++-- 20 files changed, 30 insertions(+), 30 deletions(-) (limited to 'docs') diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt index 04ab9bae1b..2325e32ac2 100644 --- a/docs/howto/custom-management-commands.txt +++ b/docs/howto/custom-management-commands.txt @@ -193,7 +193,7 @@ Attributes ---------- All attributes can be set in your derived class and can be used in -:class:`BaseCommand`'s :ref:`subclasses`. +:class:`BaseCommand`’s :ref:`subclasses`. .. attribute:: BaseCommand.args @@ -267,7 +267,7 @@ the :meth:`~BaseCommand.handle` method must be implemented. .. admonition:: Implementing a constructor in a subclass If you implement ``__init__`` in your subclass of :class:`BaseCommand`, - you must call :class:`BaseCommand`'s ``__init__``. + you must call :class:`BaseCommand`’s ``__init__``. .. code-block:: python diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 987a503e95..5ebed92187 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -120,8 +120,8 @@ Filtering sensitive information Error reports are really helpful for debugging errors, so it is generally useful to record as much relevant information about those errors as possible. For example, by default Django records the `full traceback`_ for the -exception raised, each `traceback frame`_'s local variables, and the -:class:`~django.http.HttpRequest`'s :ref:`attributes`. +exception raised, each `traceback frame`_’s local variables, and the +:class:`~django.http.HttpRequest`’s :ref:`attributes`. However, sometimes certain types of information may be too sensitive and thus may not be appropriate to be kept track of, for example a user's password or @@ -164,7 +164,7 @@ production environment (that is, where :setting:`DEBUG` is set to ``False``): .. admonition:: When using mutiple decorators If the variable you want to hide is also a function argument (e.g. - '``user``' in the following example), and if the decorated function has + '``user``’ in the following example), and if the decorated function has mutiple decorators, then make sure to place ``@sensitive_variables`` at the top of the decorator chain. This way it will also hide the function argument as it gets passed through the other decorators:: @@ -232,7 +232,7 @@ own filter class and tell Django to use it via the DEFAULT_EXCEPTION_REPORTER_FILTER = 'path.to.your.CustomExceptionReporterFilter' You may also control in a more granular way which filter to use within any -given view by setting the ``HttpRequest``'s ``exception_reporter_filter`` +given view by setting the ``HttpRequest``’s ``exception_reporter_filter`` attribute:: def my_view(request): diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt index 39c3785f7c..63533d47ff 100644 --- a/docs/intro/tutorial05.txt +++ b/docs/intro/tutorial05.txt @@ -132,7 +132,7 @@ We identify a bug Fortunately, there's a little bug in the ``polls`` application for us to fix right away: the ``Poll.was_published_recently()`` method returns ``True`` if the ``Poll`` was published within the last day (which is correct) but also if -the ``Poll``'s ``pub_date`` field is in the future (which certainly isn't). +the ``Poll``’s ``pub_date`` field is in the future (which certainly isn't). You can see this in the Admin; create a poll whose date lies in the future; you'll see that the ``Poll`` change list claims it was published recently. diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index 1a1a4d531b..1162b2a194 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -225,7 +225,7 @@ DateMixin .. attribute:: date_field The name of the ``DateField`` or ``DateTimeField`` in the - ``QuerySet``'s model that the date-based archive should use to + ``QuerySet``’s model that the date-based archive should use to determine the list of objects to display on the page. When :doc:`time zone support ` is enabled and diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 04dc52a3a1..8feb574efd 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1615,7 +1615,7 @@ in your own admin JavaScript without including a second copy, you can use the The embedded jQuery has been upgraded from 1.4.2 to 1.9.1. The :class:`ModelAdmin` class requires jQuery by default, so there is no need -to add jQuery to your ``ModelAdmin``'s list of media resources unless you have +to add jQuery to your ``ModelAdmin``’s list of media resources unless you have a specifc need. For example, if you require the jQuery library to be in the global namespace (for example when using third-party jQuery plugins) or if you need a newer version of jQuery, you will have to include your own copy. diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index dfda8add4b..477ee10d5f 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -243,7 +243,7 @@ Manager methods be called. The ``extra_fields`` keyword arguments are passed through to the - :class:`~django.contrib.auth.models.User`'s ``__init__`` method to + :class:`~django.contrib.auth.models.User`’s ``__init__`` method to allow setting arbitrary fields on a :ref:`custom User model `. diff --git a/docs/ref/contrib/comments/moderation.txt b/docs/ref/contrib/comments/moderation.txt index 796e257200..5f0badfadb 100644 --- a/docs/ref/contrib/comments/moderation.txt +++ b/docs/ref/contrib/comments/moderation.txt @@ -54,7 +54,7 @@ following model, which would represent entries in a Weblog:: Now, suppose that we want the following steps to be applied whenever a new comment is posted on an ``Entry``: -1. If the ``Entry``'s ``enable_comments`` field is ``False``, the +1. If the ``Entry``’s ``enable_comments`` field is ``False``, the comment will simply be disallowed (i.e., immediately deleted). 2. If the ``enable_comments`` field is ``True``, the comment will be diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index ac02b4b5fb..fb9264559c 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -560,7 +560,7 @@ several lines in language files. .. django-admin-option:: --no-location -Use the ``--no-location`` option to not write '``#: filename:line``' +Use the ``--no-location`` option to not write '``#: filename:line``’ comment lines in language files. Note that using this option makes it harder for technically skilled translators to understand each message's context. diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index 41baa8c106..7d751a53fe 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -90,7 +90,7 @@ Django quotes column and table names behind the scenes. The name of an orderable field in the model, typically a :class:`DateField`, :class:`DateTimeField`, or :class:`IntegerField`. This specifies the default - field to use in your model :class:`Manager`'s + field to use in your model :class:`Manager`’s :meth:`~django.db.models.query.QuerySet.latest` and :meth:`~django.db.models.query.QuerySet.earliest` methods. diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 570682be04..0f08022179 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1924,7 +1924,7 @@ as_manager .. versionadded:: 1.7 Class method that returns an instance of :class:`~django.db.models.Manager` -with a copy of the ``QuerySet``'s methods. See +with a copy of the ``QuerySet``’s methods. See :ref:`create-manager-with-queryset-methods` for more details. .. _field-lookups: diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index c970311342..38d7275aed 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1744,7 +1744,7 @@ Default:: A tuple of template loader classes, specified as strings. Each ``Loader`` class knows how to import templates from a particular source. Optionally, a tuple can be -used instead of a string. The first item in the tuple should be the ``Loader``'s +used instead of a string. The first item in the tuple should be the ``Loader``’s module, subsequent items are passed to the ``Loader`` during initialization. See :doc:`/ref/templates/api`. @@ -2478,7 +2478,7 @@ files` for more details about usage. your static files from their permanent locations into one directory for ease of deployment; it is **not** a place to store your static files permanently. You should do that in directories that will be found by - :doc:`staticfiles`'s + :doc:`staticfiles`’s :setting:`finders`, which by default, are ``'static/'`` app sub-directories and any directories you include in :setting:`STATICFILES_DIRS`). diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index f7dd0121d1..87822fe7f8 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -708,7 +708,7 @@ class. Here are the template loaders that come with Django: with your own ``admin/base_site.html`` in ``myproject.polls``. You must then make sure that your ``myproject.polls`` comes *before* ``django.contrib.admin`` in :setting:`INSTALLED_APPS`, otherwise - ``django.contrib.admin``'s will be loaded first and yours will be ignored. + ``django.contrib.admin``’s will be loaded first and yours will be ignored. Note that the loader performs an optimization when it is first imported: it caches a list of which :setting:`INSTALLED_APPS` packages have a diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 060f099c52..6bf4c0db60 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -365,7 +365,7 @@ In earlier Django versions, when a model instance containing a file from the backend storage. This opened the door to several data-loss scenarios, including rolled-back transactions and fields on different models referencing the same file. In Django 1.3, when a model is deleted the -:class:`~django.db.models.FileField`'s ``delete()`` method won't be called. If +:class:`~django.db.models.FileField`’s ``delete()`` method won't be called. If you need cleanup of 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). @@ -654,7 +654,7 @@ Password reset view now accepts ``from_email`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :func:`django.contrib.auth.views.password_reset` view now accepts a -``from_email`` parameter, which is passed to the ``password_reset_form``'s +``from_email`` parameter, which is passed to the ``password_reset_form``’s ``save()`` method as a keyword argument. If you are using this view with a custom password reset form, then you will need to ensure your form's ``save()`` method accepts this keyword argument. diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index b545cbcd64..c0f5c51194 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -183,7 +183,7 @@ Minor features * The :attr:`~django.views.generic.edit.DeletionMixin.success_url` of :class:`~django.views.generic.edit.DeletionMixin` is now interpolated with - its ``object``\'s ``__dict__``. + its ``object``’s ``__dict__``. * :class:`~django.http.HttpResponseRedirect` and :class:`~django.http.HttpResponsePermanentRedirect` now provide an ``url`` @@ -330,7 +330,7 @@ Minor features * :class:`~django.forms.ModelForm` fields can now override error messages defined in model fields by using the - :attr:`~django.forms.Field.error_messages` argument of a ``Field``'s + :attr:`~django.forms.Field.error_messages` argument of a ``Field``’s constructor. To take advantage of this new feature with your custom fields, :ref:`see the updated recommendation ` for raising a ``ValidationError``. diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 28d9c9e1f1..161ae55436 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -141,7 +141,7 @@ Miscellaneous have a custom :class:`~django.core.files.uploadhandler.FileUploadHandler` that implements ``new_file()``, be sure it accepts this new parameter. -* :class:`ModelFormSet`'s no longer +* :class:`ModelFormSet`’s no longer delete instances when ``save(commit=False)`` is called. See :attr:`~django.forms.formsets.BaseFormSet.can_delete` for instructions on how to manually delete objects from deleted forms. diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index e85ef5b063..fdd5d42c5d 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -288,7 +288,7 @@ object. In order to do this, we need to have two different querysets: ``Book`` queryset for use by :class:`~django.views.generic.list.ListView` Since we have access to the ``Publisher`` whose books we want to list, we - simply override ``get_queryset()`` and use the ``Publisher``'s + simply override ``get_queryset()`` and use the ``Publisher``’s :ref:`reverse foreign key manager`. ``Publisher`` queryset for use in :meth:`~django.views.generic.detail.SingleObjectMixin.get_object()` diff --git a/docs/topics/db/managers.txt b/docs/topics/db/managers.txt index 3b83865e60..2d3f35db8b 100644 --- a/docs/topics/db/managers.txt +++ b/docs/topics/db/managers.txt @@ -100,7 +100,7 @@ access ``self.model`` to get the model class to which they're attached. Modifying initial Manager QuerySets ----------------------------------- -A ``Manager``'s base ``QuerySet`` returns all objects in the system. For +A ``Manager``’s base ``QuerySet`` returns all objects in the system. For example, using this model:: from django.db import models @@ -111,7 +111,7 @@ example, using this model:: ...the statement ``Book.objects.all()`` will return all books in the database. -You can override a ``Manager``\'s base ``QuerySet`` by overriding the +You can override a ``Manager``’s base ``QuerySet`` by overriding the ``Manager.get_queryset()`` method. ``get_queryset()`` should return a ``QuerySet`` with the properties you require. @@ -246,7 +246,7 @@ Creating ``Manager`` with ``QuerySet`` methods In lieu of the above approach which requires duplicating methods on both the ``QuerySet`` and the ``Manager``, :meth:`QuerySet.as_manager() ` can be used to create an instance -of ``Manager`` with a copy of a custom ``QuerySet``'s methods:: +of ``Manager`` with a copy of a custom ``QuerySet``’s methods:: class Person(models.Model): ... diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 1d6052f938..9a0d0ce6b9 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -720,7 +720,7 @@ efficient code. In a newly created :class:`~django.db.models.query.QuerySet`, the cache is empty. The first time a :class:`~django.db.models.query.QuerySet` is evaluated -- and, hence, a database query happens -- Django saves the query results in -the :class:`~django.db.models.query.QuerySet`\'s cache and returns the results +the :class:`~django.db.models.query.QuerySet`’s cache and returns the results that have been explicitly requested (e.g., the next element, if the :class:`~django.db.models.query.QuerySet` is being iterated over). Subsequent evaluations of the :class:`~django.db.models.query.QuerySet` reuse the cached diff --git a/docs/topics/db/tablespaces.txt b/docs/topics/db/tablespaces.txt index 8bf1d07bca..115887f512 100644 --- a/docs/topics/db/tablespaces.txt +++ b/docs/topics/db/tablespaces.txt @@ -30,7 +30,7 @@ Declaring tablespaces for indexes --------------------------------- You can pass the :attr:`~django.db.models.Field.db_tablespace` option to a -``Field`` constructor to specify an alternate tablespace for the ``Field``'s +``Field`` constructor to specify an alternate tablespace for the ``Field``’s column index. If no index would be created for the column, the option is ignored. diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 4ef7a6f074..a823c8acb5 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -392,7 +392,7 @@ these security concerns do not apply to you: model = Author fields = '__all__' -2. Set the ``exclude`` attribute of the ``ModelForm``'s inner ``Meta`` class to +2. Set the ``exclude`` attribute of the ``ModelForm``’s inner ``Meta`` class to a list of fields to be excluded from the form. For example:: @@ -757,7 +757,7 @@ Specifying widgets to use in the form with ``widgets`` .. versionadded:: 1.6 Using the ``widgets`` parameter, you can specify a dictionary of values to -customize the ``ModelForm``'s widget class for a particular field. This +customize the ``ModelForm``’s widget class for a particular field. This works the same way as the ``widgets`` dictionary on the inner ``Meta`` class of a ``ModelForm`` works:: -- cgit v1.3 From 12806758347dfd63a3cd1bfc0d925c09fdbd9cff Mon Sep 17 00:00:00 2001 From: Tai Lee Date: Tue, 7 May 2013 19:06:03 +1000 Subject: Fixed #15511 -- Allow optional fields on ``MultiValueField` subclasses. The `MultiValueField` class gets a new ``require_all_fields`` argument that defaults to ``True``. If set to ``False``, individual fields can be made optional, and a new ``incomplete`` validation error will be raised if any required fields have empty values. The ``incomplete`` error message can be defined on a `MultiValueField` subclass or on each individual field. Skip duplicate errors. --- django/forms/fields.py | 32 ++++++++++++---- docs/ref/forms/fields.txt | 41 +++++++++++++++++++- docs/releases/1.7.txt | 5 +++ tests/forms_tests/tests/test_forms.py | 70 +++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 9 deletions(-) (limited to 'docs') diff --git a/django/forms/fields.py b/django/forms/fields.py index b07ebe84ee..e995187682 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -955,15 +955,20 @@ class MultiValueField(Field): """ default_error_messages = { 'invalid': _('Enter a list of values.'), + 'incomplete': _('Enter a complete value.'), } def __init__(self, fields=(), *args, **kwargs): + self.require_all_fields = kwargs.pop('require_all_fields', True) super(MultiValueField, self).__init__(*args, **kwargs) - # Set 'required' to False on the individual fields, because the - # required validation will be handled by MultiValueField, not by those - # individual fields. for f in fields: - f.required = False + f.error_messages.setdefault('incomplete', + self.error_messages['incomplete']) + if self.require_all_fields: + # Set 'required' to False on the individual fields, because the + # required validation will be handled by MultiValueField, not + # by those individual fields. + f.required = False self.fields = fields def validate(self, value): @@ -993,15 +998,26 @@ class MultiValueField(Field): field_value = value[i] except IndexError: field_value = None - if self.required and field_value in self.empty_values: - raise ValidationError(self.error_messages['required'], code='required') + if field_value in self.empty_values: + if self.require_all_fields: + # Raise a 'required' error if the MultiValueField is + # required and any field is empty. + if self.required: + raise ValidationError(self.error_messages['required'], code='required') + elif field.required: + # Otherwise, add an 'incomplete' error to the list of + # collected errors and skip field cleaning, if a required + # field is empty. + if field.error_messages['incomplete'] not in errors: + errors.append(field.error_messages['incomplete']) + continue try: clean_data.append(field.clean(field_value)) except ValidationError as e: # Collect all validation errors in a single list, which we'll # raise at the end of clean(), rather than raising a single - # exception for the first error we encounter. - errors.extend(e.error_list) + # exception for the first error we encounter. Skip duplicates. + errors.extend(m for m in e.error_list if m not in errors) if errors: raise ValidationError(errors) diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index ef4ed729bd..e7c6612a72 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -877,7 +877,7 @@ Slightly complex built-in ``Field`` classes * Normalizes to: the type returned by the ``compress`` method of the subclass. * Validates that the given value against each of the fields specified as an argument to the ``MultiValueField``. - * Error message keys: ``required``, ``invalid`` + * Error message keys: ``required``, ``invalid``, ``incomplete`` Aggregates the logic of multiple fields that together produce a single value. @@ -898,6 +898,45 @@ Slightly complex built-in ``Field`` classes Once all fields are cleaned, the list of clean values is combined into a single value by :meth:`~MultiValueField.compress`. + Also takes one extra optional argument: + + .. attribute:: require_all_fields + + .. versionadded:: 1.7 + + Defaults to ``True``, in which case a ``required`` validation error + will be raised if no value is supplied for any field. + + When set to ``False``, the :attr:`Field.required` attribute can be set + to ``False`` for individual fields to make them optional. If no value + is supplied for a required field, an ``incomplete`` validation error + will be raised. + + A default ``incomplete`` error message can be defined on the + :class:`MultiValueField` subclass, or different messages can be defined + on each individual field. For example:: + + from django.core.validators import RegexValidator + + class PhoneField(MultiValueField): + def __init__(self, *args, **kwargs): + # Define one message for all fields. + error_messages = { + 'incomplete': 'Enter a country code and phone number.', + } + # Or define a different message for each field. + fields = ( + CharField(error_messages={'incomplete': 'Enter a country code.'}, + validators=[RegexValidator(r'^\d+$', 'Enter a valid country code.')]), + CharField(error_messages={'incomplete': 'Enter a phone number.'}, + validators=[RegexValidator(r'^\d+$', 'Enter a valid phone number.')]), + CharField(validators=[RegexValidator(r'^\d+$', 'Enter a valid extension.')], + required=False), + ) + super(PhoneField, self).__init__( + self, error_messages=error_messages, fields=fields, + require_all_fields=False, *args, **kwargs) + .. attribute:: MultiValueField.widget Must be a subclass of :class:`django.forms.MultiWidget`. diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 161ae55436..fbbf93f627 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -122,6 +122,11 @@ Minor features ``html_email_template_name`` parameter used to send a multipart HTML email for password resets. +* :class:`~django.forms.MultiValueField` allows optional subfields by setting + the ``require_all_fields`` argument to ``False``. The ``required`` attribute + for each individual field will be respected, and a new ``incomplete`` + validation error will be raised when any required fields are empty. + Backwards incompatible changes in 1.7 ===================================== diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 89c19cc5a4..1c72d17abe 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import datetime from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.validators import RegexValidator from django.forms import * from django.http import QueryDict from django.template import Template, Context @@ -1792,6 +1793,75 @@ class FormsTestCase(TestCase): self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data, {'name' : 'fname lname'}) + def test_multivalue_optional_subfields(self): + class PhoneField(MultiValueField): + def __init__(self, *args, **kwargs): + fields = ( + CharField(label='Country Code', validators=[ + RegexValidator(r'^\+\d{1,2}$', message='Enter a valid country code.')]), + CharField(label='Phone Number'), + CharField(label='Extension', error_messages={'incomplete': 'Enter an extension.'}), + CharField(label='Label', required=False, help_text='E.g. home, work.'), + ) + super(PhoneField, self).__init__(fields, *args, **kwargs) + + def compress(self, data_list): + if data_list: + return '%s.%s ext. %s (label: %s)' % tuple(data_list) + return None + + # An empty value for any field will raise a `required` error on a + # required `MultiValueField`. + f = PhoneField() + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, []) + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, ['+61']) + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, ['+61', '287654321', '123']) + self.assertEqual('+61.287654321 ext. 123 (label: Home)', f.clean(['+61', '287654321', '123', 'Home'])) + self.assertRaisesMessage(ValidationError, + "'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home']) + + # Empty values for fields will NOT raise a `required` error on an + # optional `MultiValueField` + f = PhoneField(required=False) + self.assertEqual(None, f.clean('')) + self.assertEqual(None, f.clean(None)) + self.assertEqual(None, f.clean([])) + self.assertEqual('+61. ext. (label: )', f.clean(['+61'])) + self.assertEqual('+61.287654321 ext. 123 (label: )', f.clean(['+61', '287654321', '123'])) + self.assertEqual('+61.287654321 ext. 123 (label: Home)', f.clean(['+61', '287654321', '123', 'Home'])) + self.assertRaisesMessage(ValidationError, + "'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home']) + + # For a required `MultiValueField` with `require_all_fields=False`, a + # `required` error will only be raised if all fields are empty. Fields + # can individually be required or optional. An empty value for any + # required field will raise an `incomplete` error. + f = PhoneField(require_all_fields=False) + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, []) + self.assertRaisesMessage(ValidationError, "'Enter a complete value.'", f.clean, ['+61']) + self.assertEqual('+61.287654321 ext. 123 (label: )', f.clean(['+61', '287654321', '123'])) + six.assertRaisesRegex(self, ValidationError, + "'Enter a complete value\.', u?'Enter an extension\.'", f.clean, ['', '', '', 'Home']) + self.assertRaisesMessage(ValidationError, + "'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home']) + + # For an optional `MultiValueField` with `require_all_fields=False`, we + # don't get any `required` error but we still get `incomplete` errors. + f = PhoneField(required=False, require_all_fields=False) + self.assertEqual(None, f.clean('')) + self.assertEqual(None, f.clean(None)) + self.assertEqual(None, f.clean([])) + self.assertRaisesMessage(ValidationError, "'Enter a complete value.'", f.clean, ['+61']) + self.assertEqual('+61.287654321 ext. 123 (label: )', f.clean(['+61', '287654321', '123'])) + six.assertRaisesRegex(self, ValidationError, + "'Enter a complete value\.', u?'Enter an extension\.'", f.clean, ['', '', '', 'Home']) + self.assertRaisesMessage(ValidationError, + "'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home']) + def test_custom_empty_values(self): """ Test that form fields can customize what is considered as an empty value -- cgit v1.3 From 709cd2c4b751bcdfed7c8a27306778a758523658 Mon Sep 17 00:00:00 2001 From: Leandro Regueiro Date: Thu, 18 Apr 2013 22:04:00 +0200 Subject: Added section labels in cache docs --- docs/topics/cache.txt | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'docs') diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 2352770bad..a2491f2198 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -39,6 +39,8 @@ Django also works well with "upstream" caches, such as `Squid caches that you don't directly control but to which you can provide hints (via HTTP headers) about which parts of your site should be cached, and how. +.. _setting-up-the-cache: + Setting up the cache ==================== @@ -152,6 +154,8 @@ permanent storage -- they're all intended to be solutions for caching, not storage -- but we point this out here because memory-based caching is particularly temporary. +.. _database-caching: + Database caching ---------------- -- cgit v1.3 From fb26c4996a0c4d41aa80d28ce65ab050ffe6df6b Mon Sep 17 00:00:00 2001 From: Christopher Medrela Date: Wed, 7 Aug 2013 20:57:56 +0200 Subject: Fixed #20484 again -- added note to field documentation --- 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 736a836924..01215884c4 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -883,6 +883,9 @@ are converted to lowercase. ``192.0.2.1``. Default is disabled. Can only be used when ``protocol`` is set to ``'both'``. +If you allow for blank values, you have to allow for null values since blank +values are stored as null. + ``NullBooleanField`` -------------------- -- cgit v1.3 From 7a2296eb5bb05a4109392f8333e934d576d79d35 Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Wed, 7 Aug 2013 18:47:07 +0100 Subject: Fixed #20870 -- Documented django.utils.functional.cached_property --- docs/ref/utils.txt | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) (limited to 'docs') diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 599797bedb..8ecd69626c 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -431,6 +431,50 @@ Atom1Feed .. module:: django.utils.functional :synopsis: Functional programming tools. +.. class:: cached_property(object) + + The ``@cached_property`` decorator caches the result of a method with a + single ``self`` argument as a property. The cached result will persist as + long as the instance does. + + Consider a typical case, where a view might need to call a model's method + to perform some computation, before placing the model instance into the + context, where the template might invoke the method once more:: + + # the model + class Person(models.Model): + + def friends(self): + # expensive computation + ... + return friends + + # in the view: + if person.friends(): + + # in the template: + {% for friend in person.friends %} + + ``friends()`` will be called twice. Since the instance ``person`` in + the view and the template are the same, ``@cached_property`` can avoid + that:: + + from django.utils.functional import cached_property + + @cached_property + def friends(self): + # expensive computation + ... + return friends + + Note that as the method is now a property, in Python code it will need to + be invoked appropriately:: + + # in the view: + if person.friends: + + You may clear the cached result using ``del person.friends``. + .. function:: allow_lazy(func, *resultclasses) Django offers many utility functions (particularly in ``django.utils``) that -- cgit v1.3 From f96fe3cd1eb56bdd4433f76a933a98915ea25ece Mon Sep 17 00:00:00 2001 From: Jaime Irurzun Date: Thu, 8 Aug 2013 12:45:06 +0100 Subject: Clarify meaning of models.User.is_authenticated() --- docs/ref/contrib/auth.txt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'docs') diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index 477ee10d5f..4fe87e9872 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -114,11 +114,13 @@ Methods Always returns ``True`` (as opposed to ``AnonymousUser.is_authenticated()`` which always returns ``False``). 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 ``request.user`` has been populated by the - :class:`~django.contrib.auth.middleware.AuthenticationMiddleware` with - a :class:`~django.contrib.auth.models.User` object representing the - currently logged-in user. + imply any permissions, and doesn't check if the user is active or has + a valid session. Even though normally you will call this method on + ``request.user`` to find out whether it has been populated by the + :class:`~django.contrib.auth.middleware.AuthenticationMiddleware` + (representing the currently logged-in user), you should know this method + returns ``True`` for any :class:`~django.contrib.auth.models.User` + instance. .. method:: get_full_name() -- cgit v1.3 From fb1dd6b13a0e6b1ef64eac88467321d097942cd2 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Thu, 8 Aug 2013 14:05:55 +0100 Subject: Form.clean() does not need to return cleaned_data. If it does, that will be used as the cleaned_data. The default implementation has been changed to match this change. --- django/forms/forms.py | 7 +++++-- docs/ref/forms/validation.txt | 12 ++---------- docs/releases/1.7.txt | 5 +++++ tests/forms_tests/tests/test_extra.py | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+), 12 deletions(-) (limited to 'docs') diff --git a/django/forms/forms.py b/django/forms/forms.py index 9905eee7c7..97ee72e98e 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -297,9 +297,12 @@ class BaseForm(object): def _clean_form(self): try: - self.cleaned_data = self.clean() + cleaned_data = self.clean() except ValidationError as e: self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages) + else: + if cleaned_data is not None: + self.cleaned_data = cleaned_data def _post_clean(self): """ @@ -315,7 +318,7 @@ class BaseForm(object): not be associated with a particular field; it will have a special-case association with the field named '__all__'. """ - return self.cleaned_data + pass def has_changed(self): """ diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt index 87f9741c1b..c6d2722bf7 100644 --- a/docs/ref/forms/validation.txt +++ b/docs/ref/forms/validation.txt @@ -75,10 +75,8 @@ overridden: any validation that requires access to multiple fields from the form at once. This is where you might put in things to check that if field ``A`` is supplied, field ``B`` must contain a valid email address and the - like. The data that this method returns is the final ``cleaned_data`` - attribute for the form, so don't forget to return the full list of - cleaned data if you override this method (by default, ``Form.clean()`` - just returns ``self.cleaned_data``). + like. This method can return a completely different dictionary if it wishes, + which will be used as the ``cleaned_data``. Note that any errors raised by your ``Form.clean()`` override will not be associated with any field in particular. They go into a special @@ -403,9 +401,6 @@ example:: raise forms.ValidationError("Did not send for 'help' in " "the subject despite CC'ing yourself.") - # Always return the full collection of cleaned data. - return cleaned_data - In this code, if the validation error is raised, the form will display an error message at the top of the form (normally) describing the problem. @@ -443,9 +438,6 @@ sample) looks like this:: del cleaned_data["cc_myself"] del cleaned_data["subject"] - # Always return the full collection of cleaned data. - return cleaned_data - As you can see, this approach requires a bit more effort, not withstanding the extra design effort to create a sensible form display. The details are worth noting, however. Firstly, earlier we mentioned that you might need to check if diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index fbbf93f627..6bf13834d2 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -127,6 +127,11 @@ Minor features for each individual field will be respected, and a new ``incomplete`` validation error will be raised when any required fields are empty. +* The :meth:`~django.forms.Form.clean` method on a form no longer needs to + return ``self.cleaned_data``. If it does return a changed dictionary then + that will still be used. The default implementation no longer returns + ``self.cleaned_data``. + Backwards incompatible changes in 1.7 ===================================== diff --git a/tests/forms_tests/tests/test_extra.py b/tests/forms_tests/tests/test_extra.py index afdfaf5281..ba835495c6 100644 --- a/tests/forms_tests/tests/test_extra.py +++ b/tests/forms_tests/tests/test_extra.py @@ -620,6 +620,24 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): self.assertTrue(f.is_valid()) self.assertEqual(f.cleaned_data['username'], 'sirrobin') + def test_changing_cleaned_data_in_clean(self): + class UserForm(Form): + username = CharField(max_length=10) + password = CharField(widget=PasswordInput) + + def clean(self): + data = self.cleaned_data + + # Return a different dict. We have not changed self.cleaned_data. + return { + 'username': data['username'].lower(), + 'password': 'this_is_not_a_secret', + } + + f = UserForm({'username': 'SirRobin', 'password': 'blue'}) + self.assertTrue(f.is_valid()) + self.assertEqual(f.cleaned_data['username'], 'sirrobin') + def test_overriding_errorlist(self): @python_2_unicode_compatible class DivErrorList(ErrorList): -- cgit v1.3 From 1c4a9bd9ad1a8e61817c6aa2b0d8d0ad2c080047 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Thu, 8 Aug 2013 14:27:48 +0100 Subject: Revert change to the default Form.clean() This means it doesn't break for people who are doing `cleaned_data = super(FooForm, self).clean()`. --- django/forms/forms.py | 2 +- docs/releases/1.7.txt | 3 +-- tests/forms_tests/tests/test_extra.py | 13 +++++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/django/forms/forms.py b/django/forms/forms.py index 97ee72e98e..c2b700ce77 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -318,7 +318,7 @@ class BaseForm(object): not be associated with a particular field; it will have a special-case association with the field named '__all__'. """ - pass + return self.cleaned_data def has_changed(self): """ diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 6bf13834d2..26a0beeece 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -129,8 +129,7 @@ Minor features * The :meth:`~django.forms.Form.clean` method on a form no longer needs to return ``self.cleaned_data``. If it does return a changed dictionary then - that will still be used. The default implementation no longer returns - ``self.cleaned_data``. + that will still be used. Backwards incompatible changes in 1.7 ===================================== diff --git a/tests/forms_tests/tests/test_extra.py b/tests/forms_tests/tests/test_extra.py index ba835495c6..3bc22243fb 100644 --- a/tests/forms_tests/tests/test_extra.py +++ b/tests/forms_tests/tests/test_extra.py @@ -620,6 +620,19 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): self.assertTrue(f.is_valid()) self.assertEqual(f.cleaned_data['username'], 'sirrobin') + def test_changing_cleaned_data_nothing_returned(self): + class UserForm(Form): + username = CharField(max_length=10) + password = CharField(widget=PasswordInput) + + def clean(self): + self.cleaned_data['username'] = self.cleaned_data['username'].lower() + # don't return anything + + f = UserForm({'username': 'SirRobin', 'password': 'blue'}) + self.assertTrue(f.is_valid()) + self.assertEqual(f.cleaned_data['username'], 'sirrobin') + def test_changing_cleaned_data_in_clean(self): class UserForm(Form): username = CharField(max_length=10) -- cgit v1.3 From 7e6af9d40ce0232deb9d4c6943beef0b62a20a08 Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Thu, 8 Aug 2013 13:16:48 +0100 Subject: Added more on @cached_property, refs #20870 --- docs/ref/utils.txt | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 8ecd69626c..d31de35006 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -434,8 +434,9 @@ Atom1Feed .. class:: cached_property(object) The ``@cached_property`` decorator caches the result of a method with a - single ``self`` argument as a property. The cached result will persist as - long as the instance does. + single ``self`` argument as a property. The cached result will persist + as long as the instance does, so if the instance is passed around and the + function subsequently invoked, the cached result will be returned. Consider a typical case, where a view might need to call a model's method to perform some computation, before placing the model instance into the @@ -455,7 +456,7 @@ Atom1Feed # in the template: {% for friend in person.friends %} - ``friends()`` will be called twice. Since the instance ``person`` in + Here, ``friends()`` will be called twice. Since the instance ``person`` in the view and the template are the same, ``@cached_property`` can avoid that:: @@ -473,7 +474,20 @@ Atom1Feed # in the view: if person.friends: - You may clear the cached result using ``del person.friends``. + The cached value can be treated like an ordinary attribute of the instance:: + + # clear it, requiring re-computation next time it's called + del person.friends # or delattr(person, "friends") + + # set a value manually, that will persist on the instance until cleared + person.friends = ["Huckleberry Finn", "Tom Sawyer"] + + As well as offering potential performance advantages, ``@cached_property`` + can ensure that an attribute's value does not change unexpectedly over the + life of an instance. This could occur with a method whose computation is + based on ``datetime.now()``, or simply if a change were saved to the + database by some other process in the brief interval between subsequent + invocations of a method on the same instance. .. function:: allow_lazy(func, *resultclasses) -- cgit v1.3 From 8442268869a691767788bcbb4df90ddb28abb8f2 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 8 Aug 2013 14:13:39 -0400 Subject: Added an anchor for django.forms.Form.clean in docs --- docs/ref/forms/validation.txt | 2 ++ 1 file changed, 2 insertions(+) (limited to 'docs') diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt index c6d2722bf7..255b665c42 100644 --- a/docs/ref/forms/validation.txt +++ b/docs/ref/forms/validation.txt @@ -363,6 +363,8 @@ write a cleaning method that operates on the ``recipients`` field, like so:: Cleaning and validating fields that depend on each other ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. method:: django.forms.Form.clean + Suppose we add another requirement to our contact form: if the ``cc_myself`` field is ``True``, the ``subject`` must contain the word ``"help"``. We are performing validation on more than one field at a time, so the form's -- cgit v1.3 From f8a6a4eba1b52dd634ab3e72637cd47412dcfa6e Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Fri, 9 Aug 2013 17:31:17 +0700 Subject: Improved queryset handling and docs for (Single|Multiple)ObjectMixin. --- django/views/generic/detail.py | 20 ++++++++++++-------- django/views/generic/list.py | 20 ++++++++++++++------ .../ref/class-based-views/mixins-multiple-object.txt | 8 ++++++++ docs/ref/class-based-views/mixins-single-object.txt | 8 ++++++++ 4 files changed, 42 insertions(+), 14 deletions(-) (limited to 'docs') diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py index 23000641b4..5ce8092c67 100644 --- a/django/views/generic/detail.py +++ b/django/views/generic/detail.py @@ -57,19 +57,23 @@ class SingleObjectMixin(ContextMixin): def get_queryset(self): """ - Get the queryset to look an object up against. May not be called if - `get_object` is overridden. + Return the `QuerySet` that will be used to look up the object. + + Note that this method is called by the default implementation of + `get_object` and may not be called if `get_object` is overriden. """ if self.queryset is None: if self.model: return self.model._default_manager.all() else: - raise ImproperlyConfigured("%(cls)s is missing a queryset. Define " - "%(cls)s.model, %(cls)s.queryset, or override " - "%(cls)s.get_queryset()." % { - 'cls': self.__class__.__name__ - }) - return self.queryset._clone() + raise ImproperlyConfigured( + "%(cls)s is missing a QuerySet. Define " + "%(cls)s.model, %(cls)s.queryset, or override " + "%(cls)s.get_queryset()." % { + 'cls': self.__class__.__name__ + } + ) + return self.queryset.all() def get_slug_field(self): """ diff --git a/django/views/generic/list.py b/django/views/generic/list.py index 1aff3454f4..7381fd1be9 100644 --- a/django/views/generic/list.py +++ b/django/views/generic/list.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.core.paginator import Paginator, InvalidPage from django.core.exceptions import ImproperlyConfigured +from django.db.models.query import QuerySet from django.http import Http404 from django.utils.translation import ugettext as _ from django.views.generic.base import TemplateResponseMixin, ContextMixin, View @@ -22,18 +23,25 @@ class MultipleObjectMixin(ContextMixin): def get_queryset(self): """ - Get the list of items for this view. This must be an iterable, and may - be a queryset (in which qs-specific behavior will be enabled). + Return the list of items for this view. + + The return value must be an iterable and may be an instance of + `QuerySet` in which case `QuerySet` specific behavior will be enabled. """ if self.queryset is not None: queryset = self.queryset - if hasattr(queryset, '_clone'): - queryset = queryset._clone() + if isinstance(queryset, QuerySet): + queryset = queryset.all() elif self.model is not None: queryset = self.model._default_manager.all() else: - raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" - % self.__class__.__name__) + raise ImproperlyConfigured( + "%(cls)s is missing a QuerySet. Define " + "%(cls)s.model, %(cls)s.queryset, or override " + "%(cls)s.get_queryset()." % { + 'cls': self.__class__.__name__ + } + ) return queryset def paginate_queryset(self, queryset, page_size): diff --git a/docs/ref/class-based-views/mixins-multiple-object.txt b/docs/ref/class-based-views/mixins-multiple-object.txt index b28bd11a71..67ca5429fa 100644 --- a/docs/ref/class-based-views/mixins-multiple-object.txt +++ b/docs/ref/class-based-views/mixins-multiple-object.txt @@ -63,6 +63,14 @@ MultipleObjectMixin A ``QuerySet`` that represents the objects. If provided, the value of ``queryset`` supersedes the value provided for :attr:`model`. + .. warning:: + + ``queryset`` is a class attribute with a *mutable* value so care + must be taken when using it directly. Before using it, either call + its :meth:`~django.db.models.query.QuerySet.all` method or + retrieve it with :meth:`get_queryset` which takes care of the + cloning behind the scenes. + .. attribute:: paginate_by An integer specifying how many objects should be displayed per page. If diff --git a/docs/ref/class-based-views/mixins-single-object.txt b/docs/ref/class-based-views/mixins-single-object.txt index bbe930d79e..1fce24acc5 100644 --- a/docs/ref/class-based-views/mixins-single-object.txt +++ b/docs/ref/class-based-views/mixins-single-object.txt @@ -23,6 +23,14 @@ SingleObjectMixin A ``QuerySet`` that represents the objects. If provided, the value of ``queryset`` supersedes the value provided for :attr:`model`. + .. warning:: + + ``queryset`` is a class attribute with a *mutable* value so care + must be taken when using it directly. Before using it, either call + its :meth:`~django.db.models.query.QuerySet.all` method or + retrieve it with :meth:`get_queryset` which takes care of the + cloning behind the scenes. + .. attribute:: slug_field The name of the field on the model that contains the slug. By default, -- cgit v1.3